feat: Add encoding to test suite (#259)
* feat: Add encoding to test suite Use execution from simulation Add protocol_system to all test files and pass it to run tycho Add encoding_utils.rs #time 5h 0m #time 1m #time 7m * refactor: Move encoding swap to its own method to simplify main code Rename encoding_utils.rs to encoding.rs #time 20m #time 0m #time 0m
This commit is contained in:
1218
protocol-testing/Cargo.lock
generated
1218
protocol-testing/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ tracing = "0.1.37"
|
|||||||
# Tycho dependencies
|
# Tycho dependencies
|
||||||
tycho-common = "0.82.0"
|
tycho-common = "0.82.0"
|
||||||
tycho-client = "0.82.0"
|
tycho-client = "0.82.0"
|
||||||
tycho-simulation = { git = "https://github.com/propeller-heads/tycho-simulation.git", tag = "0.157.0", features = ["evm"] }
|
tycho-simulation = { git = "https://github.com/propeller-heads/tycho-simulation.git", tag = "0.157.1", features = ["evm"] }
|
||||||
## TODO: for local development
|
## TODO: for local development
|
||||||
#tycho-simulation = { path = "../../tycho-simulation" }
|
#tycho-simulation = { path = "../../tycho-simulation" }
|
||||||
num-bigint = "0.4"
|
num-bigint = "0.4"
|
||||||
|
|||||||
@@ -140,5 +140,6 @@ pub struct IntegrationTestsConfig {
|
|||||||
pub initialized_accounts: Option<Vec<String>>,
|
pub initialized_accounts: Option<Vec<String>>,
|
||||||
pub skip_balance_check: bool,
|
pub skip_balance_check: bool,
|
||||||
pub protocol_type_names: Vec<String>,
|
pub protocol_type_names: Vec<String>,
|
||||||
|
pub protocol_system: String,
|
||||||
pub tests: Vec<IntegrationTest>,
|
pub tests: Vec<IntegrationTest>,
|
||||||
}
|
}
|
||||||
|
|||||||
158
protocol-testing/src/encoding.rs
Normal file
158
protocol-testing/src/encoding.rs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use alloy::{primitives::Keccak256, sol_types::SolValue};
|
||||||
|
use num_bigint::BigUint;
|
||||||
|
use tycho_common::{dto::Chain, Bytes};
|
||||||
|
use tycho_simulation::{
|
||||||
|
evm::protocol::u256_num::biguint_to_u256,
|
||||||
|
protocol::models::ProtocolComponent,
|
||||||
|
tycho_execution::encoding::{
|
||||||
|
errors::EncodingError,
|
||||||
|
evm::{encoder_builders::TychoRouterEncoderBuilder, utils::bytes_to_address},
|
||||||
|
models::{
|
||||||
|
EncodedSolution, NativeAction, Solution, SwapBuilder, Transaction, UserTransferType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Encodes swap data for the Tycho router.
|
||||||
|
///
|
||||||
|
/// Assumes a single swap solution and encodes the data ready to be used by the Tycho router directly.
|
||||||
|
///
|
||||||
|
/// # 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<Transaction, EncodingError>` containing the encoded transaction data for the Tycho router,
|
||||||
|
/// or an error if encoding fails.
|
||||||
|
pub fn encode_swap(
|
||||||
|
component: ProtocolComponent,
|
||||||
|
token_in: Bytes,
|
||||||
|
token_out: Bytes,
|
||||||
|
amount_in: BigUint,
|
||||||
|
amount_out: BigUint,
|
||||||
|
) -> Result<Transaction, EncodingError> {
|
||||||
|
let chain: tycho_common::models::Chain = Chain::Ethereum.into();
|
||||||
|
let alice_address =
|
||||||
|
Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").map_err(|_| {
|
||||||
|
EncodingError::FatalError("Alice's address can't be converted to Bytes".to_string())
|
||||||
|
})?;
|
||||||
|
let encoder = TychoRouterEncoderBuilder::new()
|
||||||
|
.chain(chain)
|
||||||
|
.user_transfer_type(UserTransferType::TransferFrom)
|
||||||
|
.build()
|
||||||
|
.expect("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 encoded_solution = encoder
|
||||||
|
.encode_solutions(vec![solution.clone()])
|
||||||
|
.expect("Failed to encode router calldata")[0]
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
encode_tycho_router_call(encoded_solution, &solution, &chain.wrapped_native_token().address)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes a transaction for the Tycho Router using `singleSwap` method and regular token
|
||||||
|
/// transfers.
|
||||||
|
///
|
||||||
|
/// # Parameters
|
||||||
|
/// - `encoded_solution`: The solution already encoded by Tycho.
|
||||||
|
/// - `solution`: The high-level solution including tokens, amounts, and receiver info.
|
||||||
|
/// - `native_address`: The address used to represent the native token
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `Result<Transaction, EncodingError>` that either contains the full transaction data (to,
|
||||||
|
/// value, data), or an error if the inputs are invalid.
|
||||||
|
pub fn encode_tycho_router_call(
|
||||||
|
encoded_solution: EncodedSolution,
|
||||||
|
solution: &Solution,
|
||||||
|
native_address: &Bytes,
|
||||||
|
) -> Result<Transaction, EncodingError> {
|
||||||
|
let (mut unwrap, mut wrap) = (false, false);
|
||||||
|
if let Some(action) = solution.native_action.clone() {
|
||||||
|
match action {
|
||||||
|
NativeAction::Wrap => wrap = true,
|
||||||
|
NativeAction::Unwrap => unwrap = true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let given_amount = biguint_to_u256(&solution.given_amount);
|
||||||
|
let min_amount_out = biguint_to_u256(&solution.checked_amount);
|
||||||
|
let given_token = bytes_to_address(&solution.given_token)?;
|
||||||
|
let checked_token = bytes_to_address(&solution.checked_token)?;
|
||||||
|
let receiver = bytes_to_address(&solution.receiver)?;
|
||||||
|
|
||||||
|
let method_calldata = if encoded_solution
|
||||||
|
.function_signature
|
||||||
|
.contains("singleSwap")
|
||||||
|
{
|
||||||
|
(
|
||||||
|
given_amount,
|
||||||
|
given_token,
|
||||||
|
checked_token,
|
||||||
|
min_amount_out,
|
||||||
|
wrap,
|
||||||
|
unwrap,
|
||||||
|
receiver,
|
||||||
|
true,
|
||||||
|
encoded_solution.swaps,
|
||||||
|
)
|
||||||
|
.abi_encode()
|
||||||
|
} else {
|
||||||
|
Err(EncodingError::FatalError("Invalid function signature for Tycho router".to_string()))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let contract_interaction = encode_input(&encoded_solution.function_signature, method_calldata);
|
||||||
|
let value = if solution.given_token == *native_address {
|
||||||
|
solution.given_amount.clone()
|
||||||
|
} else {
|
||||||
|
BigUint::ZERO
|
||||||
|
};
|
||||||
|
Ok(Transaction { to: encoded_solution.interacting_with, value, data: contract_interaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes the input data for a function call to the given function signature (e.g.
|
||||||
|
/// transfer(address,uint256))
|
||||||
|
pub fn encode_input(selector: &str, mut encoded_args: Vec<u8>) -> Vec<u8> {
|
||||||
|
let mut hasher = Keccak256::new();
|
||||||
|
hasher.update(selector.as_bytes());
|
||||||
|
let selector_bytes = &hasher.finalize()[..4];
|
||||||
|
let mut call_data = selector_bytes.to_vec();
|
||||||
|
// Remove extra prefix if present (32 bytes for dynamic data)
|
||||||
|
// Alloy encoding is including a prefix for dynamic data indicating the offset or length
|
||||||
|
// but at this point we don't want that
|
||||||
|
if encoded_args.len() > 32 &&
|
||||||
|
encoded_args[..32] ==
|
||||||
|
[0u8; 31]
|
||||||
|
.into_iter()
|
||||||
|
.chain([32].to_vec())
|
||||||
|
.collect::<Vec<u8>>()
|
||||||
|
{
|
||||||
|
encoded_args = encoded_args[32..].to_vec();
|
||||||
|
}
|
||||||
|
call_data.extend(encoded_args);
|
||||||
|
call_data
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
mod adapter_builder;
|
mod adapter_builder;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod encoding;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
mod test_runner;
|
mod test_runner;
|
||||||
mod tycho_rpc;
|
mod tycho_rpc;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ use tycho_simulation::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
adapter_builder::AdapterContractBuilder,
|
adapter_builder::AdapterContractBuilder,
|
||||||
config::{IntegrationTest, IntegrationTestsConfig, ProtocolComponentWithTestConfig},
|
config::{IntegrationTest, IntegrationTestsConfig, ProtocolComponentWithTestConfig},
|
||||||
|
encoding::encode_swap,
|
||||||
rpc::RPCProvider,
|
rpc::RPCProvider,
|
||||||
tycho_rpc::TychoClient,
|
tycho_rpc::TychoClient,
|
||||||
tycho_runner::TychoRunner,
|
tycho_runner::TychoRunner,
|
||||||
@@ -173,6 +174,7 @@ impl TestRunner {
|
|||||||
test.start_block,
|
test.start_block,
|
||||||
test.stop_block,
|
test.stop_block,
|
||||||
&config.protocol_type_names,
|
&config.protocol_type_names,
|
||||||
|
&config.protocol_system,
|
||||||
)
|
)
|
||||||
.wrap_err("Failed to run Tycho")?;
|
.wrap_err("Failed to run Tycho")?;
|
||||||
|
|
||||||
@@ -226,7 +228,7 @@ fn validate_state(
|
|||||||
.wrap_err("Failed to create Tycho client")?;
|
.wrap_err("Failed to create Tycho client")?;
|
||||||
|
|
||||||
let chain = Chain::Ethereum;
|
let chain = Chain::Ethereum;
|
||||||
let protocol_system = "test_protocol";
|
let protocol_system = &config.protocol_system;
|
||||||
|
|
||||||
// Fetch data from Tycho RPC. We use block_on to avoid using async functions on the testing
|
// Fetch data from Tycho RPC. We use block_on to avoid using async functions on the testing
|
||||||
// module, in order to simplify debugging
|
// module, in order to simplify debugging
|
||||||
@@ -336,7 +338,7 @@ fn validate_state(
|
|||||||
let mut decoder = TychoStreamDecoder::new();
|
let mut decoder = TychoStreamDecoder::new();
|
||||||
let decoder_context = DecoderContext::new().vm_adapter_path(adapter_contract_path_str);
|
let decoder_context = DecoderContext::new().vm_adapter_path(adapter_contract_path_str);
|
||||||
decoder.register_decoder_with_context::<EVMPoolState<PreCachedDB>>(
|
decoder.register_decoder_with_context::<EVMPoolState<PreCachedDB>>(
|
||||||
"test_protocol",
|
protocol_system,
|
||||||
decoder_context,
|
decoder_context,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -402,7 +404,7 @@ fn validate_state(
|
|||||||
.wrap_err("Failed to get block header")?;
|
.wrap_err("Failed to get block header")?;
|
||||||
|
|
||||||
let state_msgs: HashMap<String, StateSyncMessage<BlockHeader>> = HashMap::from([(
|
let state_msgs: HashMap<String, StateSyncMessage<BlockHeader>> = HashMap::from([(
|
||||||
String::from("test_protocol"),
|
String::from(protocol_system),
|
||||||
StateSyncMessage {
|
StateSyncMessage {
|
||||||
header: BlockHeader {
|
header: BlockHeader {
|
||||||
hash: Bytes::from(bytes),
|
hash: Bytes::from(bytes),
|
||||||
@@ -490,25 +492,36 @@ fn validate_state(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
let amount_out_result = state
|
||||||
.get_amount_out(amount_in.clone(), token_in, token_out)
|
.get_amount_out(amount_in.clone(), token_in, token_out)
|
||||||
.map(|result| {
|
|
||||||
info!(
|
|
||||||
"Amount out for trading {:.1}% of max: ({} {} -> {} {}) (gas: {})",
|
|
||||||
percentage * 100.0,
|
|
||||||
amount_in,
|
|
||||||
token_in.symbol,
|
|
||||||
result.amount,
|
|
||||||
token_out.symbol,
|
|
||||||
result.gas
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.into_diagnostic()
|
.into_diagnostic()
|
||||||
.wrap_err(format!(
|
.wrap_err(format!(
|
||||||
"Error calculating amount out for Pool {id:?} at {:.1}% with input of {amount_in} {}.",
|
"Error calculating amount out for Pool {id:?} at {:.1}% with input of {amount_in} {}.",
|
||||||
percentage * 100.0,
|
percentage * 100.0,
|
||||||
token_in.symbol,
|
token_in.symbol,
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Amount out for trading {:.1}% of max: ({} {} -> {} {}) (gas: {})",
|
||||||
|
percentage * 100.0,
|
||||||
|
amount_in,
|
||||||
|
token_in.symbol,
|
||||||
|
amount_out_result.amount,
|
||||||
|
token_out.symbol,
|
||||||
|
amount_out_result.gas
|
||||||
|
);
|
||||||
|
|
||||||
|
let protocol_component = block_msg.new_pairs.get(id);
|
||||||
|
if let Some(pc) = protocol_component {
|
||||||
|
let calldata = encode_swap(
|
||||||
|
pc.clone(),
|
||||||
|
token_in.address.clone(),
|
||||||
|
token_out.address.clone(),
|
||||||
|
amount_in,
|
||||||
|
amount_out_result.amount,
|
||||||
|
);
|
||||||
|
info!("Encoded swap successfully");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,12 @@ use tycho_common::{
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum RpcError {
|
pub enum RpcError {
|
||||||
ClientError(String),
|
ClientError(String),
|
||||||
ResponseError(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for RpcError {
|
impl fmt::Display for RpcError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
RpcError::ClientError(msg) => write!(f, "RPC client error: {msg}"),
|
RpcError::ClientError(msg) => write!(f, "RPC client error: {msg}"),
|
||||||
RpcError::ResponseError(msg) => write!(f, "RPC response error: {msg}"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ impl TychoRunner {
|
|||||||
start_block: u64,
|
start_block: u64,
|
||||||
end_block: u64,
|
end_block: u64,
|
||||||
protocol_type_names: &[String],
|
protocol_type_names: &[String],
|
||||||
|
protocol_system: &str,
|
||||||
) -> miette::Result<()> {
|
) -> miette::Result<()> {
|
||||||
// Expects a .env present in the same folder as package root (where Cargo.toml is)
|
// Expects a .env present in the same folder as package root (where Cargo.toml is)
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
@@ -50,6 +51,8 @@ impl TychoRunner {
|
|||||||
"map_protocol_changes",
|
"map_protocol_changes",
|
||||||
"--protocol-type-names",
|
"--protocol-type-names",
|
||||||
&protocol_type_names.join(","),
|
&protocol_type_names.join(","),
|
||||||
|
"--protocol-system",
|
||||||
|
protocol_system,
|
||||||
"--start-block",
|
"--start-block",
|
||||||
&start_block.to_string(),
|
&start_block.to_string(),
|
||||||
"--stop-block",
|
"--stop-block",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
substreams_yaml_path: ./substreams.yaml
|
substreams_yaml_path: ./substreams.yaml
|
||||||
|
protocol_system: "vm:balancer_v2"
|
||||||
protocol_type_names:
|
protocol_type_names:
|
||||||
- "balancer_v2_pool"
|
- "balancer_v2_pool"
|
||||||
adapter_contract: "BalancerV2SwapAdapter"
|
adapter_contract: "BalancerV2SwapAdapter"
|
||||||
|
|||||||
Reference in New Issue
Block a user