(04)Technology

The shielded pool

The shielded pool breaks the on-chain link between the vault that put USDG in and the vaults that take it out. Every unit is a standard 10 USDG leaf with its own one-time key, and since version 2 of the pool nothing on chain ties a deposit to its leaves.

Chain Robinhood Chain · 4663Code 64c3213

4.1What it hides#

On a public chain an agent's vault is a public record: every payment, every buy, every balance. An agent that buys a token, or simply holds a treasury, shows its strategy to everyone watching. The pool lets an agent move USDG from its vault into new vaults that cannot be tied back to it from the chain: a private vault can then buy, hold or pay, and on chain the buyer is not linked to whoever deposited the money.

The pool is 0x6ea8…0CD6, a beacon proxy running implementation version 2 0xC04d…48ec OstraPool.sol:61-62.

4.2Leaves, fee and limits#

RuleValueSource
LeafExactly 10 USDG, and nothing else: one denomination, so a leaf’s amount says nothing about its depositOstraPool.sol:195
DepositAny amount whose value after the fee is a whole number of leavesOstraPool.sol:279-302
Fee0.30% of each deposit, charged when its batch seals; never more than 5% by contract, and fixed for good at deploymentOstraPool.sol:174-199
LimitsCurrently 250 USDG per deposit and 2,500 USDG in the pool, as deployed; the pool’s init authority can only raise themOstraPool.sol:201-209
BatchAt most 192 leavesOstraPool.sol:63-66
RefundA deposit no batch took is refunded in full, without fee, to the vault after 48 hours, by anyoneOstraPool.sol:304-314

One leaf needs a deposit of 10.030090 USDG; the largest deposit at the current limit carries 24 leaves (240.722166 USDG). The pool is deliberately small: the pool has no external audit yet (see security).

4.3From deposit to private vault#

Vaultdepositor keyWOTS transferdepositShielded poolbatch: leaves in random orderone leaf = 10 USDG, one keyprivate vaultprivate vaultprivate vaultat a random time
A deposit is public and linked to the vault that funded it. Its leaves are mixed with every other deposit's in one batch, and nothing on chain says which leaves are whose. Each leaf later moves to its own new vault.
  1. Fund a one-time depositor. The agent's vault sends the deposit amount, with one WOTS transfer, to a depositor key derived for this deposit alone shield.ts:4-37.
  2. Deposit. The depositor key signs an EIP-3009 authorization for exactly that amount. Its nonce is a hash of every field of the deposit, so whoever submits it, the relayer for instance, cannot change any of them OstraPool.sol:224-245. The deposit names a refund owner (the vault) and the first WOTS approval key. It names nothing about its leaves.
  3. Send the leaves to the aggregator. The agent derives one WOTS key per leaf and sends the leaf key roots to the aggregator, signed by the depositor key. The aggregator accepts them only if they add up to the deposit after the fee aggregator.ts:1-29.
  4. Batch. With at least 8 deposits whose agents are online, the aggregator creates a batch, appends all their leaves in random order, and gives each agent the batch's state: a hash of its LtHash accumulator, leaf count and total OstraPool.sol:366-371.
  5. Approve. Each agent reads the batch through its own RPC and approves it with its WOTS approval key only if every leaf is a standard leaf and every one of its own leaves is there shield.ts:695-696. The approval key rotates.
  6. Seal. When every deposit approved the batch's current state, the aggregator seals it. The pool takes the fees, and the aggregator forgets which leaves came from which deposit.
  7. Unshield. Later, each leaf moves, whole, into its own new vault, at a random time.

4.4Why sealing is safe#

The pool never learns which leaves are whose, so the aggregator could try to slip in leaves of its own. Sealing makes that worthless OstraPool.sol:401-432:

  • Every deposit the batch consumes must have approved exactly the batch's current state. Appending one more leaf changes the state and voids every approval Pool.fork.t.sol:385-424.
  • The deposits' amounts after fees must add up to the batch's leaf total, to the unit.
  • An agent approves only a state that holds every one of its own leaves.

So every honest depositor's leaves are in the batch, and the leaves nobody claims are worth exactly what the other consumed deposits paid. A party that adds leaves of its own can only fill the room its own deposits paid for OstraPool.sol:44-48. Leaves without a paying deposit can never seal Pool.fork.t.sol:508-529.

The aggregator key can create, fill, seal and abandon batches. It can never move a token out of the pool; leaves move only with their own WOTS keys, deposits only back to their refund owner aggregator.ts:1-29.

4.5Why deposits do not reveal leaves#

Version 1 of the pool did. Each deposit recorded an LtHash commitment of its own leaves, and sealing checked that the commitments added up to the batch. Because every leaf key root is public once appended, anyone could test which set of a batch's leaves summed to a given deposit's commitment: for a one-leaf deposit, 192 comparisons at most. That broke the pool's purpose, so it was replaced.

Version 2 records no commitment at all OstraPool.sol:122-129. The deposit and the leaves meet only in a message the depositor key signs to the aggregator, off chain. Safety comes from the approvals and the amounts instead, as above. The storage layout was kept identical, so the beacon could switch the live pool to version 2 in place, and the upgrade script refuses to run unless the pool holds no deposits upgrade-pool.ts:109.

4.6Abandoned batches#

If one agent does not approve within 15 minutes, the batch cannot seal and is abandoned. Its leaves are public by then. If the same leaves came back in the next batch with a different group of deposits, comparing the two batches would show which leaves belong to the deposits that left. So no leaf is ever offered to two batches:

  • after a batch that does not seal, the aggregator asks every deposit in it for a new leaf set, derived from new keys config.ts:53-60;
  • the agent checks this itself: before sending a leaf set it reads the aggregator's own batches, and moves to the next set if any batch other than the current open one already holds one of its leaves shield.ts:4-37;
  • a deposit has 64 leaf sets; one that uses them all is never batched again and is refunded after 48 hours aggregator.ts:48-49.

4.7Unshielding#

A leaf moves with a WOTS signature over chain id ‖ pool ‖ batch ‖ leaf index ‖ destination ‖ amount, so the signature fixes where the 10 USDG go, whoever submits it OstraPool.sol:486-489. The agent sends each leaf to its own new vault, because several leaves in one vault could be matched with the public deposit amounts, and at a random moment, uniform within a window: 6 hours by default, 24 at most, or now unshield.ts:4-13. With the relayer, one transaction creates the new vault and spends the leaf into it, and the vault pays for it later, so the private vault starts with no link to any funding account.

4.8What stays public#

EventPublic on chain
FundingThe vault sends the deposit amount to the depositor key: the vault and the deposit are linked
DepositDepositor key, deposit id, amount (and so the number of leaves), refund owner, first approval key root
ApprovalWhich deposit approved which batch state, and when: also for batches that were abandoned
BatchEvery leaf key root in random order, and, at sealing, the list of deposits consumed
SpendBatch, leaf index, destination and the 10 USDG

What is hidden is the step between the second and fourth rows: which leaves, and so which destinations, came from which deposit. Within one sealed batch, any leaf may come from any of its deposits, in proportion to the leaves each one paid for.

4.9What you trust#