Audits & analysis
Test coverage, fuzzing, invariants, and the static-analysis triage.
No professional audit yet. That is stated plainly wherever it matters, and Phase 3 of the cap roadmap is gated on one. Here is what stands in the meantime, all reproducible from the public repo.
Test suite
107 tests, 100% line/statement/branch/function coverage on all five platform contracts. The suite is layered:
| Layer | What it proves |
|---|---|
| Unit tests | Every function, every custom error, constructor validation ladder |
| Fuzz tests | Roundtrip bound: redeem(mint(x)) returns each constituent within [required − nTokens wei, required]; users never profit from rounding; reward-dust bounds in the registry |
| Invariant tests | Full backing after any call sequence: balanceOf(vault) ≥ totalSupply × units[i] / 1e18 for all constituents |
| Attack mocks | Reentrancy on transfer hooks (all four entry combinations), fee-on-transfer / lying tokens rejected by balance-delta checks |
| Dedicated guarantees | Redeem succeeds while minting is paused and supply is above cap; guardian can do exactly three things and nothing else |
| Mainnet fork tests | Full mint→transfer→redeem cycles against the real Stock Token contracts; zap quote == execution in the same block against live Uniswap v4 pools |
Run them yourself:
cd contracts && forge test
# fork suites against Robinhood Chain mainnet (self-skip when RH_RPC is unset)
RH_RPC=https://rpc.mainnet.chain.robinhood.com forge test --match-contract ForkStatic analysis: full triage
Zero unexplained findings is the policy: every tool finding is either fixed or documented with its rationale.
Slither
Zero findings of any severity in BasketToken.sol except:
calls-loop(Low): mint/redeem/isFullyBacked call constituent ERC-20s in a loop. Accepted: inherent to an in-kind multi-token vault; the set is bounded (2–20) and fixed at deploy.cyclomatic-complexity(Informational): the constructor validation ladder. Accepted: a sequence of independent input checks.- Reported High (
incorrect-exp) and Medium (divide-before-multiply) sit inside OpenZeppelin'sMath.mulDiv. These are well-known false positives on the Remco Bloemen implementation (the^is an intentional XOR in the Newton–Raphson inverse). Unmodified, audited library code.
Aderyn
2 High, both triaged as false positives / intentional:
- "State change after external call" (factory/registry): the external
calls are to protocol-owned contracts wired at deployment, or the
deliberate balance-delta pattern; every flagged function is
nonReentrant. Accepted. - "Unsafe int cast" (
IUniswapV4.sol): truncating to the low 128 bits is the defined decoding of Uniswap v4'sBalanceDelta(two int128s packed in an int256). Accepted.
Low findings (require-in-loop, 10_000 bps literals, missing events on
internal accounting, and similar) are accepted with the same rationales; the
full report ships in the repo (contracts/aderyn-report.md).
Known spec deviations
One, documented and owner-approved: the constructor takes an extra
initialSupplyCap parameter (validated 0 < initialSupplyCap ≤ maxSupplyCap) instead of the deploy script calling setSupplyCap
post-deploy; that call is guardian-gated and the guardian is a Safe, not
the deploy key. Setting it in the constructor removes a transaction and the
window where the cap is unset.
Build note: via_ir = true is enabled because the 9-argument BasketToken
constructor call in BasketFactory exceeds legacy-codegen stack depth. The
full suite passes under via-IR.
Continuous checks
CI runs forge fmt --check, the full test suite, frontend
lint/typecheck/build and scripts typecheck on every push. Contract source is
verified on Blockscout for every deployment.