A cross-chain accounting system on Oasis Sapphire. Confidential balance management, deposit/withdrawal orchestration, and EVM transaction signing β gated by a TEE-attested off-chain service (ROFL).
The Accounting module consists of these main components:
- Accounting.sol β Core accounting contract (UUPS upgradeable). Manages balances, deposits, locks, transfers, withdrawals, and emergency withdraws.
- EVMSignerAndVerifier.sol β Sapphire-confidential EVM keypair management; signs sweep, gas-funding, and withdrawal transactions for source chains using the
EIP155Signerprecompile. - EIP712SignatureVerifier.sol β Verifies user-authored EIP-712 signatures for transfer / lock / withdrawal operations.
- auth/AccountingSiweAuth.sol β SIWE-based authentication for confidential Sapphire view calls.
- Types.sol β Shared structs and enums (
TokenInfo,ChainType,EVMKeypair, β¦).
- TEE-Attested Deposits: ROFL verifies source-chain deposits off-chain via RPC; on-chain
creditDeposittrusts the TEE attestation - Per-User Deposit Addresses: Deterministic, Sapphire-derived address per
(beneficiary, chainType, version); funds swept to a single encumbered wallet - Confidential Signing: Withdrawal/sweep transactions signed inside Sapphire via
EIP155Signer+SIGN_DIGEST; private keys never leave the TEE - Fund Locking: Escrow-like functionality for service interactions with time-bounded locks
- P2P Transfers: Internal transfers between users without source-chain transactions
- Emergency Withdraw: User-driven escape hatch from the deposit address, no ROFL involvement required
- Universal Token Support: Native tokens (ETH, MATIC, BNB, β¦) and ERC20 tokens across any registered EVM chain
βββββββββββββββββββ ββββββββββββββββββββββββββββββ ββββββββββββββββββββ
β User Wallet β β Oasis Sapphire β β Source Chain β
β β β β β (Base Sepolia, β
βββββββββββββββββββ€ β ββββββββββββββββββββββββ β β Eth Sepolia,β¦) β
β β’ SIWE login βββββΆβ β Accounting (UUPS) β β ββββββββββββββββββββ€
β β’ EIP-712 sigs β β ββββββββββββββββββββββββ€ β β β’ Deposit addrs β
β β’ REST API β β β Balances / Locks β β β β’ Sweep dest. β
ββββββββββ¬βββββββββ β β Tx signing (TEE keys)β β β β’ Withdraw dest. β
β β ββββββββββββ¬ββββββββββββ β ββββββββββ¬ββββββββββ
β β β β β
βΌ β β onlyROFL β β
βββββββββββββββββββ β βΌ β β
β ROFL TEE βββββΆβ ββββββββββββββββββββββββ β β
β (Python svc) β β β creditDeposit β β β
βββββββββββββββββββ€ β β resolveWithdrawal β β β
β β’ Verify deps. β β β setRoflSignerAddress β β β
β β’ Sweep engine β β ββββββββββββββββββββββββ β β
β β’ Withdraw poll β ββββββββββββββββββββββββββββββ β
ββββββββββ¬βββββββββ β
β β
ββββββββββ RPC reads / broadcasts βββββββββββββββββββββββ
The ROFL TEE is the only authorized caller of creditDeposit. Trust anchor: TEE attestation, enforced by roflEnsureAuthorizedOrigin(roflAppID).
ROFL verifies deposits off-chain by reading the source-chain RPC directly. For each /deposits/check call:
- Fetch the transaction receipt; require
status == 1. - Wait for the per-chain finality depth.
- Match the deposit:
- ERC20: find a
Transfer(_, deposit_address, amount)log (matched bylogIndex). - Native: match
tx.to == deposit_addresswithtx.value, falling back to a balance-delta check across the deposit-address balance before/after the tx block (catches internal calls).
- ERC20: find a
- Confirm on-chain amount β₯ user-claimed amount.
Once verified, ROFL calls creditDeposit(beneficiary, tokenId, amount, depositId). The contract trusts the TEE attestation and credits the balance β no on-chain proof of the source-chain transaction is verified.
The TEE-RPC path is one of several plausible ways to bridge deposit facts into the Accounting contract. The contract surface is intentionally agnostic β creditDeposit only requires some trusted oracle. Other approaches considered:
- Hashi / ShoyuBashi + ProvethVerifier β user submits a Merkle Patricia Trie proof of the source-chain transaction; the contract validates it against a block hash supplied by a Hashi block-hash oracle adapter. Strong trust model (any single honest adapter is enough), but gas-heavy and adds an oracle dependency.
- FDC (Flare Data Connector) β Flare's attestation network signs off-chain attestations of source-chain transactions; the contract verifies the signed attestation. Removes the on-chain MPT cost but adds a fee-bearing attestation round-trip and a Flare-validator-set trust assumption.
- Direct TEE RPC verification (current) β TEE reads the source chain itself. Cheapest, fastest, no third-party oracle. Trust anchor is the TEE attestation gating
creditDeposit.
bun installCompile the contracts and generate TypeScript bindings:
bun run buildThis will:
- Compile Solidity contracts using Hardhat
- Generate TypeChain TypeScript bindings
- Create artifacts in
artifacts/andtypechain-types/
Run tests on a local Hardhat node:
bun run testFor confidential computing features (generating wallet, signing), run tests on Sapphire Localnet:
- Start the Sapphire Localnet container:
docker run -it --rm -p8544-8548:8544-8548 ghcr.io/oasisprotocol/sapphire-localnet -to "<mnemonic from hardhat.config.ts>"- Run tests against Sapphire Localnet:
bun run test -- --network sapphire-localnetGenerate test coverage reports:
bun run coverageIf the contract will be owned by an EOA, define SECRET_KEY env variable.
export SECRET_KEY=0x...If the contract will be owned by a multisig Safe contract, use --output-safe
parameter to generate the Safe Transaction Builder JSON file, sign and submit.
In this case SECRET_KEY is only mandatory for contract upgrades to deploy
proposed upgrade implementation.
The deploy task provisions both the SIWE auth helper and the Accounting proxy/implementation in one step.
# Sapphire Localnet
npx hardhat deploy --network sapphire-localnet --roflappid <rofl1β¦>
# Sapphire Testnet
npx hardhat deploy --network sapphire-testnet --roflappid <rofl1β¦>Outputs: SIWE-auth address, proxy address, implementation address, EVM signing address, owner.
# Deploy AccountingSiweAuth alone (e.g., to roll the auth contract):
npx hardhat deploy-siwe-auth --network sapphire-testnet --roflappid <rofl1β¦>The Accounting contract uses the UUPS upgradeable proxy pattern. To upgrade:
cd solidity
bun run build# Sapphire Testnet
npx hardhat upgrade --network sapphire-testnet --address 0xad3C76e4E621C0cfF7540479Ee9B0A945723A642
# Sapphire Mainnet
npx hardhat upgrade --network sapphire --address <accounting-proxy-address># Sapphire Testnet
npx hardhat upgrade --network sapphire-testnet --address 0xad3C76e4E621C0cfF7540479Ee9B0A945723A642 --output-safe accounting-upgrade-safe.json
# Sapphire Mainnet
npx hardhat upgrade --network sapphire --address <accounting-proxy-address> --output-safe accounting-upgrade-safe.jsonAfter a successful upgrade, refresh the implementation address in the Contract Addresses section below.
Tokens (setTokenInfo, gated by onlyROFL) are registered by src/services/token_info_bootstrap.py at every ROFL restart, reading the desired token list from the ACCOUNTING_TOKEN_INFO JSON env var β no manual Hardhat task. Each entry is {"chain_id": <int>} for a native token, or {"chain_id": <int>, "token_address": "0x..."} for an ERC20 token. See src/README.md β Token Info Bootstrap.
Per-chain gas prices (setGasPrice, gated by onlyROFL) are kept in sync by src/services/gas_price_bootstrap.py at every ROFL restart, reading desired values from the ACCOUNTING_GAS_PRICE JSON env var β no manual Hardhat task. See src/README.md β Gas Price Bootstrap.
roflSignerAddress is published on-chain by src/services/rofl_signer_bootstrap.py on first ROFL start. It's the address whose signed view calls satisfy the onlyROFLQuery modifier β no manual setup required, but the same address must remain stable across ROFL deployments (it's derived from the ROFL-managed query-signer keypair).
- User authenticates with SIWE (
/auth/login); receives an opaquesiweToken. - User calls
getDepositAddress(chainType, version, siweToken)(signed view call) β receives a per-user EVM address derived deterministically from a Sapphire-generated master key. - User sends funds to that address on the source chain.
- User POSTs
/deposits/checkwith(chain_id, tx_hash, amount, log_index, version). - ROFL verifies the deposit (see Deposit Verification), then runs the sweep state machine in the background:
PENDINGβ optionallyGAS_FUNDED(ERC20 only β gas tank funds the deposit address with native gas) βSWEPT(sweep tx confirmed) β callscreditDepositβ record deleted.- State persisted to disk; survives ROFL restart via a recovery loop.
- User signs an EIP-712
Transfermessage - Anyone can submit the signature to execute the transfer
transferBalance(...)decrements the sender and increments the recipient atomically within the accounting system
- User signs an EIP-712
Withdrawmessage specifying token, amount, and destination address - ROFL submits
requestWithdrawal(...)on Sapphire β assigns a destination-chain nonce, queues the request, emitsWithdrawal - Once a 1-block delay passes, ROFL calls
resolveWithdrawal(index)β marks the request resolved, emitsWithdrawalResolved, and returns a Sapphire-signed RLP transaction - ROFL broadcasts the signed transaction on the destination chain
User-driven escape hatch from a per-user deposit address, with no ROFL involvement. Useful when ROFL is unavailable or the user wants to reclaim funds before sweeping.
- User calls
requestEmergencyWithdraw(tokenId, toAddress, version)β overwrites any prior request for the same(beneficiary, tokenId, version)slot - After a 1-block delay, user calls
executeEmergencyWithdraw(...)β returns a signed transaction from the deposit address totoAddress. The user broadcasts it on the source chain
| Task | Purpose |
|---|---|
deploy |
Deploy Accounting + SIWE auth |
deploy-siwe-auth |
Deploy SiweAuth.sol standalone |
force-import |
Import an existing proxy into hardhat-upgrades |
upgrade |
UUPS upgrade Accounting implementation |
getBalance |
Read user balance |
transferERC20 |
Sign + submit an EIP-712 transfer |
withdraw / watchWithdrawal |
User-side withdrawal flow |
directWithdraw |
Withdraw on-chain without ROFL/API |
emergencyRequest / emergencyExecute / emergencyStatus |
Emergency-withdraw flow from deposit address |
getDepositAddress / checkDeposit |
User-side deposit helpers |
accounts |
List configured signer accounts |
getAuthKeyHash / sign / transfer |
Auth/SIWE helpers |
Run npx hardhat <task> --help for parameter details.
| Contract | Address |
|---|---|
| AccountingSiweAuth | 0xFc97d47F0bc8f4E50333D34c281705E0666D3fD7 |
| Accounting (Proxy) | 0xad3C76e4E621C0cfF7540479Ee9B0A945723A642 |
| Accounting (Implementation) | 0x12fb6720c445aa2d38009eb64e191e26C30b4CAA (refresh after each upgrade) |
ROFL App ID: rofl1qrmnjkx47f4tcfvfclnrtj2rad82akeum5jcpe8y
Source-chain operator addresses (derived inside Sapphire; query via cast call):
| Role | Address | Funding |
|---|---|---|
evmAddress (sweep / withdrawal signer) |
0xF0006F3222b033De6DBE4CeB1E5AE99E54Aa398F |
None β does not pay gas directly. |
gasTankAddress (funds deposit addresses for ERC20 sweeps) |
0xDfFE6d45F1D52320d8F05CCd6623b09EcA05CF53 |
Must hold native gas on every source chain (Base Sepolia at minimum). |
| Contract | Address |
|---|---|
| AccountingSiweAuth | TBD |
| Accounting (Proxy) | TBD |
| Accounting (Implementation) | TBD |
- Trust anchor for deposits: ROFL TEE attestation.
creditDepositis gated byroflEnsureAuthorizedOrigin(roflAppID)β no on-chain transaction proof is verified - Confidential signing: Sapphire's
EIP155Signer+SIGN_DIGESTprecompile keeps the contract-held EVM private key inside the secure environment; signed transactions are returned only to authorized callers - EIP-712: All user-authored balance operations require typed-data signatures, validated by
EIP712Verifier.sol - Signed view-call auth:
onlyROFLQuerymatchesmsg.senderagainst the ROFL-publishedroflSignerAddress.roflEnsureAuthorizedOriginis unavailable insideeth_call, so signed-query reads use this alternative gate - 1-block delays on
resolveWithdrawalandexecuteEmergencyWithdrawmitigate same-block read-then-act simulation attacks
contracts/
βββ Accounting.sol # Main accounting contract (UUPS proxy)
βββ EVMSignerAndVerifier.sol # EVM keypairs + tx signing
βββ EIP712SignatureVerifier.sol # User auth via EIP-712
βββ Types.sol # Shared structs and enums
βββ auth/
β βββ AccountingSiweAuth.sol # SIWE auth for view-call reads
βββ interfaces/ # Contract interfaces
βββ lib/ # Utility libraries (SliceBytes, β¦)
βββ test/ # Mock contracts for non-Sapphire tests
test/
βββ Accounting.E2E.ts # End-to-end integration test
βββ EVMSignerVerifier.ts # EVM signing tests
βββ AuthTokenDecryption.ts # SIWE auth-token tests
βββ RoflAppId.ts # ROFL app ID parsing tests
βββ utils.ts # Test utilities
- Hardhat - Development environment and testing framework
- Oasis Sapphire Contracts - Confidential computing primitives (
EIP155Signer,Sapphire,SiweAuth, β¦) - OpenZeppelin - Security-audited contract libraries (UUPS proxy, access control)
- Solidity RLP - RLP encoding/decoding for Ethereum data