
Aquifer
Quote ERC-4626 vault shares at the live rate on 1inch Aqua — no AMM deposit needed.
Demo
About
What it does Aquifer prices a swap between an ERC-4626 vault share and its underlying asset at the vault’s live convertToAssets rate. The maker never deposits into an AMM. Aqua already lets tokens stay in the maker’s wallet as virtual liquidity. What it does not ship is a way to price vault shares. A vault share is not a curve and not a peg. Its fair price is the vault’s conversion rate, and that rate moves when the vault earns yield or takes a donation. Maker points Aquifer at a vault and sets a spread plus min/max bounds. Aquifer reads the vault on every quote and every fill. Taker asks for a price. SwapVM hits the Extruction, applies the spread, and refuses anything outside the band. Fill happens from the maker’s wallet. The shares were never locked in a pool. They kept earning vault yield until the swap. No new pool. No redeem-then-dump. No invented peg. If someone donates into the vault and the rate jumps outside ±1%, the quote dies. That is the point. Demo, not audited production software. Testing Started with a handful of golden vectors for exact-in and exact-out. Ended with 100+ unit, property, and mainnet-fork tests. Every failure path got a name: wrong config length, dirty padding, spread ≥ 10000, zero min rate, band wider than ±1%, unsupported pair, self-referential vault, zero vault rate, dust that rounds to zero, recompute on a populated register, insufficient balances, unauthorized allowlist, stranger mint on the mock token. Fuzz tests: quote equals swap on both layouts, zero spread matches convertToAssets / convertToShares, spread applied once, output falls as spread rises, a round-trip never mints value, a quoted amount never exceeds the maker’s balances. Fork tests hit the real Aqua router. A donation that pushes sDAI’s rate out of band blocks every leg and every mode. If it can go wrong on a vault rate, there is a test for it. Challenges I ran into Two opcodes, one mental model. The first encoding used the opcode in the SDK table. The router never called the contract. Live Aqua is 0x20. Current main is 0x04. The SDK’s 0x21 is a fossil. The library now has buildDeployed and buildCurrent. They differ by one byte. That byte is the whole integration. Quote and swap must be the same function. Aqua requires identical amounts from quote() and swap(), and a non-upgradeable target. A view quote plus a state-changing swap was a footgun. Both ABIs are view. The swap path is side-effect free. Tests assert that. Vault rates are not oracles. convertToAssets is live and attackable. The first version let bounds sit at 0 and uint256.max. That made the Extruction a very expensive wrapper. Now the band cannot exceed ±1%, and a failed share/asset round-trip reverts. Still not an oracle. A refusal to pretend it is one. Explaining Aqua to a human. “Virtual liquidity” sounds like a slogan. The sentence that works: “The tokens stay in your wallet. The strategy can still sell them. The vault keeps paying you yield until someone takes the trade.” Accomplishments that I'm proud of One price path — quote and swap cannot disagree because they cannot branch. The opcode that actually hits the router — proven on a fork, not a mock VM. Bounds that mean something — ±1%, donation out of band reverts, inconsistent 4626 math reverts. The UI does not pretend to be a solver. Shipping stays in the CLI. Less demo-sizzle. More honesty. What I learned An Extruction that is “almost deterministic” is not deterministic. Opcode tables rot. The deployed router is the spec. ERC-4626 convertToAssets is a price, not a promise. Donations, empty vaults, and decimal offsets all lie in different ways. mulDiv rounding is a market-making decision. Both legs lean toward the maker. Aqua’s story is capital efficiency. The moment the demo deposits into a pool, you have built a zap and lost the plot. A confused judge picks the project that pasted a vault and showed a number. What's next Tighten UI errors when a vault is not 4626. Cached quote path so a public RPC timeout does not kill the demo. Backup recording of the sDAI fork quote. Short list of allowlisted real vaults. Optional Sepolia fill only with a real resolver credential — the flag in the CLI does not grant one. Longer term: a small solver, multi-chain once the opcode table stops being a special case, and an audit. Until then this stays a demo.
How it's made
he contract is the product. Everything else is a way to point at it. Vault4626Extruction — Solidity 0.8.30, Foundry, OpenZeppelin. It implements both SwapVM Extruction ABIs: current-main (IExtruction / IStaticExtruction) and the deployed Aqua router v1.0.2 (IExtructionV102 / IStaticExtructionV102). Quote is STATICCALL. Swap is CALL. Both hit the same view function. If those paths can diverge, Aqua’s invariant is dead, so they cannot diverge. OpenZeppelin’s IERC4626, IERC20Metadata, and Math.mulDiv are the pricing primitives — we did not roll our own fixed-point. Strategy args are four words: vault, spreadBps, minRate, maxRate. The decoder rejects dirty ABI padding so a packed address cannot silently become someone else’s vault. Spread is applied once. Exact-in rounds down in the maker’s favour. Exact-out rounds up. Dust that would round to zero reverts. Liquidity is checked on both balanceIn and balanceOut. A recompute guard refuses any quote where the opposite register is already populated. Rate bounds cannot be wider than ±100 bps around the live rate. A vault that fails a convertToAssets → convertToShares round-trip inside 10 bps is treated as inconsistent and rejected. AquiferStrategy packs the instruction as opcode || 0x94 || target || abi.encode(...). Opcode 0x20 is what the live Aqua router actually calls. Opcode 0x04 is current SwapVM main. They are not interchangeable. The SDK’s stale table still says 0x21. We never use that value. Fork tests against the real router prove 0x20 reaches the target and 0x04 / 0x21 do not. CLI — TypeScript, @1inch/aqua-sdk, @1inch/swap-vm-sdk, viem. prepare / ship / quote / fill. Aqua-sdk encodes ship and strategy hashes. Swap-vm-sdk builds orders, traits, quote calldata, and fill calldata. Those two packages pin adjacent versions of @1inch/sdk-core. Casts across that boundary compile and are wrong, so the CLI rebuilds Aqua’s Address and HexString from canonical hex. That is the hacky part worth mentioning: the official SDKs do not share a core, and reconstructing from hex is the only honest crossing. Private keys stay in the shell. Frontend — Next.js + viem, not a solver. Paste a vault. It reads asset(), decimals, and convertToAssets over RPC. It previews rate, spread, and bounds. It copies a CLI ship command. Wallet connect only identifies the account and network. The browser never submits strategy calldata. Local Anvil traffic goes through a same-origin /api/rpc/local relay so the client cannot pick its own upstream URL. Live quotes go through /api/aqua-quote to the SwapVM router. Partner tech. 1inch Aqua is the liquidity layer: the maker’s wallet balance is the inventory — no pool deposit. SwapVM Extruction is the reason this exists; built-in opcodes price curves and pegs, not vault conversion rates. Path C (external pricing) is the whole architecture. OpenZeppelin gives us the 4626 and decimal surface we refuse to reimplement. Foundry gives us unit, fuzz, and mainnet-fork tests against real sDAI. Three deploy scripts: local mocks, a mainnet-fork Extruction against live vault state, optional Sepolia mocks. Aqua and SwapVM are already on Sepolia. We did not redeploy them.