Skip to content

Latest commit

Β 

History

History
327 lines (232 loc) Β· 16.1 KB

File metadata and controls

327 lines (232 loc) Β· 16.1 KB

Accounting Module - Solidity Contracts

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).

Overview

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 EIP155Signer precompile.
  • 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, …).

Key Features

  • TEE-Attested Deposits: ROFL verifies source-chain deposits off-chain via RPC; on-chain creditDeposit trusts 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

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   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).

Deposit Verification

ROFL verifies deposits off-chain by reading the source-chain RPC directly. For each /deposits/check call:

  1. Fetch the transaction receipt; require status == 1.
  2. Wait for the per-chain finality depth.
  3. Match the deposit:
    • ERC20: find a Transfer(_, deposit_address, amount) log (matched by logIndex).
    • Native: match tx.to == deposit_address with tx.value, falling back to a balance-delta check across the deposit-address balance before/after the tx block (catches internal calls).
  4. 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.

Alternative Solutions

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.

Installation

bun install

Compilation

Compile the contracts and generate TypeScript bindings:

bun run build

This will:

  • Compile Solidity contracts using Hardhat
  • Generate TypeChain TypeScript bindings
  • Create artifacts in artifacts/ and typechain-types/

Testing

Local Hardhat Testing

Run tests on a local Hardhat node:

bun run test

Sapphire Localnet Testing

For confidential computing features (generating wallet, signing), run tests on Sapphire Localnet:

  1. Start the Sapphire Localnet container:
docker run -it --rm -p8544-8548:8544-8548 ghcr.io/oasisprotocol/sapphire-localnet -to "<mnemonic from hardhat.config.ts>"
  1. Run tests against Sapphire Localnet:
bun run test -- --network sapphire-localnet

Test Coverage

Generate test coverage reports:

bun run coverage

Deployment and Upgrades

If 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.

Deploy

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.

Standalone subtasks

# Deploy AccountingSiweAuth alone (e.g., to roll the auth contract):
npx hardhat deploy-siwe-auth --network sapphire-testnet --roflappid <rofl1…>

Upgrade

The Accounting contract uses the UUPS upgradeable proxy pattern. To upgrade:

1. Make contract changes and compile

cd solidity
bun run build

2. Run the upgrade task

With EOA
# Sapphire Testnet
npx hardhat upgrade --network sapphire-testnet --address 0xad3C76e4E621C0cfF7540479Ee9B0A945723A642

# Sapphire Mainnet
npx hardhat upgrade --network sapphire --address <accounting-proxy-address>
With Safe multisig
# 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.json

3. Update the README

After a successful upgrade, refresh the implementation address in the Contract Addresses section below.

Configuration

Adding Token Support

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.

Setting Gas Prices

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.

ROFL Signer Address

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).

Usage Examples

Deposit Flow

  1. User authenticates with SIWE (/auth/login); receives an opaque siweToken.
  2. User calls getDepositAddress(chainType, version, siweToken) (signed view call) β†’ receives a per-user EVM address derived deterministically from a Sapphire-generated master key.
  3. User sends funds to that address on the source chain.
  4. User POSTs /deposits/check with (chain_id, tx_hash, amount, log_index, version).
  5. ROFL verifies the deposit (see Deposit Verification), then runs the sweep state machine in the background:
    • PENDING β†’ optionally GAS_FUNDED (ERC20 only β€” gas tank funds the deposit address with native gas) β†’ SWEPT (sweep tx confirmed) β†’ calls creditDeposit β†’ record deleted.
    • State persisted to disk; survives ROFL restart via a recovery loop.

Transfer Flow

  1. User signs an EIP-712 Transfer message
  2. Anyone can submit the signature to execute the transfer
  3. transferBalance(...) decrements the sender and increments the recipient atomically within the accounting system

Withdrawal Flow

  1. User signs an EIP-712 Withdraw message specifying token, amount, and destination address
  2. ROFL submits requestWithdrawal(...) on Sapphire β€” assigns a destination-chain nonce, queues the request, emits Withdrawal
  3. Once a 1-block delay passes, ROFL calls resolveWithdrawal(index) β€” marks the request resolved, emits WithdrawalResolved, and returns a Sapphire-signed RLP transaction
  4. ROFL broadcasts the signed transaction on the destination chain

Emergency Withdraw

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.

  1. User calls requestEmergencyWithdraw(tokenId, toAddress, version) β€” overwrites any prior request for the same (beneficiary, tokenId, version) slot
  2. After a 1-block delay, user calls executeEmergencyWithdraw(...) β€” returns a signed transaction from the deposit address to toAddress. The user broadcasts it on the source chain

Hardhat Tasks

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 Addresses

Staging (Sapphire Testnet)

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).

Production (Sapphire Mainnet)

Contract Address
AccountingSiweAuth TBD
Accounting (Proxy) TBD
Accounting (Implementation) TBD

Security Considerations

  • Trust anchor for deposits: ROFL TEE attestation. creditDeposit is gated by roflEnsureAuthorizedOrigin(roflAppID) β€” no on-chain transaction proof is verified
  • Confidential signing: Sapphire's EIP155Signer + SIGN_DIGEST precompile 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: onlyROFLQuery matches msg.sender against the ROFL-published roflSignerAddress. roflEnsureAuthorizedOrigin is unavailable inside eth_call, so signed-query reads use this alternative gate
  • 1-block delays on resolveWithdrawal and executeEmergencyWithdraw mitigate same-block read-then-act simulation attacks

Development

Project Structure

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

Key Dependencies

  • 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