From 913d677ffbfba61afae066a5a3d3c1f98e2a3f0f Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Mon, 7 Apr 2025 09:20:38 +0100 Subject: [PATCH] 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;