Skip to content

Repository files navigation

Sable

The confidential cross for autonomous agents.

A sealed-bid, uniform-price batch auction where the matching engine runs on encrypted orders — built on COTI's garbled-circuit EVM.

Built for the COTI Vibe Code Challenge — Web 4 Agent Edition.

Live terminal: https://sable-cross.vercel.app — read-only, no wallet, opens on a sealed book.

CI

Full design and verification record: SABLE-EXPLAINED.md — mechanism, confidential execution model, measured cost model, failure containment, verification methodology.


The problem

An agent that wants to execute size on a transparent DEX is dead on arrival: its order is visible before it settles, so it gets front-run. Traditional finance solved this with dark pools — but a dark pool needs a trusted operator, and the record shows they cheat. Barclays LX and Credit Suisse both paid SEC penalties for misrepresenting who could see the flow.

Sable is a crossing network where the operator cannot cheat, because it cannot see.

How it works

Autonomous agents negotiate over end-to-end encrypted on-chain messages, then commit sealed orders — side, limit price and size, all encrypted. At the close of each batch a contract computes the uniform clearing price using garbled circuits, without ever decrypting a single order.

The market publishes a price. No participant reveals their hand.

The desks

Each desk is an autonomous agent carrying a private mandate — target size, reservation price, how it wants to ladder — that never leaves its own machine. Strategy is deterministic, with no model calls, so a run is reproducible and every number in it is checkable.

Before committing, desks exchange encrypted indications of interest over COTI's PrivateMessaging: "there is interest this size on this side", and deliberately no price. Only the recipient can decrypt one.

What the RFQ changes is size, not price. Sable is a uniform-price auction, so shading a limit mostly risks the fill without improving execution — bidding your true reservation value is the rational move, a property of the mechanism rather than a heuristic. What the RFQ actually buys is knowing whether to commit capital at all:

committed = clamp(visible opposing interest, probe floor, own target)

Never escrow against counterparty interest that isn't there. In the verified run, Atlas wants 70 but can only see 65 of supply, so it commits 65 and locks 6,639 quote units instead of 7,150 — 511 units of capital freed, with no loss of fill. And the IOI has to be encrypted, or announcing "I need to buy 70" is just telling the market to raise its price.

The messaging layer pays for itself: PrivateMessaging rewards each desk in proportion to the encrypted cells it stored, so the protocol funds the private negotiation it depends on.

Why a batch auction, not an order book

Continuous matching makes latency an edge again and re-imports MEV. A periodic call auction at a uniform price removes the speed race by construction — the design behind Nasdaq's closing cross, CoW Protocol, and the Budish–Cramton–Shim frequent-batch-auction literature. It is also dramatically cheaper on gcEVM: clearing happens once per batch rather than on every order.

The clearing kernel

Over a public price grid P = [p₁ … p_K], with every order encrypted:

for each tick k:
  demand(k) = Σᵢ mux( isBuyᵢ ∧ limitᵢ ≥ p_k , sizeᵢ , 0 )
  supply(k) = Σᵢ mux( ¬isBuyᵢ ∧ limitᵢ ≤ p_k , sizeᵢ , 0 )
  crossed(k) = min( demand(k), supply(k) )

k* = argmax_k crossed(k)

Demand is non-increasing in k and supply is non-decreasing, so the curves cross exactly once — k* is the volume-maximising price, not a heuristic. No Solidity branch ever touches an encrypted value: every conditional is a mux.

Allocation, and the invariant that keeps it solvent

The long side is then rationed pro-rata to order size, still encrypted. The obvious formula — size * matched / sideTotal — is subtly wrong, and wrong in a way that would drain the contract. The short side satisfies sideTotal == matched, so its ratio is exactly 1 and it never truncates; the long side rounds down. The two sides then move different quantities of base, while the contract holds exactly what was escrowed.

So Sable rounds the cumulative share and takes differences:

cum_i  = Σ sizes of same-side participants up to and including i
q_i    = ⌊ cum_i · V / T ⌋
fill_i = q_i − q_{i−1}

The quotients telescope, so the fills sum to ⌊T·V/T⌋ = V exactly on both sides, while each fill stays within one unit of its ideal share. Conservation of value is enforced by the formula, not by a reconciliation pass.

Neither the allocation nor the clearing ever branches on which side is long — essential, since that fact is itself encrypted. Each fill is offboarded to its own trader's key: only they can read it.

What leaks, and what never does

Public Encrypted forever
That an address submitted an order, and when Side (buy/sell)
The batch clearing price Limit price
Total matched volume Size
Each participant's individual fill
Everything about unmatched orders

Price discovery is a public good. Sable produces it without anyone showing their hand.

Status

The full loop runs on COTI testnet, verified end to end. Three autonomous desks negotiate privately, commit sealed orders, clear, and settle in real PrivateERC20 — with every result checked against an independent plaintext implementation of the same mechanism (scripts/agents/reference.ts) rather than against hardcoded values.

One npm run agents run, verified:

[rfq]     6 encrypted IOIs exchanged, 3.12M gas
[submit]  Atlas    inbox: BUY 20, SELL 65  -> saw only 65 vs target 70, commits 65
          Borealis inbox: BUY 70, SELL 65  -> commits 20 in full
          Cygnus   inbox: BUY 70, BUY 20   -> commits 65 in full
[clear]   price 101, volume 65, 13.13M gas
          fills 37 / 28 / 0 / 20 / 35 / 10   all matching the reference engine
[claim]   Atlas  +65 base / −6,565 quote
          Borealis  0 / 0        (unfilled, fully refunded)
          Cygnus −65 base / +6,565 quote
[capital] Atlas locked 6,639 instead of 7,150 — 511 units freed by the RFQ
[privacy] Cygnus cannot recover Atlas's fill of 37
[rewards] each desk claimed 0.0167 COTI for the encrypted cells it stored

The invariants that matter all hold on chain: fills sum to the matched volume on both sides, escrow equals payout in both tokens, no order is overfilled, and a desk attempting to decrypt another's fill gets noise.

Gas, measured: submitOrder 2.53M, clear 13.1M for 6 orders over 12 ticks, claim 1.4–4.0M depending on order count. Notably the encrypted token transfers dominate, not the clearing kernel — a 256-bit PrivateERC20 transfer costs roughly 1.2M against ~13k for a 64-bit garbled compute op. Settlement is pull-based precisely so that cost sits with each desk rather than in the clearing transaction.

Gas does not leak the book either, and this is now measured rather than argued. Two books of six orders over the same twelve ticks, sharing no order — one clearing at 101 on a volume of 65, the other not crossing at all — both cost 9,193,052 gas, identical to the unit (scripts/gas-uniformity.ts). The kernel cannot branch on an encrypted value, so it runs the same circuit whatever the values are.

That measurement needs its own care: run each book once and they differ by 17,100 gas, which is not a leak but 22,100 minus 5,000 — the EVM's gap between initialising a zero storage slot and overwriting a non-zero one. Whichever book runs first pays it. The same 17,100 separates the first and second order a trader submits, for the same reason and with nothing to do with buy versus sell. The script therefore runs each book twice and compares the repeats.

Clearing has also been measured at the contract's own bound, since that is the case whose failure is unrecoverable: 32 orders over 12 ticks costs 66,651,243 gas — 55.5% of the block limit, with price, volume and both-sided conservation all matching the reference engine. MAX_ORDERS = 32 ships with 1.8× headroom; ~46 orders fit an 80% budget. That measurement came in 24.6% above the kernel model, for a reason traced in SABLE-EXPLAINED.md §9.

Testing

Three layers, and they cover different things.

npm test runs the clearing oracle's unit tests offline in under a tenth of a second. That oracle is what run-agents, check-offline and stress-max-orders judge the contract against, so it has to be right before any of their verdicts mean anything.

npm run test:contracts runs SableCross itself on a local Hardhat node. MpcCore reaches the garbled backend as an ordinary call to address(0x64), so a plaintext stand-in placed there with hardhat_setCode lets the unmodified contract run offline — clearing, allocation, escrow, settlement and every guard. The stand-in copies two behaviours rather than inventing them: Mux(bit, a, b) returns bit ? b : a, and arithmetic wraps instead of reverting, which _allocate depends on because it evaluates both arms of every mux.

Confidentiality splits into two questions, and each layer answers one.

STRUCTURAL — what the contract chooses to reveal, and which key it binds each value to — is a property of SableCross, and the mock records every Decrypt and every OffBoardToUser so it can be asserted offline: submitting reveals one admissibility bit and nothing else, clearing reveals the price and the volume and nothing else, settling reveals nothing, and every order's mirrors and fill are bound to that order's own trader. Offboarding one desk's fill to another key would leave the arithmetic, the balances and every conservation check correct, so this is the only layer that would notice.

CRYPTOGRAPHIC — whether an outsider can actually recover a value — belongs to the real precompile and is gone by construction once values are plaintext. npm run e2e attempts it for real on the testnet: a second desk tries to decrypt the first's side, limit, size and fill, and a third tries an order that never crossed. It also checks the owner still CAN read their own, which is the control that stops "nobody can read it" from passing on data that is merely broken.

Both offline layers, plus the compile, the typecheck, the frontend lint and build, run on every push — see .github/workflows/ci.yml.

The day-1 de-risking spike that sized all of this is in SPIKE.md: the cost model, per-operation gas, and the traps that would otherwise have shipped silently.

Repo

contracts/SableCross.sol         the market: batches, escrow, clearing, allocation, claim
contracts/GasSpike.sol           the clearing kernel, instrumented for measurement
contracts/test/TestToken.sol     PrivateERC20 with an open mint, for testnet runs
contracts/test/DeskMessaging.sol deployable PrivateMessaging — the desks' RFQ channel
contracts/test/Mock*.sol         plaintext stand-ins for the precompile and the token

scripts/agents/desks.ts          the three private mandates and the market grid
scripts/agents/strategy.ts       deterministic decision layer, pure and testable
scripts/agents/reference.ts      plaintext clearing engine — the oracle for the contract
scripts/agents/desk.ts           a desk's on-chain behaviour: RFQ, submit, claim
scripts/agents/check-offline.ts  strategy + reference checks, no network, no gas
scripts/utils/                   testnet wallet plumbing: create, fund, onboard an AES key

scripts/run-agents.ts            the full agent run
scripts/cross-e2e.ts             three-trader contract test with assertions
scripts/test-rescue.ts           the escape hatch, exercised end to end
scripts/spike-gas.ts             gas curve + correctness harness
scripts/gas-uniformity.ts        two books, same shape, same gas — measured
scripts/stress-max-orders.ts     does clearing fit in a block at the contract's own limit?
scripts/preflight.ts             GO / NO-GO before a demo: RPC, batch, keys
scripts/frontend-config.ts       writes frontend/.env.local from the current run
scripts/new-wallet.ts            a fresh testnet wallet, written to .env

test/clearing.test.ts            the clearing oracle, `npm test`, offline
test/contracts/                  SableCross on a mocked precompile, `npm run test:contracts`
test/contracts/helpers.ts        the shared harness those tests are built on

SPIKE.md                         measured results and design decisions
spike-report.json                the raw gas samples behind them

frontend/                        read-only terminal — the sealed book, live

Getting started

npm install
npm run check                  # strategy + reference engine, offline, no gas
npx hardhat compile
npm run wallet                 # then fund the printed address free at faucet.coti.io

The full agent run (it funds the other two desks from the first wallet):

STAGE=setup   npm run agents   # tokens, mint, RFQ channel, the cross, approvals
STAGE=rfq     npm run agents   # encrypted indications of interest
STAGE=submit  npm run agents   # desks read their inboxes, decide, seal orders
STAGE=clear   npm run agents   # waits out the commit window, clears, verifies
STAGE=claim   npm run agents   # settles, checks balances, privacy and capital saved
STAGE=rewards npm run agents   # claims messaging rewards for the finished epoch

npm run e2e runs the same staged flow against the contract directly, without the agent layer. Both are staged and resumable because the commit window and the reward epoch are wall-clock deadlines.

Then the terminal:

npm run frontend:config        # writes frontend/.env.local with addresses + desk keys
cd frontend && npm install && npm run dev

See frontend/README.md. It is read-only, needs no wallet, and opens on a fully sealed book — unlocking a desk key is what turns █ into numbers.

Before demoing or recording it:

npm run preflight              # read-only, zero gas — GO / NO-GO in ~10s

It samples RPC availability rather than assuming it, confirms the terminal will open on a populated batch, and checks that the desk keys on disk unlock rows in that batch — keys from a previous market fail silently, and you find out when a click reveals nothing.

Testnet only. Keys live in .env, which is gitignored — never reuse them on mainnet.

License

MIT

About

Sable — the confidential cross for autonomous agents. Sealed-bid uniform-price batch auction on COTI's garbled-circuit EVM.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages