#![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::too_many_arguments
)]
use alloy::primitives::aliases::{I24, U24};
use alloy::primitives::{keccak256, Address, Bytes, U160, U256, U512};
use alloy::sol_types::{SolCall, SolValue};
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> {
(weth, pool_manager).abi_encode()
}
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)
}
}
alloy::sol! {
interface HarnessToken {
function mint(address to, uint256 amount) external;
function approve(address spender, uint256 amount) external returns (bool);
}
interface HarnessPair {
function initialize(address tokenA, address tokenB) external;
function sync() external;
}
interface HarnessV3Pool {
function initialize(address t0, address t1, uint24 fee_) external;
function setPrice(uint160 p) external;
function setLiquidity(uint128 l) external;
}
interface HarnessV4PoolManager {
function initialize(
address c0,
address c1,
uint24 fee,
int24 ts,
uint160 sqrtPriceX96,
uint128 liquidity
) external;
function _fund(address currency, uint256 amt) external;
function balanceOf(address owner, uint256 id) external view returns (uint256);
}
}
fn init_pair(a: Address, b: Address) -> Vec<u8> {
HarnessPair::initializeCall {
tokenA: a,
tokenB: b,
}
.abi_encode()
}
fn mint_to(to: Address, amount: u128) -> Vec<u8> {
HarnessToken::mintCall {
to,
amount: U256::from(amount),
}
.abi_encode()
}
fn sync_selector() -> Vec<u8> {
HarnessPair::syncCall {}.abi_encode()
}
fn approve_data(spender: Address, amount: U256) -> Vec<u8> {
HarnessToken::approveCall { spender, amount }.abi_encode()
}
fn balance_of_data(account: Address) -> Vec<u8> {
degenbot_rpc::abi::encode_balance_of(&account)
}
fn pm_balance_of_data(account: Address, currency: Address) -> Vec<u8> {
HarnessV4PoolManager::balanceOfCall {
owner: account,
id: U256::from_be_slice(currency.as_slice()),
}
.abi_encode()
}
#[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 init_v3(a: Address, b: Address, fee: u32) -> Vec<u8> {
HarnessV3Pool::initializeCall {
t0: a,
t1: b,
fee_: U24::try_from(fee).expect("fee within uint24"),
}
.abi_encode()
}
fn sqrt_price_word(p: U256) -> U160 {
assert_eq!(p >> 160, U256::ZERO, "sqrt price does not fit uint160");
let low: [u8; 20] = p.to_be_bytes::<32>()[12..32]
.try_into()
.expect("low 20 bytes");
U160::from_be_bytes(low)
}
fn set_v3_price(p: U256) -> Vec<u8> {
HarnessV3Pool::setPriceCall {
p: sqrt_price_word(p),
}
.abi_encode()
}
fn set_v3_liquidity(l: u128) -> Vec<u8> {
HarnessV3Pool::setLiquidityCall { l }.abi_encode()
}
fn init_v4(c0: Address, c1: Address, fee: u32, ts: i32, sqrt: U256, liq: u128) -> Vec<u8> {
HarnessV4PoolManager::initializeCall {
c0,
c1,
fee: U24::try_from(fee).expect("fee within uint24"),
ts: I24::try_from(ts).expect("tick spacing within int24"),
sqrtPriceX96: sqrt_price_word(sqrt),
liquidity: liq,
}
.abi_encode()
}
fn fund_v4(currency: Address, amt: u128) -> Vec<u8> {
HarnessV4PoolManager::_fundCall {
currency,
amt: U256::from(amt),
}
.abi_encode()
}
#[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()
}
#[cfg(test)]
mod tests {
use super::*;
use alloy::dyn_abi::DynSolValue;
use alloy::primitives::I256;
const SEL_INIT_PAIR: [u8; 4] = [0x48, 0x5c, 0xc9, 0x55];
const SEL_MINT: [u8; 4] = [0x40, 0xc1, 0x0f, 0x19];
const SEL_SYNC: [u8; 4] = [0xff, 0xf6, 0xca, 0xe9];
const SEL_APPROVE: [u8; 4] = [0x09, 0x5e, 0xa7, 0xb3];
const SEL_BALANCE_OF_ERC20: [u8; 4] = [0x70, 0xa0, 0x82, 0x31];
const SEL_BALANCE_OF_6909: [u8; 4] = [0x00, 0xfd, 0xd5, 0x8e];
const SEL_INIT_V3: [u8; 4] = [0x33, 0x6c, 0x8d, 0x70];
const SEL_SET_V3_PRICE: [u8; 4] = [0xd9, 0x0b, 0xda, 0x4e];
const SEL_SET_V3_LIQUIDITY: [u8; 4] = [0x3d, 0x71, 0x8d, 0xa0];
const SEL_INIT_V4: [u8; 4] = [0x81, 0x4c, 0xc4, 0xe3];
const SEL_FUND_V4: [u8; 4] = [0x9a, 0x3b, 0xcc, 0xf9];
fn ref_calldata(selector: [u8; 4], args: Vec<DynSolValue>) -> Vec<u8> {
let mut out = selector.to_vec();
out.extend_from_slice(&DynSolValue::Tuple(args).abi_encode());
out
}
fn a(n: u8) -> Address {
Address::new([n; 20])
}
#[test]
fn init_pair_calldata() {
assert_eq!(
init_pair(a(0x11), a(0x22)),
ref_calldata(
SEL_INIT_PAIR,
vec![DynSolValue::Address(a(0x11)), DynSolValue::Address(a(0x22))]
)
);
}
#[test]
fn mint_to_calldata() {
let to = a(0x33);
for amount in [0u128, 1, 0xdead_beef, u128::MAX] {
assert_eq!(
mint_to(to, amount),
ref_calldata(
SEL_MINT,
vec![
DynSolValue::Address(to),
DynSolValue::Uint(U256::from(amount), 256)
]
)
);
}
}
#[test]
fn sync_selector_calldata() {
assert_eq!(sync_selector(), SEL_SYNC.to_vec());
assert_eq!(sync_selector().len(), 4);
}
#[test]
fn approve_data_calldata() {
let spender = a(0x44);
for amount in [U256::ZERO, U256::from(1u8), U256::MAX] {
assert_eq!(
approve_data(spender, amount),
ref_calldata(
SEL_APPROVE,
vec![
DynSolValue::Address(spender),
DynSolValue::Uint(amount, 256)
]
)
);
}
}
#[test]
fn balance_of_data_calldata() {
let account = a(0x55);
assert_eq!(
balance_of_data(account),
ref_calldata(SEL_BALANCE_OF_ERC20, vec![DynSolValue::Address(account)])
);
assert_eq!(
balance_of_data(account),
degenbot_rpc::abi::encode_balance_of(&account)
);
}
#[test]
fn pm_balance_of_data_calldata() {
let (account, currency) = (a(0x66), a(0x77));
assert_eq!(
pm_balance_of_data(account, currency),
ref_calldata(
SEL_BALANCE_OF_6909,
vec![
DynSolValue::Address(account),
DynSolValue::Uint(U256::from_be_slice(currency.as_slice()), 256)
]
)
);
}
#[test]
fn init_v3_calldata() {
assert_eq!(
init_v3(a(0x11), a(0x22), 3000),
ref_calldata(
SEL_INIT_V3,
vec![
DynSolValue::Address(a(0x11)),
DynSolValue::Address(a(0x22)),
DynSolValue::Uint(U256::from(3000u32), 24)
]
)
);
}
#[test]
fn set_v3_price_calldata() {
for p in [U256::from(4_295_128_739u64), U256::from(1u8) << 159] {
assert_eq!(
set_v3_price(p),
ref_calldata(SEL_SET_V3_PRICE, vec![DynSolValue::Uint(p, 160)])
);
}
}
#[test]
fn set_v3_liquidity_calldata() {
for l in [0u128, u128::MAX] {
assert_eq!(
set_v3_liquidity(l),
ref_calldata(
SEL_SET_V3_LIQUIDITY,
vec![DynSolValue::Uint(U256::from(l), 128)]
)
);
}
}
#[test]
fn init_v4_calldata() {
let sqrt = U256::from(4_295_128_739u64);
assert_eq!(
init_v4(a(0x11), a(0x22), 3000, 60, sqrt, 1_000),
ref_calldata(
SEL_INIT_V4,
vec![
DynSolValue::Address(a(0x11)),
DynSolValue::Address(a(0x22)),
DynSolValue::Uint(U256::from(3000u32), 24),
DynSolValue::Int(I256::try_from(60i64).unwrap(), 24),
DynSolValue::Uint(sqrt, 160),
DynSolValue::Uint(U256::from(1_000u64), 128)
]
)
);
}
#[test]
fn init_v4_negative_tick_spacing_is_sign_extended() {
let cd = init_v4(
a(0x11),
a(0x22),
3000,
-60,
U256::from(4_295_128_739u64),
1_000,
);
assert!(
cd[100..131].iter().all(|&b| b == 0xff),
"ts word high bytes"
);
assert_eq!(cd[131], 0xc4, "ts word low byte");
}
#[test]
fn fund_v4_calldata() {
assert_eq!(
fund_v4(a(0x88), 5_000),
ref_calldata(
SEL_FUND_V4,
vec![
DynSolValue::Address(a(0x88)),
DynSolValue::Uint(U256::from(5_000u64), 256)
]
)
);
}
#[test]
fn executor_deploy_args_calldata() {
let args = executor_deploy_args(a(0x11), a(0x22));
assert_eq!(
args,
DynSolValue::Tuple(vec![
DynSolValue::Address(a(0x11)),
DynSolValue::Address(a(0x22))
])
.abi_encode()
);
assert_eq!(args.len(), 64);
assert!(args[0..12].iter().all(|&b| b == 0));
assert_eq!(&args[12..32], &a(0x11).into_array());
assert!(args[32..44].iter().all(|&b| b == 0));
assert_eq!(&args[44..64], &a(0x22).into_array());
}
#[test]
fn helpers_equal_encoder_output() {
let (x, y) = (a(0x11), a(0x22));
assert_eq!(
init_pair(x, y),
HarnessPair::initializeCall {
tokenA: x,
tokenB: y
}
.abi_encode()
);
assert_eq!(
mint_to(x, 7),
HarnessToken::mintCall {
to: x,
amount: U256::from(7u8)
}
.abi_encode()
);
assert_eq!(sync_selector(), HarnessPair::syncCall {}.abi_encode());
assert_eq!(
approve_data(x, U256::MAX),
HarnessToken::approveCall {
spender: x,
amount: U256::MAX
}
.abi_encode()
);
assert_eq!(balance_of_data(x), degenbot_rpc::abi::encode_balance_of(&x));
assert_eq!(
pm_balance_of_data(x, y),
HarnessV4PoolManager::balanceOfCall {
owner: x,
id: U256::from_be_slice(y.as_slice())
}
.abi_encode()
);
assert_eq!(
init_v3(x, y, 3000),
HarnessV3Pool::initializeCall {
t0: x,
t1: y,
fee_: U24::from(3000u16)
}
.abi_encode()
);
assert_eq!(
set_v3_price(U256::from(4_295_128_739u64)),
HarnessV3Pool::setPriceCall {
p: U160::from(4_295_128_739u64)
}
.abi_encode()
);
assert_eq!(
set_v3_liquidity(u128::MAX),
HarnessV3Pool::setLiquidityCall { l: u128::MAX }.abi_encode()
);
assert_eq!(
init_v4(x, y, 3000, 60, U256::from(4_295_128_739u64), 1_000),
HarnessV4PoolManager::initializeCall {
c0: x,
c1: y,
fee: U24::from(3000u16),
ts: I24::try_from(60i16).unwrap(),
sqrtPriceX96: U160::from(4_295_128_739u64),
liquidity: 1_000
}
.abi_encode()
);
assert_eq!(
fund_v4(x, 5_000),
HarnessV4PoolManager::_fundCall {
currency: x,
amt: U256::from(5_000u64)
}
.abi_encode()
);
assert_eq!(executor_deploy_args(x, y), (x, y).abi_encode());
}
}