#![expect(
clippy::expect_used,
clippy::panic,
clippy::unwrap_used,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::doc_markdown,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::too_many_arguments
)]
use alloy::primitives::{keccak256, Address, Bytes, U256, U512};
use revm::context::TxEnv;
use revm::context_interface::result::Output;
use revm::primitives::TxKind;
use revm::{ExecuteCommitEvm, ExecuteEvm};
use std::path::PathBuf;
use crate::oracle::{
call_bytes, decode_error_string, deploy, native_balance_of, new_fixture_evm,
set_code_size_limits, set_disable_nonce_check, set_native_balance, set_tx_gas_limit_cap,
transact, FixtureEvm, TxSpec, Verdict,
};
pub mod declarative;
pub use declarative::{assert_erc6909_capture, assert_profitable, ChainResult, Hop, HopPool};
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.canonicalize()
.expect("canonicalize repo root")
}
fn load_hex(rel: &str) -> Vec<u8> {
let raw = std::fs::read_to_string(repo_root().join(rel))
.unwrap_or_else(|e| panic!("read {rel}: {e}"));
let s: String = raw.trim().chars().filter(|c| !c.is_whitespace()).collect();
let s = s.strip_prefix("0x").unwrap_or(&s);
alloy::hex::decode(s).expect("hex decodes")
}
fn executor_deploy_args(weth: Address, pool_manager: Address) -> Vec<u8> {
let mut args = vec![0u8; 64];
args[12..32].copy_from_slice(weth.as_slice());
args[44..64].copy_from_slice(pool_manager.as_slice());
args
}
fn load_stub_creation(contract: &str) -> Vec<u8> {
let rel = format!("tier3-oracle/artifacts/harness/{contract}.json");
let raw = std::fs::read_to_string(repo_root().join(&rel))
.unwrap_or_else(|e| panic!("read {rel}: {e}"));
crate::oracle::parse_foundry_creation_bytecode(&raw)
.unwrap_or_else(|e| panic!("parse {contract}: {e}"))
}
#[derive(Debug, Clone, Copy)]
pub struct V2Pool {
pub pair: Address,
pub token0: Address,
pub token1: Address,
pub reserve0: u128,
pub reserve1: u128,
}
#[derive(Debug, Clone, Copy)]
pub struct V3Pool {
pub pool: Address,
pub token0: Address,
pub token1: Address,
pub fee: u32,
pub sqrt_price: U256,
pub liquidity: u128,
}
#[derive(Debug, Clone, Copy)]
pub struct V4Pool {
pub currency0: Address,
pub currency1: Address,
pub fee: u32,
pub tick_spacing: i32,
pub sqrt_price: U256,
pub liquidity: u128,
}
pub struct Harness {
pub evm: FixtureEvm,
pub executor: Address,
pub weth: Address,
pub pool_manager: Address,
pub pools: Vec<V2Pool>,
pub v3_pools: Vec<V3Pool>,
pub v4_pools: Vec<V4Pool>,
pub tokens: Vec<Address>,
}
impl Harness {
pub fn new() -> Result<Self, String> {
let mut evm = new_fixture_evm();
set_disable_nonce_check(&mut evm, true);
set_code_size_limits(&mut evm, None); set_tx_gas_limit_cap(&mut evm, u64::MAX);
let weth = deploy(
&mut evm,
Bytes::from(load_stub_creation("Token")),
8_000_000,
)?;
let pm = deploy(
&mut evm,
Bytes::from(load_stub_creation("PoolManager")),
8_000_000,
)?;
let mut init = load_hex("tier3-oracle/artifacts/executor/cmd_executor.creation.hex");
init.extend_from_slice(&executor_deploy_args(weth, pm));
let executor = deploy(&mut evm, Bytes::from(init), 30_000_000)?;
Ok(Self {
evm,
executor,
weth,
pool_manager: pm,
pools: Vec::new(),
v3_pools: Vec::new(),
v4_pools: Vec::new(),
tokens: vec![weth],
})
}
pub fn deploy_stub(&mut self, name: &str) -> Result<Address, String> {
deploy(
&mut self.evm,
Bytes::from(load_stub_creation(name)),
8_000_000,
)
}
pub fn add_token(&mut self) -> Result<Address, String> {
let t = deploy(
&mut self.evm,
Bytes::from(load_stub_creation("Token")),
8_000_000,
)?;
self.tokens.push(t);
Ok(t)
}
pub fn add_pool(
&mut self,
token_a: Address,
token_b: Address,
reserve_a: u128,
reserve_b: u128,
) -> Result<V2Pool, String> {
let pair = deploy(
&mut self.evm,
Bytes::from(load_stub_creation("Pair")),
8_000_000,
)?;
let _ = self.call(pair, &init_pair(token_a, token_b), 500_000)?;
let (t0, r0) = if token_a < token_b {
(token_a, reserve_a)
} else {
(token_b, reserve_b)
};
let (t1, r1) = if token_a < token_b {
(token_b, reserve_b)
} else {
(token_a, reserve_a)
};
self.call(t0, &mint_to(pair, r0), 200_000)?;
self.call(t1, &mint_to(pair, r1), 200_000)?;
let _ = self.call(pair, &sync_selector(), 200_000)?;
for t in [t0, t1] {
if !self.tokens.contains(&t) {
self.tokens.push(t);
}
}
let pool = V2Pool {
pair,
token0: t0,
token1: t1,
reserve0: r0,
reserve1: r1,
};
self.pools.push(pool);
Ok(pool)
}
pub fn add_v3_pool(
&mut self,
token_a: Address,
token_b: Address,
fee: u32,
sqrt_price: U256,
liquidity: u128,
amt_a: u128,
amt_b: u128,
) -> Result<V3Pool, String> {
let pool = deploy(
&mut self.evm,
Bytes::from(load_stub_creation("PoolV3")),
8_000_000,
)?;
let _ = self.call(pool, &init_v3(token_a, token_b, fee), 500_000)?;
let _ = self.call(pool, &set_v3_price(sqrt_price), 200_000)?;
let _ = self.call(pool, &set_v3_liquidity(liquidity), 200_000)?;
self.call(token_a, &mint_to(pool, amt_a), 200_000)?;
self.call(token_b, &mint_to(pool, amt_b), 200_000)?;
for t in [token_a, token_b] {
if !self.tokens.contains(&t) {
self.tokens.push(t);
}
}
let v3 = V3Pool {
pool,
token0: token_a,
token1: token_b,
fee,
sqrt_price,
liquidity,
};
self.v3_pools.push(v3);
Ok(v3)
}
pub fn add_v4_pool(
&mut self,
c0: Address,
c1: Address,
fee: u32,
tick_spacing: i32,
sqrt_price: U256,
liquidity: u128,
fund_c0: u128,
fund_c1: u128,
) -> Result<V4Pool, String> {
let _ = self.call(
self.pool_manager,
&init_v4(c0, c1, fee, tick_spacing, sqrt_price, liquidity),
500_000,
)?;
for (c, amt) in [(c0, fund_c0), (c1, fund_c1)] {
let _ = self.call(self.pool_manager, &fund_v4(c, amt), 200_000)?;
if c == Address::ZERO {
let held = native_balance_of(&mut self.evm, self.pool_manager);
set_native_balance(&mut self.evm, self.pool_manager, held + U256::from(amt));
}
}
for t in [c0, c1] {
if !self.tokens.contains(&t) {
self.tokens.push(t);
}
}
let v4 = V4Pool {
currency0: c0,
currency1: c1,
fee,
tick_spacing,
sqrt_price,
liquidity,
};
self.v4_pools.push(v4);
Ok(v4)
}
pub fn fund(&mut self, token: Address, who: Address, amount: u128) -> Result<(), String> {
if token == Address::ZERO {
let held = native_balance_of(&mut self.evm, who);
set_native_balance(&mut self.evm, who, held + U256::from(amount));
return Ok(());
}
self.call(token, &mint_to(who, amount), 200_000)
.map(|_| ())?;
if token == self.weth {
let backing = native_balance_of(&mut self.evm, token);
set_native_balance(&mut self.evm, token, backing + U256::from(amount));
}
Ok(())
}
pub fn set_native_balance(&mut self, who: Address, amount: U256) {
set_native_balance(&mut self.evm, who, amount);
}
pub fn native_balance_of(&mut self, who: Address) -> Result<U256, String> {
Ok(native_balance_of(&mut self.evm, who))
}
pub fn executor_approve_pair(&mut self, pool: V2Pool) -> Result<(), String> {
for t in [pool.token0, pool.token1] {
let data = approve_data(pool.pair, U256::MAX);
self.call_as_executor(t, &data, 200_000)?;
}
Ok(())
}
pub fn call(&mut self, to: Address, data: &[u8], gas: u64) -> Result<Bytes, String> {
call_bytes(&mut self.evm, to, Bytes::copy_from_slice(data), gas)
}
pub fn call_as_executor(
&mut self,
to: Address,
data: &[u8],
gas: u64,
) -> Result<Bytes, String> {
let tx = TxEnv::builder()
.kind(TxKind::Call(to))
.gas_limit(gas)
.data(Bytes::copy_from_slice(data))
.build()
.expect("valid call tx env");
match self.evm.transact(tx) {
Ok(res) => {
let out = match res.result {
revm::context_interface::result::ExecutionResult::Success {
output: Output::Call(b),
..
} => b,
revm::context_interface::result::ExecutionResult::Success {
output: Output::Create(..),
..
} => return Err("call returned Create".into()),
revm::context_interface::result::ExecutionResult::Revert { output, .. } => {
self.evm.commit(res.state);
return Err(format!("call_as_executor reverted: {output:?}"));
}
revm::context_interface::result::ExecutionResult::Halt { reason, .. } => {
self.evm.commit(res.state);
return Err(format!("call_as_executor halted: {reason:?}"));
}
};
self.evm.commit(res.state);
Ok(out)
}
Err(e) => Err(format!("call_as_executor transact err: {e:?}")),
}
}
pub fn execute_payload(&mut self, payload: &[u8], gas: u64) -> Result<ExecOutcome, String> {
self.execute_payload_config(payload, gas, U256::ZERO)
}
pub fn execute_payload_config(
&mut self,
payload: &[u8],
gas: u64,
config: U256,
) -> Result<ExecOutcome, String> {
let data = execute_data_config(payload, config);
match transact(
&mut self.evm,
TxSpec::Call {
to: self.executor,
data,
gas,
},
) {
Verdict::Accepted { logs, .. } => {
let swaps =
count_swap_events(&logs, &self.pools, &self.v3_pools, self.pool_manager);
Ok(ExecOutcome::Accepted { swaps })
}
Verdict::Reverted(r) => {
let reason = decode_error_string(&r);
Ok(ExecOutcome::Reverted {
reason,
raw: r.to_vec(),
})
}
Verdict::Halted(h) => Ok(ExecOutcome::Halted(h)),
}
}
pub fn balance_of(&mut self, token: Address, account: Address) -> Result<U256, String> {
let out = self.call(token, &balance_of_data(account), 200_000)?;
if out.len() < 32 {
return Err(format!("balanceOf returned {} bytes", out.len()));
}
Ok(U256::from_be_bytes::<32>(
out.as_ref()[..32].try_into().unwrap(),
))
}
pub fn pm_balance_of(&mut self, account: Address, currency: Address) -> Result<U256, String> {
let out = self.call(
self.pool_manager,
&pm_balance_of_data(account, currency),
200_000,
)?;
if out.len() < 32 {
return Err(format!("PM.balanceOf returned {} bytes", out.len()));
}
Ok(U256::from_be_bytes::<32>(
out.as_ref()[..32].try_into().unwrap(),
))
}
pub fn run_v2_path(
&mut self,
pool_indices: &[usize],
zfo: &[bool],
optimal_input: u128,
hop_outputs: &[u128],
gas: u64,
) -> Result<ExecOutcome, String> {
use degenbot_executor::composers::{HopInfo, PathInfo, V2HopInfo};
let n = pool_indices.len();
debug_assert_eq!(n, zfo.len());
debug_assert_eq!(n, hop_outputs.len());
let mut hops = Vec::with_capacity(n);
for (i, &pi) in pool_indices.iter().enumerate() {
let pool = self.pools[pi];
let hop = V2HopInfo {
pool_address: pool.pair,
token0_address: pool.token0,
token1_address: pool.token1,
fee: 30, zfo: zfo[i],
};
hops.push(HopInfo::V2(hop));
}
let path = PathInfo::new(hops);
self.run_path(&path, optimal_input, hop_outputs, gas)
}
pub fn run_path(
&mut self,
path: °enbot_executor::composers::PathInfo,
optimal_input: u128,
hop_outputs: &[u128],
gas: u64,
) -> Result<ExecOutcome, String> {
self.run_path_with_opts(
path,
optimal_input,
hop_outputs,
gas,
degenbot_executor::composers::EncodeOptions::default(),
)
}
pub fn run_path_with_opts(
&mut self,
path: °enbot_executor::composers::PathInfo,
optimal_input: u128,
hop_outputs: &[u128],
gas: u64,
opts: degenbot_executor::composers::EncodeOptions,
) -> Result<ExecOutcome, String> {
let cmd = self.encode_path_with_opts(path, optimal_input, hop_outputs, opts)?;
self.execute_payload_config(&cmd, gas, production_config(opts)?)
}
pub fn run_path_with_consumed(
&mut self,
path: °enbot_executor::composers::PathInfo,
optimal_input: u128,
hop_outputs: &[u128],
consumed_inputs: &[u128],
gas: u64,
opts: degenbot_executor::composers::EncodeOptions,
) -> Result<ExecOutcome, String> {
let cmd = self.encode_path_with_consumed(
path,
optimal_input,
hop_outputs,
consumed_inputs,
opts,
)?;
self.execute_payload_config(&cmd, gas, production_config(opts)?)
}
pub fn encode_path_with_consumed(
&self,
path: °enbot_executor::composers::PathInfo,
optimal_input: u128,
hop_outputs: &[u128],
consumed_inputs: &[u128],
opts: degenbot_executor::composers::EncodeOptions,
) -> Result<Vec<u8>, String> {
if hop_outputs.len() != path.hops.len() || consumed_inputs.len() != path.hops.len() {
return Err(format!(
"encode_path_with_consumed: per-hop arrays must have one entry per hop ({} hops, {} outputs, {} consumed)",
path.hops.len(),
hop_outputs.len(),
consumed_inputs.len()
));
}
degenbot_executor::composers::encode_cmd_stream(
&self.encode_context(),
°enbot_executor::composers::EncodeRequest::new(
path.clone(),
optimal_input,
hop_outputs.to_vec(),
consumed_inputs.to_vec(),
opts,
),
)
.ok_or_else(|| "encode_cmd_stream returned None".to_string())
}
pub fn encode_path(
&self,
path: °enbot_executor::composers::PathInfo,
optimal_input: u128,
hop_outputs: &[u128],
) -> Result<Vec<u8>, String> {
self.encode_path_with_opts(
path,
optimal_input,
hop_outputs,
degenbot_executor::composers::EncodeOptions::default(),
)
}
#[must_use]
pub fn encode_context(&self) -> degenbot_executor::composers::EncodeContext {
degenbot_executor::composers::EncodeContext::new(
self.executor,
self.pool_manager,
self.weth,
)
}
pub fn encode_path_with_opts(
&self,
path: °enbot_executor::composers::PathInfo,
optimal_input: u128,
hop_outputs: &[u128],
opts: degenbot_executor::composers::EncodeOptions,
) -> Result<Vec<u8>, String> {
let n = path.hops.len();
let consumed: Vec<u128> = std::iter::once(optimal_input)
.chain(hop_outputs.iter().copied())
.take(n)
.collect();
degenbot_executor::composers::encode_cmd_stream(
&self.encode_context(),
°enbot_executor::composers::EncodeRequest::new(
path.clone(),
optimal_input,
hop_outputs.to_vec(),
consumed,
opts,
),
)
.ok_or_else(|| "encode_cmd_stream returned None".to_string())
}
}
fn production_config(opts: degenbot_executor::composers::EncodeOptions) -> Result<U256, String> {
degenbot_executor::composers::config_for_options(opts, U256::ZERO)
.map_err(|e| format!("config_for_options: {e}"))
}
#[derive(Debug)]
pub enum ExecOutcome {
Accepted { swaps: usize },
Reverted {
reason: Option<String>,
raw: Vec<u8>,
},
Halted(String),
}
impl ExecOutcome {
#[must_use]
pub fn executed(&self, expected_swaps: usize) -> bool {
matches!(self, ExecOutcome::Accepted { swaps } if *swaps == expected_swaps)
}
}
fn init_pair(a: Address, b: Address) -> Vec<u8> {
let h = keccak256(b"initialize(address,address)");
let mut out = h.0[..4].to_vec();
out.extend_from_slice(&pad32(a));
out.extend_from_slice(&pad32(b));
out
}
fn mint_to(to: Address, amount: u128) -> Vec<u8> {
let h = keccak256(b"mint(address,uint256)");
let mut out = h.0[..4].to_vec();
out.extend_from_slice(&pad32(to));
out.extend_from_slice(&U256::from(amount).to_be_bytes::<32>());
out
}
fn sync_selector() -> Vec<u8> {
keccak256(b"sync()").0[..4].to_vec()
}
fn approve_data(spender: Address, amount: U256) -> Vec<u8> {
let h = keccak256(b"approve(address,uint256)");
let mut out = h.0[..4].to_vec();
out.extend_from_slice(&pad32(spender));
out.extend_from_slice(&amount.to_be_bytes::<32>());
out
}
fn balance_of_data(account: Address) -> Vec<u8> {
let h = keccak256(b"balanceOf(address)");
let mut out = h.0[..4].to_vec();
out.extend_from_slice(&pad32(account));
out
}
fn pm_balance_of_data(account: Address, currency: Address) -> Vec<u8> {
let h = keccak256(b"balanceOf(address,uint256)");
let mut out = h.0[..4].to_vec();
out.extend_from_slice(&pad32(account));
out.extend_from_slice(&pad32(currency));
out
}
#[must_use]
pub fn execute_data(payload: &[u8]) -> Bytes {
execute_data_config(payload, U256::ZERO)
}
#[must_use]
pub fn execute_data_config(payload: &[u8], config: U256) -> Bytes {
let executor = Address::ZERO; match degenbot_executor::composers::encode_execute_call(executor, payload, config) {
Ok(call) => Bytes::from(call.data),
Err(e) => {
let _ = e;
Bytes::new()
}
}
}
fn pad32(a: Address) -> [u8; 32] {
let mut w = [0u8; 32];
w[12..32].copy_from_slice(a.as_slice());
w
}
fn init_v3(a: Address, b: Address, fee: u32) -> Vec<u8> {
let h = keccak256(b"initialize(address,address,uint24)");
let mut out = h.0[..4].to_vec();
out.extend_from_slice(&pad32(a));
out.extend_from_slice(&pad32(b));
out.extend_from_slice(&U256::from(fee).to_be_bytes::<32>());
out
}
fn set_v3_price(p: U256) -> Vec<u8> {
let h = keccak256(b"setPrice(uint160)");
let mut out = h.0[..4].to_vec();
out.extend_from_slice(&p.to_be_bytes::<32>());
out
}
fn set_v3_liquidity(l: u128) -> Vec<u8> {
let h = keccak256(b"setLiquidity(uint128)");
let mut out = h.0[..4].to_vec();
out.extend_from_slice(&U256::from(l).to_be_bytes::<32>());
out
}
fn init_v4(c0: Address, c1: Address, fee: u32, ts: i32, sqrt: U256, liq: u128) -> Vec<u8> {
let h = keccak256(b"initialize(address,address,uint24,int24,uint160,uint128)");
let mut out = h.0[..4].to_vec();
out.extend_from_slice(&pad32(c0));
out.extend_from_slice(&pad32(c1));
out.extend_from_slice(&U256::from(fee).to_be_bytes::<32>());
out.extend_from_slice(&U256::from(ts as u32).to_be_bytes::<32>());
out.extend_from_slice(&sqrt.to_be_bytes::<32>());
out.extend_from_slice(&U256::from(liq).to_be_bytes::<32>());
out
}
fn fund_v4(currency: Address, amt: u128) -> Vec<u8> {
let h = keccak256(b"_fund(address,uint256)");
let mut out = h.0[..4].to_vec();
out.extend_from_slice(&pad32(currency));
out.extend_from_slice(&U256::from(amt).to_be_bytes::<32>());
out
}
#[must_use]
pub fn v3_amount_out(
sqrt_price: U256,
liquidity: u128,
amount_in: u128,
zero_for_one: bool,
fee: u32,
) -> u128 {
use degenbot_math::cl::sqrt_price_math::{
get_amount0_delta, get_amount1_delta, get_next_sqrt_price_from_input,
};
let fee_retained = U256::from(1_000_000u64 - u64::from(fee));
let amount_less_fee = full_mul_div(
U256::from(amount_in),
fee_retained,
U256::from(1_000_000u64),
);
let next = get_next_sqrt_price_from_input(
sqrt_price,
liquidity as i128,
amount_less_fee,
zero_for_one,
)
.expect("valid v3 next price");
let out = if zero_for_one {
get_amount1_delta(next, sqrt_price, liquidity as i128, Some(false))
} else {
get_amount0_delta(next, sqrt_price, liquidity as i128, Some(false))
}
.expect("valid v3 amount delta");
out.to::<u128>()
}
fn full_mul_div(a: U256, b: U256, denom: U256) -> U256 {
let prod = U512::from(a) * U512::from(b);
let q = prod / U512::from(denom);
q.to::<U256>()
}
fn count_swap_events(
logs: &[revm::primitives::Log],
pools: &[V2Pool],
v3_pools: &[V3Pool],
pool_manager: Address,
) -> usize {
let v2_topic = keccak256(b"Swap(address,uint256,uint256,uint256,uint256,address)");
let v3_topic = keccak256(b"SwapV3(address,uint256,uint160,int256)");
let v4_topic = keccak256(b"V4Swap(bytes32,address,address,uint256,uint256)");
logs.iter()
.filter(|l| {
if l.topics().first() == Some(&v2_topic) {
pools.iter().any(|p| l.address == p.pair)
} else if l.topics().first() == Some(&v3_topic) {
v3_pools.iter().any(|p| l.address == p.pool)
} else if l.topics().first() == Some(&v4_topic) {
l.address == pool_manager
} else {
false
}
})
.count()
}