From e9bb8c576a96f3e523dc4e5011d48028cbc4d599 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 4 Apr 2025 13:48:10 +0100 Subject: [PATCH 1/6] feat(curve): Add CurveEncoder We don't know the pool tokens in the ProtocolComponent, so we can't infer the indexes correctly. Added a call to the MetaRegistry curve contract to get the correct token indexes. To do this, I had to move the get_client to utils. We could actually refactor the transaction logic into its' own struct and use it here and in the approval logic It was assumed that all the pools will have a "factory" static attribute, even if empty --- don't change below this line --- ENG-4306 Took 2 hours 28 minutes Took 27 seconds --- config/executor_addresses.json | 3 +- config/test_executor_addresses.json | 3 +- foundry/test/executors/CurveExecutor.t.sol | 13 - src/encoding/evm/approvals/permit2.rs | 5 +- .../approvals/protocol_approvals_manager.rs | 19 +- src/encoding/evm/swap_encoder/builder.rs | 5 +- .../evm/swap_encoder/swap_encoders.rs | 344 +++++++++++++++++- src/encoding/evm/utils.rs | 19 +- 8 files changed, 371 insertions(+), 40 deletions(-) diff --git a/config/executor_addresses.json b/config/executor_addresses.json index 77d684b..b07f1e1 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -7,7 +7,8 @@ "pancakeswap_v3": "0x4929B619A8F0D9c06ed0FfD497636580D823F65d", "uniswap_v4": "0x042C0ebBEAb9d9987c2f64Ee05f2B3aeB86eAf70", "vm:balancer_v2": "0x00BE8EfAE40219Ff76287b0F9b9e497942f5BC91", - "ekubo_v2": "0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D" + "ekubo_v2": "0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D", + "vm:curve": "0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f" }, "tenderly_ethereum": { "uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E", diff --git a/config/test_executor_addresses.json b/config/test_executor_addresses.json index 4bc8412..1b868ee 100644 --- a/config/test_executor_addresses.json +++ b/config/test_executor_addresses.json @@ -7,6 +7,7 @@ "pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9", "uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a", "vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1", - "ekubo_v2": "0x2a07706473244BC757E10F2a9E86fB532828afe3" + "ekubo_v2": "0x2a07706473244BC757E10F2a9E86fB532828afe3", + "vm:curve": "0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f" } } diff --git a/foundry/test/executors/CurveExecutor.t.sol b/foundry/test/executors/CurveExecutor.t.sol index c3b9cad..c0df614 100644 --- a/foundry/test/executors/CurveExecutor.t.sol +++ b/foundry/test/executors/CurveExecutor.t.sol @@ -21,19 +21,6 @@ interface MetaRegistry { returns (int128, int128, bool); } -interface IAaveLendingPool { - function deposit( - address asset, - uint256 amount, - address onBehalfOf, - uint16 referralCode - ) external; - - function withdraw(address asset, uint256 amount, address to) - external - returns (uint256); -} - contract CurveExecutorExposed is CurveExecutor { constructor(address _nativeToken) CurveExecutor(_nativeToken) {} diff --git a/src/encoding/evm/approvals/permit2.rs b/src/encoding/evm/approvals/permit2.rs index 549197e..c1ccf2c 100644 --- a/src/encoding/evm/approvals/permit2.rs +++ b/src/encoding/evm/approvals/permit2.rs @@ -19,10 +19,7 @@ use tycho_common::Bytes; use crate::encoding::{ errors::EncodingError, - evm::{ - approvals::protocol_approvals_manager::get_client, - utils::{biguint_to_u256, bytes_to_address, encode_input, get_runtime}, - }, + evm::utils::{biguint_to_u256, bytes_to_address, encode_input, get_client, get_runtime}, models::Chain, }; diff --git a/src/encoding/evm/approvals/protocol_approvals_manager.rs b/src/encoding/evm/approvals/protocol_approvals_manager.rs index 4b06a85..8de957b 100644 --- a/src/encoding/evm/approvals/protocol_approvals_manager.rs +++ b/src/encoding/evm/approvals/protocol_approvals_manager.rs @@ -1,13 +1,12 @@ -use std::{env, sync::Arc}; +use std::sync::Arc; use alloy::{ - providers::{Provider, ProviderBuilder, RootProvider}, + providers::{Provider, RootProvider}, rpc::types::{TransactionInput, TransactionRequest}, transports::BoxTransport, }; use alloy_primitives::{Address, Bytes, TxKind, U256}; use alloy_sol_types::SolValue; -use dotenv::dotenv; use tokio::{ runtime::{Handle, Runtime}, task::block_in_place, @@ -15,7 +14,7 @@ use tokio::{ use crate::encoding::{ errors::EncodingError, - evm::utils::{encode_input, get_runtime}, + evm::utils::{encode_input, get_client, get_runtime}, }; /// A manager for checking if an approval is needed for interacting with a certain spender. @@ -72,18 +71,6 @@ impl ProtocolApprovalsManager { } } -/// Gets the client used for interacting with the EVM-compatible network. -pub async fn get_client() -> Result>, EncodingError> { - dotenv().ok(); - let eth_rpc_url = env::var("RPC_URL") - .map_err(|_| EncodingError::FatalError("Missing RPC_URL in environment".to_string()))?; - let client = ProviderBuilder::new() - .on_builtin(ð_rpc_url) - .await - .map_err(|_| EncodingError::FatalError("Failed to build provider".to_string()))?; - Ok(Arc::new(client)) -} - #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/src/encoding/evm/swap_encoder/builder.rs b/src/encoding/evm/swap_encoder/builder.rs index 65bc317..d1c90c6 100644 --- a/src/encoding/evm/swap_encoder/builder.rs +++ b/src/encoding/evm/swap_encoder/builder.rs @@ -1,8 +1,8 @@ use crate::encoding::{ errors::EncodingError, evm::swap_encoder::swap_encoders::{ - BalancerV2SwapEncoder, EkuboSwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, - UniswapV4SwapEncoder, + BalancerV2SwapEncoder, CurveSwapEncoder, EkuboSwapEncoder, UniswapV2SwapEncoder, + UniswapV3SwapEncoder, UniswapV4SwapEncoder, }, swap_encoder::SwapEncoder, }; @@ -31,6 +31,7 @@ impl SwapEncoderBuilder { "pancakeswap_v3" => Ok(Box::new(UniswapV3SwapEncoder::new(self.executor_address))), "uniswap_v4" => Ok(Box::new(UniswapV4SwapEncoder::new(self.executor_address))), "ekubo_v2" => Ok(Box::new(EkuboSwapEncoder::new(self.executor_address))), + "vm:curve" => Ok(Box::new(CurveSwapEncoder::new(self.executor_address))), _ => Err(EncodingError::FatalError(format!( "Unknown protocol system: {}", self.protocol_system diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index ad6ffc9..a4d92f7 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -1,14 +1,22 @@ use std::str::FromStr; -use alloy_primitives::{Address, Bytes as AlloyBytes}; +use alloy::{ + providers::Provider, + rpc::types::{TransactionInput, TransactionRequest}, +}; +use alloy_primitives::{Address, Bytes as AlloyBytes, TxKind, U256, U8}; use alloy_sol_types::SolValue; +use tokio::task::block_in_place; use tycho_common::Bytes; use crate::encoding::{ errors::EncodingError, evm::{ approvals::protocol_approvals_manager::ProtocolApprovalsManager, - utils::{bytes_to_address, get_static_attribute, pad_to_fixed_size}, + utils, + utils::{ + bytes_to_address, encode_input, get_runtime, get_static_attribute, pad_to_fixed_size, + }, }, models::{EncodingContext, Swap}, swap_encoder::SwapEncoder, @@ -328,6 +336,166 @@ impl SwapEncoder for EkuboSwapEncoder { } } +/// Encodes a swap on a Curve pool through the given executor address. +/// +/// # Fields +/// * `executor_address` - The address of the executor contract that will perform the swap. +/// * `vault_address` - The address of the vault contract that will perform the swap. +#[derive(Clone)] +pub struct CurveSwapEncoder { + executor_address: String, + meta_registry_address: String, +} + +impl CurveSwapEncoder { + fn get_pool_type( + &self, + pool_id: &str, + factory_address: Option<&str>, + ) -> Result { + match pool_id { + // TriPool + "0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7" => Ok(U8::from(1)), + // STETHPool + "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022" => Ok(U8::from(1)), + // TriCryptoPool + "0xD51a44d3FaE010294C616388b506AcdA1bfAAE46" => Ok(U8::from(3)), + // SUSDPool + "0xA5407eAE9Ba41422680e2e00537571bcC53efBfD" => Ok(U8::from(1)), + // FRAXUSDCPool + "0xDcEF968d416a41Cdac0ED8702fAC8128A64241A2" => Ok(U8::from(1)), + _ => match factory_address { + Some(address) => match address { + // CryptoSwapNG factory + "0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf" => Ok(U8::from(1)), + // Metapool factory + "0xB9fC157394Af804a3578134A6585C0dc9cc990d4" => Ok(U8::from(1)), + // CryptoPool factory + "0xF18056Bbd320E96A48e3Fbf8bC061322531aac99" => Ok(U8::from(2)), + // Tricrypto factory + "0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963" => Ok(U8::from(3)), + // Twocrypto factory + "0x98ee851a00abee0d95d08cf4ca2bdce32aeaaf7f" => Ok(U8::from(2)), + // StableSwap factory + "0x4F8846Ae9380B90d2E71D5e3D042dff3E7ebb40d" => Ok(U8::from(1)), + _ => Err(EncodingError::FatalError(format!( + "Unsupported curve factory address: {}", + address + ))), + }, + None => Err(EncodingError::FatalError("Unsupported curve pool type".to_string())), + }, + } + } + + fn get_coin_indexes( + &self, + pool_id: Address, + token_in: Address, + token_out: Address, + ) -> Result<(U8, U8), EncodingError> { + let (handle, _runtime) = get_runtime()?; + let client = block_in_place(|| handle.block_on(utils::get_client()))?; + let args = (pool_id, token_in, token_out); + let data = encode_input("get_coin_indices(address,address,address)", args.abi_encode()); + let tx = TransactionRequest { + to: Some(TxKind::from(Address::from_str(&self.meta_registry_address).map_err( + |_| EncodingError::FatalError("Invalid Curve meta registry address".to_string()), + )?)), + input: TransactionInput { + input: Some(alloy_primitives::Bytes::from(data)), + data: None, + }, + ..Default::default() + }; + let output = block_in_place(|| handle.block_on(async { client.call(&tx).await })); + type ResponseType = (U256, U256, bool); + + match output { + Ok(response) => { + let (i_256, j_256, _): ResponseType = ResponseType::abi_decode(&response, true) + .map_err(|_| { + EncodingError::FatalError( + "Failed to decode response for allowance".to_string(), + ) + })?; + let i = U8::from(i_256); + let j = U8::from(j_256); + Ok((i, j)) + } + Err(err) => Err(EncodingError::RecoverableError(format!( + "Curve meta registry call failed with error: {:?}", + err + ))), + } + } +} + +impl SwapEncoder for CurveSwapEncoder { + fn new(executor_address: String) -> Self { + Self { + executor_address, + meta_registry_address: "0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC".to_string(), + } + } + fn encode_swap( + &self, + swap: Swap, + encoding_context: EncodingContext, + ) -> Result, EncodingError> { + let token_approvals_manager = ProtocolApprovalsManager::new()?; + let token = bytes_to_address(&swap.token_in)?; + let approval_needed: bool; + + let component_address = Address::from_str(&swap.component.id) + .map_err(|_| EncodingError::FatalError("Invalid curve pool address".to_string()))?; + if let Some(router_address) = encoding_context.router_address { + let tycho_router_address = bytes_to_address(&router_address)?; + approval_needed = token_approvals_manager.approval_needed( + token, + tycho_router_address, + component_address, + )?; + } else { + approval_needed = true; + } + + let factory_bytes = get_static_attribute(&swap, "factory")?; + let factory = if factory_bytes.is_empty() { + None + } else { + Some(Address::from_slice(&factory_bytes).to_string()) + }; + + let pool_type = self.get_pool_type(&swap.component.id, factory.as_deref())?; + + let (i, j) = self.get_coin_indexes( + component_address, + bytes_to_address(&swap.token_in)?, + bytes_to_address(&swap.token_out)?, + )?; + + let args = ( + bytes_to_address(&swap.token_in)?, + bytes_to_address(&swap.token_out)?, + component_address, + pool_type.to_be_bytes::<1>(), + i.to_be_bytes::<1>(), + j.to_be_bytes::<1>(), + approval_needed, + ); + + Ok(args.abi_encode_packed()) + } + + fn executor_address(&self) -> &str { + &self.executor_address + } + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; @@ -844,4 +1012,176 @@ mod tests { ); } } + + mod curve { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case( + "0x5500307Bcf134E5851FB4D7D8D1Dc556dCdB84B4", + "0xdA16Cf041E2780618c49Dbae5d734B89a6Bac9b3", + "0xdAC17F958D2ee523a2206206994597C13D831ec7", + 1, + 0 + )] + #[case( + "0xef484de8C07B6e2d732A92B5F78e81B38f99f95E", + "0x865377367054516e17014CcdED1e7d814EDC9ce4", + "0xA5588F7cdf560811710A2D82D3C9c99769DB1Dcb", + 0, + 1 + )] + #[case( + "0xA5407eAE9Ba41422680e2e00537571bcC53efBfD", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "0x57Ab1ec28D129707052df4dF418D58a2D46d5f51", + 1, + 3 + )] + #[case( + "0xD51a44d3FaE010294C616388b506AcdA1bfAAE46", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + 2, + 1 + )] + #[case( + "0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + 2, + 0 + )] + fn test_curve_get_coin_indexes( + #[case] pool: &str, + #[case] token_in: &str, + #[case] token_out: &str, + #[case] expected_i: u64, + #[case] expected_j: u64, + ) { + let encoder = CurveSwapEncoder::new(String::default()); + let (i, j) = encoder + .get_coin_indexes( + Address::from_str(pool).unwrap(), + Address::from_str(token_in).unwrap(), + Address::from_str(token_out).unwrap(), + ) + .unwrap(); + assert_eq!(i, U8::from(expected_i)); + assert_eq!(j, U8::from(expected_j)); + } + } + + #[test] + fn test_curve_encode_tripool() { + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("factory".into(), Bytes::from(vec![])); + let curve_tri_pool = ProtocolComponent { + id: String::from("0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7"), + protocol_system: String::from("vm:curve"), + static_attributes, + ..Default::default() + }; + let token_in = Bytes::from("0x6B175474E89094C44Da98b954EedeAC495271d0F"); + let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + let swap = Swap { + component: curve_tri_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + let encoding_context = EncodingContext { + // The receiver was generated with `makeAddr("bob") using forge` + receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), + exact_out: false, + router_address: None, + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + }; + let encoder = + CurveSwapEncoder::new(String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f")); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + assert_eq!( + hex_swap, + String::from(concat!( + // token in + "6b175474e89094c44da98b954eedeac495271d0f", + // token out + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // pool address + "bebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + // pool type 1 + "01", + // i index + "00", + // j index + "01", + // approval needed + "01", + )) + ); + } + + #[test] + fn test_curve_encode_factory() { + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert( + "factory".into(), + Bytes::from_str("0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf").unwrap(), + ); + let curve_pool = ProtocolComponent { + id: String::from("0x02950460E2b9529D0E00284A5fA2d7bDF3fA4d72"), + protocol_system: String::from("vm:curve"), + static_attributes, + ..Default::default() + }; + let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + let token_out = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); + let swap = Swap { + component: curve_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + let encoding_context = EncodingContext { + // The receiver was generated with `makeAddr("bob") using forge` + receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), + exact_out: false, + router_address: None, + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + }; + let encoder = + CurveSwapEncoder::new(String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f")); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + assert_eq!( + hex_swap, + String::from(concat!( + // token in + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // token out + "4c9edd5852cd905f086c759e8383e09bff1e68b3", + // pool address + "02950460e2b9529d0e00284a5fa2d7bdf3fa4d72", + // pool type 1 + "01", + // i index + "01", + // j index + "00", + // approval needed + "01", + )) + ); + } } diff --git a/src/encoding/evm/utils.rs b/src/encoding/evm/utils.rs index b8af4f5..c83639d 100644 --- a/src/encoding/evm/utils.rs +++ b/src/encoding/evm/utils.rs @@ -1,5 +1,9 @@ -use std::{cmp::max, sync::Arc}; +use std::{cmp::max, env, sync::Arc}; +use alloy::{ + providers::{ProviderBuilder, RootProvider}, + transports::BoxTransport, +}; use alloy_primitives::{aliases::U24, keccak256, Address, FixedBytes, Keccak256, U256, U8}; use num_bigint::BigUint; use tokio::runtime::{Handle, Runtime}; @@ -133,6 +137,19 @@ pub fn get_runtime() -> Result<(Handle, Option>), EncodingError> { } } } + +/// Gets the client used for interacting with the EVM-compatible network. +pub async fn get_client() -> Result>, EncodingError> { + dotenv::dotenv().ok(); + let eth_rpc_url = env::var("RPC_URL") + .map_err(|_| EncodingError::FatalError("Missing RPC_URL in environment".to_string()))?; + let client = ProviderBuilder::new() + .on_builtin(ð_rpc_url) + .await + .map_err(|_| EncodingError::FatalError("Failed to build provider".to_string()))?; + Ok(Arc::new(client)) +} + #[cfg(test)] mod tests { use num_bigint::BigUint; From 1e47d0e25b7906d73981a26d0d8b882a5820b61a Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 4 Apr 2025 14:14:31 +0100 Subject: [PATCH 2/6] feat(curve): Add integration test Bonus fix of a shameful typo :| --- don't change below this line --- ENG-4306 Took 21 minutes --- config/executor_addresses.json | 2 +- config/test_executor_addresses.json | 2 +- foundry/test/TychoRouter.t.sol | 71 +++++++++++-------- foundry/test/TychoRouterTestSetup.sol | 6 +- .../evm/strategy_encoder/strategy_encoders.rs | 58 +++++++++++++++ .../evm/swap_encoder/swap_encoders.rs | 8 +-- 6 files changed, 111 insertions(+), 36 deletions(-) diff --git a/config/executor_addresses.json b/config/executor_addresses.json index b07f1e1..89ebca7 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -8,7 +8,7 @@ "uniswap_v4": "0x042C0ebBEAb9d9987c2f64Ee05f2B3aeB86eAf70", "vm:balancer_v2": "0x00BE8EfAE40219Ff76287b0F9b9e497942f5BC91", "ekubo_v2": "0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D", - "vm:curve": "0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f" + "vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211" }, "tenderly_ethereum": { "uniswap_v2": "0x00C1b81e3C8f6347E69e2DDb90454798A6Be975E", diff --git a/config/test_executor_addresses.json b/config/test_executor_addresses.json index 1b868ee..a7ec22f 100644 --- a/config/test_executor_addresses.json +++ b/config/test_executor_addresses.json @@ -8,6 +8,6 @@ "uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a", "vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1", "ekubo_v2": "0x2a07706473244BC757E10F2a9E86fB532828afe3", - "vm:curve": "0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f" + "vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211" } } diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index c446f45..cea80e7 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -843,7 +843,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // Tests swapping WETH -> DAI on a USV2 pool deal(WETH_ADDR, ALICE, 1 ether); - uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); + uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE); // Approve permit2 vm.startPrank(ALICE); @@ -855,10 +855,10 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); - uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 2659881924818443699787); + assertEq(balanceAfter - balanceBefore, 2659881924818443699787); } function testSingleSwapWithoutPermit2Integration() public { @@ -869,16 +869,16 @@ contract TychoRouterTest is TychoRouterTestSetup { deal(WETH_ADDR, ALICE, 1 ether); vm.startPrank(ALICE); IERC20(WETH_ADDR).approve(address(tychoRouterAddr), 1 ether); - uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); + uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE); // Encoded solution generated using `test_split_swap_strategy_encoder_simple_route_no_permit2` (bool success,) = tychoRouterAddr.call( hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000008f1d5c1cae37400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000058005600010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d01395000000000000000000" ); vm.stopPrank(); - uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 2659881924818443699787); + assertEq(balanceAfter - balanceBefore, 2659881924818443699787); } function testUSV4Integration() public { @@ -890,7 +890,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // USDC ──(USV4)──> ETH ───(USV4)──> PEPE // deal(USDC_ADDR, ALICE, 1 ether); - uint256 balancerBefore = IERC20(PEPE_ADDR).balanceOf(ALICE); + uint256 balanceBefore = IERC20(PEPE_ADDR).balanceOf(ALICE); // Approve permit2 vm.startPrank(ALICE); @@ -902,10 +902,10 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); - uint256 balancerAfter = IERC20(PEPE_ADDR).balanceOf(ALICE); + uint256 balanceAfter = IERC20(PEPE_ADDR).balanceOf(ALICE); assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 97191013220606467325121599); + assertEq(balanceAfter - balanceBefore, 97191013220606467325121599); } function testUSV4IntegrationInputETH() public { @@ -916,7 +916,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // ETH ───(USV4)──> PEPE // deal(ALICE, 1 ether); - uint256 balancerBefore = IERC20(PEPE_ADDR).balanceOf(ALICE); + uint256 balanceBefore = IERC20(PEPE_ADDR).balanceOf(ALICE); // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_in` (bool success,) = tychoRouterAddr.call{value: 1 ether}( @@ -925,10 +925,10 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); - uint256 balancerAfter = IERC20(PEPE_ADDR).balanceOf(ALICE); + uint256 balanceAfter = IERC20(PEPE_ADDR).balanceOf(ALICE); assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 242373460199848577067005852); + assertEq(balanceAfter - balanceBefore, 242373460199848577067005852); } function testUSV4IntegrationOutputETH() public { @@ -939,7 +939,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // USDC ───(USV4)──> ETH // deal(USDC_ADDR, ALICE, 3000_000000); - uint256 balancerBefore = ALICE.balance; + uint256 balanceBefore = ALICE.balance; // Approve permit2 vm.startPrank(ALICE); @@ -952,11 +952,11 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); - uint256 balancerAfter = ALICE.balance; + uint256 balanceAfter = ALICE.balance; assertTrue(success, "Call Failed"); - console.logUint(balancerAfter - balancerBefore); - assertEq(balancerAfter - balancerBefore, 1117254495486192350); + console.logUint(balanceAfter - balanceBefore); + assertEq(balanceAfter - balanceBefore, 1117254495486192350); } function testSingleSwapWithWrapIntegration() public { @@ -966,7 +966,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // Tests swapping WETH -> DAI on a USV2 pool, but ETH is received from the user // and wrapped before the swap deal(ALICE, 1 ether); - uint256 balancerBefore = IERC20(DAI_ADDR).balanceOf(ALICE); + uint256 balanceBefore = IERC20(DAI_ADDR).balanceOf(ALICE); // Approve permit2 vm.startPrank(ALICE); @@ -977,10 +977,10 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); - uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + uint256 balanceAfter = IERC20(DAI_ADDR).balanceOf(ALICE); assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 2659881924818443699787); + assertEq(balanceAfter - balanceBefore, 2659881924818443699787); } function testSingleSwapWithUnwrapIntegration() public { @@ -990,7 +990,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // Tests swapping DAI -> WETH on a USV2 pool, and WETH is unwrapped to ETH // before sending back to the user deal(DAI_ADDR, ALICE, 3000 ether); - uint256 balancerBefore = ALICE.balance; + uint256 balanceBefore = ALICE.balance; // Approve permit2 vm.startPrank(ALICE); @@ -1002,10 +1002,10 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); - uint256 balancerAfter = ALICE.balance; + uint256 balanceAfter = ALICE.balance; assertTrue(success, "Call Failed"); - assertEq(balancerAfter - balancerBefore, 1120007305574805922); + assertEq(balanceAfter - balanceBefore, 1120007305574805922); } function testEkuboIntegration() public { @@ -1026,7 +1026,7 @@ contract TychoRouterTest is TychoRouterTestSetup { ); deal(ALICE, 1 ether); - uint256 balancerBefore = IERC20(USDC_ADDR).balanceOf(ALICE); + uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE); // Approve permit2 vm.startPrank(ALICE); @@ -1035,10 +1035,10 @@ contract TychoRouterTest is TychoRouterTestSetup { hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000077007500010000002a07706473244bc757e10f2a9e86fb532828afe31d1499e622d69689cdf9004d05ec547d650ff2110000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000000000000000000000" ); - uint256 balancerAfter = IERC20(USDC_ADDR).balanceOf(ALICE); + uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE); assertTrue(success, "Call Failed"); - assertGe(balancerAfter - balancerBefore, 26173932); + assertGe(balanceAfter - balanceBefore, 26173932); // All input tokens are transferred to the router at first. Make sure we used // all of it (and thus our splits are correct). @@ -1055,7 +1055,7 @@ contract TychoRouterTest is TychoRouterTestSetup { // WETH ─┤ // └──(USV2)──> DAI ───(USV2)──> USDC deal(WETH_ADDR, ALICE, 1 ether); - uint256 balancerBefore = IERC20(USDC_ADDR).balanceOf(ALICE); + uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE); // Approve permit2 vm.startPrank(ALICE); @@ -1067,10 +1067,10 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); - uint256 balancerAfter = IERC20(USDC_ADDR).balanceOf(ALICE); + uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE); assertTrue(success, "Call Failed"); - assertGe(balancerAfter - balancerBefore, 26173932); + assertGe(balanceAfter - balanceBefore, 26173932); // All input tokens are transferred to the router at first. Make sure we used // all of it (and thus our splits are correct). @@ -1448,4 +1448,19 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); assertGt(IERC20(BASE_MAG7).balanceOf(tychoRouterAddr), 1379830606); } + + function testCurveIntegration() public { + deal(UWU_ADDR, ALICE, 1 ether); + + vm.startPrank(ALICE); + IERC20(UWU_ADDR).approve(tychoRouterAddr, type(uint256).max); + // Encoded solution generated using `test_split_encoding_strategy_curve` + (bool success,) = tychoRouterAddr.call( + hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000055c08ca52497e2f1534b59e2917bf524d4765257000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000005b005900010000001d1499e622d69689cdf9004d05ec547d650ff21155c08ca52497e2f1534b59e2917bf524d4765257c02aaa39b223fe8d0a0e5c4f27ead9083c756cc277146b0a1d08b6844376df6d9da99ba7f1b19e71020100010000000000" + ); + + assertEq(IERC20(WETH_ADDR).balanceOf(ALICE), 4691958787921); + + vm.stopPrank(); + } } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 0cfcc6d..a1e6839 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.26; import "../src/executors/BalancerV2Executor.sol"; +import "../src/executors/CurveExecutor.sol"; import "../src/executors/EkuboExecutor.sol"; import "../src/executors/UniswapV2Executor.sol"; import "../src/executors/UniswapV3Executor.sol"; @@ -42,6 +43,7 @@ contract TychoRouterTestSetup is Test, Constants { UniswapV4Executor public usv4Executor; BalancerV2Executor public balancerv2Executor; EkuboExecutor public ekuboExecutor; + CurveExecutor public curveExecutor; MockERC20[] tokens; function setUp() public { @@ -96,14 +98,16 @@ contract TychoRouterTestSetup is Test, Constants { new UniswapV3Executor(factoryPancakeV3, initCodePancakeV3); balancerv2Executor = new BalancerV2Executor(); ekuboExecutor = new EkuboExecutor(ekuboCore); + curveExecutor = new CurveExecutor(ETH_ADDR); - address[] memory executors = new address[](6); + address[] memory executors = new address[](7); executors[0] = address(usv2Executor); executors[1] = address(usv3Executor); executors[2] = address(pancakev3Executor); executors[3] = address(usv4Executor); executors[4] = address(balancerv2Executor); executors[5] = address(ekuboExecutor); + executors[6] = address(curveExecutor); return executors; } diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 9359a70..b3dee35 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -1773,4 +1773,62 @@ mod tests { assert_eq!(hex_calldata[1288..], expected_swaps); println!("{}", hex_calldata); } + + #[test] + fn test_split_encoding_strategy_curve() { + // UWU ──(curve 2 crypto pool)──> WETH + + let token_in = Bytes::from("0x55C08ca52497e2f1534B59E2917BF524D4765257"); // UWU + let token_out = Bytes::from("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); // USDC + + let static_attributes = HashMap::from([( + "factory".to_string(), + Bytes::from("0x98ee851a00abee0d95d08cf4ca2bdce32aeaaf7f"), + )]); + + let component = ProtocolComponent { + id: String::from("0x77146B0a1d08B6844376dF6d9da99bA7F1b19e71"), + protocol_system: String::from("vm:curve"), + static_attributes, + ..Default::default() + }; + + let swap = Swap { + component, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Some(Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()), + ) + .unwrap(); + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: token_out, + expected_amount: None, + checked_amount: Some(BigUint::from_str("1").unwrap()), + slippage: None, + // Alice + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + println!("{}", hex_calldata); + } } diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index a4d92f7..30e7849 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -375,7 +375,7 @@ impl CurveSwapEncoder { // Tricrypto factory "0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963" => Ok(U8::from(3)), // Twocrypto factory - "0x98ee851a00abee0d95d08cf4ca2bdce32aeaaf7f" => Ok(U8::from(2)), + "0x98EE851a00abeE0d95D08cF4CA2BdCE32aeaAF7F" => Ok(U8::from(2)), // StableSwap factory "0x4F8846Ae9380B90d2E71D5e3D042dff3E7ebb40d" => Ok(U8::from(1)), _ => Err(EncodingError::FatalError(format!( @@ -1131,10 +1131,8 @@ mod tests { #[test] fn test_curve_encode_factory() { let mut static_attributes: HashMap = HashMap::new(); - static_attributes.insert( - "factory".into(), - Bytes::from_str("0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf").unwrap(), - ); + static_attributes + .insert("factory".into(), Bytes::from("0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf")); let curve_pool = ProtocolComponent { id: String::from("0x02950460E2b9529D0E00284A5fA2d7bDF3fA4d72"), protocol_system: String::from("vm:curve"), From 1838ccf8a17d518e174ad926a979bc9d9c151cbd Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 4 Apr 2025 14:32:26 +0100 Subject: [PATCH 3/6] fix: Fix Ekubo test --- don't change below this line --- ENG-4306 Took 15 minutes --- config/test_executor_addresses.json | 2 +- foundry/test/TychoRouter.t.sol | 6 ++---- src/encoding/evm/strategy_encoder/strategy_encoders.rs | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/config/test_executor_addresses.json b/config/test_executor_addresses.json index a7ec22f..018a56b 100644 --- a/config/test_executor_addresses.json +++ b/config/test_executor_addresses.json @@ -7,7 +7,7 @@ "pancakeswap_v3": "0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9", "uniswap_v4": "0xF62849F9A0B5Bf2913b396098F7c7019b51A820a", "vm:balancer_v2": "0xc7183455a4C133Ae270771860664b6B7ec320bB1", - "ekubo_v2": "0x2a07706473244BC757E10F2a9E86fB532828afe3", + "ekubo_v2": "0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7", "vm:curve": "0x1d1499e622D69689cdf9004d05Ec547d650Ff211" } } diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index cea80e7..9d286a4 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -955,7 +955,6 @@ contract TychoRouterTest is TychoRouterTestSetup { uint256 balanceAfter = ALICE.balance; assertTrue(success, "Call Failed"); - console.logUint(balanceAfter - balanceBefore); assertEq(balanceAfter - balanceBefore, 1117254495486192350); } @@ -1022,17 +1021,16 @@ contract TychoRouterTest is TychoRouterTestSetup { // This allows us to change the code at that address to be the testing executor code vm.etch( 0x4f88f6630a33dB05BEa1FeF7Dc7ff7508D1c531D, - 0x2a07706473244BC757E10F2a9E86fB532828afe3.code + 0x3D7Ebc40AF7092E3F1C81F2e996cbA5Cae2090d7.code ); deal(ALICE, 1 ether); uint256 balanceBefore = IERC20(USDC_ADDR).balanceOf(ALICE); - // Approve permit2 vm.startPrank(ALICE); // Encoded solution generated using `test_split_encoding_strategy_ekubo` (bool success,) = address(tychoRouter).call{value: 1 ether}( - hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000077007500010000002a07706473244bc757e10f2a9e86fb532828afe31d1499e622d69689cdf9004d05ec547d650ff2110000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000000000000000000000" + hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000077007500010000003d7ebc40af7092e3f1c81f2e996cba5cae2090d7a4ad4f68d0b91cfd19687c881e50f3a00242828c0000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4851d02a5948496a67827242eabc5725531342527c000000000000000000000000000000000000000000" ); uint256 balanceAfter = IERC20(USDC_ADDR).balanceOf(ALICE); diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index b3dee35..6bc2aa8 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -1083,7 +1083,7 @@ mod tests { eth_chain(), swap_encoder_registry, None, - Some(Bytes::from_str("0x1d1499e622D69689cdf9004d05Ec547d650Ff211").unwrap()), + Some(Bytes::from_str("0xA4AD4f68d0b91CFD19687c881e50f3A00242828c").unwrap()), ) .unwrap(); From 913d677ffbfba61afae066a5a3d3c1f98e2a3f0f Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Mon, 7 Apr 2025 09:20:38 +0100 Subject: [PATCH 4/6] feat: Support Curve ETH Curve pools use a different address from ETH (native token) - Pass Chain into the SwapEncoderBuilder and SwapEncoder - Add native_token_curve_address and native_token_address to CurveSwapEncoder - Added integration test for this curve case --- don't change below this line --- ENG-4306 Took 1 hour 4 minutes Took 11 seconds --- foundry/src/executors/CurveExecutor.sol | 4 +- foundry/test/Constants.sol | 3 +- foundry/test/TychoRouter.t.sol | 14 ++ foundry/test/TychoRouterTestSetup.sol | 2 +- foundry/test/executors/CurveExecutor.t.sol | 8 +- .../evm/strategy_encoder/strategy_encoders.rs | 55 +++++ src/encoding/evm/swap_encoder/builder.rs | 37 ++- .../evm/swap_encoder/swap_encoder_registry.rs | 2 +- .../evm/swap_encoder/swap_encoders.rs | 211 +++++++++++++----- src/encoding/swap_encoder.rs | 5 +- 10 files changed, 270 insertions(+), 71 deletions(-) diff --git a/foundry/src/executors/CurveExecutor.sol b/foundry/src/executors/CurveExecutor.sol index 6d98777..569072f 100644 --- a/foundry/src/executors/CurveExecutor.sol +++ b/foundry/src/executors/CurveExecutor.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -error CurveExecutor__InvalidAddresses(); +error CurveExecutor__AddressZero(); interface CryptoPool { // slither-disable-next-line naming-convention @@ -39,7 +39,7 @@ contract CurveExecutor is IExecutor { constructor(address _nativeToken) { if (_nativeToken == address(0)) { - revert CurveExecutor__InvalidAddresses(); + revert CurveExecutor__AddressZero(); } nativeToken = _nativeToken; } diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index 8ff3dc4..9b0f488 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -30,7 +30,8 @@ contract Constants is Test, BaseConstants { address UNPAUSER = makeAddr("unpauser"); // Assets - address ETH_ADDR = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + address ETH_ADDR_FOR_CURVE = + address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); address WETH_ADDR = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); address DAI_ADDR = address(0x6B175474E89094C44Da98b954EedeAC495271d0F); address BAL_ADDR = address(0xba100000625a3754423978a60c9317c58a424e3D); diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 9d286a4..49fde90 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1461,4 +1461,18 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } + + function testCurveIntegrationStETH() public { + deal(ALICE, 1 ether); + + vm.startPrank(ALICE); + // Encoded solution generated using `test_split_encoding_strategy_curve_st_eth` + (bool success,) = tychoRouterAddr.call{value: 1 ether}( + hex"0a83cb080000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ae7ab96520de3a18e5e111b5eaab095312d7fe840000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000005b005900010000001d1499e622d69689cdf9004d05ec547d650ff211eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeae7ab96520de3a18e5e111b5eaab095312d7fe84dc24316b9ae028f1497c275eb9192a3ea0f67022010001000000000000" + ); + + assertEq(IERC20(STETH_ADDR).balanceOf(ALICE), 1000754689941529590); + + vm.stopPrank(); + } } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index a1e6839..025fd71 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -98,7 +98,7 @@ contract TychoRouterTestSetup is Test, Constants { new UniswapV3Executor(factoryPancakeV3, initCodePancakeV3); balancerv2Executor = new BalancerV2Executor(); ekuboExecutor = new EkuboExecutor(ekuboCore); - curveExecutor = new CurveExecutor(ETH_ADDR); + curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE); address[] memory executors = new address[](7); executors[0] = address(usv2Executor); diff --git a/foundry/test/executors/CurveExecutor.t.sol b/foundry/test/executors/CurveExecutor.t.sol index c0df614..cd15079 100644 --- a/foundry/test/executors/CurveExecutor.t.sol +++ b/foundry/test/executors/CurveExecutor.t.sol @@ -50,7 +50,7 @@ contract CurveExecutorTest is Test, Constants { function setUp() public { uint256 forkBlock = 22031795; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - curveExecutorExposed = new CurveExecutorExposed(ETH_ADDR); + curveExecutorExposed = new CurveExecutorExposed(ETH_ADDR_FOR_CURVE); metaRegistry = MetaRegistry(CURVE_META_REGISTRY); } @@ -105,7 +105,8 @@ contract CurveExecutorTest is Test, Constants { uint256 amountIn = 1 ether; deal(address(curveExecutorExposed), amountIn); - bytes memory data = _getData(ETH_ADDR, STETH_ADDR, STETH_POOL, 1); + bytes memory data = + _getData(ETH_ADDR_FOR_CURVE, STETH_ADDR, STETH_POOL, 1); uint256 amountOut = curveExecutorExposed.swap(amountIn, data); @@ -203,7 +204,8 @@ contract CurveExecutorTest is Test, Constants { uint256 initialBalance = address(curveExecutorExposed).balance; // this address already has some ETH assigned to it deal(XYO_ADDR, address(curveExecutorExposed), amountIn); - bytes memory data = _getData(XYO_ADDR, ETH_ADDR, ETH_XYO_POOL, 2); + bytes memory data = + _getData(XYO_ADDR, ETH_ADDR_FOR_CURVE, ETH_XYO_POOL, 2); uint256 amountOut = curveExecutorExposed.swap(amountIn, data); diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 6bc2aa8..2a602b6 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -1831,4 +1831,59 @@ mod tests { let hex_calldata = encode(&calldata); println!("{}", hex_calldata); } + + #[test] + fn test_split_encoding_strategy_curve_st_eth() { + // ETH ──(curve stETH pool)──> STETH + + let token_in = Bytes::from("0x0000000000000000000000000000000000000000"); // ETH + let token_out = Bytes::from("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"); // STETH + + let static_attributes = HashMap::from([("factory".to_string(), Bytes::from(vec![]))]); + + let component = ProtocolComponent { + id: String::from("0xDC24316b9AE028F1497c275EB9192a3Ea0f67022"), + protocol_system: String::from("vm:curve"), + static_attributes, + ..Default::default() + }; + + let swap = Swap { + component, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = SplitSwapStrategyEncoder::new( + eth_chain(), + swap_encoder_registry, + None, + Some(Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap()), + ) + .unwrap(); + + let solution = Solution { + exact_out: false, + given_token: token_in, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: token_out, + expected_amount: None, + checked_amount: Some(BigUint::from_str("1").unwrap()), + slippage: None, + // Alice + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap], + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + println!("{}", hex_calldata); + } } diff --git a/src/encoding/evm/swap_encoder/builder.rs b/src/encoding/evm/swap_encoder/builder.rs index d1c90c6..e1e585f 100644 --- a/src/encoding/evm/swap_encoder/builder.rs +++ b/src/encoding/evm/swap_encoder/builder.rs @@ -4,6 +4,7 @@ use crate::encoding::{ BalancerV2SwapEncoder, CurveSwapEncoder, EkuboSwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder, UniswapV4SwapEncoder, }, + models::Chain, swap_encoder::SwapEncoder, }; @@ -11,27 +12,43 @@ use crate::encoding::{ pub struct SwapEncoderBuilder { protocol_system: String, executor_address: String, + chain: Chain, } impl SwapEncoderBuilder { - pub fn new(protocol_system: &str, executor_address: &str) -> Self { + pub fn new(protocol_system: &str, executor_address: &str, chain: Chain) -> Self { SwapEncoderBuilder { protocol_system: protocol_system.to_string(), executor_address: executor_address.to_string(), + chain, } } pub fn build(self) -> Result, EncodingError> { match self.protocol_system.as_str() { - "uniswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address))), - "sushiswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address))), - "pancakeswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address))), - "vm:balancer_v2" => Ok(Box::new(BalancerV2SwapEncoder::new(self.executor_address))), - "uniswap_v3" => Ok(Box::new(UniswapV3SwapEncoder::new(self.executor_address))), - "pancakeswap_v3" => Ok(Box::new(UniswapV3SwapEncoder::new(self.executor_address))), - "uniswap_v4" => Ok(Box::new(UniswapV4SwapEncoder::new(self.executor_address))), - "ekubo_v2" => Ok(Box::new(EkuboSwapEncoder::new(self.executor_address))), - "vm:curve" => Ok(Box::new(CurveSwapEncoder::new(self.executor_address))), + "uniswap_v2" => { + Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address, self.chain)?)) + } + "sushiswap_v2" => { + Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address, self.chain)?)) + } + "pancakeswap_v2" => { + Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address, self.chain)?)) + } + "vm:balancer_v2" => { + Ok(Box::new(BalancerV2SwapEncoder::new(self.executor_address, self.chain)?)) + } + "uniswap_v3" => { + Ok(Box::new(UniswapV3SwapEncoder::new(self.executor_address, self.chain)?)) + } + "pancakeswap_v3" => { + Ok(Box::new(UniswapV3SwapEncoder::new(self.executor_address, self.chain)?)) + } + "uniswap_v4" => { + Ok(Box::new(UniswapV4SwapEncoder::new(self.executor_address, self.chain)?)) + } + "ekubo_v2" => Ok(Box::new(EkuboSwapEncoder::new(self.executor_address, self.chain)?)), + "vm:curve" => Ok(Box::new(CurveSwapEncoder::new(self.executor_address, self.chain)?)), _ => Err(EncodingError::FatalError(format!( "Unknown protocol system: {}", self.protocol_system diff --git a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs index 8a5b9e4..b90a2cf 100644 --- a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs +++ b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs @@ -38,7 +38,7 @@ impl SwapEncoderRegistry { .get(&chain.name) .ok_or(EncodingError::FatalError("No executors found for chain".to_string()))?; for (protocol, executor_address) in executors { - let builder = SwapEncoderBuilder::new(protocol, executor_address); + let builder = SwapEncoderBuilder::new(protocol, executor_address, chain.clone()); let encoder = builder.build()?; encoders.insert(protocol.to_string(), encoder); } diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 30e7849..4480663 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -18,7 +18,7 @@ use crate::encoding::{ bytes_to_address, encode_input, get_runtime, get_static_attribute, pad_to_fixed_size, }, }, - models::{EncodingContext, Swap}, + models::{Chain, EncodingContext, Swap}, swap_encoder::SwapEncoder, }; @@ -39,8 +39,8 @@ impl UniswapV2SwapEncoder { } impl SwapEncoder for UniswapV2SwapEncoder { - fn new(executor_address: String) -> Self { - Self { executor_address } + fn new(executor_address: String, _chain: Chain) -> Result { + Ok(Self { executor_address }) } fn encode_swap( @@ -93,8 +93,8 @@ impl UniswapV3SwapEncoder { } impl SwapEncoder for UniswapV3SwapEncoder { - fn new(executor_address: String) -> Self { - Self { executor_address } + fn new(executor_address: String, _chain: Chain) -> Result { + Ok(Self { executor_address }) } fn encode_swap( @@ -151,8 +151,8 @@ impl UniswapV4SwapEncoder { } impl SwapEncoder for UniswapV4SwapEncoder { - fn new(executor_address: String) -> Self { - Self { executor_address } + fn new(executor_address: String, _chain: Chain) -> Result { + Ok(Self { executor_address }) } fn encode_swap( @@ -224,11 +224,11 @@ pub struct BalancerV2SwapEncoder { } impl SwapEncoder for BalancerV2SwapEncoder { - fn new(executor_address: String) -> Self { - Self { + fn new(executor_address: String, _chain: Chain) -> Result { + Ok(Self { executor_address, vault_address: "0xba12222222228d8ba445958a75a0704d566bf2c8".to_string(), - } + }) } fn encode_swap( &self, @@ -282,8 +282,8 @@ pub struct EkuboSwapEncoder { } impl SwapEncoder for EkuboSwapEncoder { - fn new(executor_address: String) -> Self { - Self { executor_address } + fn new(executor_address: String, _chain: Chain) -> Result { + Ok(Self { executor_address }) } fn encode_swap( @@ -340,11 +340,16 @@ impl SwapEncoder for EkuboSwapEncoder { /// /// # Fields /// * `executor_address` - The address of the executor contract that will perform the swap. -/// * `vault_address` - The address of the vault contract that will perform the swap. +/// * `meta_registry_address` - The address of the Curve meta registry contract. Used to get coin +/// indexes. +/// * `native_token_curve_address` - The address used as native token in curve pools. +/// * `native_token_address` - The address of the native token. #[derive(Clone)] pub struct CurveSwapEncoder { executor_address: String, meta_registry_address: String, + native_token_curve_address: Address, + native_token_address: Bytes, } impl CurveSwapEncoder { @@ -416,7 +421,8 @@ impl CurveSwapEncoder { let (i_256, j_256, _): ResponseType = ResponseType::abi_decode(&response, true) .map_err(|_| { EncodingError::FatalError( - "Failed to decode response for allowance".to_string(), + "Failed to decode response when getting coin indexes on a curve pool" + .to_string(), ) })?; let i = U8::from(i_256); @@ -432,11 +438,17 @@ impl CurveSwapEncoder { } impl SwapEncoder for CurveSwapEncoder { - fn new(executor_address: String) -> Self { - Self { + fn new(executor_address: String, chain: Chain) -> Result { + let native_token_curve_address = + Address::from_str("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE").map_err(|_| { + EncodingError::FatalError("Invalid Curve native token address".to_string()) + })?; + Ok(Self { executor_address, meta_registry_address: "0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC".to_string(), - } + native_token_address: chain.native_token()?, + native_token_curve_address, + }) } fn encode_swap( &self, @@ -444,18 +456,31 @@ impl SwapEncoder for CurveSwapEncoder { encoding_context: EncodingContext, ) -> Result, EncodingError> { let token_approvals_manager = ProtocolApprovalsManager::new()?; - let token = bytes_to_address(&swap.token_in)?; + let token_in = if swap.token_in == self.native_token_address { + self.native_token_curve_address + } else { + bytes_to_address(&swap.token_in)? + }; + let token_out = if swap.token_out == self.native_token_address { + self.native_token_curve_address + } else { + bytes_to_address(&swap.token_out)? + }; let approval_needed: bool; let component_address = Address::from_str(&swap.component.id) .map_err(|_| EncodingError::FatalError("Invalid curve pool address".to_string()))?; if let Some(router_address) = encoding_context.router_address { - let tycho_router_address = bytes_to_address(&router_address)?; - approval_needed = token_approvals_manager.approval_needed( - token, - tycho_router_address, - component_address, - )?; + if token_in != self.native_token_curve_address { + let tycho_router_address = bytes_to_address(&router_address)?; + approval_needed = token_approvals_manager.approval_needed( + token_in, + tycho_router_address, + component_address, + )?; + } else { + approval_needed = false; + } } else { approval_needed = true; } @@ -469,15 +494,11 @@ impl SwapEncoder for CurveSwapEncoder { let pool_type = self.get_pool_type(&swap.component.id, factory.as_deref())?; - let (i, j) = self.get_coin_indexes( - component_address, - bytes_to_address(&swap.token_in)?, - bytes_to_address(&swap.token_out)?, - )?; + let (i, j) = self.get_coin_indexes(component_address, token_in, token_out)?; let args = ( - bytes_to_address(&swap.token_in)?, - bytes_to_address(&swap.token_out)?, + token_in, + token_out, component_address, pool_type.to_be_bytes::<1>(), i.to_be_bytes::<1>(), @@ -502,7 +523,10 @@ mod tests { use alloy::hex::encode; use num_bigint::BigInt; - use tycho_common::{models::protocol::ProtocolComponent, Bytes}; + use tycho_common::{ + models::{protocol::ProtocolComponent, Chain as TychoCoreChain}, + Bytes, + }; use super::*; @@ -528,8 +552,11 @@ mod tests { group_token_in: token_in.clone(), group_token_out: token_out.clone(), }; - let encoder = - UniswapV2SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); + let encoder = UniswapV2SwapEncoder::new( + String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + TychoCoreChain::Ethereum.into(), + ) + .unwrap(); let encoded_swap = encoder .encode_swap(swap, encoding_context) .unwrap(); @@ -575,8 +602,11 @@ mod tests { group_token_in: token_in.clone(), group_token_out: token_out.clone(), }; - let encoder = - UniswapV3SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); + let encoder = UniswapV3SwapEncoder::new( + String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + TychoCoreChain::Ethereum.into(), + ) + .unwrap(); let encoded_swap = encoder .encode_swap(swap, encoding_context) .unwrap(); @@ -623,8 +653,11 @@ mod tests { group_token_in: token_in.clone(), group_token_out: token_out.clone(), }; - let encoder = - BalancerV2SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); + let encoder = BalancerV2SwapEncoder::new( + String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + TychoCoreChain::Ethereum.into(), + ) + .unwrap(); let encoded_swap = encoder .encode_swap(swap, encoding_context) .unwrap(); @@ -682,8 +715,11 @@ mod tests { group_token_in: token_in.clone(), group_token_out: token_out.clone(), }; - let encoder = - UniswapV4SwapEncoder::new(String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a")); + let encoder = UniswapV4SwapEncoder::new( + String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"), + TychoCoreChain::Ethereum.into(), + ) + .unwrap(); let encoded_swap = encoder .encode_swap(swap, encoding_context) .unwrap(); @@ -747,8 +783,11 @@ mod tests { group_token_out: token_out.clone(), }; - let encoder = - UniswapV4SwapEncoder::new(String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4")); + let encoder = UniswapV4SwapEncoder::new( + String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), + TychoCoreChain::Ethereum.into(), + ) + .unwrap(); let encoded_swap = encoder .encode_swap(swap, encoding_context) .unwrap(); @@ -835,8 +874,11 @@ mod tests { split: 0f64, }; - let encoder = - UniswapV4SwapEncoder::new(String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a")); + let encoder = UniswapV4SwapEncoder::new( + String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"), + TychoCoreChain::Ethereum.into(), + ) + .unwrap(); let initial_encoded_swap = encoder .encode_swap(initial_swap, context.clone()) .unwrap(); @@ -911,7 +953,8 @@ mod tests { router_address: Some(Bytes::default()), }; - let encoder = EkuboSwapEncoder::new(String::default()); + let encoder = + EkuboSwapEncoder::new(String::default(), TychoCoreChain::Ethereum.into()).unwrap(); let encoded_swap = encoder .encode_swap(swap, encoding_context) @@ -939,7 +982,8 @@ mod tests { let group_token_out = Bytes::from("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT let intermediary_token = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC - let encoder = EkuboSwapEncoder::new(String::default()); + let encoder = + EkuboSwapEncoder::new(String::default(), TychoCoreChain::Ethereum.into()).unwrap(); let encoding_context = EncodingContext { receiver: RECEIVER.into(), @@ -1061,7 +1105,8 @@ mod tests { #[case] expected_i: u64, #[case] expected_j: u64, ) { - let encoder = CurveSwapEncoder::new(String::default()); + let encoder = + CurveSwapEncoder::new(String::default(), TychoCoreChain::Ethereum.into()).unwrap(); let (i, j) = encoder .get_coin_indexes( Address::from_str(pool).unwrap(), @@ -1100,8 +1145,11 @@ mod tests { group_token_in: token_in.clone(), group_token_out: token_out.clone(), }; - let encoder = - CurveSwapEncoder::new(String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f")); + let encoder = CurveSwapEncoder::new( + String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"), + TychoCoreChain::Ethereum.into(), + ) + .unwrap(); let encoded_swap = encoder .encode_swap(swap, encoding_context) .unwrap(); @@ -1155,8 +1203,11 @@ mod tests { group_token_in: token_in.clone(), group_token_out: token_out.clone(), }; - let encoder = - CurveSwapEncoder::new(String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f")); + let encoder = CurveSwapEncoder::new( + String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"), + TychoCoreChain::Ethereum.into(), + ) + .unwrap(); let encoded_swap = encoder .encode_swap(swap, encoding_context) .unwrap(); @@ -1182,4 +1233,62 @@ mod tests { )) ); } + #[test] + fn test_curve_encode_st_eth() { + // This test is for the stETH pool, which is a special case in Curve + // where the token in is ETH but not as the zero address. + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("factory".into(), Bytes::from(vec![])); + let curve_pool = ProtocolComponent { + id: String::from("0xDC24316b9AE028F1497c275EB9192a3Ea0f67022"), + protocol_system: String::from("vm:curve"), + static_attributes, + ..Default::default() + }; + let token_in = Bytes::from("0x0000000000000000000000000000000000000000"); + let token_out = Bytes::from("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"); + let swap = Swap { + component: curve_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + let encoding_context = EncodingContext { + // The receiver was generated with `makeAddr("bob") using forge` + receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), + exact_out: false, + router_address: None, + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + }; + let encoder = CurveSwapEncoder::new( + String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"), + TychoCoreChain::Ethereum.into(), + ) + .unwrap(); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); + + assert_eq!( + hex_swap, + String::from(concat!( + // token in + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + // token out + "ae7ab96520de3a18e5e111b5eaab095312d7fe84", + // pool address + "dc24316b9ae028f1497c275eb9192a3ea0f67022", + // pool type 1 + "01", + // i index + "00", + // j index + "01", + // approval needed + "01", + )) + ); + } } diff --git a/src/encoding/swap_encoder.rs b/src/encoding/swap_encoder.rs index 56fc724..3051523 100644 --- a/src/encoding/swap_encoder.rs +++ b/src/encoding/swap_encoder.rs @@ -1,6 +1,6 @@ use crate::encoding::{ errors::EncodingError, - models::{EncodingContext, Swap}, + models::{Chain, EncodingContext, Swap}, }; /// A trait for protocol-specific swap encoding, where each implementation should handle the @@ -10,7 +10,8 @@ pub trait SwapEncoder: Sync + Send { /// /// # Arguments /// * `executor_address` - The address of the contract that will execute the swap - fn new(executor_address: String) -> Self + /// * `chain` - The chain on which the swap will be executed + fn new(executor_address: String, chain: Chain) -> Result where Self: Sized; From 739fb46d205251e39188afe983d8ce5ff3cb5d60 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 8 Apr 2025 09:29:38 +0100 Subject: [PATCH 5/6] feat: Add protocol_specific_addresses.json file This way we can define protocol and chain specific addresses in this file and use them in the Encoders --- don't change below this line --- ENG-4306 Took 1 hour 4 minutes --- config/protocol_specific_addresses.json | 11 + src/encoding/evm/constants.rs | 2 + src/encoding/evm/swap_encoder/builder.rs | 71 ++- .../evm/swap_encoder/swap_encoder_registry.rs | 24 +- .../evm/swap_encoder/swap_encoders.rs | 467 +++++++++++------- src/encoding/swap_encoder.rs | 10 +- 6 files changed, 370 insertions(+), 215 deletions(-) create mode 100644 config/protocol_specific_addresses.json diff --git a/config/protocol_specific_addresses.json b/config/protocol_specific_addresses.json new file mode 100644 index 0000000..c503d02 --- /dev/null +++ b/config/protocol_specific_addresses.json @@ -0,0 +1,11 @@ +{ + "ethereum": { + "vm:balancer_v2": { + "vault_address": "0xba12222222228d8ba445958a75a0704d566bf2c8" + }, + "vm:curve": { + "native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + "meta_registry_address": "0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC" + } + } +} \ No newline at end of file diff --git a/src/encoding/evm/constants.rs b/src/encoding/evm/constants.rs index b3d33fc..feba1cc 100644 --- a/src/encoding/evm/constants.rs +++ b/src/encoding/evm/constants.rs @@ -2,6 +2,8 @@ use std::{collections::HashSet, sync::LazyLock}; pub const DEFAULT_EXECUTORS_JSON: &str = include_str!("../../../config/executor_addresses.json"); pub const DEFAULT_ROUTERS_JSON: &str = include_str!("../../../config/router_addresses.json"); +pub const PROTOCOL_SPECIFIC_CONFIG: &str = + include_str!("../../../config/protocol_specific_addresses.json"); /// These protocols support the optimization of grouping swaps. /// diff --git a/src/encoding/evm/swap_encoder/builder.rs b/src/encoding/evm/swap_encoder/builder.rs index e1e585f..bbe2c2d 100644 --- a/src/encoding/evm/swap_encoder/builder.rs +++ b/src/encoding/evm/swap_encoder/builder.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::encoding::{ errors::EncodingError, evm::swap_encoder::swap_encoders::{ @@ -13,42 +15,67 @@ pub struct SwapEncoderBuilder { protocol_system: String, executor_address: String, chain: Chain, + config: Option>, } impl SwapEncoderBuilder { - pub fn new(protocol_system: &str, executor_address: &str, chain: Chain) -> Self { + pub fn new( + protocol_system: &str, + executor_address: &str, + chain: Chain, + config: Option>, + ) -> Self { SwapEncoderBuilder { protocol_system: protocol_system.to_string(), executor_address: executor_address.to_string(), chain, + config, } } pub fn build(self) -> Result, EncodingError> { match self.protocol_system.as_str() { - "uniswap_v2" => { - Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address, self.chain)?)) + "uniswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new( + self.executor_address, + self.chain, + self.config, + )?)), + "sushiswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new( + self.executor_address, + self.chain, + self.config, + )?)), + "pancakeswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new( + self.executor_address, + self.chain, + self.config, + )?)), + "vm:balancer_v2" => Ok(Box::new(BalancerV2SwapEncoder::new( + self.executor_address, + self.chain, + self.config, + )?)), + "uniswap_v3" => Ok(Box::new(UniswapV3SwapEncoder::new( + self.executor_address, + self.chain, + self.config, + )?)), + "pancakeswap_v3" => Ok(Box::new(UniswapV3SwapEncoder::new( + self.executor_address, + self.chain, + self.config, + )?)), + "uniswap_v4" => Ok(Box::new(UniswapV4SwapEncoder::new( + self.executor_address, + self.chain, + self.config, + )?)), + "ekubo_v2" => { + Ok(Box::new(EkuboSwapEncoder::new(self.executor_address, self.chain, self.config)?)) } - "sushiswap_v2" => { - Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address, self.chain)?)) + "vm:curve" => { + Ok(Box::new(CurveSwapEncoder::new(self.executor_address, self.chain, self.config)?)) } - "pancakeswap_v2" => { - Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address, self.chain)?)) - } - "vm:balancer_v2" => { - Ok(Box::new(BalancerV2SwapEncoder::new(self.executor_address, self.chain)?)) - } - "uniswap_v3" => { - Ok(Box::new(UniswapV3SwapEncoder::new(self.executor_address, self.chain)?)) - } - "pancakeswap_v3" => { - Ok(Box::new(UniswapV3SwapEncoder::new(self.executor_address, self.chain)?)) - } - "uniswap_v4" => { - Ok(Box::new(UniswapV4SwapEncoder::new(self.executor_address, self.chain)?)) - } - "ekubo_v2" => Ok(Box::new(EkuboSwapEncoder::new(self.executor_address, self.chain)?)), - "vm:curve" => Ok(Box::new(CurveSwapEncoder::new(self.executor_address, self.chain)?)), _ => Err(EncodingError::FatalError(format!( "Unknown protocol system: {}", self.protocol_system diff --git a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs index b90a2cf..130e4cd 100644 --- a/src/encoding/evm/swap_encoder/swap_encoder_registry.rs +++ b/src/encoding/evm/swap_encoder/swap_encoder_registry.rs @@ -2,7 +2,10 @@ use std::{collections::HashMap, fs}; use crate::encoding::{ errors::EncodingError, - evm::{constants::DEFAULT_EXECUTORS_JSON, swap_encoder::builder::SwapEncoderBuilder}, + evm::{ + constants::{DEFAULT_EXECUTORS_JSON, PROTOCOL_SPECIFIC_CONFIG}, + swap_encoder::builder::SwapEncoderBuilder, + }, models::Chain, swap_encoder::SwapEncoder, }; @@ -33,12 +36,27 @@ impl SwapEncoderRegistry { DEFAULT_EXECUTORS_JSON.to_string() }; let config: HashMap> = serde_json::from_str(&config_str)?; - let mut encoders = HashMap::new(); let executors = config .get(&chain.name) .ok_or(EncodingError::FatalError("No executors found for chain".to_string()))?; + + let protocol_specific_config: HashMap>> = + serde_json::from_str(PROTOCOL_SPECIFIC_CONFIG)?; + let protocol_specific_config = protocol_specific_config + .get(&chain.name) + .ok_or(EncodingError::FatalError( + "No protocol specific config found for chain".to_string(), + ))?; + let mut encoders = HashMap::new(); for (protocol, executor_address) in executors { - let builder = SwapEncoderBuilder::new(protocol, executor_address, chain.clone()); + let builder = SwapEncoderBuilder::new( + protocol, + executor_address, + chain.clone(), + protocol_specific_config + .get(protocol) + .cloned(), + ); let encoder = builder.build()?; encoders.insert(protocol.to_string(), encoder); } diff --git a/src/encoding/evm/swap_encoder/swap_encoders.rs b/src/encoding/evm/swap_encoder/swap_encoders.rs index 4480663..eff2e0d 100644 --- a/src/encoding/evm/swap_encoder/swap_encoders.rs +++ b/src/encoding/evm/swap_encoder/swap_encoders.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; use alloy::{ providers::Provider, @@ -39,7 +39,11 @@ impl UniswapV2SwapEncoder { } impl SwapEncoder for UniswapV2SwapEncoder { - fn new(executor_address: String, _chain: Chain) -> Result { + fn new( + executor_address: String, + _chain: Chain, + _config: Option>, + ) -> Result { Ok(Self { executor_address }) } @@ -93,7 +97,11 @@ impl UniswapV3SwapEncoder { } impl SwapEncoder for UniswapV3SwapEncoder { - fn new(executor_address: String, _chain: Chain) -> Result { + fn new( + executor_address: String, + _chain: Chain, + _config: Option>, + ) -> Result { Ok(Self { executor_address }) } @@ -151,7 +159,11 @@ impl UniswapV4SwapEncoder { } impl SwapEncoder for UniswapV4SwapEncoder { - fn new(executor_address: String, _chain: Chain) -> Result { + fn new( + executor_address: String, + _chain: Chain, + _config: Option>, + ) -> Result { Ok(Self { executor_address }) } @@ -224,12 +236,23 @@ pub struct BalancerV2SwapEncoder { } impl SwapEncoder for BalancerV2SwapEncoder { - fn new(executor_address: String, _chain: Chain) -> Result { - Ok(Self { - executor_address, - vault_address: "0xba12222222228d8ba445958a75a0704d566bf2c8".to_string(), - }) + fn new( + executor_address: String, + _chain: Chain, + config: Option>, + ) -> Result { + let config = config.ok_or(EncodingError::FatalError( + "Missing balancer specific addresses in config".to_string(), + ))?; + let vault_address = config + .get("vault_address") + .ok_or(EncodingError::FatalError( + "Missing balancer vault address in config".to_string(), + ))? + .to_string(); + Ok(Self { executor_address, vault_address }) } + fn encode_swap( &self, swap: Swap, @@ -282,7 +305,11 @@ pub struct EkuboSwapEncoder { } impl SwapEncoder for EkuboSwapEncoder { - fn new(executor_address: String, _chain: Chain) -> Result { + fn new( + executor_address: String, + _chain: Chain, + _config: Option>, + ) -> Result { Ok(Self { executor_address }) } @@ -348,7 +375,7 @@ impl SwapEncoder for EkuboSwapEncoder { pub struct CurveSwapEncoder { executor_address: String, meta_registry_address: String, - native_token_curve_address: Address, + native_token_curve_address: String, native_token_address: Bytes, } @@ -438,31 +465,51 @@ impl CurveSwapEncoder { } impl SwapEncoder for CurveSwapEncoder { - fn new(executor_address: String, chain: Chain) -> Result { - let native_token_curve_address = - Address::from_str("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE").map_err(|_| { - EncodingError::FatalError("Invalid Curve native token address".to_string()) - })?; + fn new( + executor_address: String, + chain: Chain, + config: Option>, + ) -> Result { + let config = config.ok_or(EncodingError::FatalError( + "Missing curve specific addresses in config".to_string(), + ))?; + let native_token_curve_address = config + .get("native_token_address") + .ok_or(EncodingError::FatalError( + "Missing native token curve address in config".to_string(), + ))? + .to_string(); + let meta_registry_address = config + .get("meta_registry_address") + .ok_or(EncodingError::FatalError( + "Missing meta registry address in config".to_string(), + ))? + .to_string(); Ok(Self { executor_address, - meta_registry_address: "0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC".to_string(), + meta_registry_address, native_token_address: chain.native_token()?, native_token_curve_address, }) } + fn encode_swap( &self, swap: Swap, encoding_context: EncodingContext, ) -> Result, EncodingError> { let token_approvals_manager = ProtocolApprovalsManager::new()?; + let native_token_curve_address = Address::from_str(&self.native_token_curve_address) + .map_err(|_| { + EncodingError::FatalError("Invalid Curve native token curve address".to_string()) + })?; let token_in = if swap.token_in == self.native_token_address { - self.native_token_curve_address + native_token_curve_address } else { bytes_to_address(&swap.token_in)? }; let token_out = if swap.token_out == self.native_token_address { - self.native_token_curve_address + native_token_curve_address } else { bytes_to_address(&swap.token_out)? }; @@ -471,7 +518,7 @@ impl SwapEncoder for CurveSwapEncoder { let component_address = Address::from_str(&swap.component.id) .map_err(|_| EncodingError::FatalError("Invalid curve pool address".to_string()))?; if let Some(router_address) = encoding_context.router_address { - if token_in != self.native_token_curve_address { + if token_in != native_token_curve_address { let tycho_router_address = bytes_to_address(&router_address)?; approval_needed = token_approvals_manager.approval_needed( token_in, @@ -555,6 +602,7 @@ mod tests { let encoder = UniswapV2SwapEncoder::new( String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), TychoCoreChain::Ethereum.into(), + None, ) .unwrap(); let encoded_swap = encoder @@ -605,6 +653,7 @@ mod tests { let encoder = UniswapV3SwapEncoder::new( String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), TychoCoreChain::Ethereum.into(), + None, ) .unwrap(); let encoded_swap = encoder @@ -656,6 +705,10 @@ mod tests { let encoder = BalancerV2SwapEncoder::new( String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), TychoCoreChain::Ethereum.into(), + Some(HashMap::from([( + "vault_address".to_string(), + "0xba12222222228d8ba445958a75a0704d566bf2c8".to_string(), + )])), ) .unwrap(); let encoded_swap = encoder @@ -718,6 +771,7 @@ mod tests { let encoder = UniswapV4SwapEncoder::new( String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"), TychoCoreChain::Ethereum.into(), + None, ) .unwrap(); let encoded_swap = encoder @@ -786,6 +840,7 @@ mod tests { let encoder = UniswapV4SwapEncoder::new( String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"), TychoCoreChain::Ethereum.into(), + None, ) .unwrap(); let encoded_swap = encoder @@ -877,6 +932,7 @@ mod tests { let encoder = UniswapV4SwapEncoder::new( String::from("0xF62849F9A0B5Bf2913b396098F7c7019b51A820a"), TychoCoreChain::Ethereum.into(), + None, ) .unwrap(); let initial_encoded_swap = encoder @@ -954,7 +1010,8 @@ mod tests { }; let encoder = - EkuboSwapEncoder::new(String::default(), TychoCoreChain::Ethereum.into()).unwrap(); + EkuboSwapEncoder::new(String::default(), TychoCoreChain::Ethereum.into(), None) + .unwrap(); let encoded_swap = encoder .encode_swap(swap, encoding_context) @@ -983,7 +1040,8 @@ mod tests { let intermediary_token = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC let encoder = - EkuboSwapEncoder::new(String::default(), TychoCoreChain::Ethereum.into()).unwrap(); + EkuboSwapEncoder::new(String::default(), TychoCoreChain::Ethereum.into(), None) + .unwrap(); let encoding_context = EncodingContext { receiver: RECEIVER.into(), @@ -1062,6 +1120,19 @@ mod tests { use super::*; + fn curve_config() -> Option> { + Some(HashMap::from([ + ( + "native_token_address".to_string(), + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE".to_string(), + ), + ( + "meta_registry_address".to_string(), + "0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC".to_string(), + ), + ])) + } + #[rstest] #[case( "0x5500307Bcf134E5851FB4D7D8D1Dc556dCdB84B4", @@ -1105,8 +1176,12 @@ mod tests { #[case] expected_i: u64, #[case] expected_j: u64, ) { - let encoder = - CurveSwapEncoder::new(String::default(), TychoCoreChain::Ethereum.into()).unwrap(); + let encoder = CurveSwapEncoder::new( + String::default(), + TychoCoreChain::Ethereum.into(), + curve_config(), + ) + .unwrap(); let (i, j) = encoder .get_coin_indexes( Address::from_str(pool).unwrap(), @@ -1117,178 +1192,192 @@ mod tests { assert_eq!(i, U8::from(expected_i)); assert_eq!(j, U8::from(expected_j)); } - } - #[test] - fn test_curve_encode_tripool() { - let mut static_attributes: HashMap = HashMap::new(); - static_attributes.insert("factory".into(), Bytes::from(vec![])); - let curve_tri_pool = ProtocolComponent { - id: String::from("0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7"), - protocol_system: String::from("vm:curve"), - static_attributes, - ..Default::default() - }; - let token_in = Bytes::from("0x6B175474E89094C44Da98b954EedeAC495271d0F"); - let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); - let swap = Swap { - component: curve_tri_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - let encoding_context = EncodingContext { - // The receiver was generated with `makeAddr("bob") using forge` - receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), - exact_out: false, - router_address: None, - group_token_in: token_in.clone(), - group_token_out: token_out.clone(), - }; - let encoder = CurveSwapEncoder::new( - String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"), - TychoCoreChain::Ethereum.into(), - ) - .unwrap(); - let encoded_swap = encoder - .encode_swap(swap, encoding_context) + #[test] + fn test_curve_encode_tripool() { + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("factory".into(), Bytes::from(vec![])); + let curve_tri_pool = ProtocolComponent { + id: String::from("0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7"), + protocol_system: String::from("vm:curve"), + static_attributes, + ..Default::default() + }; + let token_in = Bytes::from("0x6B175474E89094C44Da98b954EedeAC495271d0F"); + let token_out = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + let swap = Swap { + component: curve_tri_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + let encoding_context = EncodingContext { + // The receiver was generated with `makeAddr("bob") using forge` + receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), + exact_out: false, + router_address: None, + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + }; + let encoder = CurveSwapEncoder::new( + String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"), + TychoCoreChain::Ethereum.into(), + curve_config(), + ) .unwrap(); - let hex_swap = encode(&encoded_swap); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); - assert_eq!( - hex_swap, - String::from(concat!( - // token in - "6b175474e89094c44da98b954eedeac495271d0f", - // token out - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // pool address - "bebc44782c7db0a1a60cb6fe97d0b483032ff1c7", - // pool type 1 - "01", - // i index - "00", - // j index - "01", - // approval needed - "01", - )) - ); - } + assert_eq!( + hex_swap, + String::from(concat!( + // token in + "6b175474e89094c44da98b954eedeac495271d0f", + // token out + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // pool address + "bebc44782c7db0a1a60cb6fe97d0b483032ff1c7", + // pool type 1 + "01", + // i index + "00", + // j index + "01", + // approval needed + "01", + )) + ); + } - #[test] - fn test_curve_encode_factory() { - let mut static_attributes: HashMap = HashMap::new(); - static_attributes - .insert("factory".into(), Bytes::from("0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf")); - let curve_pool = ProtocolComponent { - id: String::from("0x02950460E2b9529D0E00284A5fA2d7bDF3fA4d72"), - protocol_system: String::from("vm:curve"), - static_attributes, - ..Default::default() - }; - let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); - let token_out = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); - let swap = Swap { - component: curve_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - let encoding_context = EncodingContext { - // The receiver was generated with `makeAddr("bob") using forge` - receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), - exact_out: false, - router_address: None, - group_token_in: token_in.clone(), - group_token_out: token_out.clone(), - }; - let encoder = CurveSwapEncoder::new( - String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"), - TychoCoreChain::Ethereum.into(), - ) - .unwrap(); - let encoded_swap = encoder - .encode_swap(swap, encoding_context) + #[test] + fn test_curve_encode_factory() { + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert( + "factory".into(), + Bytes::from("0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf"), + ); + let curve_pool = ProtocolComponent { + id: String::from("0x02950460E2b9529D0E00284A5fA2d7bDF3fA4d72"), + protocol_system: String::from("vm:curve"), + static_attributes, + ..Default::default() + }; + let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + let token_out = Bytes::from("0x4c9EDD5852cd905f086C759E8383e09bff1E68B3"); + let swap = Swap { + component: curve_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + let encoding_context = EncodingContext { + // The receiver was generated with `makeAddr("bob") using forge` + receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), + exact_out: false, + router_address: None, + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + }; + let encoder = CurveSwapEncoder::new( + String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"), + TychoCoreChain::Ethereum.into(), + curve_config(), + ) .unwrap(); - let hex_swap = encode(&encoded_swap); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); - assert_eq!( - hex_swap, - String::from(concat!( - // token in - "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - // token out - "4c9edd5852cd905f086c759e8383e09bff1e68b3", - // pool address - "02950460e2b9529d0e00284a5fa2d7bdf3fa4d72", - // pool type 1 - "01", - // i index - "01", - // j index - "00", - // approval needed - "01", - )) - ); - } - #[test] - fn test_curve_encode_st_eth() { - // This test is for the stETH pool, which is a special case in Curve - // where the token in is ETH but not as the zero address. - let mut static_attributes: HashMap = HashMap::new(); - static_attributes.insert("factory".into(), Bytes::from(vec![])); - let curve_pool = ProtocolComponent { - id: String::from("0xDC24316b9AE028F1497c275EB9192a3Ea0f67022"), - protocol_system: String::from("vm:curve"), - static_attributes, - ..Default::default() - }; - let token_in = Bytes::from("0x0000000000000000000000000000000000000000"); - let token_out = Bytes::from("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"); - let swap = Swap { - component: curve_pool, - token_in: token_in.clone(), - token_out: token_out.clone(), - split: 0f64, - }; - let encoding_context = EncodingContext { - // The receiver was generated with `makeAddr("bob") using forge` - receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), - exact_out: false, - router_address: None, - group_token_in: token_in.clone(), - group_token_out: token_out.clone(), - }; - let encoder = CurveSwapEncoder::new( - String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"), - TychoCoreChain::Ethereum.into(), - ) - .unwrap(); - let encoded_swap = encoder - .encode_swap(swap, encoding_context) + assert_eq!( + hex_swap, + String::from(concat!( + // token in + "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + // token out + "4c9edd5852cd905f086c759e8383e09bff1e68b3", + // pool address + "02950460e2b9529d0e00284a5fa2d7bdf3fa4d72", + // pool type 1 + "01", + // i index + "01", + // j index + "00", + // approval needed + "01", + )) + ); + } + #[test] + fn test_curve_encode_st_eth() { + // This test is for the stETH pool, which is a special case in Curve + // where the token in is ETH but not as the zero address. + let mut static_attributes: HashMap = HashMap::new(); + static_attributes.insert("factory".into(), Bytes::from(vec![])); + let curve_pool = ProtocolComponent { + id: String::from("0xDC24316b9AE028F1497c275EB9192a3Ea0f67022"), + protocol_system: String::from("vm:curve"), + static_attributes, + ..Default::default() + }; + let token_in = Bytes::from("0x0000000000000000000000000000000000000000"); + let token_out = Bytes::from("0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"); + let swap = Swap { + component: curve_pool, + token_in: token_in.clone(), + token_out: token_out.clone(), + split: 0f64, + }; + let encoding_context = EncodingContext { + // The receiver was generated with `makeAddr("bob") using forge` + receiver: Bytes::from("0x1d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e"), + exact_out: false, + router_address: None, + group_token_in: token_in.clone(), + group_token_out: token_out.clone(), + }; + let encoder = CurveSwapEncoder::new( + String::from("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f"), + TychoCoreChain::Ethereum.into(), + Some(HashMap::from([ + ( + "native_token_address".to_string(), + "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE".to_string(), + ), + ( + "meta_registry_address".to_string(), + "0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC".to_string(), + ), + ])), + ) .unwrap(); - let hex_swap = encode(&encoded_swap); + let encoded_swap = encoder + .encode_swap(swap, encoding_context) + .unwrap(); + let hex_swap = encode(&encoded_swap); - assert_eq!( - hex_swap, - String::from(concat!( - // token in - "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - // token out - "ae7ab96520de3a18e5e111b5eaab095312d7fe84", - // pool address - "dc24316b9ae028f1497c275eb9192a3ea0f67022", - // pool type 1 - "01", - // i index - "00", - // j index - "01", - // approval needed - "01", - )) - ); + assert_eq!( + hex_swap, + String::from(concat!( + // token in + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + // token out + "ae7ab96520de3a18e5e111b5eaab095312d7fe84", + // pool address + "dc24316b9ae028f1497c275eb9192a3ea0f67022", + // pool type 1 + "01", + // i index + "00", + // j index + "01", + // approval needed + "01", + )) + ); + } } } diff --git a/src/encoding/swap_encoder.rs b/src/encoding/swap_encoder.rs index 3051523..9d23aad 100644 --- a/src/encoding/swap_encoder.rs +++ b/src/encoding/swap_encoder.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::encoding::{ errors::EncodingError, models::{Chain, EncodingContext, Swap}, @@ -11,7 +13,13 @@ pub trait SwapEncoder: Sync + Send { /// # Arguments /// * `executor_address` - The address of the contract that will execute the swap /// * `chain` - The chain on which the swap will be executed - fn new(executor_address: String, chain: Chain) -> Result + /// * `config` - Additional configuration parameters for the encoder, like vault or registry + /// address + fn new( + executor_address: String, + chain: Chain, + config: Option>, + ) -> Result where Self: Sized; From c0b50c06616cf18c55cdbedf0196cda748a1a138 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Tue, 8 Apr 2025 16:18:44 +0100 Subject: [PATCH 6/6] fix: Add empty dicts for unichain and base in config Also fix example: we need a checked amount or slippage set now --- don't change below this line --- ENG-4332 Took 6 minutes --- config/protocol_specific_addresses.json | 4 +++- examples/encoding-example/main.rs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config/protocol_specific_addresses.json b/config/protocol_specific_addresses.json index c503d02..4ec2a27 100644 --- a/config/protocol_specific_addresses.json +++ b/config/protocol_specific_addresses.json @@ -7,5 +7,7 @@ "native_token_address": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "meta_registry_address": "0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC" } - } + }, + "base": {}, + "unichain": {} } \ No newline at end of file diff --git a/examples/encoding-example/main.rs b/examples/encoding-example/main.rs index 7ccc0bc..5edc965 100644 --- a/examples/encoding-example/main.rs +++ b/examples/encoding-example/main.rs @@ -56,8 +56,8 @@ fn main() { given_token: weth.clone(), given_amount: BigUint::from_str("1_000000000000000000").expect("Failed to create amount"), checked_token: usdc.clone(), - exact_out: false, // it's an exact in solution - checked_amount: None, // the amount out will not be checked in execution + exact_out: false, // it's an exact in solution + checked_amount: Some(BigUint::from(1u64)), swaps: vec![simple_swap], ..Default::default() };