Search projects, hackathons, sponsors and tech

    ← All projects

    Stylus Hook

    Uniswap v4 hooks written entirely in Rust on Arbitrum Stylus

    Demo

    About

    This continues a project from ETHGlobal Scaling Ethereum 2024, built when Stylus was still testnet-only and Uniswap v4 was still in development. Back then the contract v4 actually called was a Solidity proxy, forwarding every callback to the Rust code behind it. It was never Stylus end to end, because a hook must sit at an address whose low 14 bits encode its callbacks, and Stylus could not deploy to a mined one. Both have shipped since. Stylus is on mainnet and CREATE2s through StylusDeployer, so we finished it: the contract the PoolManager calls is now the Stylus contract itself, with no Solidity in it. The other half is finding out whether that is worth doing. Entering a Stylus contract costs ~7,900 gas more, storage is identical, and arithmetic is 3-640x cheaper. So most hooks should stay in Solidity, and we publish that: a counter hook costs 2.8x more in Rust. Where it pays, it pays widely. Against the TWAMM live on Base and Unichain, at 32 concurrent order streams, Solidity spends 2,849,561 gas per swap and Rust spends 1,891,213. Replaying v4's swap math is 7.1x cheaper per tick. And a post-quantum signature check is 11.3M gas in Solidity against 33k in Rust, because SHAKE is not reachable through the EVM's keccak opcode. Porting v4-core also found a failing input in Uniswap's own TickMath fuzz test. The fix is one character.

    How it's made

    Rust on stylus-sdk 0.10.9, compiled to wasm32 and run on Arbitrum Stylus. stylus/base-hook is BaseHook.sol's counterpart: the v4 types, the permission flags, all ten IHooks callbacks, and the calls back into the singleton. A test pins all ten selectors against the compiled IHooks.sol. Solidity's onlyPoolManager can't be forgotten because BaseHook is abstract; Rust has no abstract types, so a proc macro inserts the guards instead. And hook-miner reimplements StylusDeployer's salt derivation, keccak(salt || initData), to find an address carrying the declared flags. Two things worth stealing. The SDK has no immutable, so build.rs bakes the pool manager in as a Rust const rather than paying a cold SLOAD per callback: 2,568 gas a swap. And rustc at opt-level 2 or 3 emits a DataCount section ArbOS refuses to activate, so Stylus looks capped at size-optimised; --llvm-memory-copy-fill-lowering fixes it. Both builds pass cargo stylus check and only fail at deploy. Every ratio here doubled when we found it. Forge can't run WASM, so the Solidity twins are tested through a real local v4 stack and the Rust side against TestVM, cross-pinned to the same values. Every gas figure is a receipt from an Arbitrum Nitro dev node, not an estimate. Hackiest bit: the TWAMM is 41 KB, so it needs ArbOS 61 code fragments, but cargo stylus get-initcode refuses fragmented contracts. We deploy once with --wasm-file, read the root back off chain, re-wrap it in the EVM prelude, mine against that, and hand it to StylusDeployer ourselves.