Solidity implementation of a Multi-Asset Shielded Pool (MASP): private pooled transfers over ERC-20 assets, with deposits, transfers and withdrawals proven in zero knowledge.
Notes are commitments in a quaternary Merkle tree of depth 11. Deposits are escrowed on submission and inserted in batches under a single tree-update proof. Spends consume notes by nullifier and produce new commitments, verified against one of the last 64 roots. Leaf insertion is proven rather than computed on-chain, so per-transaction cost does not depend on tree depth.
Every spend carries two Groth16 proofs: a transaction proof (4x6) and a tree-update proof (tree_update_batch) that advances the root. Each circuit's public inputs are compressed to three field elements [y, digest, z] before pairing: a Fiat–Shamir challenge z, a Horner evaluation y, and the circuit's Poseidon commitment digest to its coefficients. Verification cost does not depend on the number of logical public inputs.
Both spend proofs are checked in one BN254 pairing call. The two circuits' setups share alpha, beta and gamma, so the check folds into six pairing terms. flushBatch carries only a tree-update proof and uses the single-proof verifier.
flowchart TB
subgraph G["Governance"]
GOV["LelantosGovernor<br/>TimelockController"]
FB["FeeBurner<br/>treasury"]
end
subgraph P["Peripherals"]
NA["NativeAdapter"]
SW["SwapWrapper"]
GC["GenericCallWrapper"]
AD["UniV3Adapter · UniV4Adapter"]
BU["Bundler<br/>one per relayer"]
end
PX["DelayedUpgradeProxy"]
M["MASP"]
GOV -->|"owner · proxy admin"| PX
PX -.->|"delegatecall"| M
M -->|"sweep"| FB
NA -->|"depositAuthorized · withdraw · cancelDeposit"| PX
SW -->|"depositAuthorized · withdraw · cancelDeposit"| PX
GC -->|"depositAuthorized · withdraw · cancelDeposit"| PX
SW -->|"ISwapAdapter.swap"| AD
BU -->|"transfer · withdraw · flushBatch"| PX
BU -->|"withdrawNative"| NA
BU -->|"swap"| SW
BU -->|"execute"| GC
M --> V["BatchedGroth16Verifier"]
M --> V2["TreeUpdateBatchGroth16Verifier"]
M -.->|"delegatecall"| YO["YieldOps"]
M -.->|"delegatecall"| DO["DepositOps"]
YO --> YV["ERC4626Venue"]
YV --> VAULT["ERC-4626 vault"]
M --> P2["Permit2"]
| Component | Role |
|---|---|
MASP |
Commitment tree, nullifier set, escrow ledger, asset registry, fee accrual and yield index. ERC-20 only. |
DelayedUpgradeProxy |
The pool's proxy. Upgrades and verifier replacements are queued and activate after an immutable delay. MASP cannot be initialized without it. |
YieldOps, DepositOps |
External libraries reached by delegatecall. They run against the pool's storage and hold no state. |
NativeAdapter |
Wraps native coin into the deposit path and unwraps it out of the withdraw path. |
SwapWrapper |
Atomic unshield, swap, re-shield through an allowlisted ISwapAdapter (UniV3Adapter, UniV4Adapter). |
GenericCallWrapper |
Atomic unshield, arbitrary calls, re-shield into up to four notes. |
LelantosNameRegistrar, LelantosNameResolver |
Handles: a label mapped to its holder's shielded address, registered through GenericCallWrapper and served as ENS subnames of each parent name a resolver is deployed for. |
Bundler |
Lands a relayer's chained tree-advancing calls in one transaction. One per relayer, deployed by the permissionless BundlerFactory. |
LelantosGovernor, LelantosToken |
Governance, executing through a TimelockController. |
FeeBurner |
The pool's treasury. Auctions accrued fees for the governance token and burns the proceeds. |
ERC4626Venue |
Yield venue, one per (assetId, vault). |
Peripherals hold no pool state and no privileged role: none is registered with the pool or named by it. Their authority derives from SNARK public inputs (pi.recipient, pi.relayer, pi.payer) or from a Permit2 allowance over their own balance.
- The
TimelockControllerownsMASPandSwapWrapperand is the pool's proxy admin. It executes proposals fromLelantosGovernor. Vote weight is a delegated-balance snapshot ofLelantosToken, a fixed-supplyERC20Voteswith no mint function and no owner. - Every administrative action is a proposal subject to the vote and the Timelock delay. A guardian holding
CANCELLER_ROLEmay cancel a queued operation; it cannot propose, execute, or call the pool. - For and Abstain votes close
quorumVoteCutoffseconds before the proposal deadline; Against stays open until the deadline. - Upgrades and verifier replacements are queued on the proxy and activate only after
UPGRADE_DELAY, which is immutable. Activation is permissionless. Until then the current implementation serves every call. - A pause halts every proof-dependent entry point and defers pending activations by its duration.
cancelDepositandsweepstay open while paused. - Raises to
withdrawBps,perfBpsandcancelDelayare queued for 30 days and applied through the permissionlesscommitExitTerms. OwnableInitdeclares norenounceOwnership, so neither owned contract can be left ownerless.
An asset id may be registered with an ERC-4626 venue. The pool keeps bufferBps of the position unlent and supplies the remainder to the vault. Yield is a property of the asset id: a plain id and a yield id may share one token.
Notes in a yield asset are denominated in normalized units worth gross / supply of the token. publicIn and publicOut remain plain integers, and the index applies only at the token boundary.
- The index is derived from holdings (
venue.totalAssets() + idle) and is not stored. - The pool transfers the underlying to the venue before calling
deposit; no venue holds an allowance over the pool. - An id's venue is written once, at registration, and cannot be changed.
- A venue that cannot service a draw reverts
VenueDrainedwith the spend's nullifiers unconsumed.emergencyUnwindwithdraws the position to idle and halts further supply.
See src/README.md.
Requires Foundry and just. The symbolic, Echidna and Quint suites additionally require halmos, echidna, and node with quint.
just install # install forge dependencies
just build # compile
just test # run the test suite
just test-fuzz # fuzz and invariant suites at the fuzz profile
just halmos # symbolic suite
just echidna # Echidna property suites
just quint-test # replay the committed Quint traces
just size # check runtime sizes against EIP-170 under the deploy profile
just snapshot # refresh .gas-snapshot
just ci # everything CI runs, in orderMASP, YieldOps and DepositOps compile at optimizer_runs = 1000 under every profile so that the pool fits EIP-170; all other sources use 1_000_000 under the default profile (compilation_restrictions in foundry.toml).
| Document | Contents |
|---|---|
| src/README.md | Contract reference: state, flows, and the checks each entry point performs. |
| test/README.md | Test layout, bases, fixtures and conventions. |
| test/symbolic/README.md | Halmos suite. |
| test/echidna/README.md | Echidna suite. |
| test/quint/README.md | Quint model-based suite. |
| test/fixtures/README.md | Fixture files and how to regenerate them. |
| packages/abi/README.md | The @lelantos-org/contracts ABI package. |
MIT. See LICENSE.
src/verifiers/Verifier.sol and src/verifiers/TreeUpdateBatchVerifier.sol are snarkJS codegen output and carry SPDX-License-Identifier: GPL-3.0 with their upstream terms.