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
0xf4ab80f0b773a41907c365a77a73628f73750abathe floor, enforced · no owner · no admin · holds nothing between transactions
SounderLensours
0xdbd0dd015baab7aa9bfc6ab618f203d4f727d6f3the 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.
| Address | 0xf4ab80f0b773a41907c365a77a73628f73750aba |
| Router it calls | 0x8876789976dEcBfCbBbe364623C63652db8C0904 |
| Allowance system | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
| Size | 3,398 bytes of runtime code |
| Compiler | solc 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);
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.
04
What it refuses, and by name
A refusal that cannot say why is indistinguishable from a bug.
| Error | What happened |
|---|---|
TooLittleReceived | the trade came up short of the floor you set |
DeadlinePassed | the reading it was built from is stale |
NotTheRouterFunction | the calldata is not the router’s own execute |
RouterReverted | the route itself failed, and the router’s own reason is passed back |
NothingReceived | the route ran and delivered nothing at all |
TransferFailed | the token would not hand the proceeds on |
Reentered | something 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.
| Address | 0xdbd0dd015baab7aa9bfc6ab618f203d4f727d6f3 |
| Singleton it reads | 0x8366a39CC670B4001A1121B8F6A443A643e40951 |
| Storage slot | 6 |
| Size | 3,249 bytes of runtime code |
| Compiler | solc 0.8.26, optimizer on, 400 runs |
What it answers
| Function | What 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,50007
What they cannot do
The absences are the design, not an oversight.
| Neither contract has | Which means |
|---|---|
| an owner or admin | there is no function on them we can call and you cannot |
| a pause | we cannot stop your trade once it is in flight |
| an upgrade path | the code you read today is the code that runs tomorrow |
| a fee | there is nowhere for one to go |
| a treasury | the 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
cast call 0xdbd0dd015baab7aa9bfc6ab618f203d4f727d6f3 \ "readV4(bytes32)((bool,uint160,int24,uint128,uint24,uint24,uint24,uint24,uint24))" \ 0x3bb34a44f1b2b5f32c034c38a53065a521a47b199700fa9bd19d60985ff24bf1 \ --rpc-url https://robinhood-rpc.publicnode.com
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.
| Contract | Constructor arguments |
|---|---|
SounderLens | 0x8366a39CC670B4001A1121B8F6A443A643e40951, 6 |
SounderExecutor | 0x8876789976dEcBfCbBbe364623C63652db8C0904, 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 router | Address |
|---|---|
| v3 factory | 0x1f7d7550B1b028f7571E69A784071F0205FD2EfA |
| v4 singleton | 0x8366a39CC670B4001A1121B8F6A443A643e40951 |
| Permit2 | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
| v3 init code hash | 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54 |
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.
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.