BasketToken is a single-contract, non-upgradeable, in-kind mint/redeem vault.
The design goal is to make the contract's own attack surface as close to zero
as possible and to be explicit about the risks that remain.
| Property | Mechanism |
|---|---|
| Full backing | Mint pulls ceil(amount × units / 1e18) per constituent and verifies the actual balance delta; redeem pays floor(...). All rounding favors the vault. Invariant-tested (test/BasketToken.invariant.t.sol). |
| Redeem can never be blocked by the protocol | redeem has no pause, no cap, no guardian gate — verified by a dedicated test that redeems while minting is paused and the cap is below supply. |
| No admin control over funds | The guardian can only pause minting, move the supply cap (≤ immutable maxSupplyCap) and change the fee recipient. There is no withdraw, sweep, upgrade, delegatecall or selfdestruct path. |
| Reentrancy | nonReentrant on mint/redeem; attack mocks covering all four re-entry combinations. |
| Non-standard tokens | Balance-delta check rejects fee-on-transfer / lying constituents at mint time. |
- Issuer risk (dominant risk). Constituents are Robinhood Stock Tokens —
debt instruments of Robinhood Assets (Jersey) Ltd. The issuer can pause or
freeze the underlying tokens. If any constituent's transfers are frozen,
redeemof the whole basket reverts until the freeze is lifted, because a redeem is all-or-nothing by design (no partial-redeem escape hatch exists — deliberately, to keep the contract minimal). Supply caps exist to bound exposure to this risk. - Unaudited code. The contract is small (~160 nSLOC) and heavily tested, but has not undergone a professional audit. Supply caps are the mitigation.
- Weight drift. Units are fixed at deploy; weights drift with prices. This is documented product behavior, not a vulnerability.
The curation platform wraps the unchanged BasketToken core. The curator
license is earned by burning VIM — there is no staking, delegation,
cooldown, or reward pool.
The VIM token is external and third-party. VIM was launched on Virtuals
Protocol — a Virtuals AgentTokenV4 at
0x43E7…47aF:
a 1,000,000,000-supply ERC-20 whose owner controls a 1% buy/sell tax and an
address blacklist. It is not any contract in src/: the repo's
VimenToken.sol lives under test/mocks/ and is a reference/test stand-in
only, never deployed. The platform integrates VIM through exactly ONE
call — burnFrom, in CuratorRegistry — and that surface is verified safe
against the deployed token's bytecode:
-
The token's
burnFromroutes to_burn, which skips the blacklist (_beforeTokenTransferexempts burns,to == address(0)) and skips the 1% tax (burns never enter the taxed_transferpath), and reducestotalSupply. So the token owner can neither block nor tax a license burn. -
The platform touches VIM nowhere else —
FeeSplitterpays curators in basket tokens, never VIM — so VIM's tax / blacklist / owner cannot reach anything beyond the (safe) license burn. -
CuratorRegistry—burnForLicense()burnsLICENSE_BURN, a fixed constant of 25,000 VIM, from the caller (viaburnFrom) for a permanent, non-revocable license. Same amount for everyone, forever — no admin, no owner, no tunable knob. Holds no funds;_licensedset before the externalburnFrom(CEI);nonReentrant;AlreadyLicensedguard. -
FeeSplitter—feeRecipientof every factory basket; the 60/40 split are constants. The curator's 60% is transferred directly to their wallet in one hop — no pool, no claim step. Permissionlessdistribute. The factory link is a one-shot deployer-onlyinitFactory, frozen afterwards. -
CuratorGuardian— the restricted guardian of every curated basket. Curated baskets are wired with THIS contract as theirguardian, not the protocol Safe directly. It exposes onlyraiseCap(advance a basket's supply cap, admin-only, strictly upward, bounded by the basket's immutablemaxSupplyCap) and has NOsetFeeRecipient/setMintPausedpath. Itsadmin(the Safe) is immutable; no owner, no upgrade, no admin transfer. -
BasketFactory— permissionless creation gated onregistry.isLicensed; wires every basket withfeeRecipient = splitterandguardian = CuratorGuardian, so curators can neither redirect fees nor touch caps, and the protocol can only advance caps.
Trust properties: after deployment wiring the platform has exactly one
bounded lever — the protocol Safe can raise a curated basket's supply cap
(upward-only, via CuratorGuardian), and nothing else. The registry has no
admin at all; the license burn is a fixed constant. Everything is immutable.
Burning is irreversible
and idempotent-guarded; a license can never be revoked, and existing baskets
are immutable BasketToken instances unaffected by anything a curator does
afterward. distribute rounds the curator's 60% down, so at most the
remaining ≤1 wei of a distribution goes to the treasury; a 1-wei fee balance
floors the curator share to 0 and pays the single wei to treasury (tested).
The curator's share cannot be inflated or redirected: the 60% constant and the
destination (curatorOf[basket], set factory-only at publish) are both frozen.
Un-chokeable curated fee stream. Because a curated basket's guardian is
the CuratorGuardian contract — which lacks any setFeeRecipient /
setMintPaused function — no caller, not even the protocol Safe, can redirect
or pause a curated basket's mint fees. The Safe is only the guardian's admin
and can do exactly one thing: raise the cap, upward, never down. This is what
makes "burn 25,000 VIM for a non-revocable license, 60% of fees forever"
enforceable rather than merely promised. (First-party baskets keep the Safe as
their direct guardian; only factory-published baskets use the restricted one.)
Coverage: the burn redesign removed the staking/delegation/reward machinery
and added the restricted CuratorGuardian. Platform tests: 35 (Platform.t.sol
Platform.edge.t.sol), including an explicit proof that neither the Safe nor anyone else cansetFeeRecipient/setMintPauseda curated basket, and a proof thatburnForLicenseworks against the real token's tax + blacklist (both exempt burns); the full suite has since grown past 230 tests with the V2 platform, all green. Additional accepted Slither finding: benign== 0early-return indistribute.
Note: via_ir = true is now enabled — the 9-argument BasketToken
constructor call in BasketFactory exceeds legacy-codegen stack depth
(NFR-1's "unless needed" clause). The full suite passes under via-IR.
BasketToken2 keeps mint/redeem byte-for-byte V1 (in-kind, price-free,
ungated) and adds exactly one mutation: rebalance, callable only by the
per-basket agent key. The security claims, each enforced in code and tested:
- Policy bounds are unbypassable constants: cooldown ≥ 1 day, turnover ≤ 25% of NAV, slippage ≤ 1% of NAV per rebalance, validated in the constructor; a curator can only choose stricter values.
- The agent is caged:
onlyAgentprotects onlyrebalance; no path to mint, redeem, fees or custody. The only outflows are toMakerRegistry- whitelisted settlement contracts (delta-checked to the exact amount) and the sweep to the immutable per-basketBasketDistributor. Worst-case compromised agent key ≈ the slippage budget of NAV per cooldown window. - Backing survives every trade: units are recomputed as
floor(balance × 1e18 / supply)after each rebalance, so redemption never depends on the oracle being right. - Oracles are fail-safe and frozen: each registered asset's feed is fixed at registration and can never be repointed; a stale feed reverts the rebalance, an uncovered TWAP window reports stale rather than a guessable price. No feed is ever read on mint or redeem.
- The distributor cannot double-pay, over-pay, or brick: paginated snapshot deduped per cycle, Σ floor ≤ pot, revert-on-transfer skips the wallet instead of failing the cycle; the interval is immutable and there is no owner.
Before the immutable V2 deploy the layer went through two adversarial internal reviews; blocking findings (constructor validation, oracle storage-layout verification against the live PoolManager) were fixed or verified before deployment.
- FR-2: constructor takes an extra final parameter
initialSupplyCap(validated0 < initialSupplyCap <= maxSupplyCap). The spec had the deploy script callsetSupplyCappost-deploy, but that call is guardian-gated and the guardian is a Safe, not the deploy key — the call would revert. Setting the initial cap in the constructor removes the extra transaction and the window where the cap is unset. Approved by the project owner on 2026-07-10.
Zero findings of any severity in src/BasketToken.sol other than:
calls-loop(Low):mint/redeem/isFullyBackedcall constituent ERC-20s in a loop. Accepted — inherent to an in-kind multi-token vault; the constituent set is bounded (2–20) and fixed at deploy.cyclomatic-complexity(Informational): constructor validation ladder. Accepted — it is a sequence of independent input checks.- The reported High (
incorrect-exp) and Medium (divide-before-multiply) findings are all inside OpenZeppelinMath.mulDiv(v5.6.1) — well-known false positives on the Remco Bloemen mulDiv implementation (the^is an intentional XOR in the Newton–Raphson inverse). Not our code; unmodified audited library.
3 High, all triaged as false positives / intentional:
- H-1 "state change after external call" (
BasketFactory.createBasket, lines 62/78) — the external calls are to protocol-owned, trusted contracts:new BasketToken(...)(its constructor only doescode.lengthreads, no callback into the caller) andsplitter.register(factory-gated, writes a mapping, no callback). No attacker-controlled reentrancy path exists, so the post-call writes (curatorOf,_baskets.push, event) are safe. In the burn redesignCuratorRegistry.burnForLicensesets_licensedbefore the externalburnFrom(CEI) and isnonReentrant, so it is not flagged. Accepted. - H-2 contract name reused (
VimenZap/VimenZap2/VimenZap3share theVimenZaptype name across files) — deliberate: each is an independently deployed, immutable router version; only the latest is wired in the UI. Accepted. - H-3 unsafe int cast (
IUniswapV4.solBalanceDelta unpacking) — the truncation to the low 128 bits is the defined encoding of Uniswap v4'sBalanceDelta(two int128s packed in an int256); the cast is the decoder. Accepted.
Low findings (costly-op/require in loop, large literals 10_000 bps,
state-change-without-event on internal accounting, "could be immutable" on
guardian-settable fields, unchecked return on the quoter's revert-trick call,
unused error) — same rationales as the Slither triage above; all accepted.
Full report: contracts/aderyn-report.md.
No formal bug bounty yet. Please report privately via GitHub private vulnerability reporting before any public disclosure. Good-faith reports will be acknowledged within 48 hours, and rewards for critical findings will be negotiated case by case.