Privacy and recovery
Nobody can tell who lent to whom. That is the whole claim, and it is worth being precise about what it does and does not cover — because pseudonymity is brittle, and one leak anywhere retroactively de-anonymises a pseudonym's entire history.
Layer 1 — pseudonym to pseudonym: public
Ledger state contains the full graph, with every amount, duration and timestamp. Anyone can read the complete relationship structure in pseudonym space.
- Every offer's full terms — unit size, collateral demanded, interest, fees, term
- Every loan's amounts, expiry, grace, status and both claim flags
- The complete graph of which offer a loan was drawn from
- Every escrowed coin's value, because contract-held shielded coins publish it
- That a shield happened, and the address that did it
- An unshield's payout address and amount, in the same transaction
Layer 2 — pseudonym to wallet: closed
Every value flow is a Zswap input or output. No address appears anywhere in the market contract. Layer 2 is what makes Layer 1 harmless.
- Who posted any offer — the contract has no lenderId field at all
- Who drew any loan — and no borrowerId field either
- Which wallet holds any position token
- The link between a shielded address and any position on the board
- The preimage of any offer or loan id, which is derived from a wallet seed
Why tokens have to be shielded
Every symbol on the board starts with a w because a market can only hold shielded value. This is what that means in practice.
A public token carries its holder in the open, so a market that escrowed one would publish who lent to whom by construction. Instead each supported asset has a wrapper contract: it takes the unshielded token, holds it in reserve, and mints you an equal amount of a shielded twin. Unshielding runs the same trade in reverse. The rate is one to one in both directions, always, and you can cross back whenever you like.
Shielding needs no cooperation from whoever issued the underlying asset, which is what lets any listed token be lent or posted as collateral here. It also gives every asset the same on-ramp, so there is one thing to learn rather than one per market.
A wrapper contract never holds your coin between transactions: shielding receives and mints in a single call, unshielding burns and releases in a single call. The only thing it keeps is the reserve backing the shielded units in circulation, and that reserve is always at least what has been minted. Each asset gets its own contract rather than one contract pooling every asset's reserves, so a problem with one can never reach the others.
Shielding is the one step that names your public address. Unshielding names the address you pay out to. Everything between the two — posting an offer, borrowing, repaying, claiming — is unlinked from both, which is why holding a shielded balance beats shielding around each transaction.
Known residual leaks
Each one is listed with what actually mitigates it, rather than left to be discovered.
A shield transaction names your public address and the amount.
Unavoidable, and the reason the wrapper is published as general-purpose shielding infrastructure rather than lending-branded — so its user set is larger than this protocol's.
A shield immediately followed by an offer or a borrow re-links through timing.
Hold a shielded balance rather than shielding per transaction. It also halves a roughly ten-transaction round trip.
Unshielding discloses the payout address as a circuit argument.
Unavoidable. Amounts that closely match a recent shield are the correlation to avoid; the interface warns when they do.
A settled loan record stays in ledger state permanently.
It is unattributed — no field on it names a party. Deleting it never bought privacy anyway, because historical state is recoverable per contract action from the indexer.
Seizure reveals that a given offer defaulted a loan.
Inherent to the mechanism.
Asking an indexer 'do I hold colour X' across every offer hands it your whole portfolio.
The recovery scan must run locally. This interface never sends a colour query out.
Your seed phrase is the backup
A DApp's private-state store is a local database and is not derived from your seed phrase. Reinstall a wallet and the store is gone. That is why authorization here is a coin you hold rather than a secret the DApp remembers — coins come back from a seed, private state does not.
Positions are bearer tokens
Each position is a single-unit shielded coin with a colour unique to it. To act, you spend it into the circuit; the ledger checks the nullifier before the proof is even considered. Ownership is proven by the spend being valid at all.
The reconstruction scan
After a reinstall the wallet holds the coins but not the meaning. This interface enumerates offers from public state, computes each expected colour, and checks your wallet for a balance at it. Public data plus the seed, nothing stored locally.
The scan runs locally
Asking an indexer whether you hold a colour, across every offer on the board, would hand it your entire portfolio — reintroducing the exact linkage the architecture exists to prevent.
One number you must keep
Everything about a position token is recoverable from your seed except this.
Recovery rebuilds a position token from mint up to its first bounce — which is exactly the window in which losing it costs anything, because that is when it is the key to an unclaimed leg. After a claim, the coin your wallet holds carries a contract-derived nonce instead.
Reconstruction scan — 10 live keys found
What the scan would return for this wallet, computed from public state and the seed alone.
offer token
sUSDC market · Offer · 3,000 sUSDC unfilled
Still the live key to 1 loan drawn from this offer.
offer token
sUSDC market · Offer · 20,000 sUSDC unfilled
Cancelling returns the unfilled principal and gives this token back.
offer token
sBTC market · Offer · 1.26 sBTC unfilled
Still the live key to 1 loan drawn from this offer.
offer token
sETH market · Offer · 80 sETH unfilled
Cancelling returns the unfilled principal and gives this token back.
Fees and DUST
DUST is shielded and uses the same commitment and nullifier paradigm, so paying your own fees should not expose you. Self-funded is the default here.
Sponsorship exists, but it relocates the leak rather than removing it: a sponsor sees the contract call, the pseudonym and the network identity — exactly the link this architecture exists to break. If it is ever offered it must be a labelled onboarding path, never the silent default.
Contracts cannot pay their own fees, and DUST cannot be transferred. It is generated by holding NIGHT.
Anonymity set is concurrent activity
Throughput is privacy. At three loans an hour, shielding is decorative because timing correlation does the rest of the work for an observer.
This is the thread connecting several otherwise-unrelated design choices: anything that caps throughput, fragments markets, or pushes users toward non-standard denominations shrinks the set you hide in.
It is also why position ids are hashes of a caller-held secret rather than a global counter — a shared counter capped the protocol at roughly one id-allocating operation per block.