Production systems for finance, cryptography, and critical operations.
All posts

Production Patterns for Arbitrum Stylus (Part 3): Vault Aggregation Onchain

One USDC deposit, four live ERC-4626 vaults, user-owned weights. Part 3 of our Stylus series is about the split that has to survive every exit: atomic deposits, disabled adapters that cannot strand positions, and a periphery with no privilege.

Arbitrum Stylus production milestone · Part 3

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?

4Live ERC-4626 protocols: Aave, Morpho, Fluid, Euler
1Atomic deposit that either lands everywhere or nowhere
0Admin knobs on anyone's allocation

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
View 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
Open on YouTube

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 One

vault-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.

PathHow it feelsWhy it exists
Mini App SignatureTransferOne 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 + depositThe 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.

Next step

Turn this into a scoped direction

Talk with us about the system you need. We will scope delivery, operating ownership, and the next step after go-live.