This is an implementation for a dead-simple end-to-end cross-chain solver workflow supporting any number of EVM chains.
Default setup: local Evolve ↔ Ethereum Sepolia, but easily extensible to N chains.
- Multi-chain support: Configure any number of EVM chains (not limited to 2)
- Bidirectional: Solver works in all directions between configured chains
- All-to-all routing: Every chain can send to every other chain
- Chain-agnostic: Core logic uses chain IDs, not hardcoded names
make deploydeploys contracts to ALL configured chains- Use ONE funded private key per chain, plus ONE user key
- Start with ONE token: USDC. Make adding/switching tokens trivial (data-driven, not code edits)
- After transfers, print balances on all chains
.
├── Makefile
├── .env
├── solver-cli/ # Rust CLI for deployment & intents
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs
│ ├── commands/
│ ├── chain/
│ ├── deployment/
│ ├── state/
│ └── utils/
├── oracle-operator/ # Independent oracle operator service
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs
│ ├── config.rs
│ └── operator.rs
├── rebalancer/ # Independent inventory rebalancer service
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs
│ ├── config.rs
│ ├── service.rs
│ ├── planner.rs
│ ├── client.rs
│ └── signer.rs
├── oif/
│ ├── oif-contracts/ # Solidity contracts (foundry)
│ │ ├── src/
│ │ ├── script/
│ │ └── broadcast/ # Deployment artifacts
│ └── oif-solver/ # Core solver engine
│ ├── config/demo/ # Generated solver config
│ └── crates/
└── .config/ # Generated configs/state (gitignored)
├── solver.toml # Solver configuration
├── rebalancer.toml # Rebalancer configuration
└── oracle.toml # Oracle operator configuration
Notes:
oif/oif-contracts/broadcast/contains deployment receiptsoif/oif-solver/config/demo/has generated solver config (networks.toml, gas.toml).config/state.jsontracks CLI state.config/rebalancer.tomlstores rebalancer configuration
Create .env (never commit). Two configuration formats supported:
# Chain configs
EVOLVE_RPC=http://127.0.0.1:8545
EVOLVE_PK=0x...
EVOLVE_CHAIN_ID=1234
SEPOLIA_RPC=https://sepolia.infura.io/v3/KEY
SEPOLIA_PK=0x...
SEPOLIA_CHAIN_ID=11155111
# Solver/operator keys (default to SEPOLIA_PK)
SOLVER_PRIVATE_KEY=0x...
ORACLE_OPERATOR_PK=0x...
# User key
USER_PK=0x...
# Token config
TOKEN_SYMBOL=USDC
TRANSFER_AMOUNT=1000000# Explicit chain list
CHAINS=evolve,sepolia,arbitrum
# Per-chain config
EVOLVE_RPC=http://127.0.0.1:8545
EVOLVE_PK=0x...
SEPOLIA_RPC=https://sepolia.infura.io/v3/KEY
SEPOLIA_PK=0x...
ARBITRUM_RPC=https://sepolia-rollup.arbitrum.io/rpc
ARBITRUM_PK=0x...
# ... rest same as aboveImplementation requirements:
- CLI auto-detects configuration format
TRANSFER_AMOUNTis in token base units (USDC: 6 decimals => 1 USDC = 1000000).
- MockERC20 (USDC, 6 decimals) deployed to both chains via Deploy.s.sol
- Token addresses stored in deployment broadcast JSON
- CLI reads addresses from forge broadcast output
- For testing: CLI auto-mints tokens to user if balance insufficient
CRITICAL: Solver and Oracle Operator are SEPARATE services with DIFFERENT keys.
The solver cannot attest to its own fills. That would be:
Solver: "I filled the order"
Solver: "I confirm I filled the order" ← Same entity!
Solver: "Pay me"
This is "trust me, I did the work" with no verification.
┌──────────────┐ ┌──────────────────┐
│ Solver │ │ Oracle Operator │
│ (key A) │ │ (key B) │
└──────────────┘ └──────────────────┘
│ │
│ 1. Fill order │
│ (key A) │
│ │
│ 2. Watch fills
│ 3. Verify happened
│ 4. Sign attestation
│ (key B) ← Different key!
│ 5. Submit to oracle
│ │
│ 6. Poll oracle │
│ 7. Claim (key A) │
-
Solver (
oif-solver/):- Fills orders on destination chain
- Polls CentralizedOracle.isProven()
- Claims escrowed funds once oracle confirms
-
Oracle Operator (
oracle-operator/):- Watches ALL configured chains for OutputFilled events
- Verifies fills occurred
- Signs attestations with operator key
- N-chain routing: When a fill is detected, the operator queries each chain's InputSettlerEscrow.orderStatus(orderId) to find where the order originated. The attestation is then submitted to the correct origin chain.
- Submits to CentralizedOracle contracts on the origin chain
- Independent process, separate key
For local E2E testing, you CAN use the same key for both (simpler setup), but understand this defeats the trust model. For production, these MUST be separate entities.
- Rust CLI (
solver-cli/): Handles deployment, state management, intent submission, balance verification - Foundry: Contract compilation and deployment via forge scripts
- OIF Solver: Core solver engine from
oif/oif-solver/ - Oracle Operator: Independent service that signs attestations
- Rebalancer: Independent inventory balancing service for cross-chain token distribution
Hard requirements:
- Everything runnable via Make targets
- CLI outputs deterministic, copy-pasteable addresses
make start
- starts local evolve node (ev-reth)
make deploy
- runs
solver-cli deploy - deploys OIF contracts to ALL configured chains (including CentralizedOracle)
- generates solver config and oracle operator config
- prints deployed addresses summary
make solver-start (or just: make solver)
- starts OIF solver service using generated config
- watches ALL chains for intents
- fills orders on any destination chain
- waits for oracle attestations before claiming
make operator-start (or just: make operator)
- starts oracle operator service (SEPARATE process)
- watches ALL chains for OutputFilled events
- signs attestations with operator key
- submits to CentralizedOracle contracts on source chains
make rebalancer-start (or just: make rebalancer)
- starts rebalancer service using
.config/rebalancer.toml - monitors per-chain inventory balances
- plans and submits Hyperlane
transferRemoterebalancing transfers (unlessdry_run = true)
make intent
- runs
solver-cli intent submit(defaults to first two chains) - use
make intent FROM=sepolia TO=evolvefor reverse direction - mints tokens to user if needed (testnet)
- prints intent ID and tx hash
make balances
- runs
solver-cli balancesto print balances on ALL chains - shows user + solver balances for configured token
The CLI handles all deployment and intent operations.
solver-cli init # Initialize project state
solver-cli deploy # Deploy contracts to all configured chains
solver-cli deploy --chains evolve,sepolia # Deploy to specific chains only
solver-cli configure # Generate solver and oracle configs
solver-cli fund # Fund solver on all chains
solver-cli fund --chain sepolia # Fund solver on specific chain
solver-cli intent submit --amount 1000000 --from evolve --to sepolia
solver-cli intent submit --amount 1000000 # Uses first two chains by default
solver-cli intent list # List all intents
solver-cli intent status --id <id>
solver-cli balances # Check balances on all chains
solver-cli balances --chain sepolia # Check balances on specific chain
solver-cli rebalancer start # Start rebalancer (continuous)
solver-cli rebalancer start --once # Run one rebalance cycle
solver-cli rebalancer start --config .config/rebalancer.tomlsolver-cli/src/
├── main.rs # CLI entry point (clap)
├── commands/
│ ├── init.rs # Project initialization
│ ├── deploy.rs # Contract deployment orchestration
│ ├── intent.rs # Intent submission
│ └── verify.rs # Balance verification
├── chain/
│ ├── client.rs # EVM chain client (alloy)
│ └── contracts.rs # Contract ABIs and interactions
├── deployment/
│ ├── deployer.rs # Multi-chain deployment logic
│ └── forge.rs # Forge script runner
├── state/
│ ├── state_file.rs # State persistence (.config/state.json)
│ └── types.rs # State data structures
└── utils/
├── env.rs # Environment loading
└── output.rs # Formatted output helpers
All state and generated configs live in .config/:
.config/state.json- Deployed contract addresses, tokens, solver address, intent history.config/solver.toml- Solver configuration (all chains, all-to-all routes).config/oracle.toml- Oracle operator configuration.config/rebalancer.toml- Rebalancer configuration.config/aggregator.json- OIF Aggregator configuration.config/oracle-state.json- Oracle operator runtime state (block tracking)
Chain configs use HashMap<u64, ChainConfig> structure:
pub type ChainConfigs = HashMap<u64, ChainConfig>;
pub struct ChainConfig {
pub name: String,
pub chain_id: u64,
pub rpc: String,
pub contracts: ContractAddresses,
pub tokens: HashMap<String, TokenInfo>,
pub deployer: Option<String>,
}solver-cli configure generates all configs into .config/:
.config/solver.toml- Solver configuration with all chains and all-to-all routes.config/oracle.toml- Oracle operator configuration for all chains.config/rebalancer.toml- Rebalancer configuration for inventory balancing.config/aggregator.json- Aggregator configuration with supported asset routes
The solver runs from oif/oif-solver/ using generated config.
- Solver watches InputSettlerEscrow on source chain
- When intent detected, calculates profitability (gas costs vs spread)
- If profitable (or within configured loss threshold), fills on destination
- Waits for oracle operator to submit attestation
- Polls CentralizedOracle.isProven() on source chain
- Claims escrowed funds once oracle confirms fill
- Watches OutputSettlerSimple on both chains for OutputFilled events
- Extracts fill details (solver, timestamp, orderId, output)
- Computes attestation payload hash
- Signs attestation:
sign(keccak256(chainId, oracle, application, payloadHash)) - Submits to CentralizedOracle contract on source chain
- Oracle stores attestation, making it available via
isProven()
Solver automatically:
- Simulates fill transaction for gas estimate
- Fetches current gas prices
- Converts to USD value
- Accepts/rejects based on
min_profit_marginconfig
The system includes an OIF Aggregator service that provides:
- Quote aggregation from multiple solvers
- Unified HTTP API for quotes and orders
- Solver health monitoring and circuit breakers
- Automatic asset discovery
User/Client → Aggregator (port 4000) → Solver(s) (port 3000+)
Generated at .config/aggregator.json:
- Server settings (host, port)
- Registered solver endpoints
- Aggregation settings (timeouts, retries)
- Circuit breaker configuration
- Metrics and monitoring
# Terminal 1: Start aggregator
make aggregator
# Terminal 2: Start solver
make solver
# Terminal 3: Start oracle operator
make operator
# Terminal 4: Submit intents (via CLI or aggregator API)
make intentSee AGGREGATOR_INTEGRATION.md for detailed integration guide.
The system includes an independent Rebalancer service that keeps solver inventory distributed across chains according to configured weights.
Generated at .config/rebalancer.toml:
- Poll interval and execution limits (
poll_interval_seconds, transfer bps bounds) - Per-chain signer/account settings
- Per-asset token settings (
type,address,collateral_token) - Weight and min-weight thresholds for rebalance triggers
Signer behavior:
type = "env"loadsREBALANCER_<CHAIN_NAME>_PKfirst, thenREBALANCER_PRIVATE_KEYtype = "file"uses explicit private key stringtype = "aws_kms"uses KMS key id + region
# Continuous mode
make rebalancer
# Single cycle
solver-cli rebalancer start --onceRebalancer behavior:
- Polls balances and detects deficit chains (below
min_weight) - Plans surplus -> deficit transfers
- Uses Hyperlane
quoteTransferRemote+transferRemoteoncollateral_token - Skips on-chain submission when
dry_run = true
CLI outputs formatted summaries (for any number of chains):
═══ SUMMARY ═══
Chain │ Account │ Balance
─────────┼─────────┼────────
evolve │ User │ 0 USDC
evolve │ Solver │ 10 USDC
sepolia │ User │ 1 USDC
sepolia │ Solver │ 9 USDC
arbitrum │ User │ 0 USDC
arbitrum │ Solver │ 0 USDC
═══════════════
# 1. Start local chains
make start
# 2. Deploy contracts
make deploy
# 3. Start solver (in separate terminal)
make solver
# 4. Start oracle operator (in another terminal)
make operator
# 5. Start rebalancer (optional, for inventory balancing)
make rebalancer
# 6. Submit intent
make intent
# 7. Verify balances
make balances# 1. Start local chains
make start
# 2. Deploy contracts
make deploy
# 3. Start aggregator (Terminal 1)
make aggregator
# 4. Start solver (Terminal 2)
make solver
# 5. Start oracle operator (Terminal 3)
make operator
# 6. Start rebalancer (Terminal 4, optional)
make rebalancer
# 7. Submit intent
make intent
# 8. Verify balances
make balancesNote: All services must be running for the full flow to work:
- Solver: Fills orders on destination chain
- Oracle Operator: Signs attestations
- Aggregator (optional): Aggregates quotes from multiple solvers
- Rebalancer (optional): Maintains cross-chain inventory distribution
-
make deploy: deploys fresh contracts (including CentralizedOracle), prints addresses -
make aggregator: starts OIF aggregator on port 4000 -
make solver: starts solver watching all chains (with HTTP API on port 3000) -
make operator: starts oracle operator (separate service) -
make rebalancer: starts inventory rebalancer service -
make intent: submits intent- Solver fills on destination
- Oracle operator signs attestation
- Solver claims on source after oracle confirms
-
make balances: shows correct balance changes- User: source decreased, destination increased
- Solver: source increased, destination decreased
- Quote aggregation from multiple solvers
- Automatic solver health monitoring
- Circuit breaker for failing solvers
- Asset discovery and caching
- Unified REST API