Uniswap v4 hooks written entirely in Rust
No Solidity anywhere in the hook. The contract the PoolManager calls
is the Stylus contract, and it lands on its own permission address.
The original adapted a Uniswap limit-order hook to Stylus and won 1st place in the Arbitrum Stylus track.
Unreleased and moving. The
version of the day needed transient storage Arbitrum did not have, so the repo had to vendor a
no-cancun fork of the protocol to run at all.
Testnet only. And it could not deploy a contract to a mined CREATE2 address, which is the one thing a hook needs, so the hook had to be Solidity.
You cannot finish a bridge between two things that are both still moving. The honest call was to stop and wait.
cargo stylus deploy goes through StylusDeployer, which does
CREATE2 on a non-zero saltEvery reason to stop in 2024 had been removed. So we finished it.
Free. A Stylus contract is ABI-equivalent to a Solidity one, so the ten callbacks just work.
The low 14 bits encode the
callbacks. The PoolManager reads them off the address and never calls one it does
not see. This was the blocker.
The ABI was never the obstacle. The address was.
BaseHook.sol's counterpart in
Stylus: v4 types, permission flags, all ten callbacks, and the calls back into the
singleton.
Rust has no abstract types, so a proc macro inserts the caller and pool-key checks that Solidity gets from inheritance. Costs nothing.
Mines the CREATE2 salt whose StylusDeployer address carries exactly the declared flags.
A test asserts all ten callback selectors computed from the Rust
ABI equal those in the compiled IHooks.sol. A Rust hook is only a hook if the calls
land on the right methods.
// 1. storage and entry point — the hook owns its state #[storage] #[entrypoint] pub struct Counter { before_swap_count: StorageMap<FixedBytes<32>, StorageU256> } // 2. declare the permissions the address will encode fn permissions(&self) -> Permissions { Permissions::none().with_before_swap() } // 3. constructor validates the mined address, or reverts HookGuards::validate_hook_address(self) // 4. implement only the declared callbacks #[guarded_hooks] #[public] impl IHooks for Counter { fn before_swap(..) { /* guards already inserted */ } } // 5. mine the salt, then deploy to the address it prints cargo run -p stylus-hook-miner -- --permissions before-swap
./prove-native-hook.bash stands up an Arbitrum Nitro node, mines the
address, deploys, and exercises every declared callback.
ok landed on the mined address ok the code at that address is an activated Stylus program ok its constructor ran and validated the address ok beforeAddLiquidity reached the Rust hook ok beforeSwap reached the Rust hook ok afterSwap reached the Rust hook ok beforeRemoveLiquidity reached the Rust hook ok beforeDonate reverts with HookNotImplemented
About 7,900 gas per call that Solidity never pays, even with the program cached.
Same slots, same trie, same EVM prices. Measured 1.06×. There is no such thing as Stylus storage.
3× to 640× cheaper, depending on how badly the work fits a 256-bit word.
So a port pays off only above roughly 12,000 gas of Solidity arithmetic per call. That is the whole decision.
Storage barely moves. Everything that is real arithmetic does.
Against akshatmittal/v4-twamm-hook by Uniswap Labs and Zaha Studio,
audited by ABDK and Certora, live on Base and Unichain.
| Streams at once | Rust | Solidity | Saving |
|---|---|---|---|
| idle | 147,365 | 132,321 | −15,044 |
| 1 | 340,930 | 351,474 | +10,544 |
| 8 | 695,352 | 880,744 | +185,392 |
| 32 | 1,891,213 | 2,849,561 | +958,348 |
Each extra stream costs 52,163 gas in Rust against 83,256 in Solidity. On a busy protocol the quiet benchmark is the worst case, and it already wins.
| Solidity | Rust | Ratio | |
|---|---|---|---|
| One Keccak-f[1600] permutation | 101,662 | 158 | 643× |
| SHAKE256, 200 B in, 1088 B out | 1,250,263 | 2,559 | 489× |
| One ML-DSA verification | 11,353,437 | 33,372 | 340× |
keccak256 has the 0x01 pad compiled in and SHAKE pads
with 0x1f, so SHAKE cannot come from any built-in, in either language.
A swap costs ~115,000 gas. Solidity would spend ninety-nine swaps to check one signature.
Rust spends a third of one.
stylus/base-hook — the ten callbacks, ready to build onstylus/hook-miner — the address, solvedFEEDBACK.md — what porting v4-core taught usgithub.com/youtpout/stylus-hook