Bridge & Settlement
EVM bridge mechanics, settlement contract, and on-chain state commitments.
The bridge and settlement layer connects the off-chain matching engine to the on-chain EVM. It serves two functions: (1) minting/unlocking wrapped tokens for bridge operations, and (2) committing cryptographic state roots to a settlement contract for public verifiability.
Overview
Core (TickResult)
|
+-- BridgeInstruction > Bridge Signer > EVM (reth) -- mints/unlocks
|
+-- TickResult > Settlement Service > commitBatch > EVM (reth) -- state rootsAfter each matching tick, the core engine produces a TickResult containing bridge instructions, a deterministic state_hash, and a trades_root (merkle root over all trades in the tick). The event processor routes these to the bridge signer for token operations and to the settlement service for batch commitment.
ERC-20 Token Bridge
Each asset (e.g. AAPL, USD, BTC) has a corresponding OlympusToken ERC-20 contract on the EVM side. Tokens are per-asset, not per-instrument -- multiple instruments that share the same base asset share the same token. For example, instruments "AAPL-USD" and "AAPL-BTC" both use the same AAPLo token for their base asset.
Tokens follow a naming convention: name = "Olympus {ticker}", symbol = "{ticker}o" (e.g. name = "Olympus AAPL", symbol = "AAPLo"). Tokens are deployed via the OlympusTokenFactory genesis contract at 0x0D00 using CREATE2 (salt = keccak256(symbol)), e.g. keccak256("AAPLo"). This gives deterministic, restart-stable token addresses when using a persistent bridge signer key.
OlympusTokenFactory Contract
The factory is injected at genesis at 0x0D00 and initialized by the bridge signer at startup (same pattern as OlympusSettlement). It serves as the operator for all child tokens, so all mint/burn calls are proxied through the factory.
contract OlympusTokenFactory {
address public operator;
mapping(bytes32 => address) public tokens; // keccak256(symbol) => token address
function initialize(address _operator) external;
function createToken(string calldata name, string calldata symbol) external returns (address);
function mint(address token, address to, uint256 amount) external onlyOperator;
function burnFrom(address token, address from, uint256 amount) external onlyOperator;
function getToken(string calldata symbol) external view returns (address);
}OlympusToken Contract
contract OlympusToken {
string public name; // e.g. "Olympus AAPL"
string public symbol; // e.g. "AAPLo"
uint8 public decimals; // 18 (constant)
address public operator; // Factory address — can mint and burn
function mint(address to, uint256 amount) external onlyOperator;
function burnFrom(address from, uint256 amount) external;
// Standard ERC-20: transfer, approve, transferFrom
}The factory (as operator of all child tokens) has privileged access:
mint()— only callable by the operator. No pre-funding of total supply needed.burnFrom()— when called by the operator, skips the allowance check. Other callers require allowance.
Persistent Bridge Signer
Set OLYMPUS_BRIDGE_SIGNER_KEY to a hex-encoded private key (with or without 0x prefix) for deterministic, restart-stable token addresses. If unset, a random key is generated and a warning is logged. Token addresses can be queried on-chain via factory.getToken(symbol), e.g. factory.getToken("AAPLo").
Bridge Instructions
DeployToken
When a new instrument is added via AdminAddInstrument, the core engine emits a DeployToken instruction for each asset that does not already have a token. If the asset token already exists (e.g. AAPLo was deployed for "AAPL-USD" and now "AAPL-BTC" is added), no new deployment occurs.
BridgeInstruction::DeployToken {
asset, // e.g., "AAPL"
token_name, // e.g., "Olympus AAPL"
token_symbol, // e.g., "AAPLo"
}The event processor:
- Computes
salt = keccak256(symbol)(e.g.keccak256("AAPLo")) and builds the CREATE2 init_code (OlympusToken creation bytecode + ABI-encoded constructor args with factory as operator) - Precomputes the deterministic address via
compute_create2_address(TOKEN_FACTORY_ADDRESS, salt, init_code) - Registers the precomputed address in the in-memory
TokenRegistrykeyed by asset (e.g. "AAPL") - Sends
factory.createToken("Olympus AAPL", "AAPLo")transaction to reth - The factory deploys the token via CREATE2 and stores the address in its on-chain registry
Mint
When a user locks assets for bridge withdrawal, the bridge mints ERC-20 tokens on the EVM:
BridgeInstruction::Mint {
account_id, // Exchange account
evm_address, // Destination EVM address
asset, // e.g., "AAPL"
amount, // Amount to mint
nonce, // Tick sequence (monotonic, for idempotency)
}The bridge:
- Checks the nonce hasn't been processed (idempotency)
- Looks up the ERC-20 token address from the
TokenRegistry - Encodes a
factory.mint(token, recipient, amount)calldata - Signs an EIP-1559 transaction to the factory contract (value = 0)
- Submits it to reth's transaction pool
- The factory proxies the call to the child token; the recipient's
balanceOfincreases andtotalSupplyincreases
Unlock
When a user calls CoreWriter.unlockAsset() on-chain, the core engine processes the unlock and emits an Unlock instruction. The event processor then signs a burnFrom transaction:
BridgeInstruction::Unlock {
account_id, // Exchange account to credit
asset, // Asset to unlock (e.g., "AAPL")
amount, // Amount to unlock
burn_nonce, // References the on-chain burn event
}The bridge:
- Looks up the ERC-20 token address from the
TokenRegistry - Encodes a
factory.burnFrom(token, user, amount)calldata - Signs the transaction from the bridge signer to the factory (operator privilege — no allowance needed)
- Submits it to reth
- The factory proxies the call to the child token; the user's
balanceOfdecreases andtotalSupplydecreases
Failure Handling & Auto-Rollback
If a mint fails (no token deployed for the asset, signing error, or tx submission failure), the event processor automatically sends an AdminForceUnlock transaction back to the engine to return the locked funds to the user's available balance. This prevents funds from being permanently stuck in the locked state.
An admin can also manually force-unlock stuck funds via POST /api/v1/admin/accounts/{address}/force-unlock.
Decimal Scaling
The core engine uses fixed-point i64 values at per-asset scales (e.g., USD scale=2, AAPL scale=0). The EVM uses 18-decimal fixed-point (wei). The bridge converts between these using raw_to_str(amount, scale) → decimal_to_wei(decimal_string). The BridgeInstruction::Mint and Unlock variants carry the asset's scale field so the event processor can perform accurate conversion.
Token Deployment
ERC-20 tokens are deployed automatically for all assets at startup (derived from registered instruments) and at runtime when new instruments are added via AdminAddInstrument. The BridgeInstruction::DeployToken event triggers CREATE2 deployment through the factory contract.
OLP Native Token
OLP is the native gas token of the Olympus EVM chain (equivalent to ETH on Ethereum mainnet). The genesis block allocates 21,000,000 OLP to the bridge signer address. OLP is distributed to users via the admin distribute-olp endpoint, which signs native value transfer transactions from the bridge signer.
OLP is not bridgeable to the core layer — it exists only on EVM as a gas token.
Settlement Contract
The settlement contract (contracts/Settlement.sol) is injected at genesis at 0x0B00 and initialized by the bridge signer at startup. It stores state commitments that allow anyone to verify trade inclusion.
Contract Interface
contract OlympusSettlement {
address public operator;
uint64 public latestSequence;
mapping(uint64 => bytes32) public stateRoots;
mapping(uint64 => bytes32) public tradesRoots;
function initialize(address _operator) external;
function commitBatch(
uint64 fromSeq,
uint64 toSeq,
bytes32 stateRoot,
bytes32 tradesRoot,
uint64 tradeCount
) external onlyOperator;
event BatchCommitted(
uint64 indexed fromSeq,
uint64 indexed toSeq,
bytes32 stateRoot,
bytes32 tradesRoot,
uint64 tradeCount,
uint256 timestamp
);
}Batch Commitment Flow
The SettlementService (crates/bridge/src/settlement.rs) accumulates TickResults and commits them in batches:
- Each tick result is added to the pending batch
- When the batch reaches
SIM_SETTLEMENT_BATCH_SIZEticks (default 100) orSIM_SETTLEMENT_BATCH_AGE_MSmilliseconds have elapsed (default 5000), a batch is flushed - The batch computes:
stateRoot= SHA-256 of all state hashes concatenatedtradesRoot= SHA-256 of all per-tick trade roots concatenatedtradeCount= total trades across all ticks in the batch
- The service ABI-encodes a
commitBatch(fromSeq, toSeq, stateRoot, tradesRoot, tradeCount)call - The bridge signer signs and submits the transaction to reth
Contract Initialization
The contract bytecode is injected at genesis at address 0x0B00 (alongside CoreWriter at 0x0A00, OlympusReader at 0x0C00, and OlympusTokenFactory at 0x0D00). At startup, the bridge signer calls initialize(address) to set itself as the operator. This consumes nonce 0, the same slot the deploy transaction used to occupy.
EIP-1559 Transaction Signing
The bridge signs all transactions using EIP-1559 (Type 2) format:
- Chain ID: 1337
- Max fee per gas: 7 wei (reth's
MIN_PROTOCOL_BASE_FEE— the minimum the tx pool accepts) - Max priority fee per gas: 0
- Gas limit: Set per transaction type
The genesis base fee is 0, so the effective transaction cost is zero. max_fee_per_gas is set to 7 wei solely to satisfy reth's transaction pool minimum; any lower value is rejected as underpriced.
Blockscout Contract Verification
When BLOCKSCOUT_API is set, the node automatically verifies all genesis contracts and runtime-deployed ERC-20 tokens on Blockscout at startup. Verification runs as fire-and-forget background tasks (tokio spawns) that never block the engine or event processor.
For each contract, the verifier:
- Waits 5 seconds for Blockscout to index the address from the touch transaction
- Checks if the contract is already verified (skips if so)
- Submits flattened Solidity source via the Blockscout
/api/v2/smart-contracts/{addr}/verification/via/flattened-codeendpoint - Polls up to 15 times (30 seconds) for confirmation
Genesis contracts (Settlement, CoreWriter, Reader, TokenFactory) are verified immediately after the touch transactions. ERC-20 asset tokens are verified after each deploy_token_for_asset call, with ABI-encoded constructor arguments (token name, symbol, and factory address as operator) included in the submission.
All errors are logged as warnings — verification failures never affect the operation of the exchange.
Nonce Management
The bridge signer (LocalSigner) tracks nonces locally:
- Settlement
initializetransaction consumes nonce 0 - TokenFactory
initializetransaction consumes nonce 1 - Touch transactions for Blockscout discovery consume nonces 2-3 (sent automatically at startup for CoreWriter and Reader so Blockscout indexes their addresses)
- Token deployment transactions (
factory.createToken) consume subsequent nonces (one per asset) - Subsequent bridge (factory.mint/burnFrom) and settlement (commitBatch) operations share the same signer
- The event processor is single-threaded, so nonce increments are sequential with no race conditions
- Nonce is incremented only after successful signing
Design decision: single signer. Bridge operations (mint/burn), token deployments, and settlement operations (commitBatch) share the same LocalSigner. Since the event processor is a single-threaded async loop that processes ticks sequentially, there are no nonce conflicts. This is simpler than maintaining separate signers with separate nonce tracking.
Pause / Resume
Administrators can pause and resume the bridge via the admin API:
# Pause bridge — rejects new lock/unlock instructions
curl -X POST localhost:3000/api/v1/admin/bridge/pause \
-H "X-Admin-Key: <key>"
# Resume bridge — resumes processing
curl -X POST localhost:3000/api/v1/admin/bridge/resume \
-H "X-Admin-Key: <key>"When paused:
LockAssettransactions are rejected (order update =Rejected)UnlockAssettransactions are silently skipped- Settlement continues to commit state roots (settlement is independent of bridge pause state)
- The matching engine continues to operate normally
Invariant Tracking
The bridge tracks an invariant delta — the difference between minted and burned amounts. This metric (bridge_invariant_delta) is exposed via Prometheus and visible in Grafana. Any non-zero drift indicates a potential accounting issue.
Idempotency
The BridgeService tracks processed nonces to prevent duplicate mints:
let mut processed_nonces: HashSet<u64> = HashSet::new();If a bridge instruction arrives with a nonce that has already been processed (e.g., during replay after crash recovery), it is silently skipped. This ensures that replaying the tick log does not double-mint tokens.