Skip to content

Cell Seal: Integration ​

Cell Seal sells contracts plus an SDK to tokenization platforms. This document answers: what the platform owns, what Cell Seal owns, what a fund token must satisfy, who changes the public supply, and which invariants the platform's indexer reconciles. The step-by-step procedure for bringing a fund onto the rail, in both cases (no token yet, existing token), is in onboarding.md. Decided 2026-10-24. The parts of the current prototype not yet updated to this document are listed in section 8.

1. Product boundary ​

The tokenization platform ownsCell Seal provides
Fund tokenIssuer account, token contract (SAC or custom SEP-41), the mint and burn rights, asset flagsNo rights on the token. The wrapper only calls transfer to move tokens into and out of its pool
InvestorsKYC, whitelist, records, the contract with the fundfund-policy: an on-chain copy of investor status, written by the platform through the SDK
Order lifecycleRequest, cut-off, NAV, approval, payout, reconciliation, every screensettlement: order reference, sealed legs, DvP, settlement window
FiguresNAV, dealing calendar, register, reportingA register rebuilt from decrypted data, by the SDK, in the fund's browser
KeysWallets of investors and of fund opsPrivacy key derived from a wallet signature, proof generation, decryption, sealed box: all in the SDK
InfrastructureIts backend, database and apps, including the three host services the SDK needs: Stellar RPC, a permanent event archive, an order store for ciphertext (sdk.md section 2.1)The SDK, the interface of each host service, and a reference implementation of the event archive in the sandbox

The product has three parts: the contract set (seal-token wrapper, settlement, fund-policy, seal-verifier, seal-auditor, fund-registry), @cell-seal/sdk, and the onboarding procedure in onboarding.md. The sandbox is a reference tokenization platform with the rail integrated: an amount-blind backend (backend.md), a fund console and an investor app (frontend.md, design/). A platform uses it to run one full cycle before integrating the SDK into its own apps, and reads it as a worked integration. It is not the product and is not operated for real funds.

Each tokenization platform runs one shared contract set (settlement, fund-policy, seal-verifier, seal-auditor, fund-registry) and one wrapper per fund. The platform admin key (K6 in keys.md) belongs to the platform. Cell Seal keeps the audited wasm hashes and publishes new releases.

2. Two onboarding cases ​

Case 1: the fund has no token yetCase 2: the fund already has a token
Fund tokenThe platform creates the token together with the wrapperThe token exists and has public holders
Supply at start0Partly or fully held by public holders
Public cap tableShows only the wrapper as holder, from the first transactionHistory before the wrap date is public and cannot be erased
Existing holdersNoneSelf-wrap their balance if they want, or stay public
State after onboardingEvery share is sealedHybrid: part public on the platform's rail, part sealed in the wrapper
Extra work for the platformNoneAuthorize the wrapper and the settlement account on the token (section 3), guide holders to wrap, run two rails side by side
Contract differencesNone. Case 1 is Case 2 with an initial supply of 0

Both cases use the same contract set, the same SDK and the same close_window procedure. The differences are in the token (section 3) and in the platform accepting a hybrid state (section 5). The procedures, checklists and the first settlement window of each case are in onboarding.md.

2.1 Onboarding order ​

Case 1: create the token. Case 2: token already existsIf AUTH_REQUIRED: authorize the wrapper and the settlement accountDeploy the wrapper pointing at token, verifier, auditor, policyViewing key ceremony k-of-n, register the auditor keyfund-registry.create_fund(token, terms, issuer admin, settlement account, auditor key)register(settlement account)Write investors into fund-policy (section 7)Case 2: existing holders register, then deposit(from = to = self)PlatformFund token (platform's)Share wrappersettlementKey holders

3. What the fund token must satisfy ​

ConditionWhyCase 1Case 2
A SEP-41 contract (SAC of a classic asset, or a Soroban token)The wrapper calls transfer(from, wrapper, amount) and transfer(wrapper, to, amount)Chosen at creationCheck
7 decimal placesAmounts in the circuits and in settlement are i128 at scale 10^7Set at creationA token with another scale is converted by the SDK at the boundary. Recorded in the fund configuration
If AUTH_REQUIRED: the wrapper contract and the settlement account are authorized by the issuerThe wrapper holds the pool. The settlement account receives the public netDone at creationOne issuer operation before the wrapper is deployed
If AUTH_CLAWBACK_ENABLED: the issuer commits not to claw back the wrapper addressClawing back the wrapper's pool breaks the backing of every sealed balanceLeave the flag offContractual commitment, or clear the flag if no holder needs it yet
No SEP-8 (regulated asset, per-transaction approval)The SEP-8 authorize/deauthorize sandwich applies only to classic transactions. A transfer invoked by a contract does not go through the approval server. A holder's deposit would be rejectedDo not useBlocker. A platform on SEP-8 must move transfer control for the sealed part to fund-policy and keep SEP-8 for the public part
Mint and burn stay with the platform's issuerCell Seal does not hold and does not ask for issuance rights

The asset flags and SEP-8 cannot be checked by the contracts at deploy time. They are the platform's onboarding checklist (onboarding.md section 4.2). The SDK checks that balance(wrapper) changes by exactly the amount after the first deposit, to detect a blocked token.

4. Supply: who mints, who burns, what close_window does ​

Principle: settlement does not mint and does not burn. It only wraps and unwraps the net of the window, between the settlement account and the wrapper. Creating and destroying fund tokens is the issuer's job, on the platform's rail.

StepCase 1 (token created by the platform)Case 2 (existing token)
Before close_window, net > 0The issuer mints net public tokens into the settlement accountThe issuer transfers net public tokens from the issuing account (or treasury) into the settlement account
close_window(net > 0)deposit(from = to = settlement account, net): net public tokens leave the settlement account for the wrapper's pool. The settlement account receives net sealedsame
close_window(net < 0)withdraw(from = to = settlement account, |net|, proof): |net| public tokens leave the pool for the settlement accountsame
close_window(net = 0)No token moves. The window closes, WindowClosed is emitted, the next window openssame
After close_window, net < 0The issuer burns the |net| public tokens sitting in the settlement accountThe issuer recalls |net| to the issuing account, or burns

Two checks settlement runs before wrapping: balance(token, settlement account) >= net (error 4108 NetNotFunded), and the window is due (4104 WindowNotDue). Two signatures are still required: the settlement account for deposit / withdraw, and the issuer admin as the approval of the net figure that is about to become public.

What the market sees is unchanged from the earlier design: once per window, one aggregate number moves between the issuer and the settlement account, then between the settlement account and the wrapper. There is no per-investor figure. What changes is that the guarantee "supply changes by exactly net" moves from the contract to the issuer's discipline. The platform's indexer reconciles (section 6) to detect drift.

Between the issuer funding the account and close_window running, the settlement account holds unwrapped public tokens. This interval should be short: the SDK folds the two steps into one operator action, fund then close immediately.

5. The hybrid state in Case 2 ​

Public holders on the platform are not forced to wrap. The fund therefore runs two rails until every holder has wrapped, possibly forever:

Public rail (the platform's)Sealed rail (Cell Seal)
WhoHolders who have not wrappedHolders who have called deposit into the wrapper
SubscriptionThe issuer mints or transfers public tokens to the holder, public amountsettlement account to investor inside the wrapper, sealed amount
RedemptionThe holder transfers public tokens back to the issuer, public amountinvestor to settlement account inside the wrapper, sealed amount
Moving from one rail to the otherThe holder calls deposit(from = to = self): public amount, but a balance that was already publicOnly through a redemption. Investors never withdraw (L3)
RegisterThe token's public balancesRegister rebuilt from decrypted data
What the public cap table showsEach public holder, and the wrapper as one holder

Rules for the platform in Case 2, so that the sealed part does not leak through the public part:

  • New shares for a sealed investor always go settlement account to investor inside the wrapper. Never mint public tokens to an investor and let them self-wrap: that amount is public and tied to an address.
  • Holders self-wrap existing balances only. The wrapper allows deposit with from == to for any account approved by the policy. This is a change from the old on_deposit rule (settlement account only) and is listed in section 8.
  • The net of a window counts the sealed part only. The platform adds its public figures on its side.

6. Reconciliation ​

The old invariant T1 (total_supply(token) = balance(wrapper)) no longer holds: in Case 2 there are public holders, and in Case 1 the settlement account may briefly hold unwrapped tokens. The new invariants:

#InvariantChecked where
T1balance(token, wrapper) = the sum of all sealed claims inside the wrapperThe primitive's conservation proof. The fund reconciles its register with balance(wrapper)
T1abalance(token, wrapper) after close_window = supply_after in the WindowClosed eventThe platform's indexer (reference: sandbox seal-jobs, backend.md section 6.3)
T1bbalance(token, wrapper) changes only at close_window and at a holder's self-wrap deposit (Case 2)The platform's indexer matches the wrapper's Deposit/Withdraw events against WindowClosed
T1cEvery WindowClosed with net > 0 is preceded by one issuer funding of exactly net into the settlement account. With net < 0 it is followed by one recall of exactly |net|The platform's indexer reads the fund token's transfer/mint/burn events. Any drift raises the highest alert level

supply_after in WindowClosed is the wrapper's token balance, that is the sealed supply, not the total_supply of the fund token.

7. Integration points and the SDK ​

PointThe platform doesThe SDK provides
Onboard a fundCreate or pick the token, authorize, viewing key ceremony, create_fundseal.platform.deployFund(params), ops.accounts.register({ wrappers })
InvestorsWhen KYC changes, write it to fund-policyops.investors.set(account, status, kycExpiry)
Holder self-wrap (Case 2)A "Move your shares to sealed" screen in the platform's appseal.token(wrapper).register({ auditorId }), then seal.token(wrapper).wrap(amount)
OrdersOrders carry only order_id, never an amountseal.orders.createSubscription, seal.orders.createRedemption, the sealed box for the off-chain amount. Share delivery is part of ops.window.close(plan)
WindowBefore closing: fund net public tokens into the settlement account on the platform's rail. After closing with net < 0: recallops.window.plan() returns net. ops.window.close(plan) calls close_window then delivers shares. The SDK does not mint and does not burn
RegisterRebuild the register from decrypted data in the fund's browserops.register.build()
DisclosureKey holders approve by scopethreshold/

Order of operations for one window on the platform side:

  1. plan = ops.window.plan(): net, number of orders, cover level.
  2. If net > 0: the issuer funds net public tokens into the settlement account (mint in Case 1, transfer in Case 2). If net < 0: nothing before.
  3. ops.window.close(plan): close_window(net), then submit_leg for every subscription.
  4. If net < 0: the issuer recalls |net| public tokens from the settlement account.
  5. The platform's indexer reconciles T1a to T1c.

8. Changes against the current prototype ​

The testnet prototype was written before the decision above. What must change, in order:

#TodayMust becomeWhere
1close_window calls mint(to, amount, operator) on the fund token when net > 0, burn when net < 0No call on the fund token other than balance. Check balance(settlement account) >= net before deposit (4108 NetNotFunded)settlement/src/contract.rs
2on_deposit: from == to == settlement accountfrom == to, and to passes the policy. Settlement account and investors can both self-wrapseal-token SealHooks
3share-token is the fund's tokenshare-token is a sample token for tests and the Case 1 demo only. Its issuer mints into the settlement account before close_windowshare-token, the testnet deploy script, the cycle test
4supply_after reads balance(wrapper)Unchanged. The meaning is renamed: sealed supplyDocs
5MissingCase 2 integration test: wrapper pointing at the SAC of an existing classic asset, holder self-wraps, window closes with net < 0, issuer recallssettlement/src/test/, testnet
6create_fund always deploys share-tokencreate_fund takes the address of an existing token (production, both cases) or deploys the sample token (sandbox only)fund-registry, contracts.md section 7

Invariants T1 and C3 in contracts.md section 9 are restated from section 6 above.

Cell Private technical documentation.