A yield aggregator is a custody problem wearing a product face. One USDC deposit, four live ERC-4626 vaults, weights the user owns. The contract has to make that split atomic, reversible, and impossible to strand.
This is the third milestone in our Stylus production series. Part 1 asked how to coordinate multiple liquidity sources under real market conditions. Part 2 asked how to keep an asynchronous oracle from stranding funds. This time the question is different: how do you take one deposit, split it across four production vaults in a single transaction, let the user own the allocation, and still guarantee an exit after a vault is later disabled?
Watch the pattern, not the product
The surface is Vaulty: a USDC yield aggregator embedded as a Mini App. The engineering problem is a router over live ERC-4626 vaults that must not leave a partial position, let an owner move someone else's money, or trap funds in a vault that later gets turned off. This walkthrough is the live flow: pick weights, deposit, rebalance, or exit.
Open-source reference
Stylus contracts, ERC-4626 adapters, Permit2 periphery, share math, and the mainnet runbook. Study the pattern, fork it, extend it.
- Rust
- Arbitrum Stylus
- ERC-4626
- Open source
Live walkthrough
End-to-end on Vaulty: pick weights, deposit USDC, rebalance, and exit across Aave, Morpho, Fluid, and Euler. Not a mock.
- Arbitrum One
- USDC
- E2E
Reference implementation, not a consumer product we operate. This post describes an open-source architecture WakeUp Labs published for the Stylus ecosystem. The Mini App is a distribution surface. The contracts are the reference.
Why this problem, after DEX aggregation and randomness
A DEX aggregator fails loudly. Quotes miss, routes revert, the user tries again. A randomness app fails if the callback never comes, which is why Part 2 spent its energy on the gap between request and answer.
A vault aggregator fails quietly. One adapter reverts and three legs have already moved. An owner disables a protocol and a user still holds shares there. The first depositor gets diluted by an inflation attack. Gas estimation under-reports a Stylus call and the transaction mines out of gas with money in flight.
Those are not product features. They are the reasons most “one-click yield” demos never become production software.
Architecture: one ledger, four adapters, no privilege at the door
If you remember one diagram, remember this: the core owns the per-user share ledger. Each adapter is a thin, single-vault ERC-4626 wrapper. The periphery is a Permit2 front door with no privilege the core recognizes.
User / Mini App
|
v
[ periphery ] -- stateless Permit2 front door. Optional.
|
v
[ core ] -- share ledger. Split. Rebalance. Redeem.
| | | |
v v v v
[adapter] [adapter] [adapter] [adapter]
Aave Morpho Fluid Euler
| | | |
v v v v
live ERC-4626 vaults on Arbitrum Onevault-coreThe system of record. It owns the per-user, per-adapter share ledger, the adapter registry, and the split / rebalance / redeem math. There is no owner-level allocation knob. The owner's job is registry maintenance: add_adapter and set_enabled. Never allocation.
vault-peripheryStateless on purpose. It pulls USDC via a single-use Permit2 SignatureTransfer and calls depositFor. A compromised periphery can at most donate its own transient balance. It cannot mint an unbacked claim against other users' custodied USDC.
vault-adapterOne Stylus binary, four deployed instances. Never one instance serving several vaults. init(vault, core) is one-shot. Mutating calls are onlyCore. The adapter custodies the vault shares; core never holds them.
mock-vaultA textbook ERC-4626 stand-in used only on Arbitrum Sepolia, so the core's plumbing can be exercised without touching real protocol money. Real protocol behavior was validated against Aave, Morpho, Fluid, and Euler on Arbitrum One.
Design rule: if a contract can hold a user's claim, it is core. Adapters hold vault shares on core's behalf and nothing else. The periphery holds no privilege at all.
The hard part is the split
The user thinks they are picking an allocation: 40% Aave, 30% Morpho, 20% Fluid, 10% Euler. The contract has to make that allocation a single atomic state transition, then keep it true across rebalance, disable, and exit.
t0 rebalance(weights) -- user-only. Unwind, measure, store, re-split.
t1 deposit(usdc) -- snapshot total_assets, split by bps, one deposit per leg.
-- one failing adapter reverts the entire tx.
... the position lives as per-adapter shares inside core ...
t2 redeem(bps) -- exit a fraction of the caller's own position back to USDC in core.
t3 Mini App / wallet -- second step moves that USDC to the user's account.Atomic in, two-step out
Deposit is whole-transaction atomic: a single failing adapter reverts the entire deposit rather than leaving a partial position. Withdraw is two steps by design. redeem(bps) exits a fraction of the caller's own position, basis points of that position, not a raw share count, because every adapter has its own share price, back to USDC held by vault-core. The second step, moving that USDC into the user's Lemon account, lives in the Mini App, not in the contract.
Rebalance is user-only
Calling rebalance(new_weights) fully unwinds the caller's current position across every adapter they hold shares in, including adapters the owner later disabled. It measures the real USDC proceeds, stores the new weight set, and re-splits those proceeds against it. There is no admin path that can do this on someone else's behalf.
Pick a failure. See what the contract does.
These are the cases we actually designed for. The state machine has to be boringly correct in every one of them.
1. One adapter reverts mid-deposit
The deposit path snapshots every active adapter's total_assets() once, divides the incoming amount by basis points, and pushes one deposit per adapter leg. If any leg fails, the whole transaction reverts. Partial positions are not a representable state.
2. The owner disables a vault the user still holds
There is no remove_adapter. That function was deleted on purpose: the guard it existed to enforce only existed because the function did. Disabling an adapter blocks new money from choosing it as a weight target. It never blocks money out. Both redeem and rebalance's unwind iterate the full registry filtered by held shares, not by the enabled set. A disabled adapter cannot strand a position.
3. Someone donates tokens to an adapter
Adapters have no sweep(), no rescue(), no privileged recovery. Native USDC or an unrelated ERC-20 sent directly to an adapter stays there permanently, including from us. Donated vault shares do increase total_assets(), that is a gift, not an exploit, and the inflation-attack defense lives in core's share math, not in a privileged backdoor on the adapter.
4. The first depositor gets diluted
Core keeps a per-user, per-adapter share ledger with a virtual-offset floor-rounding scheme: the standard ERC-4626 inflation-attack mitigation, applied per adapter rather than to one aggregate pool. The formula lives in one place on-chain and is mirrored off-chain for display. There is no single global share scalar, because every vault has its own share price.
5. The node under-reports gas and the tx mines OOG
This one bit us on live Arbitrum One, twice, before the core was even deployed. eth_estimateGas under-reported Stylus calls. The identical call succeeded via eth_call. The mined transaction used exactly the estimated limit and failed. Morpho's withdraw walks a market queue; it is the expensive one. The rule we shipped: pin an explicit gas limit well above the estimate. Unused gas is refunded on Arbitrum, so the headroom is free. A wagmi writeContract that trusts automatic estimation will reproduce this and look like a contract bug.
Getting funds in: Permit2 for smart accounts, approve for everyone else
Lemon's smart-account wallet cannot do a plain approve + deposit two-transaction flow. The periphery exists because of that constraint, the same one Part 2 already worked around.
| Path | How it feels | Why it exists |
|---|---|---|
Mini App SignatureTransfer | One single-use EIP-712 permit per deposit. No prior approval. | Lemon signs the Permit2 struct client-side and submits it through native permits[] support. |
Classic approve + deposit | The boring path. | Any EOA can call vault-core::depositFor directly. The periphery is optional. |
Fitting inside the WASM gate
Arbitrum One enforces a 24KB compressed WASM size limit per contract fragment. In practice the ceiling is closer to 22KB, because ArbOS on Arbitrum One does not support multi-fragment programs. Every contract in this repo has to clear that gate.
The build recipe is the one we already committed to in Part 2: workspace-root release profile (opt-level = "z", LTO, panic = "abort", strip) plus a nightly -Cpanic=immediate-abort / -Zbuild-std pass for the final size reduction. The four adapters are four instances of the same WASM, wired to different vault addresses at init. One blueprint, four deployments, never one instance serving several vaults.
What you can actually observe
Production vault-core and vault-periphery have been live on Arbitrum One since 10 August 2026, with four adapter instances pointed at Morpho gtUSDCc, Fluid fUSDC, Euler eUSDC-2, and Aave stataArbUSDCn.
The mainnet smoke test ran a full cycle against real USDC and real vaults: set weights 40/30/20/10, deposit 1 USDC, confirm shares landed in that ratio, rebalance to 10/20/30/40, redeem 100%. Net cost of the whole cycle: 7 units, seven millionths of a dollar, of ERC-4626 round-down. All adapter share totals back to zero after exit.
Measured gas, which now calibrates every client: deposit 1,604,156 · redeem 1,741,442 · weight-setting rebalance 318,244 · full rebalance 2,607,088. A 2,000,000 limit out-of-gassed the first real rebalance. Scripts and frontend now pin 6,000,000.
The three properties that generalize
1. Make partial success unrepresentable. If the split cannot complete, nothing moves.
2. Give the user the only path that reallocates their money. Registry is admin. Allocation is not.
3. Disabling a vault must never be able to strand a position. Money out is independent of the enabled set.
The specific surface here is a yield Mini App. The pattern is not. Any router that fans a single deposit into several external protocols, vaults, lending markets, structured products, has this shape. Building it in Stylus meant we could express the per-adapter share ledger, the unwind-then-resplit rebalance, and the privilege-free periphery in Rust, with the WASM size limit and live gas behavior forcing real tradeoffs instead of theoretical ones.
What's next
This is the third milestone in an open-source series validating Stylus through production reference implementations. Part 1 covered DEX aggregation. Part 2 covered verifiable randomness. Each milestone answers a different engineering question, and every one of them ships fully open source so other teams building on Stylus can study, fork, and extend the pattern.
We announced this initiative earlier this year as three production Mini Apps on Arbitrum. Vaulty is the DeFi utility in that set: a user-facing allocator on top of live ERC-4626 vaults, running in a production-like environment so Stylus performance is visible where it actually matters.
Full source, architecture notes, and the deploy runbook: github.com/wakeuplabs/ArbitrumMiniApp-3-Vaulty.
If you are building a Stylus router that has to split, rebalance, and exit against live protocol vaults, we would like to compare notes.
About WakeUp Labs
At WakeUp Labs, we build production-grade blockchain infrastructure and digital products for leading ecosystems, startups, and enterprises.
Our engineering philosophy is simple: new infrastructure only proves its value when it runs in production. Rather than evaluating emerging technologies through isolated prototypes, we validate them by building real systems that interact with real users and real operational constraints.
If you are building on Arbitrum or exploring Stylus for production systems, we would love to connect.