Build a custom vault
What you build (and what you don't)
How it wires together
// The interfaces you implement
interface IVaultFactory {
// Called ONCE by the VaultPortal at create time. taxToken is the PREDICTED
// token address (not deployed yet) — do NOT call into it here.
function newVault(
address taxToken,
address quoteToken, // pricing/dividend token; address(0) = native BNB
address creator,
bytes calldata vaultData // your create params, abi-encoded per vaultDataSchema()
) external returns (address vault);
function isQuoteTokenSupported(address quoteToken) external view returns (bool);
function vaultDataSchema() external pure returns (VaultDataSchema memory); // create-form UI
}
interface IVault { // your vault, extends VaultBase
receive() external payable; // receives the token's vault-bucket tax (BNB)
function vaultUISchema() external pure returns (VaultUISchema memory); // interaction-page UI
// Optional per-holder accounting (dividends): return true and the token will
// call setShare(holder, eligibleShare) after every transfer. See "Per-holder rewards".
function wantsShareUpdates() external view returns (bool);
function setShare(address holder, uint256 share) external; // onlyTaxToken
}Step 1 — the vault
Step 2 — the factory
contract MyVaultFactory is WhaleVaultFactoryBaseV2 {
address public immutable implementation; // your vault logic (deployed once)
constructor(address impl, address vaultPortal)
WhaleVaultFactoryBaseV2(vaultPortal) { implementation = impl; }
function newVault(address taxToken, address quoteToken, address creator, bytes calldata vaultData)
external override returns (address vault)
{
if (msg.sender != _getVaultPortal()) revert OnlyVaultPortal(); // only the platform may call
if (taxToken == address(0) || creator == address(0)) revert ZeroAddress();
if (quoteToken != address(0)) revert UnsupportedQuoteToken(); // this example wants BNB
// decode YOUR create params (must match vaultDataSchema below)
(address dividendToken, uint256 minimumShareBalance) =
abi.decode(vaultData, (address, uint256));
vault = Clones.clone(implementation); // one independent vault per token
MyVault(payable(vault)).initialize(taxToken, quoteToken, dividendToken, minimumShareBalance, creator);
}
function isQuoteTokenSupported(address quoteToken) external pure override returns (bool) {
return quoteToken == address(0); // BNB only
}
}Step 3 — the two schemas (your free UI)
// create-form schema: 2 inputs -> abi.encode(address, uint256) -> vaultData
function vaultDataSchema() public pure override returns (VaultDataSchema memory s) {
s.description = "Dividend any-token vault: snowball round every ~3 min.";
s.fields = new FieldDescriptor[](2);
s.fields[0] = FieldDescriptor("dividendToken", "address", "Reward token address", 0);
s.fields[1] = FieldDescriptor("minimumShareBalance", "uint256", "Min hold to qualify", 18);
s.isArray = false;
}
// interaction-page schema: the buttons holders see on the token page
function vaultUISchema() public pure returns (VaultUISchema memory s) {
s.vaultType = "MyDividendVault";
s.description = "Hold to earn; claim anytime.";
s.methods = new VaultMethodSchema[](2);
// claimDividend() -> write, no inputs
// pendingOf(addr) -> read, returns uint256
}Step 4 — deploy
VaultPortal · LaunchFactoryV2 · chain 56
0x1230B67525247DA20e56E9f8CAaA263ae670401a ↗Step 5 — use it on the create page
Step 6 — get listed (optional)
Security checklist: restrict newVault to the VaultPortal (OnlyVaultPortal); never call taxToken during init; keep funds under the guardian model (creator config-only); use ReentrancyGuard + SafeERC20; for price-sensitive logic avoid spot price (use Portal + TWAP). A working, audited reference is the Dividend Any-Token Vault (DividendWorldCupVault + its factory).