Skip to content

Cell Seal: Security ​

Threat model, leak catalogue, invariants, dependencies that are not yet assured, and the assurance plan. This document does not replace an audit. It is the input to one.

1. What must be protected ​

#AssetIf lost
A1Confidentiality of amounts: the size of every order, the balance of every investorExactly what the product exists to prevent: front-running, repriced raises, copycat runs
A2Integrity of shares: no share is created, destroyed or moved to the wrong ownerDirect financial loss, legal liability for the issuer
A3The fund's and the regulator's ability to readThe fund breaches its record-keeping and reporting obligations
A4The investor's ability to spend: openings, keys, event historyThe assets still exist but cannot be used
A5Metadata: who invested in which fund, and whenCommercially sensitive information, even though it is not an amount

2. Adversaries ​

AdversaryCapabilityWants
Chain observerReads everything public, forever, with analysis toolingA1: infer sizes from public data
Malicious investorOne valid account, can build arbitrary proofsA2: spend more than the balance. A1: read other people's numbers
Backend attackerFull read and write on the backend that hosts the order store and the event archive: the sandbox backend, or the platform'sA1, A5. Tamper with order state
Cell Seal insiderAs above on the sandbox, plus release rights on the SDK package and the contract wasmA1. Plant malicious code in a build
Malicious or compromised fund opsThe signer role of one fundA2: deliver the wrong shares. A1 for that fund
Malicious key holderOne key shareA1, when colluding up to the threshold
Upstream third partyA bug in the circuits, the verifier, or the primitiveA1, A2

Out of scope: an issuer that deliberately defrauds its own investors through off-chain channels (a bank transfer that never arrives, a wrong NAV). Cell Seal provides evidence for a dispute. It cannot prevent that behaviour.

3. Leak catalogue: where an amount can leak ​

Each leak is a way to infer a sealed amount from public data or from data the backend can see.

#LeakMechanismBlocked where
L1An order carries a plaintext amountThe platform's event or API has an amount fieldAn order carries only order_id. The amount is a sealed box. settlement has no amount parameter other than net
L2Cash leg paid in a public stablecoinpayout / NAV = sharesThe on-chain cash leg only goes through the cash wrapper
L3An investor unwraps shares directlyAn unwrap has a public amounton_withdraw of the share wrapper allows only the settlement account
L4An unwrap right after a large orderThe timing matches the orderclose_window refuses to run before next_close
L5A fee paid as a separate transferfee / rate = sizeThe fee is deducted inside result. There is no separate fee transfer
L6A window with too few ordersnet is about one orderThe Privacy indicator before closing. Longer windows per fund configuration. Windows run even when empty
L7A deposit straight to an investordeposit(from, to, amount) has a public amount and a public recipienton_deposit: from == to
L8An auditor_id outside the fundThe fund is blind to that investor, and a stranger can readon_register enforces the fund's auditor_id
L9A stablecoin top-up that can be tied to an orderA deposit into the cash wrapper has a public amount. Topping up exactly the amount to invest and investing immediately gives it awayThe cash wrapper is shared across the platform, so a deposit does not point at a fund. The SDK warns on a top-up equal to a pending order (E_* codes do not block it) and the platform's app recommends round amounts, topped up ahead of time, away from the moment the order is placed. This is a mitigation, not an elimination
L10The number of legs in a periodThe number of transfers to an address is publicAccepted. Addresses and transaction counts are outside the confidentiality scope by design
L11Shares delivered at settlement equal net exactly when there is a single subscriptionA special case of L6As L6
L12Amounts in URLs, logs, analytics, error reportsLeak through web infrastructurefrontend.md section 8. End-to-end tests scan the network
L13Ciphertext sizeA sealed box is longer for a larger numberOrderSecret pads amount to a fixed length before encryption
L14Proving timeIn theory depends on the witnessThe circuits have a fixed constraint count. No measurable difference by amount. Recorded, not addressed
L15Case 2: new shares minted publicly to an investor who then self-wrapsAmount and address are publicRules for the platform in integration.md section 5. The indexer warns when a self-wrap Deposit follows right after a public transfer from the issuer to the same address
L16The issuer funds the settlement account with more than netThe public surplus reveals that the next period has subscriptionsThe SDK refuses to close when the public balance of the settlement account differs from net (E_NET_NOT_FUNDED below, E_NET_OVERFUNDED above). The indexer reconciles T1c
L17Bank transfer referenceThe bank sees the amount, which is inherentOut of scope. The reference contains nothing but the order code
L18A stablecoin unwrap that can be tied to a payoutA withdraw from the cash wrapper has a public amount. Unwrapping exactly the payout right after a redemption settles gives payout / NAV = sharesThe mirror of L9. The cash wrapper is shared across the platform. The SDK warns when the unwrap amount equals a payout received in the last window. The platform's app recommends keeping stablecoin sealed and unwrapping round amounts later. Mitigation, not elimination

4. Invariants ​

On-chain invariants (T1-T2, C1-C6) are in contracts.md section 9. System-wide invariants:

#InvariantChecked by
S1No backend process, sandbox or platform host service, ever holds a plaintext amount, a private key, or a rootSchema and API review. Log-scanning test. No numeric column with monetary meaning other than public data
S2On-chain leg state changes only on final events recorded by the event archiveNo endpoint writes on-chain leg state. Integration tests
S3Off-chain leg state changes only on an attestation signed by a fund_ops:signerThe backend checks the signature and the role at signing time
S4The browser main thread never receives sk, vk, a root, or a key shareSDK module boundary. Lint blocks the import
S5The SDK builds no proof when an opening diverges from the on-chain commitment, or when the verification key diverges from the pinned oneUnit and integration tests
S6Every use of the viewing key has a disclosure request approved up to the threshold and a line in the unlock logThe key-holder client checks the request itself. The audit_log hash chain
S7The viewing key k never exists in one place, except in the clawback ceremonyDKG design. Operating procedure

5. Analysis by adversary ​

AdversaryScenarioOutcome
ObserverWatches every event of a fund for a yearSees: who the investors are, how often each address transacts, NAV, weekly supply. Does not see: the size of any order, anyone's balance. Residual risk: L6, L9, L10
Malicious investorA proof that overspends, a negative amount, a replayed proofThe circuits enforce range and conservation. C_spend changes after every spend so an old proof is void. (order_id, role) settles only once
Malicious investorDoes not send the opening after a secondary transferThe register drifts until the next reconciliation with the viewing key. The fund's client flags the transfer. A fund may freeze the receiving account until the opening arrives (cryptography.md section 7)
Backend attackerReads the whole databaseGets metadata (A5) and ciphertext. Does not get A1
Backend attackerSets an order's status to settledThe UI shows the wrong state, but no asset moves. Reconciliation with the chain detects it. Cannot forge an attestation without a fund ops wallet key
Backend attackerSwaps the fund's box_pk for their own keyThe client checks the wallet signature on box_pk before encrypting. For the fund, box_pk is also anchored in FundInfo in fund-registry
Backend attackerServes a forged or incomplete event stream to a walletThe SDK checks the rebuilt opening against the on-chain commitment. On divergence it refuses. Worst case is denial of service
InsiderPlants key-stealing code in the web application buildThe largest remaining risk of the web model. Mitigations: reproducible builds, published hashes, the platform building the SDK from source, strict CSP. Not fully eliminated without a hardware wallet that supports Grumpkin keys
Malicious fund opsDelivers shares to the wrong person or in the wrong amountA transaction needs both ops_root and the settlement account's signature (multisig). The recipient must be whitelisted. Everything is in audit_log and on chain. Detectable, not fully preventable
Malicious fund opsPulls an investor's allowance without payingon_spender_transfer forces the path through settle_dvp, which requires both legs. Paying the wrong amount is still possible: the investor checks afterwards and has evidence
Key holderOne party reads on its ownBelow threshold
Key holderEnough parties collude to reach the thresholdThey read everything in that key epoch. This is the chosen trust model. Shares must sit with independent legal entities

6. Dependencies not yet assured ​

DependencyStatus (2026-09-20)ImpactAction
Verifier rs-soroban-ultrahonkREADME states it is not auditedA soundness bug allows forged proofs: A2 is lostCondition for lifting the pilot cap
Non-zk proof flavourThe recipe does not enable --zk. The verifier implements only the non-zk flavourA proof may leak information about the witness: affects A1Raised with upstream. Condition for lifting the pilot cap
Confidential Token moduleDeveloper preview. One audit round done, report not published. Not in any stable releaseThe surface may change. Unknown bugsPin the commit. Track the v0.9.0 branch. Rerun the compatibility tests on every upgrade
Freighter and SEP-0053Message signing support is uneven across walletsThe signer root cannot be derivedThe SDK checks determinism at enrolment and falls back to a raw root (keys.md section 2.2)
Stellar RPCLimited event retention windowIf the indexer falls behind, events are lost for goodAlerting, backfill path
Stablecoin as a SACIts issuer can freeze or claw back the cash wrapper addressAffects every holder in the wrapperState it clearly to the fund. Choose a stablecoin with a clear policy

7. Assurance plan ​

StageActivity
FoundationThe upstream test vectors pass. Real proofs on the local network in CI. Schema and log scanning tests for S1
Settlement completeFuzz settlement: leg shapes, repeated order_id, call order, early close_window. Property tests for T1, T2, C1-C6. End-to-end network scan for L12
Before mainnetIndependent cryptographic review of R1-R6 (cryptography.md section 10). External audit of settlement, fund-policy, fund-registry and SealHooks, for example through the Soroban Audit Bank. Rehearsal of the clawback ceremony and of an ops_root compromise on testnet
Mainnet pilotPer-fund supply cap in settlement. The cap is lifted when: the verifier is audited, the zk-flavour question has an answer, and the Cell Seal contracts are audited

8. Incident response ​

IncidentFirst hourAfterwards
Suspected soundness bug in the verifier or a circuitIssuer admin freezes the affected accounts. Stop close_windowReconcile T1, T2. Coordinate with upstream. Notify the funds
A fund's ops_root is compromisedRotate the settlement account's signers if they are also suspectedCreate a new settlement account, move the balance, update the registry, notify investors
Backend compromised (the sandbox's, or a platform's host services)Shut down gateway, keep indexer running. Rotate JWT_SECRETForensics. Restore from backup. Reconcile order state against the chain and attestations. Notify about leaked metadata
Web build tampered withPull the build, restore the hash-verified oneTreat every key used during that window as compromised. Handle per keys.md section 7
Indexer lost eventsStop accepting new orders with an on-chain legBackfill from an archive node. Check event-chain continuity per contract
On-chain verification key changed outside the planThe SDK blocks proving by itself (E_VK_MISMATCH)Investigate the platform admin key. The timelock buys reaction time

9. Test strategy ​

LayerToolingRuns when
Contracts: unit, property, fuzzcargo test, proptest, cargo-fuzzEvery commit
Contracts: integration with real proofslocal network in a containerEvery pull request
SDK: vectors, property, compatibilityVitestEvery commit
Sandbox backend: API, state machinecargo test with a real PostgresEvery commit
End to end, on the sandboxPlaywright on the local networkEvery pull request into the main branch
Amount leaksSchema, log and network scansEvery pull request
LoadWindow-close script with 50 ordersBefore every release
RehearsalsClawback, ops_root compromise, indexer losing eventsBefore mainnet

Mock verifiers are forbidden at every layer from integration upwards: a green test with a fake verifier says nothing about the real system.

Cell Private technical documentation.