* refactor: Misc improvements to code - Decouple validating logic from TychoRunner - Move all data fetching and decoding the tycho message into the same method - Split validate_state into validate_state, validate_token_balances and simulate_and_execute - Make rpc_provider and runtime attributes of TestRunner - Add references where possible to avoid clones - Remove unnecessary code - Make clippy happy #time 2h 36m #time 0m #time 3m * chore: Use tycho deps and foundry from tycho_simulation This is to try to decrease the risk of using conflicting versions in the different repositories #time 32m #time 0m * chore: Read RPC_URL in main.rs #time 10m * fix: Support eth trades (skip balance and allowance overwrites) and set balance overwrite to amount in For tokens like USDC setting the balance super high was making us getting blacklisted #time 1h 12m * fix: Fix curve tests and filter components_by_id with the expected_component_ids #time 1h 30m #time 0m * fix: Don't use all the possible executor addresses. Hardcode just one for the test Refactor overwrites logic: - renamed functions - moved logic around that fits together - don't use StateOverrides and then convert to alloy overrides. Use alloy's directly #time 1h 21m * fix: Assume that the executors mapping starts at storage value 1 Move setup_router_overwrites away from the rpc and into the execution file Delete unnecessary get_storage_at #time 33m
395 lines
14 KiB
Rust
395 lines
14 KiB
Rust
//! 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 serde_json::Value;
|
|
use tycho_simulation::foundry_evm::traces::identifier::SignaturesIdentifier;
|
|
|
|
/// 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(DynSolValue::Tuple(values)) =
|
|
DynSolType::Tuple(param_types.clone()).abi_decode(&calldata)
|
|
{
|
|
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;
|
|
|
|
// 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} [Error] {error}");
|
|
found_error = true;
|
|
}
|
|
|
|
if let Some(revert_reason) = call_obj.get("revertReason") {
|
|
println!("{}[Revert] {}", result_indent, 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, 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 let Some(stripped) = output.strip_prefix("0x08c379a0") {
|
|
// Error(string) selector
|
|
if let Ok(decoded) = hex::decode(stripped) {
|
|
if let Ok(alloy::dyn_abi::DynSolValue::String(reason_str)) =
|
|
alloy::dyn_abi::DynSolType::String.abi_decode(&decoded)
|
|
{
|
|
println!(
|
|
"{}{}",
|
|
result_indent,
|
|
format!("[Revert] {}", reason_str).red()
|
|
);
|
|
found_error = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !found_error {
|
|
println!("{}[Return] {}", result_indent, output);
|
|
}
|
|
} else if !found_error {
|
|
println!("{}[Return]", result_indent);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|