Building a custom vault
Bạn có thể viết vault của riêng mình, một contract nhận doanh thu của một tax token CheesePad và làm bất cứ điều gì bạn thiết kế với nó: chia nó, stream nó, lock nó, buy back, donate. Bạn không cần bất cứ thứ gì được whitelist và bạn không đụng tới các contract của chính CheesePad. Bạn ship hai contract, một vault và một vault factory, và một token creator trỏ launch của họ tới factory của bạn. Nếu factory của bạn qua được các kiểm tra launch, vault của bạn được deploy và đấu nối tự động.
Con đường nhanh nhất là mở rộng hai base contract mà CheesePad cung cấp, VaultBase và
VaultFactoryBase, thứ xử lý phần boilerplate và để lại cho bạn những phần thú vị.
Mục lục
- Mô hình tư duy
- Handshake launch
- Mở rộng các base contract
- Ba quy tắc để nhận doanh thu
- Hai schema
- Revenue mode native vs ERC20
- Một ví dụ tối giản hoàn chỉnh
- Checklist trước khi bạn ship
- Các cạm bẫy
- Các bản triển khai tham chiếu
Mô hình tư duy
tax on every buy/sell
trader ──swap──▶ tax token ──▶ settles tax ──splits revenue──▶ YOUR VAULT
│
you decide what happens here
(distribute / claim / lock / ...)
- Thuế của một token được thu và settle theo từng đợt, không phải ở mỗi giao dịch.
- Khi nó settle, slice fee-receiver được gửi tới bất kỳ địa chỉ nào là fee receiver của token. Với một launch dùng vault, địa chỉ đó là vault của bạn.
- Doanh thu đến dưới dạng native BNB nếu quote token của sale là WBNB, hoặc dưới dạng quote ERC20 trong các trường hợp khác. Vault của bạn được cho biết cái nào lúc construction.
Hai contract, hai công việc:
| Contract | Công việc |
|---|---|
| Vault | Giữ doanh thu, hiện thực logic payout của bạn (distribute, claim, ...). Một cái cho mỗi launch. |
| Vault factory | Xác thực một launch và deploy một vault mới cho nó. Một factory phục vụ nhiều launch. |
Handshake launch
Khi một creator launch với factory của bạn (createMicroSaleTaxTokenWithVaultV2, hoặc tương đương
Melt Pool createBondingCurveTaxTokenWithVaultV2), CheesePad chạy các kiểm tra này fail-closed. Bất
kỳ thất bại nào cũng revert toàn bộ launch:
factory.diamond()trả về địa chỉ launchpad, chứng minh factory của bạn nhắm đúng platform.factory.supportsQuoteToken(quoteToken)trả về true.factory.supportsLaunchRevision(1)trả về true.factory.reviewLaunch(payload)trả về(true, ""), veto hook của bạn.factory.createVault(quoteToken, creator, vaultData)deploy vault.isNativeRevenue()của vault bằngquoteToken == WBNB. Một sự không khớp sẽ revert.- Launchpad gắn vault làm fee receiver của token và gọi
vault.bind(token)một lần.
Sau đây, vault của bạn đang chạy và nhận doanh thu. Bạn không bao giờ gọi CheesePad; nó gọi bạn.
Mở rộng các base contract
Vault: mở rộng VaultBase
VaultBase cho bạn, miễn phí:
| Được cung cấp | Nó là gì |
|---|---|
diamond, quoteToken, creator, isNativeRevenue | launch context bất biến |
token, bound | tax token đã bind, được set một lần bởi launchpad qua bind |
bind(token) | chỉ launchpad, một lần; bạn không override nó |
receive() | chỉ chấp nhận native revenue khi isNativeRevenue, từ chối sai tài sản |
onRevenue(amount) | tín hiệu best-effort từ token khi nó settle; phát ra RevenueReceived |
_revenueBalance() | số dư doanh thu hiện tại của bạn, bằng đúng tài sản |
_pay(payee, amount) | trả bằng đúng tài sản; trả về false thay vì revert, và dung nạp các token không trả giá trị như USDT |
platform() | địa chỉ backstop của platform, nếu bạn opt in vào nó |
Bạn hiện thực ba hàm (từ IVault):
function distribute() external; // permissionless payout poke
function description() external view returns (string memory); // one-liner for listings
function vaultUISchema() external view returns (VaultUISchema memory); // see The two schemas
Constructor nhận launch context:
constructor(address _diamond, address _quoteToken, address _creator, bool _isNativeRevenue)
Factory: mở rộng VaultFactoryBase
VaultFactoryBase xử lý phần plumbing revision và guard chỉ-launchpad. Bạn hiện thực:
function _reviewV1(LaunchDataV1 memory d) internal view returns (bool ok, string memory reason);
function createVault(address quoteToken, address creator, bytes calldata vaultData)
external onlyDiamond returns (address vault);
function supportsQuoteToken(address quoteToken) external view returns (bool);
function vaultDataSchema() external view returns (VaultDataSchema memory);
_reviewV1 là veto của bạn: nó nhận các tham số launch (LaunchDataV1) và trả về (false, reason)
để chặn một launch bạn không muốn phục vụ. Nó không nhận vaultData, nên bạn xác thực các launch param
và các per-vault param một cách độc lập.
Ba quy tắc để nhận doanh thu
Doanh thu được giao tới vault của bạn trong khi token đang giữa chừng settle. Phá vỡ những quy tắc này và bạn có thể brick mọi lần chuyển bị đánh thuế của token:
receive()vàonRevenue()phải rẻ và chỉ tích lũy. Chỉ bookkeeping và event. Không swap, không external call nào đáng kể.- Không bao giờ gọi ngược vào token từ
receive()hoặconRevenue(). Đọc nó giữa chừng settle có thể revert hoặc reenter. - Làm mọi công việc payout trong một giao dịch riêng. Một cú poke
distribute()permissionless, hoặc một cú pullclaim(). Không bao giờ đẩy tiền ra trong lúc nhận doanh thu.
Đây là lý do mọi vault CheesePad tích lũy lúc nhận và trả ra sau.
Hai schema
Vault và factory của bạn tự mô tả chính nó. Một UI CheesePad đọc hai schema và render form tạo của bạn và UI vault của bạn một cách tổng quát, không có code đặc thù cho vault của bạn.
vaultDataSchema() trên factory: form tạo
Mô tả cách vaultData được ABI-encode, để một UI có thể render form launch và encode các input của
creator.
function vaultDataSchema() external pure returns (VaultDataSchema memory s) {
s.description = "Fixed payee list; revenue splits by bps";
s.isArray = false; // vaultData = abi.encode(tuple); true => tuple[]
s.fields = new Field[](2);
s.fields[0] = Field("payees", FieldType.Address, true, 0, "Recipient addresses");
s.fields[1] = Field("bps", FieldType.Uint16, true, 0, "Basis points per payee (sum 10000)");
}
createVault của bạn sau đó decode vaultData theo đúng thứ tự và kiểu mà schema khai báo:
(address[] memory payees, uint16[] memory bps) = abi.decode(vaultData, (address[], uint16[]));
vaultUISchema() trên vault: các hành động và trạng thái đọc được
Một danh sách phương thức. Mỗi cái hoặc là một hành động ghi (một UI render một nút và một form nhập)
hoặc một trường đọc (isWrite = false; một UI gọi nó và render kết quả có kiểu). Đây là cách một UI
hiển thị trạng thái vault của bạn, bao gồm cả các trường nó chưa từng thấy, mà không biết contract của
bạn.
Để phơi ra một trường trạng thái, khai báo một phương thức đọc với một output có kiểu:
m.name = "lockDuration";
m.selector = bytes4(keccak256("lockDuration()"));
m.isWrite = false;
m.outputs = new Field[](1);
m.outputs[0] = Field("lockDuration", FieldType.Time, false, 0, "Seconds until unlock");
Một UI gọi selector, decode giá trị trả về bằng kiểu đã khai báo, và định dạng nó. Nó chỉ hiển thị
những gì bạn khai báo, nên hãy liệt kê mọi lệnh đọc bạn muốn phơi ra. Danh sách kiểu đầy đủ (Field,
Method, FieldType, ApprovalHint) nằm trong events-and-data.md.
Revenue mode native vs ERC20
isNativeRevenue được set lúc construction và phải bằng quoteToken == WBNB. Launchpad khẳng định
điều này lúc launch và revert khi có sự không khớp.
- Native mode (quote là WBNB): doanh thu đến dưới dạng BNB qua
receive()._revenueBalance()đọc số dư BNB của vault;_paygửi BNB. - ERC20 mode: doanh thu đến dưới dạng quote token (số dư tăng lên, không có lệnh gọi
receive()).VaultBase.receive()từ chối BNB lạc để tiền không bị mắc kẹt._revenueBalance()đọc số dư quote;_paythực hiện một lần chuyển ERC20.
Nếu bạn build trên VaultBase và dùng _revenueBalance() và _pay(), cả hai mode đều được xử lý cho
bạn.
Một ví dụ tối giản hoàn chỉnh
Một vault stream 100% doanh thu tới một người nhận duy nhất được set lúc launch.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {VaultBase} from "./VaultBase.sol";
import {VaultFactoryBase} from "./VaultFactoryBase.sol";
import {LaunchDataV1} from "./IVaultFactory.sol";
import {VaultUISchema, VaultDataSchema, Method, Field, FieldType} from "./IVaultSchema.sol";
contract SingleRecipientVault is VaultBase {
address public recipient;
constructor(address _diamond, address _quote, address _creator, bool _native, address _recipient)
VaultBase(_diamond, _quote, _creator, _native)
{
require(_recipient != address(0), "bad recipient");
recipient = _recipient;
}
// Permissionless: push the whole balance to the recipient.
function distribute() external nonReentrant {
uint256 bal = _revenueBalance();
if (bal == 0) return;
_pay(recipient, bal); // production should track owed[] on failure; see the checklist
}
function description() external pure returns (string memory) {
return "Streams all revenue to a single recipient";
}
function vaultUISchema() external pure returns (VaultUISchema memory s) {
s.vaultType = "SingleRecipient";
s.description = "Streams all revenue to a single recipient";
s.methods = new Method[](2);
s.methods[0].name = "distribute";
s.methods[0].selector = bytes4(keccak256("distribute()"));
s.methods[0].isWrite = true;
s.methods[0].hint = "Send accrued revenue to the recipient";
s.methods[1].name = "recipient";
s.methods[1].selector = bytes4(keccak256("recipient()"));
s.methods[1].isWrite = false;
s.methods[1].outputs = new Field[](1);
s.methods[1].outputs[0] = Field("recipient", FieldType.Address, false, 0, "Who receives the revenue");
s.methods[1].hint = "Revenue recipient";
}
}
contract SingleRecipientVaultFactory is VaultFactoryBase {
address public immutable weth;
constructor(address _diamond, address _weth) VaultFactoryBase(_diamond) { weth = _weth; }
function supportsQuoteToken(address) external pure returns (bool) { return true; }
// Veto launches that would never fund the vault.
function _reviewV1(LaunchDataV1 memory d) internal pure override returns (bool, string memory) {
if (d.feeReceiverBps == 0) return (false, "feeReceiver bps must be > 0");
if (d.buyTaxBps == 0 && d.sellTaxBps == 0) return (false, "tax must be > 0");
return (true, "");
}
function createVault(address quoteToken, address creator, bytes calldata vaultData)
external onlyDiamond returns (address vault)
{
address recipient = abi.decode(vaultData, (address));
// isNativeRevenue MUST be (quoteToken == weth); the launchpad enforces this.
vault = address(new SingleRecipientVault(diamond, quoteToken, creator, quoteToken == weth, recipient));
}
function vaultDataSchema() external pure returns (VaultDataSchema memory s) {
s.description = "The single address that receives all revenue";
s.isArray = false;
s.fields = new Field[](1);
s.fields[0] = Field("recipient", FieldType.Address, false, 0, "Revenue recipient");
}
}
Deploy factory một lần. Constructor của nó cần địa chỉ launchpad cộng với địa chỉ WBNB; suy ra WBNB từ
router của launchpad theo cách scripts/launchpad/deploy-vault-factory.ts làm, và khẳng định
factory.weth() == router.WETH(). Sau đó các creator truyền địa chỉ factory của bạn và
abi.encode(recipient) làm vaultData lúc launch.
Checklist trước khi bạn ship
- Vault mở rộng
VaultBase; hiện thựcdistribute(),description(),vaultUISchema(). - Factory mở rộng
VaultFactoryBase; hiện thực_reviewV1,createVault(onlyDiamond),supportsQuoteToken,vaultDataSchema(). createVaultbuild vault vớiisNativeRevenue == (quoteToken == weth).createVaultdecodevaultDatatheo đúng thứ tự và kiểu củavaultDataSchema().receive()vàonRevenue()rẻ, chỉ tích lũy, và không bao giờ gọi ngược vào token.- Mọi payout xảy ra trong
distribute()hoặcclaim(), không bao giờ lúc nhận doanh thu. - Dùng
_pay()và kiểm tra giá trị trả về của nó. Nó báo cáo thất bại thay vì revert, nên hãy giữ số dư của một payee thất bại làmowedcho mộtclaim()sau, và một payee tồi không thể brick tất cả mọi người. vaultUISchema()khai báo mọi trường đọc bạn muốn một UI hiển thị, với các output có kiểu.- Các trường số lượng dùng
decimals = 18cho WBNB, hoặc decimals của quote token trong các trường hợp khác.
Các cạm bẫy
- Không thể sweep theo thiết kế. Các vault CheesePad được dự định là sovereign và immutable, nên
người mua có thể xác minh không có admin drain. Nếu bạn thêm một escape hatch, hãy mong đợi nó bị đọc
như một điểm trừ về niềm tin. Nếu bạn muốn một platform backstop, hãy opt in một cách tường minh qua
platform()vàonlyPlatform, theo cách GiftVault làm cho đường rescue của nó. distribute()là permissionless. Bất kỳ ai cũng có thể poke nó, nên hãy làm cho phép tính payout an toàn khi được gọi bởi bất kỳ ai vào bất kỳ lúc nào, và no-op khi số dư bằng không.- Integer-division dust trong logic chia nên chuyển sang
distributetiếp theo, không revert. reviewLaunchkhông bao giờ thấyvaultData. Bạn không thể cross-validate các launch param với các per-vault param; chúng được xác thực riêng theo thiết kế.- Một phiên bản factory là một địa chỉ. Các factory là stateless và immutable. Một phiên bản mới là
một deployment mới. Để hỗ trợ một revision launch-data trong tương lai, hãy ship một factory được cập
nhật với một review hook mới và mở rộng
supportsLaunchRevision.
Các bản triển khai tham chiếu
Trong contracts/launchpad/vaults/:
SplitterVault.solvàSplitterVaultFactory.sol: các payee cố định theo bps.GiftVault.solvàGiftVaultFactory.sol: streaming gắn danh tính với một EIP-712 relayer bind và một fallback holder-dividend. Cũng cho thấy platform backstop opt-in.VaultBase.sol,VaultFactoryBase.sol,IVault.sol,IVaultFactory.sol,IVaultSchema.sol: các base contract và interface bạn build dựa trên.