Uniswap v4 · Arbitrum Stylus

Hooks that compute

Uniswap v4 hooks written entirely in Rust, compiled to WASM and executed by Arbitrum Stylus. Stylus makes arithmetic 3× to 640× cheaper and changes nothing else — so it is worth reaching for exactly when a hook has to think on every swap.

v4-core v4.0.0 Stylus SDK 0.10.9 Solidity 0.8.30 Rust 1.91

The premise

Hooks opened an enormous design space for AMMs, and then the EVM closed most of it again. Every callback runs inside every swap in the pool, so a hook that computes anything serious prices itself out. Uniswap's own TWAMM example costs about 100,000 gas per interval and was never deployed by anyone. A pm-AMM must solve a transcendental invariant on every trade. A hook that verifies a post-quantum signature is not expensive in Solidity, it is impossible.

Stylus executes WASM instead of EVM bytecode. Arithmetic gets much cheaper, which moves that boundary — but only that boundary, and only in one direction.

Stylus is not faster than Solidity. It is differently priced. Entering a Stylus contract costs about 7,900 gas that a Solidity contract does not pay, even cached. Storage costs exactly the same in both. Arithmetic costs 3× to 640× less.

So a port pays off only when a hook does roughly 12,000 gas of Solidity arithmetic per call — about 45 mulDivs, or 11 rpows, or eleven times what a twelve-iteration Newton curve solve needs. Below that line a Rust hook loses, and loses reliably.

Where the three costs come from

Entering: worse

A Stylus contract is compressed WASM. The node fetches, decompresses and instantiates it before running a byte, and Arbitrum charges that as init gas on every call. A one-off bid on CacheManager cuts it by roughly 6×; it never reaches zero.

Storage: identical

There is no such thing as Stylus storage. A Stylus contract writes the same 32-byte slots in the same account trie, and ArbOS charges EVM prices. Measured, Rust is 1.8 % cheaper, and that margin is the hashing around the access, not the access.

Computing: far better

3× on 512-bit mulDiv, 4.5× on a full-range sqrt, 7.1× on v4's tick math, 640× on a Keccak permutation the EVM has no opcode for. This is the entire case.

How much cheaper depends on the word size

The pattern behind every measurement: Rust gains most where the work fits a 256-bit word worst.

Operation What it needs Solidity Rust Ratio
xorshift64 64-bit words 115 2.8 41.6×
integer sqrt 512-bit intermediate plus a bit scan 2,009 362 5.6×
plain a * b / c one MUL, one DIV, checked 278 51 5.5×
mulDiv 512-bit intermediate 694 204 3.4×
rpow in Q96 products that fit in 256 bits 3,058 1,019 3.0×
one storage write a host operation either way 2,397 2,265 1.06×

A U256 in WASM is four 64-bit limbs and only the non-zero ones are paid for, while MUL and DIV cost the EVM 5 gas whatever the operands are. So 64-bit words gain most, Q96 fixed point gains least — Q96 is exactly the shape the EVM was built for — and everything a hook actually works with sits in between.

Which hooks are worth it

The repository benchmarks this rather than asserting it, including on hooks that are deployed and used. The honest summary of what came back:

Worth porting Not worth porting
Small-word cryptography — hashing, signature and proof verification. The only category where Solidity does not merely lose but cannot play. Anything whose cost is bookkeeping. Read a counter, add to it, write it back — the exact shape where Stylus has nothing to offer.
Replaying v4's own swap math, which every hook that simulates a swap before allowing it already does. 7.1× per tick crossed. Anything whose cost is an external call. EulerSwap spends 2 % of its gas on its curve and the rest querying lending vaults.
Curve solvers, Newton iterations, TWAP and volatility oracles, dynamic fees computed from a model rather than looked up. Q96 fixed-point work that already takes FullMath's single-DIV fast path, where the gain is only 1.65×.
Batch scanning or sorting inside a callback, where the loop dominates. Anything expensive only because of the algorithm it chose. Fix the algorithm first, in Solidity.

That last row is the trap worth naming. The most expensive v4 hook found anywhere spends 4.3 million gas per swap, and about 90 % of it goes to computing a Gaussian CDF by bisecting on its own inverse — a 405× penalty against the same function written properly. Porting it to Rust would look like a spectacular result and would be the algorithm wearing a language's clothes.

The finding is not that Stylus computes slowly. It is that most hooks do not compute enough for it to matter — and the ones that would are the ones nobody can currently afford to write. The benchmarks have every figure behind that sentence.

How a hook is built here

Uniswap v4 imposes two things on a hook, and only two. It must answer the IHooks Solidity ABI, and it must live at an address whose low 14 bits encode the callbacks it wants; the PoolManager reads those bits off the address and never calls one the address does not advertise.

A Stylus contract is ABI-equivalent to a Solidity one, so the first is free. The second needs CREATE2 with a mined salt, and cargo stylus deploy routes through the on-chain StylusDeployer, which uses CREATE2 whenever it is handed a non-zero salt. So the hook the pool manager calls is the Rust contract itself. There is no Solidity in it, and nothing forwarding to it.

stylus/base-hook

BaseHook.sol's counterpart in Stylus: the v4 value types, the permission flags, the ten IHooks callbacks, and the calls a hook makes back into the pool manager.

stylus/base-hook-macros

#[guarded_hooks]. Solidity's guard cannot be forgotten because BaseHook is abstract; Rust has no abstract types, so a proc macro inserts the caller and pool-key checks instead, at no gas cost.

stylus/hook-miner

Mines the CREATE2 salt whose StylusDeployer address carries exactly the flags the hook declares. A host CLI, not a contract.

Every hook has a pure-Solidity twin in uniswap/src, kept so the two can be compared behaviour-for-behaviour and gas-for-gas. Where both implement the same maths they are pinned to the same values: the Gaussian ports agree to the wei, and the v4 math port is tested against v4-core's own vectors.

Hook in Rust What it does Measured against
native-counter Counts four callbacks per pool. The smallest complete hook, and the worst case for Stylus. Counter.sol
native-twamm A complete TWAMM: orders, order pools, an expiry grid, earnings factors, settlement against the v4 singleton. Saves 34 % of a swap once 32 order streams run at once, which on a busy protocol is the ordinary case. the TWAMM live on Base and Unichain
native-gaussian solstat's Gaussian CDF and solve, ported bit-exactly, for a pm-AMM. PmAmmMath.sol
native-v4-math Pool.swap's loop: next initialised tick, price the step, cross, repeat. v4-core's own libraries
native-crypto Keccak-f[1600], SHAKE256 and ML-DSA's number-theoretic transform. CryptoBench.sol, in unrolled assembly
native-compute Arithmetic sweeps, for locating the crossover. ComputeHook.sol

Two traps that cost an order of magnitude each

Both compile. Both pass cargo stylus check. Neither announces itself, and together they account for most of the distance between a Stylus hook that wins and one that loses.

An optimisation level you cannot reach by default. At opt-level 2 or 3, rustc emits memory.copy, which brings a DataCount section that ArbOS refuses to activate — so a Stylus contract appears to be capped at "s", compiled for size on a platform that charges for execution. Passing --llvm-memory-copy-fill-lowering to wasm-opt lowers those ops back to loops and drops the section. Every benchmark here was measuring size-optimised code until that was found, and every ratio roughly doubled when it was fixed.

Constants built from strings. The first port of v4's TickMath lost by 3.6×, because its nineteen fixed-point factors were &str parsed with from_str_radix inside the loop. Hoisting them to real const values moved the ratio from 0.28× to 4.57× with the algorithm untouched. In Solidity a literal is a literal and this bug cannot be written.

Against the official guidance

Arbitrum publishes gas optimisation best practices for Stylus. It says up front that its multipliers are directional and that you should benchmark your own contract, which is what this repository is. Four claims are checkable, and they do not all hold.

The docs say Measured here
Compute-heavy loops: ~50–100× 10.6× at best, on 64-bit xorshift. 256-bit work runs 1.65× to 4.5×.
Storage operations: no gain 1.8 % cheaper. Agrees, for every practical purpose.
Set opt-level = "z" for smaller binaries Makes them bigger, 18,555 → 18,928 bytes, because the SDK's own pinned wasm-opt -Oz already runs afterwards.
ecrecover: 3,000 gas → ~300, ~10× No mechanism for this is visible. There is no signature hostio, so a contract calls the 0x01 precompile at its EVM price or implements secp256k1 itself.

The 50–100× figure is the one that matters, because it is the number a team would use to decide whether to port. Nothing measured here comes within a factor of five of it.

Read next