From e9bb8c576a96f3e523dc4e5011d48028cbc4d599 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Fri, 4 Apr 2025 13:48:10 +0100 Subject: [PATCH] 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;