Skip to content

Commit 2b7adca

Browse files
authored
feat: Minimum solver profitability (#155)
1 parent aa14bf4 commit 2b7adca

30 files changed

Lines changed: 1679 additions & 471 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ oif-solver/
153153

154154
- Validates intents and converts them to orders
155155
- Implements execution strategies (when to execute)
156+
- Evaluates order profitability against minimum thresholds
156157
- Generates fill and claim transactions
157158
- Manages order-specific logic for different protocols
158159

@@ -235,6 +236,8 @@ You can also use a single configuration file. See `config/example.toml` for a co
235236
[solver]
236237
id = "oif-solver-local"
237238
monitoring_timeout_minutes = 5
239+
# Minimum profitability percentage required for order execution
240+
min_profitability_pct = 1.0
238241

239242
# Networks configuration - defines supported chains and tokens
240243
[networks.31337] # Origin chain
@@ -503,6 +506,7 @@ cargo run --bin solver -- --config config/demo.toml
503506
#### Intent Operations
504507

505508
The demo supports two submission modes:
509+
506510
- **Offchain**: Intents are submitted to the solver API (default)
507511
- **Onchain**: Intents are submitted directly to the blockchain via InputSettler.open()
508512

@@ -599,14 +603,14 @@ The demo tool generates files in the `demo-output/` directory following a clear
599603
- **`.req.json`** - Request payloads sent to the API
600604
- **`.res.json`** - Responses received from the API
601605

602-
| File | Description | Generated By |
603-
|------|-------------|--------------|
604-
| `post_intent.req.json` | Intent submission request payload | `intent build` |
605-
| `post_intent.res.json` | Intent submission response | `intent submit` |
606-
| `get_quote.req.json` | Quote request payload | `intent build` |
607-
| `get_quote.res.json` | Quote response with pricing | `quote get` |
608-
| `post_quote.req.json` | Signed quote acceptance request | `quote accept` |
609-
| `post_quote.res.json` | Quote acceptance response | `quote accept` |
606+
| File | Description | Generated By |
607+
| ---------------------- | --------------------------------- | --------------- |
608+
| `post_intent.req.json` | Intent submission request payload | `intent build` |
609+
| `post_intent.res.json` | Intent submission response | `intent submit` |
610+
| `get_quote.req.json` | Quote request payload | `intent build` |
611+
| `get_quote.res.json` | Quote response with pricing | `quote get` |
612+
| `post_quote.req.json` | Signed quote acceptance request | `quote accept` |
613+
| `post_quote.res.json` | Quote acceptance response | `quote accept` |
610614

611615
### Environment Setup Details
612616

config/demo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ include = [
1010
[solver]
1111
id = "oif-solver-demo"
1212
monitoring_timeout_minutes = 5
13+
min_profitability_pct = 1.0
1314

1415
# ============================================================================
1516
# STORAGE
@@ -76,7 +77,7 @@ max_gas_price_gwei = 100
7677
# PRICING
7778
# ============================================================================
7879
[pricing]
79-
primary = "mock"
80+
primary = "coingecko"
8081

8182
[pricing.implementations.mock]
8283
# Uses default ETH/USD price of 4615.16
@@ -87,6 +88,11 @@ primary = "mock"
8788
cache_duration_seconds = 60
8889
rate_limit_delay_ms = 1200
8990

91+
# Custom prices for demo/test tokens (in USD)
92+
[pricing.implementations.coingecko.custom_prices]
93+
TOKA = "200.00"
94+
TOKB = "195.00"
95+
9096
# ============================================================================
9197
# SETTLEMENT
9298
# ============================================================================

config/demo/gas.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ claim = 121995
1111
open = 143116
1212
fill = 76068
1313
claim = 59953
14+
15+
[gas.flows.eip3009_escrow]
16+
# Gas units captured by scripts/e2e/estimate_gas_eip3009_escrow.sh on local anvil
17+
open = 130254
18+
fill = 77298
19+
claim = 60084

config/testnet.toml

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
include = [
44
"testnet/networks.toml",
5-
"testnet/api.toml"
5+
"testnet/api.toml",
6+
"testnet/gas.toml"
67
]
78

89
[solver]
910
id = "oif-solver-testnet-usdc"
1011
monitoring_timeout_minutes = 5
12+
min_profitability_pct = 1.0
1113

1214
# ============================================================================
1315
# STORAGE
@@ -41,20 +43,21 @@ private_key = "solver_private_key"
4143
min_confirmations = 3 # Higher confirmations for testnets
4244

4345
[delivery.implementations.evm_alloy]
44-
network_ids = [84532, 11155420, 421614, 11155111]
46+
network_ids = [84532, 11155420, 11155111, 421614] # Include all configured networks
4547

4648
# ============================================================================
4749
# DISCOVERY
4850
# ============================================================================
4951
[discovery]
5052

5153
[discovery.implementations.onchain_eip7683]
52-
network_ids = [84532, 11155420, 421614, 11155111]
54+
network_ids = [84532, 11155420]
55+
polling_interval_secs = 30 # Use WebSocket subscriptions instead of polling
5356

5457
[discovery.implementations.offchain_eip7683]
5558
api_host = "127.0.0.1"
5659
api_port = 8081
57-
network_ids = [84532, 11155420, 421614, 11155111]
60+
network_ids = [84532, 11155420]
5861

5962
# ============================================================================
6063
# ORDER
@@ -69,35 +72,48 @@ primary = "simple"
6972
[order.strategy.implementations.simple]
7073
max_gas_price_gwei = 100
7174

75+
# ============================================================================
76+
# PRICING
77+
# ============================================================================
78+
[pricing]
79+
primary = "coingecko"
80+
81+
[pricing.implementations.mock]
82+
# Uses default ETH/USD price of 4615.16
83+
84+
[pricing.implementations.coingecko]
85+
# Free tier configuration (no API key required)
86+
# api_key = "CG-YOUR-API-KEY-HERE"
87+
cache_duration_seconds = 60
88+
rate_limit_delay_ms = 1200
89+
7290
# ============================================================================
7391
# SETTLEMENT
7492
# ============================================================================
7593
[settlement]
7694

7795
[settlement.domain]
7896
chain_id = 1
79-
address = "0xB686e4A97821E259d1BB15c5bf13ec96553176d7"
97+
address = "0x557d6E91bAC4b2D36fB66f0231fF93db9810e203"
8098

8199
[settlement.implementations.direct]
82100
order = "eip7683"
83-
network_ids = [84532, 11155420, 421614, 11155111]
101+
network_ids = [84532, 11155420]
84102
dispute_period_seconds = 60
85103
# Oracle selection strategy when multiple oracles are available (First, RoundRobin, Random)
86104
oracle_selection_strategy = "First"
87105

88106
# Oracle configuration with multiple oracle support
89107
[settlement.implementations.direct.oracles]
90108
# Input oracles (on origin chains)
91-
input = { 84532 = ["0xDD40C9a79D813e4200795014b8E9e575317596F6"], 11155420 = ["0x6dc56CB6C61f3c5a9148cE1fD1AF7a13B8903980"], 421614 = ["0xedF6cd53008C3EA43e976214F5f6a1A8dBDc6460"], 11155111 = ["0xc04c8A84295F36B53AC109d47C041a258B788268"] }
109+
input = { 84532 = ["0xF92e489E524B19744DbB14f8e8daAdc43c50234d"], 11155420 = ["0xF92e489E524B19744DbB14f8e8daAdc43c50234d"] }
92110
# Output oracles (on destination chains)
93-
output = { 84532 = ["0xDD40C9a79D813e4200795014b8E9e575317596F6"], 11155420 = ["0x6dc56CB6C61f3c5a9148cE1fD1AF7a13B8903980"], 421614 = ["0xedF6cd53008C3EA43e976214F5f6a1A8dBDc6460"], 11155111 = ["0xc04c8A84295F36B53AC109d47C041a258B788268"] }
111+
output = { 84532 = ["0xF92e489E524B19744DbB14f8e8daAdc43c50234d"], 11155420 = ["0xF92e489E524B19744DbB14f8e8daAdc43c50234d"] }
94112

95113
# Valid routes: from origin chain -> to destination chains
96114
[settlement.implementations.direct.routes]
97-
84532 = [11155420, 421614, 11155111] # Base Sepolia can go to all other networks
98-
11155420 = [84532, 421614, 11155111] # Optimism Sepolia can go to all other networks
99-
421614 = [84532, 11155420, 11155111] # Arbitrum Sepolia can go to all other networks
100-
11155111 = [84532, 11155420, 421614] # Ethereum Sepolia can go to all other networks
115+
84532 = [11155420] # Can go from origin to destination
116+
11155420 = [84532] # Can go from destination to origin
101117

102118
# ============================================================================
103119
# DEMO SCRIPT CONFIGURATION

config/testnet/api.toml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# API Server Configuration - Testnet Setup
2+
# Configures the HTTP API for receiving off-chain intents
3+
14
[api]
25
enabled = true
36
host = "127.0.0.1"
@@ -6,4 +9,18 @@ timeout_seconds = 30
69
max_request_size = 1048576 # 1MB
710

811
[api.implementations]
9-
discovery = "offchain_eip7683"
12+
discovery = "offchain_eip7683"
13+
14+
# JWT Authentication Configuration
15+
[api.auth]
16+
enabled = false
17+
jwt_secret = "${JWT_SECRET:-MySuperDuperSecureSecret123!}"
18+
access_token_expiry_hours = 1
19+
refresh_token_expiry_hours = 720 # 30 days
20+
issuer = "oif-solver-testnet"
21+
22+
# Quote Configuration
23+
[api.quote]
24+
# Quote validity duration in seconds
25+
# Default is 20 seconds. Customize as needed:
26+
validity_seconds = 60 # 1 minute validity

config/testnet/gas.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[gas]
2+
3+
[gas.flows.compact_resource_lock]
4+
# Gas units captured by scripts/e2e/estimate_gas_compact.sh
5+
open = 0
6+
fill = 76068
7+
claim = 121995
8+
9+
[gas.flows.permit2_escrow]
10+
# Gas units captured by cast estimate command
11+
open = 141899
12+
fill = 90027
13+
claim = 75573
14+
15+
[gas.flows.eip3009_escrow]
16+
# Gas units captured
17+
open = 130254
18+
fill = 77298
19+
claim = 60084

crates/solver-config/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ rust-version.workspace = true
66

77
[dependencies]
88
regex = "1.10"
9+
rust_decimal = { workspace = true }
910
serde = { version = "1.0", features = ["derive"] }
1011
solver-types = { path = "../solver-types" }
1112
thiserror = "1.0"

crates/solver-config/src/builders/config.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
//! This module provides utilities for constructing Config instances with
44
//! sensible defaults, particularly useful for testing scenarios.
55
6+
use rust_decimal::Decimal;
7+
68
use crate::{
79
AccountConfig, ApiConfig, Config, DeliveryConfig, DiscoveryConfig, GasConfig, NetworksConfig,
810
OrderConfig, SettlementConfig, SolverConfig, StorageConfig, StrategyConfig,
@@ -16,6 +18,7 @@ use std::collections::HashMap;
1618
pub struct ConfigBuilder {
1719
solver_id: String,
1820
monitoring_timeout_minutes: u64,
21+
min_profitability_pct: Decimal,
1922
storage_primary: String,
2023
storage_cleanup_interval_seconds: u64,
2124
min_confirmations: u64,
@@ -38,6 +41,7 @@ impl ConfigBuilder {
3841
Self {
3942
solver_id: "test-solver".to_string(),
4043
monitoring_timeout_minutes: 1,
44+
min_profitability_pct: Decimal::ZERO,
4145
storage_primary: "memory".to_string(),
4246
storage_cleanup_interval_seconds: 60,
4347
min_confirmations: 1,
@@ -109,12 +113,19 @@ impl ConfigBuilder {
109113
self
110114
}
111115

116+
/// Sets the minimum profitability percentage.
117+
pub fn with_min_profitability_pct(mut self, min_profitability_pct: Decimal) -> Self {
118+
self.min_profitability_pct = min_profitability_pct;
119+
self
120+
}
121+
112122
/// Builds the `Config` with the configured values.
113123
pub fn build(self) -> Config {
114124
Config {
115125
solver: SolverConfig {
116126
id: self.solver_id,
117127
monitoring_timeout_minutes: self.monitoring_timeout_minutes,
128+
min_profitability_pct: self.min_profitability_pct,
118129
},
119130
networks: self.networks.unwrap_or_default(),
120131
storage: StorageConfig {

crates/solver-config/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ mod loader;
1616
pub use builders::config::ConfigBuilder;
1717

1818
use regex::Regex;
19+
use rust_decimal::Decimal;
1920
use serde::{Deserialize, Serialize};
2021
use solver_types::{networks::deserialize_networks, NetworksConfig};
2122
use std::collections::HashMap;
@@ -97,6 +98,8 @@ pub struct SolverConfig {
9798
/// Defaults to 480 minutes (8 hours) if not specified.
9899
#[serde(default = "default_monitoring_timeout_minutes")]
99100
pub monitoring_timeout_minutes: u64,
101+
/// Minimum profitability percentage required to execute orders.
102+
pub min_profitability_pct: Decimal,
100103
}
101104

102105
/// Returns the default monitoring timeout in minutes.
@@ -708,6 +711,7 @@ mod tests {
708711
[solver]
709712
id = "${TEST_SOLVER_ID}"
710713
monitoring_timeout_minutes = 5
714+
min_profitability_pct = 1.0
711715
712716
[networks.1]
713717
input_settler_address = "0x1234567890123456789012345678901234567890"
@@ -759,6 +763,10 @@ network_ids = [1, 2]
759763

760764
let config: Config = config_str.parse().unwrap();
761765
assert_eq!(config.solver.id, "test-solver");
766+
assert_eq!(
767+
config.solver.min_profitability_pct,
768+
Decimal::from_str("1.0").unwrap()
769+
);
762770

763771
// Clean up
764772
std::env::remove_var("TEST_SOLVER_ID");
@@ -770,6 +778,7 @@ network_ids = [1, 2]
770778
[solver]
771779
id = "test"
772780
monitoring_timeout_minutes = 5
781+
min_profitability_pct = 5.0 # Minimum profitability percentage required to execute orders
773782
774783
[networks.1]
775784
input_settler_address = "0x1234567890123456789012345678901234567890"
@@ -853,6 +862,7 @@ network_ids = [2, 3] # Network 2 overlaps with impl1
853862
[solver]
854863
id = "test"
855864
monitoring_timeout_minutes = 5
865+
min_profitability_pct = 5.0 # Minimum profitability percentage required to execute orders
856866
857867
[networks.1]
858868
input_settler_address = "0x1234567890123456789012345678901234567890"
@@ -913,6 +923,7 @@ network_ids = [1, 2]
913923
[solver]
914924
id = "test"
915925
monitoring_timeout_minutes = 5
926+
min_profitability_pct = 5.0 # Minimum profitability percentage required to execute orders
916927
917928
[networks.1]
918929
input_settler_address = "0x1234567890123456789012345678901234567890"
@@ -975,6 +986,7 @@ network_ids = [1, 2, 999] # Network 999 doesn't exist
975986
[solver]
976987
id = "test"
977988
monitoring_timeout_minutes = 5
989+
min_profitability_pct = 5.0 # Minimum profitability percentage required to execute orders
978990
979991
[networks.1]
980992
input_settler_address = "0x1234567890123456789012345678901234567890"

0 commit comments

Comments
 (0)