Skip to content
Sounder

Robinhood Chain · 4663 · deployed 2026-09-04

Two contracts of our own.

Everything else Sounder publishes is a website, and a website is a thing you have to trust. These answer from inside the chain, to anyone, without going through us. One enforces the floor on a trade instead of promising it. The other exists because of a mistake we made, and makes that mistake impossible to repeat without being caught.

SounderExecutorours

0xf4ab80f0b773a41907c365a77a73628f73750aba

the floor, enforced · no owner · no admin · holds nothing between transactions

SounderLensours

0xdbd0dd015baab7aa9bfc6ab618f203d4f727d6f3

the reading, checkable · view only · it cannot hold a token

01

Why there are contracts at all

A number on a page is a claim. A number a contract will act on is a commitment.

Sounder spent its first weeks measuring: reading four pool geometries out of their own storage, walking depth tick by tick, and publishing what each venue could actually absorb. All of that ran on a server we control, behind a domain we own. Every figure on this site could have been made up, and you would have had no way to tell short of rebuilding the whole thing.

These two contracts close that gap from opposite ends. The lens lets anyone take the same reading without asking us. The executor makes the reading binding: it will not let a trade settle for less than was promised, and it does not take our word for what arrived — it counts.

02

SounderExecutor

The floor, enforced rather than promised.

A router will accept your transaction and hand back less than it quoted. Its own minimum is real, but it is checked inside the same contract that decides what you got — which is fine right up until it is not. This one sits outside that loop.

Address0xf4ab80f0b773a41907c365a77a73628f73750aba
Router it calls0x8876789976dEcBfCbBbe364623C63652db8C0904
Allowance system0x000000000022D473030F116dDEE9F6B43aC78BA3
Size3,398 bytes of runtime code
Compilersolc 0.8.26, optimizer on, 400 runs

Both constructor arguments are fixed at deployment and there is no function that can change them. It can only ever call one address, with one function selector. Calldata that is not the router’s own execute is rejected before anything moves.

03

How the floor is enforced

By counting, which is the only method that does not require anyone to be honest.

The contract records its own balance of the token you are buying, runs the route, and records it again. The difference is what actually arrived. If that number is below the floor you set, the whole transaction reverts and nothing has moved.

uint256 before = IERC20(tokenOut).balanceOf(address(this));

(bool ok, bytes memory reason) = router.call(routerCalldata);
if (!ok) revert RouterReverted(reason);

received = IERC20(tokenOut).balanceOf(address(this)) - before;
if (received < floor) revert TooLittleReceived(received, floor);
the shape of it, from SounderExecutor.fill

That check does not depend on the router being honest, on the pool being what we think it is, or on Sounder having read the depth correctly. It depends only on arithmetic the contract performs on its own balance.

Run against live chain state on a real NVDA route, it filled within a hundredth of a basis point of the reading Sounder published for the same block, and refused a floor set above what the chain would pay.

04

What it refuses, and by name

A refusal that cannot say why is indistinguishable from a bug.

ErrorWhat happened
TooLittleReceivedthe trade came up short of the floor you set
DeadlinePassedthe reading it was built from is stale
NotTheRouterFunctionthe calldata is not the router’s own execute
RouterRevertedthe route itself failed, and the router’s own reason is passed back
NothingReceivedthe route ran and delivered nothing at all
TransferFailedthe token would not hand the proceeds on
Reenteredsomething tried to call back in mid-trade

05

SounderLens

The reading, taken from inside the chain.

A v4 pool has no contract of its own — its state lives in a singleton’s raw storage, which is why most tools quote v4 badly or not at all. The lens reads that storage directly and hands back price, tick, liquidity and, crucially, the fee a swap is actually charged.

Address0xdbd0dd015baab7aa9bfc6ab618f203d4f727d6f3
Singleton it reads0x8366a39CC670B4001A1121B8F6A443A643e40951
Storage slot6
Size3,249 bytes of runtime code
Compilersolc 0.8.26, optimizer on, 400 runs

What it answers

FunctionWhat it gives you
swapFee(protocolFeePips, lpFeePips)the fee a swap actually pays, composed the way v4 composes it
readV4(poolId)price, tick, liquidity, both fees and the composition
readV4ByKey(key)the same reading, which also proves the key is that pool
readV3(pool)price, tick, liquidity, fee, spacing and both tokens
readManyV4(poolIds)one reading per pool, because a router asks about all of them

06

The fee we got wrong

This contract is the correction, published where it can be checked.

A Uniswap v4 pool keeps two fees in the same storage word. There is the LP fee it advertises, and beside it a protocol fee taken off the input before the LP’s share is charged. The two do not add, and charging only the first understates what a swap costs.

Sounder charged only the LP fee and read about five basis points rich on every v4 pool it quoted, for weeks, and nobody noticed — including us. It surfaced when the chain was finally asked to run one of Sounder’s own transactions and disagreed by 6.83 basis points. Composed properly, the disagreement is under one.

protocol first, off the input; the LP's share on what is left

swapFee = protocolFee + (lpFee * (1_000_000 - protocolFee)) / 1_000_000

for the NVDA pool: 500 + (3000 * 999_500) / 1_000_000 = 3,498 pips
                   not 3,000, and not 3,500
the composition, as the contract performs it

07

What they cannot do

The absences are the design, not an oversight.

Neither contract hasWhich means
an owner or adminthere is no function on them we can call and you cannot
a pausewe cannot stop your trade once it is in flight
an upgrade paththe code you read today is the code that runs tomorrow
a feethere is nowhere for one to go
a treasurythe executor sweeps both tokens back to the caller before returning

The lens goes further: every function on it is view, so it cannot hold a token or move one under any circumstances. The executor holds your tokens only for the length of a single call, and a balance left in it afterwards is a bug rather than a policy.

08

Call them yourself

Neither of these needs our permission, our frontend, or our uptime.

cast call 0xdbd0dd015baab7aa9bfc6ab618f203d4f727d6f3 \
  "swapFee(uint24,uint24)(uint24)" 500 3000 \
  --rpc-url https://robinhood-rpc.publicnode.com

# 3498
the fee composition, straight from the chain
cast call 0xdbd0dd015baab7aa9bfc6ab618f203d4f727d6f3 \
  "readV4(bytes32)((bool,uint160,int24,uint128,uint24,uint24,uint24,uint24,uint24))" \
  0x3bb34a44f1b2b5f32c034c38a53065a521a47b199700fa9bd19d60985ff24bf1 \
  --rpc-url https://robinhood-rpc.publicnode.com
a whole v4 pool, out of the singleton's storage

The executor takes the route Sounder writes and the floor you are willing to accept. Spending your tokens needs an approval to Permit2 and a Permit2 allowance to the executor — neither is requested by the contract itself, because an allowance is yours to give and a contract that asks for one on your behalf is a contract that can spend it later.

09

Reproduce the bytecode

Constructor arguments are published so the deployed code can be rebuilt exactly.

ContractConstructor arguments
SounderLens0x8366a39CC670B4001A1121B8F6A443A643e40951, 6
SounderExecutor0x8876789976dEcBfCbBbe364623C63652db8C0904, 0x000000000022D473030F116dDEE9F6B43aC78BA3

Both were compiled with solc 0.8.26, optimizer on, 400 runs. The router address was not taken from a deployment list — the canonical Universal Router address exists on this chain but its pool manager points at an address with no code, so it is a copy that cannot execute. This one was found by reading which contract the chain’s own swap traffic passes through, and confirmed against its bytecode: the v3 factory, the v4 singleton, Permit2 and the v3 init code hash are all baked into it as immutables.

The published source

Both contracts are verified on the chain’s explorer, so the code that runs can be read beside the bytecode rather than taken from us. The match is partial: the runtime bytecode is identical, and what differs is the metadata hash, which carries build paths rather than logic.

Baked into the routerAddress
v3 factory0x1f7d7550B1b028f7571E69A784071F0205FD2EfA
v4 singleton0x8366a39CC670B4001A1121B8F6A443A643e40951
Permit20x000000000022D473030F116dDEE9F6B43aC78BA3
v3 init code hash0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54

10

What is not safe yet

Said plainly, because the alternative is finding out later.

These contracts are new and they have not been audited. Code that has not been attacked is not code that is known to be safe, and no amount of testing against live chain state changes that. The executor is the one that touches money, and it is the one to be careful with.

Nothing on this site is an invitation to route size through an unaudited contract. The lens cannot hurt you — it cannot hold a token. The executor can, and until it has been looked at by people who break things for a living, that is worth treating as a real risk rather than a formality.

What comes next is an audit, and only then an invitation for anyone else to use it. That order is deliberate, and it is the last item on the roadmap.

Read the source.

Both contracts are short on purpose — three thousand bytes each, no libraries, no inheritance, nothing that has to be traced through four files to understand. If something in here is wrong, it should be possible to see that in an afternoon.