From 3f4e27a34890b8865d59956d009bd4a44aa7fe54 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Mon, 3 Feb 2025 22:40:43 -0500 Subject: [PATCH 1/2] fix: Fix bug with token indexing when wrapping/unwrapping The issue was that we weren't indexing WETH properly since it was looking for the WETH address in tokens, when only native ETH would be in there - Found by adding integration tests for the wrapping and unwrapping cases. --- foundry/src/TychoRouter.sol | 1 + foundry/test/TychoRouter.t.sol | 58 ++++++++ .../evm/strategy_encoder/strategy_encoders.rs | 128 ++++++++++++++++-- 3 files changed, 177 insertions(+), 10 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index b9f4d1c..0814d16 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -207,6 +207,7 @@ contract TychoRouter is tokenInIndex = swapData.tokenInIndex(); tokenOutIndex = swapData.tokenOutIndex(); split = swapData.splitPercentage(); + currentAmountIn = split > 0 ? (amounts[tokenInIndex] * split) / 0xffffff : remainingAmounts[tokenInIndex]; diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index a842595..eae3e9e 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -699,6 +699,64 @@ contract TychoRouterTest is TychoRouterTestSetup { assertGt(balancerAfter - balancerBefore, 26173932); } + function testSingleSwapWithWrapIntegration() public { + // Test created with calldata from our router encoder, replacing the executor + // address with the USV2 executor address. + + // 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); + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(WETH_ADDR).approve(address(permit2Address), type(uint256).max); + // Encoded solution generated using + // `test_split_swap_strategy_encoder_simple_route_wrap` + // but manually replacing the executor address + // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test + // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + (bool success,) = tychoRouterAddr.call{value: 1 ether}( + hex"4860f9ed0000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067c9179300000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067a1919b000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041cea77a63613f6a02aaee522c91f9569b8377a7f0200d141fafa3e1c42011e1c668555b49a1e7dd960091d0e33764ad24db6550bc761e228864495b478f1a23721b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00020000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625abc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(DAI_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertGt(balancerAfter - balancerBefore, 26173932); + } + + function testSingleSwapWithUnwrapIntegration() public { + // Test created with calldata from our router encoder, replacing the executor + // address with the USV2 executor address. + + // 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; + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(DAI_ADDR).approve(address(permit2Address), type(uint256).max); + // Encoded solution generated using + // `test_split_swap_strategy_encoder_simple_route_unwrap` + // but manually replacing the executor address + // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test + // `5615deb798bb3e4dfa0139dfa1b3d433cc23b72f` + (bool success,) = tychoRouterAddr.call( + hex"4860f9ed0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000a2a15d09519be000000000000000000000000000000000000000000000000000000000000067c9185300000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067a1925b000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041fd1c3dfce5afcb47988cc68165d5de64186cedbeb7eee6fc9cd087bceeaacdfe1ab799d60e0c628f24edfd9819b94ed60846dd23240c481f1d6e5470a7815a891c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c005a00010000005615deb798bb3e4dfa0139dfa1b3d433cc23b72fbd0625ab6b175474e89094c44da98b954eedeac495271d0fa478c2975ab1ea89e8196811f51a7b7ade33eb113ede3eca2a72b3aecc820e955b36f38437d013950100000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = ALICE.balance; + + assertTrue(success, "Call Failed"); + assertGt(balancerAfter - balancerBefore, 26173932); + } + function testSplitSwapIntegration() public { // Test created with calldata from our router encoder, replacing the executor // address with the USV2 executor address. diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 38dbeda..43c8fcd 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -9,6 +9,7 @@ use crate::encoding::{ errors::EncodingError, evm::{ approvals::permit2::Permit2, + constants::WETH_ADDRESS, swap_encoder::SWAP_ENCODER_REGISTRY, utils::{biguint_to_u256, bytes_to_address, encode_input, percentage_to_uint24}, }, @@ -108,10 +109,27 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { // runs) intermediary_tokens.sort(); + let (mut unwrap, mut wrap) = (false, false); + if let Some(action) = solution.native_action.clone() { + match action { + NativeAction::Wrap => wrap = true, + NativeAction::Unwrap => unwrap = true, + } + } + let mut tokens = Vec::with_capacity(2 + intermediary_tokens.len()); - tokens.push(solution.given_token.clone()); + if wrap { + tokens.push(WETH_ADDRESS.clone()); + } else { + tokens.push(solution.given_token.clone()); + } tokens.extend(intermediary_tokens); - tokens.push(solution.checked_token.clone()); + + if unwrap { + tokens.push(WETH_ADDRESS.clone()); + } else { + tokens.push(solution.checked_token.clone()); + } let mut swaps = vec![]; for swap in solution.swaps.iter() { @@ -164,13 +182,6 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { } let encoded_swaps = self.ple_encode(swaps); - let (mut unwrap, mut wrap) = (false, false); - if let Some(action) = solution.native_action { - match action { - NativeAction::Wrap => wrap = true, - NativeAction::Unwrap => unwrap = true, - } - } let method_calldata = ( biguint_to_u256(&solution.given_amount), bytes_to_address(&solution.given_token)?, @@ -247,7 +258,10 @@ mod tests { use tycho_core::{dto::ProtocolComponent, Bytes}; use super::*; - use crate::encoding::models::Swap; + use crate::encoding::{ + evm::constants::{NATIVE_ADDRESS, WETH_ADDRESS}, + models::Swap, + }; #[test] fn test_executor_strategy_encode() { @@ -403,6 +417,100 @@ mod tests { assert_eq!(hex_calldata[1288..], expected_swaps); } + #[test] + fn test_split_swap_strategy_encoder_simple_route_wrap() { + // Performs a single swap from WETH to DAI on a USV2 pool, wrapping ETH + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + + // Set up a mock private key for signing + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: WETH_ADDRESS.clone(), + token_out: dai.clone(), + split: 0f64, + }; + + let encoder = SplitSwapStrategyEncoder::new(private_key, Chain::Ethereum).unwrap(); + let solution = Solution { + exact_out: false, + given_token: NATIVE_ADDRESS.clone(), + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: dai, + expected_amount: BigUint::from_str("3_000_000000000000000000").unwrap(), + check_amount: None, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap], + native_action: Some(NativeAction::Wrap), + ..Default::default() + }; + let router_address = Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(); + + let (calldata, _) = encoder + .encode_strategy(solution, router_address) + .unwrap(); + + let hex_calldata = encode(&calldata); + println!("{}", hex_calldata); + } + + #[test] + fn test_split_swap_strategy_encoder_simple_route_unwrap() { + // Performs a single swap from DAI to WETH on a USV2 pool, unwrapping ETH at the end + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + + // Set up a mock private key for signing + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let dai = Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap(); + + let swap = Swap { + component: ProtocolComponent { + id: "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: dai.clone(), + token_out: WETH_ADDRESS.clone(), + split: 0f64, + }; + + let encoder = SplitSwapStrategyEncoder::new(private_key, Chain::Ethereum).unwrap(); + let solution = Solution { + exact_out: false, + given_token: dai, + given_amount: BigUint::from_str("3_000_000000000000000000").unwrap(), + checked_token: NATIVE_ADDRESS.clone(), + expected_amount: BigUint::from_str("1_000000000000000000").unwrap(), + check_amount: None, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + swaps: vec![swap], + native_action: Some(NativeAction::Unwrap), + ..Default::default() + }; + let router_address = Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(); + + let (calldata, _) = encoder + .encode_strategy(solution, router_address) + .unwrap(); + + let hex_calldata = encode(&calldata); + println!("{}", hex_calldata); + } + #[test] fn test_split_swap_strategy_encoder_complex_route() { // Note: This test does not assert anything. It is only used to obtain integration test From ff283bc33383aa38ddeca7891795c0bcac1164fc Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 4 Feb 2025 11:49:27 -0500 Subject: [PATCH 2/2] fix: test fixes after merge --- src/encoding/evm/strategy_encoder/strategy_encoders.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index 55b0514..8dc9eb7 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -480,7 +480,7 @@ mod tests { given_token: NATIVE_ADDRESS.clone(), given_amount: BigUint::from_str("1_000000000000000000").unwrap(), checked_token: dai, - expected_amount: BigUint::from_str("3_000_000000000000000000").unwrap(), + expected_amount: Some(BigUint::from_str("3_000_000000000000000000").unwrap()), check_amount: None, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), @@ -527,7 +527,7 @@ mod tests { given_token: dai, given_amount: BigUint::from_str("3_000_000000000000000000").unwrap(), checked_token: NATIVE_ADDRESS.clone(), - expected_amount: BigUint::from_str("1_000000000000000000").unwrap(), + expected_amount: Some(BigUint::from_str("1_000000000000000000").unwrap()), check_amount: None, sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(),