Skip to content

Commit 516207e

Browse files
committed
feat(bridge): cross-chain ROSE bridge with modular Accounting proxy
Add a trustless cross-chain ROSE bridge between Oasis Sapphire and EVM chains, and restructure the Accounting contract into delegated modules behind its UUPS proxy. Bridge: - Lock/mint and burn/release flows for native ROSE <-> xERC20 xROSE, authorized by EIP-712 BridgeWithdraw signatures and settled by the ROFL TEE. - Vendored xERC20 (defi-wonderland) with provenance README; CREATE3 deployment of xROSE and ROFLBridge on the destination chain. - Config-driven chain ids, gas pricing, and route registration via a generic addBridgeAssetToken task; deposit verification reuses the existing receipt/proof path. Accounting modularization: - Extract history into a delegated AccountingHistoryModule and the lock subsystem into a delegated LockModule; lift shared history and withdrawal internals into AccountingStorage. - Wire both modules through the upgrade task and harden custody and bridge-out fund safety. Custody-tx executor: - Durable per-chain queue for every tx signed by Accounting.evmAddress(), with auto-recovery for stuck transactions and an owner-authorized clear surface. - Skip non-record sidecar files in the state-dir scan so the executor no longer aborts startup on the clear-watcher cursor. Ops: - Serve the API via the ROFL auto proxy hostname, record the fresh bridge deployment addresses, and ship the container image at v31. Verified end-to-end on testnet: Sapphire ROSE -> Base xROSE and back, with steady-state value conservation.
1 parent 2906239 commit 516207e

120 files changed

Lines changed: 30777 additions & 1343 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ SAPPHIRE_RPC_URL=https://testnet.sapphire.oasis.io
2626
# SAPPHIRE_CHAIN_ID=23294
2727
# SAPPHIRE_RPC_URL=https://sapphire.oasis.io
2828

29+
# =============================================================================
30+
# ROSE Bridge configuration
31+
# =============================================================================
32+
33+
# ROFLBridge contract (CREATE3)
34+
ROFL_BRIDGE_ADDRESS=
35+
# xROSE token (CREATE3)
36+
XROSE_ADDRESS=
37+
# Mint/burn limits in wei — must match the values configured on ROFLBridge.
38+
BRIDGE_MINT_LIMIT_WEI=250000000000000000000000
39+
BRIDGE_BURN_LIMIT_WEI=250000000000000000000000
40+
# Reconciler tick (seconds) for ROFL-TEE-driven route writes.
41+
BRIDGE_ROUTE_RECONCILE_INTERVAL=30
42+
2943
# Gas configuration
3044
ACCOUNTING_GAS_LIMIT=500000
3145

@@ -71,6 +85,13 @@ SIWE_DOMAINS=http://localhost:8000
7185
# Uses SQLite via diskcache for process-safe storage
7286
# AUTH_TOKEN_STORAGE_DIR=.auth_tokens
7387

88+
# Durable per-chain queue for every tx signed by Accounting.evmAddress()
89+
# (bridge mint/burn, sapphire release, normal withdrawals). Default
90+
# /data/custody-tx-executor maps to the named volume declared in compose.yaml.
91+
# Setting this to a tmpfs path is a fail-closed gate, not a usable default —
92+
# the executor needs disk persistence so nonce reservations survive restarts.
93+
# CUSTODY_TX_STATE_DIR=/data/custody-tx-executor
94+
7495
# SIWE nonce expiration time in seconds (default: 300 = 5 minutes)
7596
# Bounds the window for SIWE message replay at the API level
7697
# SIWE_NONCE_EXPIRY_SECONDS=300

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ jobs:
8787
run: |
8888
test -f solidity/artifacts/contracts/Accounting.sol/Accounting.json || (echo "Solidity artifacts not found!" && exit 1)
8989
90+
- name: Storage layout invariants (delegatecall safety)
91+
# Pre-flight the Accounting<->module delegatecall invariants (storage
92+
# layout, fallback dispatcher, signer/secret isolation) with --bail, so a
93+
# slot mismatch fails fast instead of being masked by the full suite below.
94+
run: cd solidity && bun run test:hardhat -- --bail --grep "storage|fallback dispatcher|setBridgeModule|signer isolation"
95+
env:
96+
BASE_SEPOLIA_RPC_URL: ${{ secrets.BASE_SEPOLIA_RPC_URL }}
97+
9098
- name: Run Solidity tests
9199
run: make solidity-test
92100

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,4 @@ temp/
8383
tmp/
8484
*.orc
8585
accounting-module
86+
solidity/.openzeppelin/

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ COPY . .
1212

1313
# Verify Solidity artifacts exist (run 'make solidity-build' locally if this fails)
1414
RUN test -f solidity/artifacts/contracts/Accounting.sol/Accounting.json \
15+
&& test -f solidity/artifacts/contracts/AccountingHistoryModule.sol/AccountingHistoryModule.json \
1516
&& test -f solidity/artifacts/contracts/auth/AccountingSiweAuth.sol/AccountingSiweAuth.json \
1617
|| (echo "ERROR: Solidity artifacts not found. Run 'make solidity-build' before building Docker image." && exit 1)
1718

Makefile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: install dev run test lint typecheck clean solidity solidity-build solidity-test solidity-lib solidity-clean solidity-coverage format format-check openapi openapi-check
1+
.PHONY: install dev run test lint typecheck clean solidity solidity-build solidity-test solidity-ci solidity-lib solidity-clean solidity-coverage format format-check openapi openapi-check
22

33
install:
44
uv sync --no-dev
@@ -50,6 +50,15 @@ solidity-build:
5050
solidity-test:
5151
cd solidity && bun run test
5252

53+
# Mirrors the solidity job in .github/workflows/ci.yml: clean compile,
54+
# size gate, upgrade-safety, targeted storage-layout pre-flight, full suite.
55+
# Run before pushing if you want CI-equivalent verification locally.
56+
solidity-ci: solidity-clean solidity-build
57+
cd solidity && bun run check:size
58+
cd solidity && npx hardhat run scripts/validate-upgrade.ts
59+
cd solidity && bun run test -- --bail --grep "storage|fallback dispatcher|setBridgeModule|signer isolation"
60+
cd solidity && bun run test
61+
5362
solidity-clean:
5463
cd solidity && rm -rf dist artifacts cache typechain-types ignition/deployments
5564

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A reusable library for apps running in ROFL that enables:
1010

1111
| Path | What's there |
1212
|------|--------------|
13-
| [`solidity/`](solidity/README.md) | On-chain contracts (Accounting, EVMSignerAndVerifier, AccountingSiweAuth), deployment & Hardhat tasks. |
13+
| [`solidity/`](solidity/README.md) | On-chain contracts (Accounting, AccountingHistoryModule, EVMSignerAndVerifier, AccountingSiweAuth), deployment & Hardhat tasks. |
1414
| [`src/`](src/README.md) | Python service that runs in the ROFL TEE — deposit verification, sweep state machine, withdrawal resolution. |
1515
| [`docs/api-reference.md`](docs/api-reference.md) | HTTP API reference: request/response shapes, auth, error codes. |
1616

compose.yaml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
services:
22
accounting-module:
33
build: .
4-
image: "docker.io/ptrusr/accounting-module:staging"
4+
image: "ghcr.io/rube-de/flexvaults:v31"
55
platform: linux/amd64
66
environment:
77
- API_HOST=${API_HOST:-0.0.0.0}
@@ -18,6 +18,12 @@ services:
1818
- WITHDRAWAL_POLL_INTERVAL=${WITHDRAWAL_POLL_INTERVAL:-12}
1919
- WITHDRAWAL_RESOLUTION_TIMEOUT=${WITHDRAWAL_RESOLUTION_TIMEOUT:-60}
2020
- MIN_WITHDRAWAL_GAS_BALANCE=${MIN_WITHDRAWAL_GAS_BALANCE:-10000000000000}
21+
- BRIDGE_ROUTE_RECONCILE_INTERVAL=${BRIDGE_ROUTE_RECONCILE_INTERVAL:-30}
22+
# Bridge — destination (chain-agnostic via CREATE3)
23+
- ROFL_BRIDGE_ADDRESS=${ROFL_BRIDGE_ADDRESS}
24+
- XROSE_ADDRESS=${XROSE_ADDRESS}
25+
- BRIDGE_MINT_LIMIT_WEI=${BRIDGE_MINT_LIMIT_WEI}
26+
- BRIDGE_BURN_LIMIT_WEI=${BRIDGE_BURN_LIMIT_WEI}
2127
# JWT Authentication configuration
2228
- JWT_EXPIRY_HOURS=${JWT_EXPIRY_HOURS:-12}
2329
- JWT_REFRESH_EXPIRY_DAYS=${JWT_REFRESH_EXPIRY_DAYS:-7}
@@ -35,14 +41,19 @@ services:
3541
- AUTH_CLIENTS=${AUTH_CLIENTS:-[]}
3642
# Sweep engine state
3743
- SWEEP_STATE_DIR=/data/sweep-engine
44+
# Custody-tx executor state — durable per-chain queue for every tx
45+
# signed by Accounting.evmAddress() (bridge mint/burn, sapphire release,
46+
# normal withdrawals). Persistence is fail-closed: tmpfs is not a usable
47+
# default, the executor refuses to drain without a real disk-backed mount.
48+
- CUSTODY_TX_STATE_DIR=${CUSTODY_TX_STATE_DIR:-/data/custody-tx-executor}
3849
volumes:
3950
- /run/rofl-appd.sock:/run/rofl-appd.sock
4051
- sweep-engine-data:/data/sweep-engine
52+
- custody-tx-executor-data:/data/custody-tx-executor
4153
ports:
4254
- "8000:8000"
43-
annotations:
44-
net.oasis.proxy.ports.8000.custom_domain: testnet.privana.finance
4555
restart: on-failure
4656

4757
volumes:
4858
sweep-engine-data:
59+
custody-tx-executor-data:

docs/api-reference.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,15 @@ POST /auth/token {grant_type=authorization_code, code, code_verifier, …}
9797

9898
| Field | Description | History kind type(s) |
9999
| --- | --- | --- |
100-
| `kind` | Entry type. Known values are `deposit`, `withdraw`, `createLock`, `transferFromLock`, and `transferBalance`; undecoded entries return `unknown`. | all |
100+
| `kind` | Entry type. Known values are `deposit`, `withdraw`, `createLock`, `transferFromLock`, `transferBalance`, `modifyLock`, and `unlockLock`; undecoded entries return `unknown`. | all |
101101
| `timestamp` | Entry timestamp. | all |
102-
| `token_id` | Token identifier. | `deposit`, `withdraw`, `createLock`, `transferFromLock`, `transferBalance` |
103-
| `amount` | Token amount as a decimal string. | `deposit`, `withdraw`, `createLock`, `transferFromLock`, `transferBalance` |
104-
| `chain_id` | Source chain for `token_id`, when known. | `deposit`, `withdraw`, `createLock`, `transferFromLock`, `transferBalance` |
102+
| `token_id` | Token identifier. | `deposit`, `withdraw`, `createLock`, `transferFromLock`, `transferBalance`, `modifyLock`, `unlockLock` |
103+
| `amount` | Token amount as a decimal string. For `modifyLock`, this is the additional locked amount and can be `0` for expiry-only changes. For `unlockLock`, this is the amount returned to available balance. | `deposit`, `withdraw`, `createLock`, `transferFromLock`, `transferBalance`, `modifyLock`, `unlockLock` |
104+
| `chain_id` | Source chain for `token_id`, when known. | `deposit`, `withdraw`, `createLock`, `transferFromLock`, `transferBalance`, `modifyLock`, `unlockLock` |
105105
| `deposit_id` | Deposit identifier. | `deposit` |
106-
| `counterparty` | Address payload for non-deposit entries: withdrawal destination, lock service, or transfer recipient. | `withdraw`, `createLock`, `transferFromLock`, `transferBalance` |
106+
| `counterparty` | Address payload for non-deposit entries: withdrawal destination for `withdraw`, lock service for `createLock`/`modifyLock`/`unlockLock`, and the other side relative to the authenticated user for paired internal transfer rows. | `withdraw`, `createLock`, `transferFromLock`, `transferBalance`, `modifyLock`, `unlockLock` |
107+
| `from_address` | Sender/original user for paired internal transfer rows. | `transferFromLock`, `transferBalance` |
108+
| `to_address` | Recipient user for paired internal transfer rows. | `transferFromLock`, `transferBalance` |
107109

108110
## Deposit Flow
109111

0 commit comments

Comments
 (0)