feat: Simulate txs against RPC (#261)
* feat: (WIP) Simulate txs against RPC * fix: include test_executor_addresses.json We should probably fetch this from execution instead. Will do later * feat: (WIP) Attempt to use TokenProxyOverwriteFactory * feat: (WIP) Attempt to use TokenProxyOverwriteFactory * feat: Add simulating execution using debug_traceCall Decoded amount out from execution and compared with the one from simulation Allow for printing the execution traces Moved all execution methods into execution.rs Created traces.rs (heavily clauded) that prints the traces nicely and gets the function selectors frm 4byte (I decided not to use EtherscanIdentifier from foundry because adding foundry as a dependency is really heavy and I expect problems because of it) Temporary: hardcoded storage slots for test_weighted_pool_v4 to pass #time 1h 55m * feat: Use BalanceSlotDetector and AllowanceSlotDetector from tycho_ethereum #time 1h 14m * fix: Improve traces.rs #time 2m * feat: Add skip_execution Also get tycho router bytecode at compile time #time 22m * test: Skip tests that are before the Shanghai upgrade Remove unnecessary TODOs #time 35m * feat: Point tycho dependencies to specific commits #time 9m * fix: Set historical_trade to true in TychoRouterEncoderBuilder #time 41m #time 0m * feat: Improve traces.rs (use foundry instead of 4bytes) Improved the format of the printed trace as well #time 1h 7m * fix: After merge fixes #time 3m * feat: Use new executors_addresses in tycho-execution and read it at startup #time 1h 25m * feat: Create a new flag to enable execution_traces #time 9m * fix: Small code, log and trace improvements Add trying to decode method's calldatas in tracing #time 1m * fix: Simplify simulate_transactions_with_tracing by introducing a new method: bytes_to_fixed_32 #time 13m * fix: After merge fixes #time 1m * fix: Include executor's bytecode at compile time #time 11m --------- Co-authored-by: TAMARA LIPOWSKI <data.lipowski@extaccount.com> Co-authored-by: Diana Carvalho <diana@propellerheads.xyz>
This commit is contained in:
4151
protocol-testing/Cargo.lock
generated
4151
protocol-testing/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,15 +9,18 @@ miette = { version = "7.6.0", features = ["fancy"] }
|
||||
# Logging & Tracing
|
||||
tracing = "0.1.37"
|
||||
# Tycho dependencies
|
||||
tycho-common = "0.83.4"
|
||||
tycho-client = "0.83.4"
|
||||
tycho-simulation = { git = "https://github.com/propeller-heads/tycho-simulation.git", tag = "0.159.0", features = ["evm"] }
|
||||
tycho-common = { git = "https://github.com/propeller-heads/tycho-indexer.git", rev = "28d013a906c497d95e27f01f48fc887fb22dbbbc" }
|
||||
tycho-client = { git = "https://github.com/propeller-heads/tycho-indexer.git", rev = "28d013a906c497d95e27f01f48fc887fb22dbbbc" }
|
||||
tycho-simulation = { git = "https://github.com/propeller-heads/tycho-simulation.git", rev = "6585823a859a29bd64600cc1d2fa7d502d48d3e6" }
|
||||
## TODO: for local development
|
||||
#tycho-simulation = { path = "../../tycho-simulation" }
|
||||
#tycho-simulation = { path = "../../tycho-simulation", features = ["evm"] }
|
||||
tycho-ethereum = { git = "https://github.com/propeller-heads/tycho-indexer.git", rev = "28d013a906c497d95e27f01f48fc887fb22dbbbc", features = ["onchain_data"] }
|
||||
num-bigint = "0.4"
|
||||
num-traits = "0.2"
|
||||
num-rational = "0.4.2"
|
||||
# EVM dependencies
|
||||
alloy = { version = "1.0.27", features = ["arbitrary", "json", "dyn-abi", "sol-types", "contract", "provider-http"] }
|
||||
alloy = { version = "1.0.30", features = ["arbitrary", "json", "dyn-abi", "sol-types", "contract", "provider-http", "providers", "signer-local", "rpc-types-eth", "rpc-types-trace"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde_json = "1.0.140"
|
||||
clap = "4.5.31"
|
||||
@@ -33,3 +36,7 @@ colored = "3.0.0"
|
||||
similar = "2.7.0"
|
||||
termsize = "0.1.9"
|
||||
itertools = "0.14.0"
|
||||
# Foundry dependencies (same versions as tycho-simulation)
|
||||
foundry-config = { git = "https://github.com/foundry-rs/foundry", rev = "5a552bb0de7126fa35170fd84532bbd3d40cd348" }
|
||||
foundry-evm = { git = "https://github.com/foundry-rs/foundry", rev = "5a552bb0de7126fa35170fd84532bbd3d40cd348" }
|
||||
revm = { version = "27.0.3", features = ["alloydb", "serde"] }
|
||||
|
||||
@@ -22,6 +22,8 @@ pub struct ProtocolComponentWithTestConfig {
|
||||
pub base: ProtocolComponentExpectation,
|
||||
#[serde(default = "default_false")]
|
||||
pub skip_simulation: bool,
|
||||
#[serde(default = "default_false")]
|
||||
pub skip_execution: bool,
|
||||
}
|
||||
|
||||
impl ProtocolComponentExpectation {
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
//! Transaction encoding utilities for swap solutions.
|
||||
//!
|
||||
//! This module provides functions to encode swap parameters into executable transactions
|
||||
//! using the Tycho framework. It handles the conversion of high-level swap
|
||||
//! specifications into low-level transaction data that can be executed on-chain.
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use alloy::{primitives::Keccak256, sol_types::SolValue};
|
||||
@@ -16,6 +22,51 @@ use tycho_simulation::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::execution::EXECUTORS_JSON;
|
||||
|
||||
/// Creates a Solution for the given swap parameters.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `component`: The protocol component to swap through
|
||||
/// - `token_in`: Input token address
|
||||
/// - `token_out`: Output token address
|
||||
/// - `amount_in`: Amount of input token to swap
|
||||
/// - `amount_out`: Expected amount of output token
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Result<Solution, EncodingError>` containing the solution, or an error if creation fails.
|
||||
pub fn get_solution(
|
||||
component: ProtocolComponent,
|
||||
token_in: Bytes,
|
||||
token_out: Bytes,
|
||||
amount_in: BigUint,
|
||||
amount_out: BigUint,
|
||||
) -> miette::Result<Solution> {
|
||||
let alice_address = Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2")
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to parse Alice's address for Tycho router encoding")?;
|
||||
|
||||
let swap = SwapBuilder::new(component, token_in.clone(), token_out.clone()).build();
|
||||
|
||||
let slippage = 0.0025; // 0.25% slippage
|
||||
let bps = BigUint::from(10_000u32);
|
||||
let slippage_percent = BigUint::from((slippage * 10000.0) as u32);
|
||||
let multiplier = &bps - slippage_percent;
|
||||
let min_amount_out = (amount_out * &multiplier) / &bps;
|
||||
|
||||
Ok(Solution {
|
||||
sender: alice_address.clone(),
|
||||
receiver: alice_address.clone(),
|
||||
given_token: token_in,
|
||||
given_amount: amount_in,
|
||||
checked_token: token_out,
|
||||
exact_out: false,
|
||||
checked_amount: min_amount_out,
|
||||
swaps: vec![swap],
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Encodes swap data for the Tycho router.
|
||||
///
|
||||
/// Assumes a single swap solution and encodes the data ready to be used by the Tycho router
|
||||
@@ -29,7 +80,7 @@ use tycho_simulation::{
|
||||
/// - `amount_out`: Expected amount of output token
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Result<Transaction>` containing the encoded transaction data for the Tycho
|
||||
/// A `Result<Transaction, EncodingError>` containing the encoded transaction data for the Tycho
|
||||
/// router, or an error if encoding fails.
|
||||
pub fn encode_swap(
|
||||
component: ProtocolComponent,
|
||||
@@ -37,46 +88,34 @@ pub fn encode_swap(
|
||||
token_out: Bytes,
|
||||
amount_in: BigUint,
|
||||
amount_out: BigUint,
|
||||
) -> miette::Result<Transaction> {
|
||||
) -> miette::Result<(Transaction, Solution)> {
|
||||
let chain: tycho_common::models::Chain = Chain::Ethereum.into();
|
||||
let alice_address = Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2")
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to parse Alice's address for Tycho router encoding")?;
|
||||
|
||||
let encoder = TychoRouterEncoderBuilder::new()
|
||||
.chain(chain)
|
||||
.user_transfer_type(UserTransferType::TransferFrom)
|
||||
.executors_addresses(EXECUTORS_JSON.to_string())
|
||||
.historical_trade()
|
||||
.build()
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to build encoder")?;
|
||||
|
||||
let swap = SwapBuilder::new(component, token_in.clone(), token_out.clone()).build();
|
||||
|
||||
let slippage = 0.0025; // 0.25% slippage
|
||||
let bps = BigUint::from(10_000u32);
|
||||
let slippage_percent = BigUint::from((slippage * 10000.0) as u32);
|
||||
let multiplier = &bps - slippage_percent;
|
||||
let min_amount_out = (amount_out * &multiplier) / &bps;
|
||||
|
||||
let solution = Solution {
|
||||
sender: alice_address.clone(),
|
||||
receiver: alice_address.clone(),
|
||||
given_token: token_in,
|
||||
given_amount: amount_in,
|
||||
checked_token: token_out,
|
||||
exact_out: false,
|
||||
checked_amount: min_amount_out,
|
||||
swaps: vec![swap],
|
||||
..Default::default()
|
||||
};
|
||||
let solution = get_solution(component, token_in, token_out, amount_in, amount_out)?;
|
||||
|
||||
let encoded_solution = encoder
|
||||
.encode_solutions(vec![solution.clone()])
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to encode router calldata")?[0]
|
||||
.wrap_err("Failed to encode solution")?[0]
|
||||
.clone();
|
||||
|
||||
encode_tycho_router_call(encoded_solution, &solution, &chain.wrapped_native_token().address)
|
||||
.into_diagnostic()
|
||||
let transaction = encode_tycho_router_call(
|
||||
encoded_solution,
|
||||
&solution,
|
||||
&chain.wrapped_native_token().address,
|
||||
)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to encode router calldata")?;
|
||||
Ok((transaction, solution))
|
||||
}
|
||||
|
||||
/// Encodes a transaction for the Tycho Router using `singleSwap` method and regular token
|
||||
|
||||
328
protocol-testing/src/execution.rs
Normal file
328
protocol-testing/src/execution.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
//! Swap execution simulation module.
|
||||
//!
|
||||
//! This module provides functionality to simulate executing swaps through RPC requests
|
||||
//! using state overwrites and historical blockchain data. It allows testing swap execution
|
||||
//! against specific block states without actually performing on-chain transactions.
|
||||
|
||||
use std::{collections::HashMap, str::FromStr, sync::LazyLock};
|
||||
|
||||
use alloy::{
|
||||
primitives::{Address, U256},
|
||||
rpc::types::{Block, TransactionRequest},
|
||||
};
|
||||
use miette::{miette, IntoDiagnostic, WrapErr};
|
||||
use num_bigint::BigUint;
|
||||
use serde_json::Value;
|
||||
use tracing::info;
|
||||
use tycho_common::traits::{AllowanceSlotDetector, BalanceSlotDetector};
|
||||
use tycho_ethereum::entrypoint_tracer::{
|
||||
allowance_slot_detector::{AllowanceSlotDetectorConfig, EVMAllowanceSlotDetector},
|
||||
balance_slot_detector::{BalanceSlotDetectorConfig, EVMBalanceSlotDetector},
|
||||
};
|
||||
use tycho_simulation::{
|
||||
evm::protocol::u256_num::u256_to_biguint, tycho_execution::encoding::models::Solution,
|
||||
};
|
||||
|
||||
use crate::rpc::RPCProvider;
|
||||
const ROUTER_BYTECODE_JSON: &str = include_str!("../../evm/test/router/TychoRouter.runtime.json");
|
||||
pub const EXECUTORS_JSON: &str = include_str!("../test_executor_addresses.json");
|
||||
|
||||
// Include all executor bytecode files at compile time
|
||||
const UNISWAP_V2_BYTECODE_JSON: &str =
|
||||
include_str!("../../evm/test/executors/UniswapV2.runtime.json");
|
||||
const UNISWAP_V3_BYTECODE_JSON: &str =
|
||||
include_str!("../../evm/test/executors/UniswapV3.runtime.json");
|
||||
const UNISWAP_V4_BYTECODE_JSON: &str =
|
||||
include_str!("../../evm/test/executors/UniswapV4.runtime.json");
|
||||
const BALANCER_V2_BYTECODE_JSON: &str =
|
||||
include_str!("../../evm/test/executors/BalancerV2.runtime.json");
|
||||
const BALANCER_V3_BYTECODE_JSON: &str =
|
||||
include_str!("../../evm/test/executors/BalancerV3.runtime.json");
|
||||
const CURVE_BYTECODE_JSON: &str = include_str!("../../evm/test/executors/Curve.runtime.json");
|
||||
const MAVERICK_V2_BYTECODE_JSON: &str =
|
||||
include_str!("../../evm/test/executors/MaverickV2.runtime.json");
|
||||
const EKUBO_BYTECODE_JSON: &str = include_str!("../../evm/test/executors/Ekubo.runtime.json");
|
||||
|
||||
/// Mapping from protocol component patterns to executor bytecode JSON strings
|
||||
static EXECUTOR_MAPPING: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("uniswap_v2", UNISWAP_V2_BYTECODE_JSON);
|
||||
map.insert("sushiswap", UNISWAP_V2_BYTECODE_JSON);
|
||||
map.insert("pancakeswap_v2", UNISWAP_V2_BYTECODE_JSON);
|
||||
map.insert("uniswap_v3", UNISWAP_V3_BYTECODE_JSON);
|
||||
map.insert("pancakeswap_v3", UNISWAP_V3_BYTECODE_JSON);
|
||||
map.insert("uniswap_v4", UNISWAP_V4_BYTECODE_JSON);
|
||||
map.insert("balancer_v2", BALANCER_V2_BYTECODE_JSON);
|
||||
map.insert("balancer_v3", BALANCER_V3_BYTECODE_JSON);
|
||||
map.insert("curve", CURVE_BYTECODE_JSON);
|
||||
map.insert("maverick_v2", MAVERICK_V2_BYTECODE_JSON);
|
||||
map.insert("ekubo", EKUBO_BYTECODE_JSON);
|
||||
map
|
||||
});
|
||||
|
||||
/// Executor addresses loaded from test_executor_addresses.json at startup
|
||||
pub static EXECUTOR_ADDRESSES: LazyLock<HashMap<String, Address>> = LazyLock::new(|| {
|
||||
let json_value: Value = serde_json::from_str(&EXECUTORS_JSON)
|
||||
.expect("Failed to parse test_executor_addresses.json");
|
||||
|
||||
let ethereum_addresses = json_value["ethereum"]
|
||||
.as_object()
|
||||
.expect("Missing 'ethereum' key in test_executor_addresses.json");
|
||||
|
||||
let mut addresses = HashMap::new();
|
||||
for (protocol_name, address_value) in ethereum_addresses {
|
||||
let address_str = address_value
|
||||
.as_str()
|
||||
.unwrap_or_else(|| panic!("Invalid address format for protocol '{protocol_name}'"));
|
||||
|
||||
let address = Address::from_str(address_str).unwrap_or_else(|_| {
|
||||
panic!("Invalid address '{address_str}' for protocol '{protocol_name}'")
|
||||
});
|
||||
|
||||
addresses.insert(protocol_name.clone(), address);
|
||||
}
|
||||
addresses
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StateOverride {
|
||||
pub code: Option<Vec<u8>>,
|
||||
pub balance: Option<U256>,
|
||||
pub state_diff: HashMap<alloy::primitives::Bytes, alloy::primitives::Bytes>,
|
||||
}
|
||||
|
||||
impl StateOverride {
|
||||
pub fn new() -> Self {
|
||||
Self { code: None, balance: None, state_diff: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn with_code(mut self, code: Vec<u8>) -> Self {
|
||||
self.code = Some(code);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_balance(mut self, balance: U256) -> Self {
|
||||
self.balance = Some(balance);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_state_diff(
|
||||
mut self,
|
||||
slot: alloy::primitives::Bytes,
|
||||
value: alloy::primitives::Bytes,
|
||||
) -> Self {
|
||||
self.state_diff.insert(slot, value);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Get executor bytecode JSON based on component ID
|
||||
fn get_executor_bytecode_json(component_id: &str) -> miette::Result<&'static str> {
|
||||
for (pattern, executor_json) in EXECUTOR_MAPPING.iter() {
|
||||
if component_id.contains(pattern) {
|
||||
return Ok(executor_json);
|
||||
}
|
||||
}
|
||||
Err(miette!("Unknown component type '{}' - no matching executor found", component_id))
|
||||
}
|
||||
|
||||
/// Get executor address for a given component ID
|
||||
fn get_executor_address(component_id: &str) -> miette::Result<Address> {
|
||||
if let Some(&address) = EXECUTOR_ADDRESSES.get(component_id) {
|
||||
return Ok(address);
|
||||
}
|
||||
Err(miette!("No executor address found for component type '{}'", component_id))
|
||||
}
|
||||
|
||||
/// Load executor bytecode from embedded constants based on solution component
|
||||
fn load_executor_bytecode(solution: &Solution) -> miette::Result<Vec<u8>> {
|
||||
let first_swap = solution.swaps.first().unwrap();
|
||||
let component_id = &first_swap.component;
|
||||
|
||||
let executor_json = get_executor_bytecode_json(&component_id.protocol_system)?;
|
||||
|
||||
let json_value: serde_json::Value = serde_json::from_str(executor_json)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to parse executor JSON")?;
|
||||
|
||||
let bytecode_str = json_value["runtimeBytecode"]
|
||||
.as_str()
|
||||
.ok_or_else(|| miette!("No bytecode field found in executor JSON"))?;
|
||||
|
||||
// Remove 0x prefix if present
|
||||
let bytecode_hex =
|
||||
if let Some(stripped) = bytecode_str.strip_prefix("0x") { stripped } else { bytecode_str };
|
||||
|
||||
hex::decode(bytecode_hex)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to decode executor bytecode from hex")
|
||||
}
|
||||
|
||||
/// Calculate gas fees based on block base fee
|
||||
fn calculate_gas_fees(block_header: &Block) -> miette::Result<(U256, U256)> {
|
||||
let base_fee = block_header
|
||||
.header
|
||||
.base_fee_per_gas
|
||||
.ok_or_else(|| miette::miette!("Block does not have base fee (pre-EIP-1559)"))?;
|
||||
// Set max_priority_fee_per_gas to a reasonable value (2 Gwei)
|
||||
let max_priority_fee_per_gas = U256::from(2_000_000_000u64); // 2 Gwei
|
||||
// Set max_fee_per_gas to base_fee * 2 + max_priority_fee_per_gas to handle fee fluctuations
|
||||
let max_fee_per_gas = U256::from(base_fee) * U256::from(2u64) + max_priority_fee_per_gas;
|
||||
|
||||
info!(
|
||||
"Gas pricing: base_fee={}, max_priority_fee_per_gas={}, max_fee_per_gas={}",
|
||||
base_fee, max_priority_fee_per_gas, max_fee_per_gas
|
||||
);
|
||||
|
||||
Ok((max_fee_per_gas, max_priority_fee_per_gas))
|
||||
}
|
||||
|
||||
/// Set up all state overrides needed for simulation
|
||||
async fn setup_state_overrides(
|
||||
solution: &Solution,
|
||||
transaction: &tycho_simulation::tycho_execution::encoding::models::Transaction,
|
||||
user_address: Address,
|
||||
executor_bytecode: &[u8],
|
||||
rpc_url: String,
|
||||
block: &Block,
|
||||
) -> miette::Result<HashMap<Address, StateOverride>> {
|
||||
let mut state_overwrites = HashMap::new();
|
||||
let token_address = Address::from_slice(&solution.given_token[..20]);
|
||||
|
||||
// Extract executor address from the encoded solution's swaps data.
|
||||
// The solution should only have one swap for the test, so this should be safe.
|
||||
let executor_address = if let Some(first_swap) = solution.swaps.first() {
|
||||
get_executor_address(&first_swap.component.protocol_system)?
|
||||
} else {
|
||||
return Err(miette!("No swaps in solution - cannot determine executor address"));
|
||||
};
|
||||
|
||||
// Add bytecode overwrite for the executor
|
||||
state_overwrites
|
||||
.insert(executor_address, StateOverride::new().with_code(executor_bytecode.to_vec()));
|
||||
|
||||
// Add ETH balance override for the user to ensure they have enough gas funds
|
||||
state_overwrites.insert(
|
||||
user_address,
|
||||
StateOverride::new().with_balance(U256::from_str("100000000000000000000").unwrap()), // 100 ETH
|
||||
);
|
||||
|
||||
let detector = EVMBalanceSlotDetector::new(BalanceSlotDetectorConfig {
|
||||
rpc_url: rpc_url.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
.into_diagnostic()?;
|
||||
|
||||
let results = detector
|
||||
.detect_balance_slots(
|
||||
&[solution.given_token.clone()],
|
||||
(**user_address).into(),
|
||||
(*block.header.hash).into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let balance_slot =
|
||||
if let Some(Ok((_storage_addr, slot))) = results.get(&solution.given_token.clone()) {
|
||||
slot
|
||||
} else {
|
||||
return Err(miette!("Couldn't find balance storage slot for token {token_address}"));
|
||||
};
|
||||
|
||||
let detector = EVMAllowanceSlotDetector::new(AllowanceSlotDetectorConfig {
|
||||
rpc_url,
|
||||
..Default::default()
|
||||
})
|
||||
.into_diagnostic()?;
|
||||
|
||||
let results = detector
|
||||
.detect_allowance_slots(
|
||||
&[solution.given_token.clone()],
|
||||
(**user_address).into(),
|
||||
transaction.to.clone(), // tycho router
|
||||
(*block.header.hash).into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let allowance_slot =
|
||||
if let Some(Ok((_storage_addr, slot))) = results.get(&solution.given_token.clone()) {
|
||||
slot
|
||||
} else {
|
||||
return Err(miette!("Couldn't find allowance storage slot for token {token_address}"));
|
||||
};
|
||||
|
||||
state_overwrites.insert(
|
||||
token_address,
|
||||
StateOverride::new()
|
||||
.with_state_diff(
|
||||
alloy::primitives::Bytes::from(allowance_slot.to_vec()),
|
||||
alloy::primitives::Bytes::from(U256::MAX.to_be_bytes::<32>()),
|
||||
)
|
||||
.with_state_diff(
|
||||
alloy::primitives::Bytes::from(balance_slot.to_vec()),
|
||||
alloy::primitives::Bytes::from(U256::MAX.to_be_bytes::<32>()),
|
||||
),
|
||||
);
|
||||
|
||||
Ok(state_overwrites)
|
||||
}
|
||||
|
||||
/// Simulate a trade using eth_call for historical blocks
|
||||
pub async fn simulate_trade_with_eth_call(
|
||||
rpc_provider: &RPCProvider,
|
||||
transaction: &tycho_simulation::tycho_execution::encoding::models::Transaction,
|
||||
solution: &Solution,
|
||||
block_number: u64,
|
||||
block: &Block,
|
||||
) -> miette::Result<BigUint> {
|
||||
let executor_bytecode = load_executor_bytecode(solution)?;
|
||||
let user_address = Address::from_slice(&solution.sender[..20]);
|
||||
let (max_fee_per_gas, max_priority_fee_per_gas) = calculate_gas_fees(block)?;
|
||||
// Convert main transaction to alloy TransactionRequest
|
||||
let execution_tx = TransactionRequest::default()
|
||||
.to(Address::from_slice(&transaction.to[..20]))
|
||||
.input(transaction.data.clone().into())
|
||||
.value(U256::from_str(&transaction.value.to_string()).unwrap_or_default())
|
||||
.from(user_address)
|
||||
.max_fee_per_gas(
|
||||
max_fee_per_gas
|
||||
.try_into()
|
||||
.unwrap_or(u128::MAX),
|
||||
)
|
||||
.max_priority_fee_per_gas(
|
||||
max_priority_fee_per_gas
|
||||
.try_into()
|
||||
.unwrap_or(u128::MAX),
|
||||
);
|
||||
let tycho_router_address = Address::from_slice(&transaction.to[..20]);
|
||||
|
||||
// Copy router storage and code from current block to historical block
|
||||
let router_override = rpc_provider
|
||||
.copy_contract_storage_and_code(tycho_router_address, ROUTER_BYTECODE_JSON)
|
||||
.await
|
||||
.wrap_err("Failed to create router override")?;
|
||||
|
||||
// Set up state overrides including router override
|
||||
let mut state_overwrites = setup_state_overrides(
|
||||
solution,
|
||||
transaction,
|
||||
user_address,
|
||||
&executor_bytecode,
|
||||
rpc_provider.url.to_string(),
|
||||
block,
|
||||
)
|
||||
.await?; // Include executor override for historical blocks
|
||||
|
||||
// Add the router override
|
||||
state_overwrites.insert(tycho_router_address, router_override);
|
||||
|
||||
let execution_amount_out = rpc_provider
|
||||
.simulate_transactions_with_tracing(execution_tx, block_number, state_overwrites)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
info!("Execution transaction failed with error: {}", e);
|
||||
e
|
||||
})
|
||||
.wrap_err("Execution simulation failed")?;
|
||||
|
||||
Ok(u256_to_biguint(execution_amount_out))
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
mod adapter_builder;
|
||||
mod config;
|
||||
mod encoding;
|
||||
mod execution;
|
||||
mod rpc;
|
||||
mod test_runner;
|
||||
mod traces;
|
||||
mod tycho_rpc;
|
||||
mod tycho_runner;
|
||||
mod utils;
|
||||
@@ -38,7 +40,11 @@ struct Args {
|
||||
|
||||
/// Enable tracing during vm simulations
|
||||
#[arg(long, default_value_t = false)]
|
||||
vm_traces: bool,
|
||||
vm_simulation_traces: bool,
|
||||
|
||||
/// Enable tracing during execution simulations
|
||||
#[arg(long, default_value_t = false)]
|
||||
execution_traces: bool,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
@@ -91,7 +97,8 @@ fn main() -> miette::Result<()> {
|
||||
args.package,
|
||||
args.match_test,
|
||||
args.db_url,
|
||||
args.vm_traces,
|
||||
args.vm_simulation_traces,
|
||||
args.execution_traces,
|
||||
);
|
||||
|
||||
test_runner.run_tests()
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use alloy::{
|
||||
contract::{ContractInstance, Interface},
|
||||
dyn_abi::DynSolValue,
|
||||
eips::eip1898::BlockId,
|
||||
primitives::{address, Address, U256},
|
||||
primitives::{address, keccak256, map::AddressHashMap, Address, FixedBytes, U256},
|
||||
providers::{Provider, ProviderBuilder},
|
||||
rpc::types::Block,
|
||||
rpc::types::{
|
||||
state::AccountOverride,
|
||||
trace::geth::{
|
||||
GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingCallOptions,
|
||||
GethDebugTracingOptions,
|
||||
},
|
||||
Block, TransactionRequest,
|
||||
},
|
||||
sol_types::SolValue,
|
||||
transports::http::reqwest::Url,
|
||||
};
|
||||
use miette::{IntoDiagnostic, WrapErr};
|
||||
use serde_json::Value;
|
||||
use tracing::info;
|
||||
use tycho_common::Bytes;
|
||||
|
||||
use crate::{
|
||||
execution::{StateOverride, EXECUTOR_ADDRESSES},
|
||||
traces::print_call_trace,
|
||||
};
|
||||
|
||||
const NATIVE_ALIASES: &[Address] = &[
|
||||
address!("0x0000000000000000000000000000000000000000"),
|
||||
@@ -17,13 +35,14 @@ const NATIVE_ALIASES: &[Address] = &[
|
||||
const ERC_20_ABI: &str = r#"[{"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"}]"#;
|
||||
|
||||
pub struct RPCProvider {
|
||||
url: Url,
|
||||
pub url: Url,
|
||||
trace: bool,
|
||||
}
|
||||
|
||||
impl RPCProvider {
|
||||
pub fn new(url: String) -> Self {
|
||||
pub fn new(url: String, trace: bool) -> Self {
|
||||
let url = url.as_str().parse().unwrap();
|
||||
RPCProvider { url }
|
||||
RPCProvider { url, trace }
|
||||
}
|
||||
|
||||
pub async fn get_token_balance(
|
||||
@@ -81,6 +100,216 @@ impl RPCProvider {
|
||||
.wrap_err("Failed to fetch block header")
|
||||
.and_then(|block_opt| block_opt.ok_or_else(|| miette::miette!("Block not found")))
|
||||
}
|
||||
|
||||
/// Helper function to get the contract's storage at the given slot at the latest block.
|
||||
pub async fn get_storage_at(
|
||||
&self,
|
||||
contract_address: Address,
|
||||
slot: FixedBytes<32>,
|
||||
) -> miette::Result<FixedBytes<32>> {
|
||||
let provider = ProviderBuilder::new().connect_http(self.url.clone());
|
||||
let storage_value = provider
|
||||
.get_storage_at(contract_address, slot.into())
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to fetch storage slot")?;
|
||||
|
||||
Ok(storage_value.into())
|
||||
}
|
||||
|
||||
pub async fn copy_contract_storage_and_code(
|
||||
&self,
|
||||
contract_address: Address,
|
||||
router_bytecode_json: &str,
|
||||
) -> miette::Result<StateOverride> {
|
||||
let json_value: serde_json::Value = serde_json::from_str(router_bytecode_json)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to parse router JSON")?;
|
||||
|
||||
let bytecode_str = json_value["runtimeBytecode"]
|
||||
.as_str()
|
||||
.ok_or_else(|| miette::miette!("No runtimeBytecode field found in router JSON"))?;
|
||||
|
||||
// Remove 0x prefix if present
|
||||
let bytecode_hex = if let Some(stripped) = bytecode_str.strip_prefix("0x") {
|
||||
stripped
|
||||
} else {
|
||||
bytecode_str
|
||||
};
|
||||
|
||||
let router_bytecode = hex::decode(bytecode_hex)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to decode router bytecode from hex")?;
|
||||
|
||||
// Start with the router bytecode override
|
||||
let mut state_override = StateOverride::new().with_code(router_bytecode);
|
||||
|
||||
for (protocol_name, &executor_address) in EXECUTOR_ADDRESSES.iter() {
|
||||
let storage_slot = self.calculate_executor_storage_slot(executor_address);
|
||||
|
||||
match self
|
||||
.get_storage_at(contract_address, storage_slot)
|
||||
.await
|
||||
{
|
||||
Ok(value) => {
|
||||
state_override = state_override.with_state_diff(
|
||||
alloy::primitives::Bytes::from(storage_slot.to_vec()),
|
||||
alloy::primitives::Bytes::from(value.to_vec()),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
info!(
|
||||
"Failed to fetch executor approval for {} ({:?}): {}",
|
||||
protocol_name, executor_address, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(state_override)
|
||||
}
|
||||
|
||||
/// Calculate storage slot for Solidity mapping.
|
||||
///
|
||||
/// The solidity code:
|
||||
/// keccak256(abi.encodePacked(bytes32(key), bytes32(slot)))
|
||||
pub fn calculate_executor_storage_slot(&self, key: Address) -> FixedBytes<32> {
|
||||
// Convert key (20 bytes) to 32-byte left-padded array (uint256)
|
||||
let mut key_bytes = [0u8; 32];
|
||||
key_bytes[12..].copy_from_slice(key.as_slice());
|
||||
|
||||
// The base of the executor storage slot is 1, since there is only one
|
||||
// variable that is initialized before it (which is _roles in AccessControl.sol).
|
||||
// In this case, _roles gets slot 0.
|
||||
// The slots are given in order to the parent contracts' variables first and foremost.
|
||||
let slot = U256::from(1);
|
||||
|
||||
// Convert U256 slot to 32-byte big-endian array
|
||||
let slot_bytes = slot.to_be_bytes::<32>();
|
||||
|
||||
// Concatenate key_bytes + slot_bytes, then keccak hash
|
||||
let mut buf = [0u8; 64];
|
||||
buf[..32].copy_from_slice(&key_bytes);
|
||||
buf[32..].copy_from_slice(&slot_bytes);
|
||||
keccak256(buf)
|
||||
}
|
||||
|
||||
fn bytes_to_fixed_32(bytes: &[u8]) -> [u8; 32] {
|
||||
let mut arr = [0u8; 32];
|
||||
let len = bytes.len().min(32);
|
||||
// Right-pad by copying to the end of the array
|
||||
arr[32 - len..].copy_from_slice(&bytes[bytes.len() - len..]);
|
||||
arr
|
||||
}
|
||||
|
||||
pub async fn simulate_transactions_with_tracing(
|
||||
&self,
|
||||
transaction: TransactionRequest,
|
||||
block_number: u64,
|
||||
state_overwrites: HashMap<Address, StateOverride>,
|
||||
) -> miette::Result<U256> {
|
||||
let provider = ProviderBuilder::new().connect_http(self.url.clone());
|
||||
// Convert our StateOverride to alloy's state override format
|
||||
let mut alloy_state_overrides = AddressHashMap::default();
|
||||
for (address, override_data) in state_overwrites {
|
||||
let mut account_override = AccountOverride::default();
|
||||
|
||||
if let Some(code) = override_data.code {
|
||||
account_override.code = Some(alloy::primitives::Bytes::from(code));
|
||||
}
|
||||
|
||||
if let Some(balance) = override_data.balance {
|
||||
account_override.balance = Some(balance);
|
||||
}
|
||||
|
||||
if !override_data.state_diff.is_empty() {
|
||||
// Convert Bytes to FixedBytes<32> for storage slots
|
||||
let mut state_diff = HashMap::default();
|
||||
for (slot, value) in override_data.state_diff {
|
||||
let slot_bytes = Self::bytes_to_fixed_32(&slot);
|
||||
let value_bytes = Self::bytes_to_fixed_32(&value);
|
||||
state_diff.insert(FixedBytes(slot_bytes), FixedBytes(value_bytes));
|
||||
}
|
||||
account_override.state_diff = Some(state_diff);
|
||||
}
|
||||
|
||||
alloy_state_overrides.insert(address, account_override);
|
||||
}
|
||||
|
||||
// Configure tracing options - use callTracer for better formatted results
|
||||
let tracing_options = GethDebugTracingOptions {
|
||||
tracer: Some(GethDebugTracerType::BuiltInTracer(
|
||||
GethDebugBuiltInTracerType::CallTracer,
|
||||
)),
|
||||
config: Default::default(),
|
||||
tracer_config: Default::default(),
|
||||
timeout: None,
|
||||
};
|
||||
|
||||
let trace_options = GethDebugTracingCallOptions {
|
||||
tracing_options,
|
||||
state_overrides: if alloy_state_overrides.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(alloy_state_overrides)
|
||||
},
|
||||
block_overrides: None,
|
||||
};
|
||||
|
||||
let result: Value = provider
|
||||
.client()
|
||||
.request("debug_traceCall", (transaction, BlockId::from(block_number), trace_options))
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to debug trace call many")?;
|
||||
|
||||
if self.trace {
|
||||
print_call_trace(&result, 0).await;
|
||||
}
|
||||
let has_error = result
|
||||
.as_object()
|
||||
.and_then(|obj| obj.get("error"))
|
||||
.is_some();
|
||||
|
||||
let has_failed = result
|
||||
.as_object()
|
||||
.and_then(|obj| obj.get("failed"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
if has_error || has_failed {
|
||||
if let Some(result_obj) = result.as_object() {
|
||||
if let Some(error) = result_obj.get("error") {
|
||||
return Err(miette::miette!("Transaction execution failed: {}", error));
|
||||
}
|
||||
if let Some(revert_reason) = result_obj.get("revertReason") {
|
||||
return Err(miette::miette!("Transaction reverted: {}", revert_reason));
|
||||
}
|
||||
}
|
||||
return Err(miette::miette!("Transaction failed"));
|
||||
} else {
|
||||
info!("Transaction successfully simulated.");
|
||||
}
|
||||
|
||||
let mut executed_amount_out = U256::ZERO;
|
||||
if let Some(result_obj) = result.as_object() {
|
||||
if let Some(gas_used) = result_obj
|
||||
.get("gasUsed")
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
let gas_used_decoded = U256::from_str_radix(gas_used.trim_start_matches("0x"), 16)
|
||||
.into_diagnostic()?;
|
||||
info!("Gas used: {}", gas_used_decoded);
|
||||
}
|
||||
if let Some(output) = result_obj
|
||||
.get("output")
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
executed_amount_out = U256::abi_decode(&Bytes::from_str(output).into_diagnostic()?)
|
||||
.into_diagnostic()?;
|
||||
}
|
||||
}
|
||||
Ok(executed_amount_out)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -95,7 +324,7 @@ mod tests {
|
||||
async fn get_token_balance_native_token() {
|
||||
let eth_rpc_url = env::var("RPC_URL").expect("Missing RPC_URL in environment");
|
||||
|
||||
let rpc_provider = RPCProvider::new(eth_rpc_url);
|
||||
let rpc_provider = RPCProvider::new(eth_rpc_url, false);
|
||||
let token_address = address!("0x0000000000000000000000000000000000000000");
|
||||
let wallet_address = address!("0x787B8840100d9BaAdD7463f4a73b5BA73B00C6cA");
|
||||
let block_number = 21998530;
|
||||
@@ -115,7 +344,7 @@ mod tests {
|
||||
async fn get_token_balance_erc20_token() {
|
||||
let eth_rpc_url = env::var("RPC_URL").expect("Missing RPC_URL in environment");
|
||||
|
||||
let rpc_provider = RPCProvider::new(eth_rpc_url);
|
||||
let rpc_provider = RPCProvider::new(eth_rpc_url, false);
|
||||
let token_address = address!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48");
|
||||
let wallet_address = address!("0x787B8840100d9BaAdD7463f4a73b5BA73B00C6cA");
|
||||
let block_number = 21998530;
|
||||
@@ -132,7 +361,7 @@ mod tests {
|
||||
async fn get_block_header() {
|
||||
let eth_rpc_url = env::var("RPC_URL").expect("Missing RPC_URL in environment");
|
||||
|
||||
let rpc_provider = RPCProvider::new(eth_rpc_url);
|
||||
let rpc_provider = RPCProvider::new(eth_rpc_url, false);
|
||||
let block_number = 21998530;
|
||||
|
||||
let block_header = rpc_provider
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::{collections::HashMap, env, path::PathBuf, str::FromStr, sync::LazyLock};
|
||||
|
||||
use alloy::primitives::U256;
|
||||
use alloy::primitives::{Address, U256};
|
||||
use figment::{
|
||||
providers::{Format, Yaml},
|
||||
Figment,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use miette::{miette, IntoDiagnostic, WrapErr};
|
||||
use num_bigint::BigUint;
|
||||
use num_traits::Zero;
|
||||
use num_bigint::{BigInt, BigUint};
|
||||
use num_rational::BigRational;
|
||||
use num_traits::{Signed, ToPrimitive, Zero};
|
||||
use postgres::{Client, Error, NoTls};
|
||||
use tokio::runtime::Runtime;
|
||||
use tracing::{debug, error, info, warn};
|
||||
@@ -39,6 +40,7 @@ use crate::{
|
||||
adapter_builder::AdapterContractBuilder,
|
||||
config::{IntegrationTest, IntegrationTestsConfig, ProtocolComponentWithTestConfig},
|
||||
encoding::encode_swap,
|
||||
execution,
|
||||
rpc::RPCProvider,
|
||||
tycho_rpc::TychoClient,
|
||||
tycho_runner::TychoRunner,
|
||||
@@ -54,7 +56,8 @@ static CLONE_TO_BASE_PROTOCOL: LazyLock<HashMap<&str, &str>> = LazyLock::new(||
|
||||
|
||||
pub struct TestRunner {
|
||||
db_url: String,
|
||||
vm_traces: bool,
|
||||
vm_simulation_traces: bool,
|
||||
execution_traces: bool,
|
||||
substreams_path: PathBuf,
|
||||
adapter_contract_builder: AdapterContractBuilder,
|
||||
match_test: Option<String>,
|
||||
@@ -68,6 +71,7 @@ impl TestRunner {
|
||||
match_test: Option<String>,
|
||||
db_url: String,
|
||||
vm_traces: bool,
|
||||
execution_traces: bool,
|
||||
) -> Self {
|
||||
let base_protocol = CLONE_TO_BASE_PROTOCOL
|
||||
.get(protocol.as_str())
|
||||
@@ -96,7 +100,8 @@ impl TestRunner {
|
||||
|
||||
Self {
|
||||
db_url,
|
||||
vm_traces,
|
||||
vm_simulation_traces: vm_traces,
|
||||
execution_traces,
|
||||
substreams_path,
|
||||
adapter_contract_builder,
|
||||
match_test,
|
||||
@@ -112,10 +117,7 @@ impl TestRunner {
|
||||
|
||||
// Skip if test files don't exist
|
||||
if !self.config_file_path.exists() {
|
||||
warn!(
|
||||
"Config file not found at {}.",
|
||||
self.config_file_path.display()
|
||||
);
|
||||
warn!("Config file not found at {}.", self.config_file_path.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -237,7 +239,8 @@ impl TestRunner {
|
||||
stop_block,
|
||||
config,
|
||||
&self.adapter_contract_builder,
|
||||
self.vm_traces,
|
||||
self.vm_simulation_traces,
|
||||
self.execution_traces,
|
||||
)
|
||||
},
|
||||
&test.expected_components,
|
||||
@@ -268,7 +271,8 @@ fn validate_state(
|
||||
stop_block: u64,
|
||||
config: &IntegrationTestsConfig,
|
||||
adapter_contract_builder: &AdapterContractBuilder,
|
||||
vm_traces: bool,
|
||||
vm_simulation_traces: bool,
|
||||
execution_traces: bool,
|
||||
) -> miette::Result<()> {
|
||||
let rt = Runtime::new().unwrap();
|
||||
|
||||
@@ -314,10 +318,8 @@ fn validate_state(
|
||||
.map(|s| (s.component_id.to_lowercase(), s))
|
||||
.collect();
|
||||
|
||||
info!("Found {} protocol components", components_by_id.len());
|
||||
info!("Found {} protocol states", protocol_states_by_id.len());
|
||||
|
||||
info!("Validating state...");
|
||||
debug!("Found {} protocol components", components_by_id.len());
|
||||
debug!("Found {} protocol states", protocol_states_by_id.len());
|
||||
|
||||
// Step 1: Validate that all expected components are present on Tycho after indexing
|
||||
debug!("Validating {:?} expected components", expected_components.len());
|
||||
@@ -350,10 +352,21 @@ fn validate_state(
|
||||
info!("All expected components were successfully found on Tycho and match the expected state");
|
||||
|
||||
// Step 2: Validate Token Balances
|
||||
let rpc_url = env::var("RPC_URL")
|
||||
.into_diagnostic()
|
||||
.wrap_err("Missing RPC_URL in environment")?;
|
||||
let rpc_provider = RPCProvider::new(rpc_url, execution_traces);
|
||||
|
||||
match config.skip_balance_check {
|
||||
true => info!("Skipping balance check"),
|
||||
false => {
|
||||
validate_token_balances(&components_by_id, &protocol_states_by_id, start_block, &rt)?;
|
||||
validate_token_balances(
|
||||
&components_by_id,
|
||||
&protocol_states_by_id,
|
||||
start_block,
|
||||
&rt,
|
||||
&rpc_provider,
|
||||
)?;
|
||||
info!("All token balances match the values found onchain")
|
||||
}
|
||||
}
|
||||
@@ -368,7 +381,7 @@ fn validate_state(
|
||||
|
||||
info!("Components to simulate: {}", simulation_component_ids.len());
|
||||
for id in &simulation_component_ids {
|
||||
info!(" Simulating component: {}", id);
|
||||
info!("Simulating component: {}", id);
|
||||
}
|
||||
|
||||
if simulation_component_ids.is_empty() {
|
||||
@@ -376,6 +389,13 @@ fn validate_state(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Filter out components that have skip_execution = true
|
||||
let execution_component_ids: std::collections::HashSet<String> = expected_components
|
||||
.iter()
|
||||
.filter(|c| !c.skip_execution)
|
||||
.map(|c| c.base.id.clone().to_lowercase())
|
||||
.collect();
|
||||
|
||||
let adapter_contract_path;
|
||||
let mut adapter_contract_path_str: Option<&str> = None;
|
||||
|
||||
@@ -386,7 +406,7 @@ fn validate_state(
|
||||
adapter_contract_path = match adapter_contract_builder.find_contract(adapter_contract_name)
|
||||
{
|
||||
Ok(path) => {
|
||||
info!("Found adapter contract at: {}", path.display());
|
||||
debug!("Found adapter contract at: {}", path.display());
|
||||
path
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -412,7 +432,7 @@ fn validate_state(
|
||||
tycho_simulation::evm::engine_db::SHARED_TYCHO_DB.clear();
|
||||
|
||||
let mut decoder = TychoStreamDecoder::new();
|
||||
let mut decoder_context = DecoderContext::new().vm_traces(vm_traces);
|
||||
let mut decoder_context = DecoderContext::new().vm_traces(vm_simulation_traces);
|
||||
|
||||
if let Some(vm_adapter_path) = adapter_contract_path_str {
|
||||
decoder_context = decoder_context.vm_adapter_path(vm_adapter_path);
|
||||
@@ -461,7 +481,8 @@ fn validate_state(
|
||||
state,
|
||||
component: component.clone(),
|
||||
component_tvl: None,
|
||||
// Neither UniswapV4 with hooks not certain balancer pools are currently supported for SDK testing
|
||||
// Neither UniswapV4 with hooks not certain balancer pools are currently supported for
|
||||
// SDK testing
|
||||
entrypoints: vec![],
|
||||
};
|
||||
states.insert(component_id.clone(), component_with_state);
|
||||
@@ -477,10 +498,6 @@ fn validate_state(
|
||||
let bytes = [0u8; 32];
|
||||
|
||||
// Get block header to extract the timestamp
|
||||
let rpc_url = env::var("RPC_URL")
|
||||
.into_diagnostic()
|
||||
.wrap_err("Missing RPC_URL in environment")?;
|
||||
let rpc_provider = RPCProvider::new(rpc_url);
|
||||
let block_header = rt
|
||||
.block_on(rpc_provider.get_block_header(stop_block))
|
||||
.wrap_err("Failed to get block header")?;
|
||||
@@ -505,7 +522,7 @@ fn validate_state(
|
||||
.block_on(tycho_client.get_tokens(Chain::Ethereum, None, None))
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to get tokens")?;
|
||||
info!("Loaded {} tokens", all_tokens.len());
|
||||
debug!("Loaded {} tokens", all_tokens.len());
|
||||
|
||||
rt.block_on(decoder.set_tokens(all_tokens));
|
||||
|
||||
@@ -514,7 +531,7 @@ fn validate_state(
|
||||
let message: FeedMessage = FeedMessage { state_msgs, sync_states: Default::default() };
|
||||
|
||||
let block_msg = rt
|
||||
.block_on(decoder.decode(message))
|
||||
.block_on(decoder.decode(&message))
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to decode message")?;
|
||||
|
||||
@@ -527,7 +544,6 @@ fn validate_state(
|
||||
for (id, state) in block_msg.states.iter() {
|
||||
if let Some(tokens) = pairs.get(id) {
|
||||
let formatted_token_str = format!("{:}/{:}", &tokens[0].symbol, &tokens[1].symbol);
|
||||
info!("Amount out for {}: calculating for tokens {:?}", id, formatted_token_str);
|
||||
state
|
||||
.spot_price(&tokens[0], &tokens[1])
|
||||
.map(|price| info!("Spot price {:?}: {:?}", formatted_token_str, price))
|
||||
@@ -584,7 +600,7 @@ fn validate_state(
|
||||
))?;
|
||||
|
||||
info!(
|
||||
"Amount out for trading {:.1}% of max: ({} {} -> {} {}) (gas: {})",
|
||||
"Simulated amount out for trading {:.1}% of max: ({} {} -> {} {}) (gas: {})",
|
||||
percentage * 100.0,
|
||||
amount_in,
|
||||
token_in.symbol,
|
||||
@@ -593,22 +609,68 @@ fn validate_state(
|
||||
amount_out_result.gas
|
||||
);
|
||||
|
||||
let protocol_component = block_msg.new_pairs.get(id);
|
||||
if let Some(pc) = protocol_component {
|
||||
encode_swap(
|
||||
pc.clone(),
|
||||
token_in.address.clone(),
|
||||
token_out.address.clone(),
|
||||
amount_in,
|
||||
amount_out_result.amount,
|
||||
)?;
|
||||
info!("Encoded swap successfully");
|
||||
// Only execute for components that should have execution
|
||||
if !execution_component_ids.contains(id) {
|
||||
info!("Skipping execution for component {id}");
|
||||
continue;
|
||||
}
|
||||
|
||||
let protocol_component = block_msg.new_pairs.get(id).unwrap();
|
||||
|
||||
let (calldata, solution) = encode_swap(
|
||||
protocol_component.clone(),
|
||||
token_in.address.clone(),
|
||||
token_out.address.clone(),
|
||||
amount_in,
|
||||
amount_out_result.amount.clone(),
|
||||
)?;
|
||||
|
||||
info!("Simulating swap at historical block {}", block_header.header.number);
|
||||
// Simulate the trade using debug_traceCall with overwrites
|
||||
let execution_amount_out =
|
||||
rt.block_on(execution::simulate_trade_with_eth_call(
|
||||
&rpc_provider,
|
||||
&calldata,
|
||||
&solution,
|
||||
stop_block,
|
||||
&block_header,
|
||||
));
|
||||
|
||||
match execution_amount_out {
|
||||
Ok(amount_out) => {
|
||||
info!(
|
||||
"Simulating execution passed with {} {} -> {} {}",
|
||||
solution.given_amount,
|
||||
token_in.symbol,
|
||||
amount_out,
|
||||
token_out.symbol
|
||||
);
|
||||
|
||||
// Compare execution amount out with simulation amount out
|
||||
let diff = BigInt::from(amount_out_result.amount) -
|
||||
BigInt::from(amount_out.clone());
|
||||
|
||||
let slippage: BigRational =
|
||||
BigRational::new(diff.abs(), BigInt::from(amount_out));
|
||||
if slippage.to_f64() > Some(0.05) {
|
||||
return Err(miette!(
|
||||
"Execution amount and simulation amount differ more than 5%!"
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(miette!(
|
||||
"Simulating execution failed for {} -> {}: {}",
|
||||
token_in.symbol,
|
||||
token_out.symbol,
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("✅ Simulation validation passed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -619,12 +681,8 @@ fn validate_token_balances(
|
||||
protocol_states_by_id: &HashMap<String, ResponseProtocolState>,
|
||||
start_block: u64,
|
||||
rt: &Runtime,
|
||||
rpc_provider: &RPCProvider,
|
||||
) -> miette::Result<()> {
|
||||
let rpc_url = env::var("RPC_URL")
|
||||
.into_diagnostic()
|
||||
.wrap_err("Missing RPC_URL in environment")?;
|
||||
let rpc_provider = RPCProvider::new(rpc_url.to_string());
|
||||
|
||||
for (id, component) in components_by_id.iter() {
|
||||
let component_state = protocol_states_by_id.get(id);
|
||||
|
||||
@@ -640,8 +698,8 @@ fn validate_token_balances(
|
||||
}
|
||||
|
||||
info!("Validating token balance for component {} and token {}", component.id, token);
|
||||
let token_address = alloy::primitives::Address::from_slice(&token[..20]);
|
||||
let component_address = alloy::primitives::Address::from_str(component.id.as_str())
|
||||
let token_address = Address::from_slice(&token[..20]);
|
||||
let component_address = Address::from_str(component.id.as_str())
|
||||
.expect("Failed to parse component address");
|
||||
let node_balance = rt.block_on(rpc_provider.get_token_balance(
|
||||
token_address,
|
||||
@@ -666,7 +724,7 @@ fn validate_token_balances(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use dotenv::dotenv;
|
||||
use glob::glob;
|
||||
@@ -725,6 +783,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_token_balance_validation() {
|
||||
dotenv().ok();
|
||||
let eth_rpc_url = env::var("RPC_URL").expect("Missing RPC_URL in environment");
|
||||
let rpc_provider = RPCProvider::new(eth_rpc_url, false);
|
||||
// Setup mock data
|
||||
let block_number = 21998530;
|
||||
let token_bytes = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap();
|
||||
@@ -756,13 +817,21 @@ mod tests {
|
||||
|
||||
let rt = Runtime::new().unwrap();
|
||||
dotenv().ok();
|
||||
let result =
|
||||
validate_token_balances(&components_by_id, &protocol_states_by_id, block_number, &rt);
|
||||
let result = validate_token_balances(
|
||||
&components_by_id,
|
||||
&protocol_states_by_id,
|
||||
block_number,
|
||||
&rt,
|
||||
&rpc_provider,
|
||||
);
|
||||
assert!(result.is_ok(), "Should pass when balance check is performed and balances match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_balance_validation_fails_on_mismatch() {
|
||||
dotenv().ok();
|
||||
let eth_rpc_url = env::var("RPC_URL").expect("Missing RPC_URL in environment");
|
||||
let rpc_provider = RPCProvider::new(eth_rpc_url, false);
|
||||
// Setup mock data
|
||||
let block_number = 21998530;
|
||||
let token_bytes = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap();
|
||||
@@ -791,8 +860,13 @@ mod tests {
|
||||
|
||||
let rt = Runtime::new().unwrap();
|
||||
dotenv().ok();
|
||||
let result =
|
||||
validate_token_balances(&components_by_id, &protocol_states_by_id, block_number, &rt);
|
||||
let result = validate_token_balances(
|
||||
&components_by_id,
|
||||
&protocol_states_by_id,
|
||||
block_number,
|
||||
&rt,
|
||||
&rpc_provider,
|
||||
);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Should fail when balance check is performed and balances do not match"
|
||||
|
||||
402
protocol-testing/src/traces.rs
Normal file
402
protocol-testing/src/traces.rs
Normal file
@@ -0,0 +1,402 @@
|
||||
//! Transaction trace analysis with foundry signature decoding.
|
||||
//!
|
||||
//! This module provides utilities for analyzing Ethereum transaction traces
|
||||
//! and decoding method signatures using foundry's comprehensive signature database.
|
||||
|
||||
use alloy::dyn_abi::{DynSolType, DynSolValue};
|
||||
use colored::Colorize;
|
||||
use foundry_evm::traces::identifier::SignaturesIdentifier;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Decode method selectors and return function info
|
||||
pub async fn decode_method_selector_with_info(input: &str) -> Option<(String, Vec<DynSolType>)> {
|
||||
if input.len() < 10 || !input.starts_with("0x") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let selector_bytes = hex::decode(&input[2..10]).ok()?;
|
||||
if selector_bytes.len() != 4 {
|
||||
return None;
|
||||
}
|
||||
let selector: [u8; 4] = selector_bytes.try_into().ok()?;
|
||||
let selector_fixed: alloy::primitives::FixedBytes<4> = selector.into();
|
||||
|
||||
// Use foundry's signature identifier
|
||||
if let Ok(sig_identifier) = SignaturesIdentifier::new(true) {
|
||||
if let Some(signature) = sig_identifier
|
||||
.identify_function(selector_fixed)
|
||||
.await
|
||||
{
|
||||
let formatted_sig = format!(
|
||||
"{}({})",
|
||||
signature.name,
|
||||
signature
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|p| p.ty.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
);
|
||||
|
||||
// Filter out scam/honeypot signatures
|
||||
if is_legitimate_signature(&formatted_sig) {
|
||||
// Parse parameter types
|
||||
let param_types: Vec<DynSolType> = signature
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|p| p.ty.as_str().parse().ok())
|
||||
.collect();
|
||||
|
||||
return Some((signature.name.clone(), param_types));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Decode method selectors using foundry's signature database with scam filtering
|
||||
pub async fn decode_method_selector(input: &str) -> Option<String> {
|
||||
if let Some((name, param_types)) = decode_method_selector_with_info(input).await {
|
||||
let type_names: Vec<String> = param_types
|
||||
.iter()
|
||||
.map(|t| t.to_string())
|
||||
.collect();
|
||||
return Some(format!("{}({})", name, type_names.join(",")));
|
||||
}
|
||||
|
||||
// Return unknown if not found
|
||||
if input.len() >= 10 {
|
||||
Some(format!("{} (unknown)", &input[0..10]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode function calldata and format with parameter values
|
||||
pub async fn decode_function_with_params(input: &str) -> Option<String> {
|
||||
if input.len() < 10 || !input.starts_with("0x") {
|
||||
return decode_method_selector(input).await;
|
||||
}
|
||||
|
||||
if let Some((name, param_types)) = decode_method_selector_with_info(input).await {
|
||||
// Try to decode the calldata
|
||||
if input.len() > 10 {
|
||||
let calldata_hex = &input[10..]; // Skip the 4-byte selector
|
||||
if let Ok(calldata) = hex::decode(calldata_hex) {
|
||||
if let Ok(decoded_values) =
|
||||
DynSolType::Tuple(param_types.clone()).abi_decode(&calldata)
|
||||
{
|
||||
if let DynSolValue::Tuple(values) = decoded_values {
|
||||
let formatted_params: Vec<String> = values
|
||||
.iter()
|
||||
.zip(param_types.iter())
|
||||
.map(|(value, ty)| format_parameter_value(value, ty))
|
||||
.collect();
|
||||
|
||||
return Some(format!("{}({})", name, formatted_params.join(", ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if decoding fails, put the whole calldata inside the method call
|
||||
return Some(format!("{}({})", name, input));
|
||||
}
|
||||
|
||||
// Return unknown if not found
|
||||
Some(format!("{} (unknown)", &input[0..10]))
|
||||
}
|
||||
|
||||
/// Format a parameter value for display
|
||||
fn format_parameter_value(value: &DynSolValue, _ty: &DynSolType) -> String {
|
||||
match value {
|
||||
DynSolValue::Address(addr) => format!("{:#x}", addr),
|
||||
DynSolValue::Uint(uint, _) => {
|
||||
let value_str = uint.to_string();
|
||||
// Add scientific notation for large numbers
|
||||
if value_str.len() > 15 {
|
||||
format!("{} [{}e{}]", value_str, &value_str[0..4], value_str.len() - 1)
|
||||
} else {
|
||||
value_str
|
||||
}
|
||||
}
|
||||
DynSolValue::Int(int, _) => int.to_string(),
|
||||
DynSolValue::Bool(b) => b.to_string(),
|
||||
DynSolValue::Bytes(bytes) => format!("0x{}", hex::encode(bytes)),
|
||||
DynSolValue::FixedBytes(bytes, _) => format!("0x{}", hex::encode(bytes)),
|
||||
DynSolValue::String(s) => format!("\"{}\"", s),
|
||||
DynSolValue::Array(arr) | DynSolValue::FixedArray(arr) => {
|
||||
let elements: Vec<String> = arr
|
||||
.iter()
|
||||
.map(|v| format_parameter_value(v, _ty))
|
||||
.collect();
|
||||
format!("[{}]", elements.join(", "))
|
||||
}
|
||||
DynSolValue::Tuple(tuple) => {
|
||||
let elements: Vec<String> = tuple
|
||||
.iter()
|
||||
.map(|v| format_parameter_value(v, _ty))
|
||||
.collect();
|
||||
format!("({})", elements.join(", "))
|
||||
}
|
||||
DynSolValue::Function(_) => "function".to_string(),
|
||||
DynSolValue::CustomStruct { .. } => "struct".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a signature looks legitimate (not a scam/honeypot)
|
||||
fn is_legitimate_signature(signature: &str) -> bool {
|
||||
let sig_lower = signature.to_lowercase();
|
||||
|
||||
// Reject obvious scam patterns
|
||||
let scam_patterns = [
|
||||
"watch_tg",
|
||||
"_tg_",
|
||||
"telegram",
|
||||
"discord",
|
||||
"twitter",
|
||||
"social",
|
||||
"invite",
|
||||
"gift",
|
||||
"bonus",
|
||||
"airdrop",
|
||||
"referral",
|
||||
"ref_",
|
||||
"_reward",
|
||||
"claim_reward",
|
||||
"_bonus",
|
||||
"_gift",
|
||||
"_invite",
|
||||
"honeypot",
|
||||
"rug",
|
||||
"scam",
|
||||
"phish",
|
||||
"sub2juniononyoutube",
|
||||
"youtube",
|
||||
"sub2",
|
||||
"junion",
|
||||
];
|
||||
|
||||
for pattern in &scam_patterns {
|
||||
if sig_lower.contains(pattern) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reject signatures that are suspiciously long (likely auto-generated scam functions)
|
||||
if signature.len() > 80 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject signatures with too many underscores (common in scam functions)
|
||||
let underscore_count = signature.matches('_').count();
|
||||
if underscore_count > 3 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject signatures that look like random hex or encoded data
|
||||
if signature
|
||||
.matches(char::is_numeric)
|
||||
.count() >
|
||||
signature.len() / 2
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Trace printing with foundry-style formatting and colors
|
||||
pub async fn print_call_trace(call: &Value, depth: usize) {
|
||||
if depth == 0 {
|
||||
println!("{}", "Traces:".cyan().bold());
|
||||
}
|
||||
|
||||
if let Some(call_obj) = call.as_object() {
|
||||
// Parse trace data
|
||||
let call_type = call_obj
|
||||
.get("type")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("UNKNOWN");
|
||||
|
||||
let _from = call_obj
|
||||
.get("from")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("0x?");
|
||||
|
||||
let to = call_obj
|
||||
.get("to")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("0x?");
|
||||
|
||||
let gas_used = call_obj
|
||||
.get("gasUsed")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("0x0");
|
||||
|
||||
let _value = call_obj
|
||||
.get("value")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("0x0");
|
||||
|
||||
// Convert hex values for display
|
||||
let gas_used_dec = if let Some(stripped) = gas_used.strip_prefix("0x") {
|
||||
u64::from_str_radix(stripped, 16).unwrap_or(0)
|
||||
} else {
|
||||
gas_used.parse().unwrap_or(0)
|
||||
};
|
||||
|
||||
// Check if call failed
|
||||
let has_error = call_obj.get("error").is_some();
|
||||
let has_revert = call_obj.get("revertReason").is_some();
|
||||
let has_other_error = ["revert", "reverted", "message", "errorMessage", "reason"]
|
||||
.iter()
|
||||
.any(|field| call_obj.get(*field).is_some());
|
||||
let call_failed = has_error || has_revert || has_other_error;
|
||||
|
||||
// Debug: if there's any failure, print all fields to help identify the error structure
|
||||
if call_failed && depth <= 2 {
|
||||
eprintln!("DEBUG: Failed call at depth {}: {:#?}", depth, call_obj);
|
||||
}
|
||||
|
||||
// Create tree structure prefix
|
||||
let tree_prefix = if depth == 0 { "".to_string() } else { " ".repeat(depth) + "├─ " };
|
||||
|
||||
// Get input for method signature decoding
|
||||
let input = call_obj
|
||||
.get("input")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("0x");
|
||||
|
||||
// Decode method signature with parameters
|
||||
let method_sig = if !input.is_empty() && input != "0x" {
|
||||
decode_function_with_params(input)
|
||||
.await
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
} else {
|
||||
format!("{}()", call_type.to_lowercase())
|
||||
};
|
||||
|
||||
// Format the main call line with colors
|
||||
let gas_str = format!("[{}]", gas_used_dec);
|
||||
let call_part = format!("{}::{}", to, method_sig);
|
||||
|
||||
if call_failed {
|
||||
println!("{}{} {}", tree_prefix, gas_str, call_part.red());
|
||||
} else {
|
||||
println!("{}{} {}", tree_prefix, gas_str, call_part.green());
|
||||
}
|
||||
|
||||
// Print return/revert information with proper indentation
|
||||
let result_indent = " ".repeat(depth + 1) + "└─ ← ";
|
||||
|
||||
// Check for various error/revert patterns
|
||||
let mut found_error = false;
|
||||
|
||||
if let Some(error) = call_obj.get("error") {
|
||||
println!("{}{}", result_indent, format!("[Error] {}", error));
|
||||
found_error = true;
|
||||
}
|
||||
|
||||
if let Some(revert_reason) = call_obj.get("revertReason") {
|
||||
println!("{}{}", result_indent, format!("[Revert] {}", revert_reason));
|
||||
found_error = true;
|
||||
}
|
||||
|
||||
// Check for other possible error fields
|
||||
for error_field in ["revert", "reverted", "message", "errorMessage", "reason"] {
|
||||
if let Some(error_val) = call_obj.get(error_field) {
|
||||
println!("{}{}", result_indent, format!("[{}] {}", error_field, error_val));
|
||||
found_error = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for revert data in output (sometimes revert reasons are hex-encoded in output)
|
||||
if let Some(output) = call_obj
|
||||
.get("output")
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
if !output.is_empty() && output != "0x" {
|
||||
// Try to decode revert reason from output if it looks like revert data
|
||||
if output.starts_with("0x08c379a0") {
|
||||
// Error(string) selector
|
||||
if let Ok(decoded) = hex::decode(&output[10..]) {
|
||||
if let Ok(reason) = alloy::dyn_abi::DynSolType::String.abi_decode(&decoded)
|
||||
{
|
||||
if let alloy::dyn_abi::DynSolValue::String(reason_str) = reason {
|
||||
println!(
|
||||
"{}{}",
|
||||
result_indent,
|
||||
format!("[Revert] {}", reason_str).red()
|
||||
);
|
||||
found_error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_error {
|
||||
println!("{}{}", result_indent, format!("[Return] {}", output));
|
||||
}
|
||||
} else if !found_error {
|
||||
println!("{}{}", result_indent, "[Return]");
|
||||
}
|
||||
}
|
||||
|
||||
// If we haven't found any output yet and there was no explicit error, show empty return
|
||||
if !found_error && call_obj.get("output").is_none() {
|
||||
println!("{}{}", result_indent, "[Return]".green());
|
||||
}
|
||||
|
||||
// Recursively print nested calls
|
||||
if let Some(calls) = call_obj.get("calls") {
|
||||
if let Some(calls_array) = calls.as_array() {
|
||||
for nested_call in calls_array {
|
||||
Box::pin(print_call_trace(nested_call, depth + 1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_foundry_signature_decoder() {
|
||||
// Test foundry signature resolution
|
||||
let transfer_input = "0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000016345785d8a0000";
|
||||
let result = decode_method_selector(transfer_input).await;
|
||||
println!("Foundry decoded transfer: {result:?}");
|
||||
|
||||
// Test some other common selector
|
||||
let approve_input = "0x095ea7b3000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000016345785d8a0000";
|
||||
let result = decode_method_selector(approve_input).await;
|
||||
println!("Foundry decoded approve: {result:?}");
|
||||
|
||||
// Should return something (either signature or unknown)
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_print_call_trace_with_foundry_decoding() {
|
||||
// Test trace with ERC20 transfer
|
||||
let trace_json = json!({
|
||||
"type": "CALL",
|
||||
"from": "0x1234567890abcdef1234567890abcdef12345678",
|
||||
"to": "0xabcdef1234567890abcdef1234567890abcdef12",
|
||||
"gasUsed": "0x5208",
|
||||
"value": "0x0",
|
||||
"input": "0xa9059cbb000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000000000000000000000000000016345785d8a0000",
|
||||
"output": "0x0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"calls": []
|
||||
});
|
||||
|
||||
// This test mainly ensures the function runs without panicking
|
||||
print_call_trace(&trace_json, 0).await;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{collections::HashMap, error::Error as StdError, fmt};
|
||||
|
||||
use tracing::info;
|
||||
use tracing::debug;
|
||||
use tycho_client::{rpc::RPCClient, HttpRPCClient};
|
||||
use tycho_common::{
|
||||
dto::{
|
||||
@@ -125,7 +125,7 @@ impl TychoClient {
|
||||
min_quality: Option<i32>,
|
||||
max_days_since_last_trade: Option<u64>,
|
||||
) -> Result<HashMap<Bytes, Token>, RpcError> {
|
||||
info!("Loading tokens from Tycho...");
|
||||
debug!("Loading tokens from Tycho...");
|
||||
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let res = self
|
||||
|
||||
@@ -10,7 +10,7 @@ use figment::{
|
||||
Figment,
|
||||
};
|
||||
use miette::{miette, IntoDiagnostic};
|
||||
use tracing::{error, info};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
/// Build a Substreams package with modifications to the YAML file.
|
||||
pub fn build_spkg(yaml_file_path: &PathBuf, initial_block: u64) -> miette::Result<String> {
|
||||
@@ -86,7 +86,7 @@ pub fn build_spkg(yaml_file_path: &PathBuf, initial_block: u64) -> miette::Resul
|
||||
// Restore the original YAML from backup
|
||||
fs::copy(&backup_file_path, yaml_file_path).into_diagnostic()?;
|
||||
fs::remove_file(&backup_file_path).into_diagnostic()?;
|
||||
info!("Spkg built successfully: {}", spkg_name);
|
||||
debug!("Spkg built successfully: {}", spkg_name);
|
||||
|
||||
Ok(spkg_name)
|
||||
}
|
||||
|
||||
14
protocol-testing/test_executor_addresses.json
Normal file
14
protocol-testing/test_executor_addresses.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"ethereum": {
|
||||
"uniswap_v2": "0xaE04CA7E9Ed79cBD988f6c536CE11C621166f41B",
|
||||
"uniswap_v3": "0xbab7124C9662B15C6b9AF0b1f329907dD55a24FC",
|
||||
"uniswap_v4": "0x2C2EaB81Cf983602153E67b1890164BC4CABC6ed",
|
||||
"vm:balancer_v2": "0xB5b8dc3F0a1Be99685a0DEd015Af93bFBB55C411",
|
||||
"vm:balancer_v3": "0xec5cE4bF6FbcB7bB0148652c92a4AEC8c1d474Ec",
|
||||
"sushiswap_v2": "0x2017ad7035D781C14699C8E44ed62d3083723A18",
|
||||
"pancakeswap_v2": "0xC9db3FEB380E4fd9af239e2595ECdEcE3b5c34A4",
|
||||
"pancakeswap_v3": "0x9D32e9F569B22Ae8d8C6f788037C1CD53632A059",
|
||||
"vm:curve": "0x879F3008D96EBea0fc584aD684c7Df31777F3165",
|
||||
"vm:maverick_v2": "0xF35e3F5F205769B41508A18787b62A21bC80200B"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user