From a7aa4d7ebb956ec290b5853245f0e1f0077d708a Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Fri, 21 Feb 2025 00:23:24 -0500 Subject: [PATCH 1/2] fix: Native ETH input/output integration tests/fixes - Only for single swaps - Used USV4 for this because it's the only DEX we support that allows native ETH swaps - For Native ETH input single swaps, we need to properly check the remaining amount (we were treating them wronly like ERC20 tokens) - For Native ETH output single swaps, we were passing the incorrect currency (the settle always needs to be the out token and the take always needs to be the in token, this should not depend on the zeroForOne value). --- foundry/src/TychoRouter.sol | 16 ++- foundry/src/executors/UniswapV4Executor.sol | 4 +- foundry/test/TychoRouter.t.sol | 61 ++++++++- .../evm/strategy_encoder/strategy_encoders.rs | 123 +++++++++++++++++- 4 files changed, 195 insertions(+), 9 deletions(-) diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 377d64b..41ddde3 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -124,7 +124,13 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { revert TychoRouter__NegativeSlippage(amountOut, minAmountOut); } - uint256 leftoverAmountIn = IERC20(tokenIn).balanceOf(address(this)); + uint256 leftoverAmountIn; + if (tokenIn == address(0)) { + leftoverAmountIn = address(this).balance; + } else { + leftoverAmountIn = IERC20(tokenIn).balanceOf(address(this)); + } + if (leftoverAmountIn > 0) { revert TychoRouter__AmountInNotFullySpent(leftoverAmountIn); } @@ -209,7 +215,13 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { revert TychoRouter__NegativeSlippage(amountOut, minAmountOut); } - uint256 leftoverAmountIn = IERC20(tokenIn).balanceOf(address(this)); + uint256 leftoverAmountIn; + if (tokenIn == address(0)) { + leftoverAmountIn = address(this).balance; + } else { + leftoverAmountIn = IERC20(tokenIn).balanceOf(address(this)); + } + if (leftoverAmountIn > 0) { revert TychoRouter__AmountInNotFullySpent(leftoverAmountIn); } diff --git a/foundry/src/executors/UniswapV4Executor.sol b/foundry/src/executors/UniswapV4Executor.sol index be069e6..b47a835 100644 --- a/foundry/src/executors/UniswapV4Executor.sol +++ b/foundry/src/executors/UniswapV4Executor.sol @@ -71,8 +71,8 @@ contract UniswapV4Executor is IExecutor, V4Router { hookData: bytes("") }) ); - params[1] = abi.encode(key.currency0, amountIn); - params[2] = abi.encode(key.currency1, uint256(0)); + params[1] = abi.encode(tokenIn, amountIn); // currency to settle + params[2] = abi.encode(tokenOut, uint256(0)); // currency to take swapData = abi.encode(actions, params); } else { PathKey[] memory path = new PathKey[](pools.length); diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index e31f082..5396d93 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -734,8 +734,7 @@ contract TychoRouterTest is TychoRouterTestSetup { } function testUSV4Integration() public { - // Test created with calldata from our router encoder, replacing the executor - // address with the USV4 executor address. + // Test created with calldata from our router encoder. // Performs a sequential swap from USDC to PEPE though ETH using two // consecutive USV4 pools @@ -749,8 +748,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.startPrank(ALICE); IERC20(USDC_ADDR).approve(address(permit2Address), type(uint256).max); // Encoded solution generated using `test_split_encoding_strategy_usv4` - // but manually replacing the executor address - // `5c2f5a71f67c01775180adc06909288b4c329308` with the one in this test + // and ensuring that the encoded executor address is the one in this test // `f62849f9a0b5bf2913b396098f7c7019b51a820a` (bool success,) = tychoRouterAddr.call( hex"d499aa88000000000000000000000000000000000000000000000000000000003b9aca00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000067ddee9e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067b668a6000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041bdf91011918dcb5f59ab3588212a035c770a2839fe2c19060491370fa89685b8469def9e83c7b9cf8f0ef5088a3179556a6ba1096cefbe83c09a1182981c93e41c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009400920001000000f62849f9a0b5bf2913b396098f7c7019b51a820abd0625aba0b86991c6218b36c1d19d4a2e9eb0ce3606eb486982508145454ce325ddbe47a25d4ec3d231193300f62849f9a0b5bf2913b396098f7c7019b51a820a91dd73460000000000000000000000000000000000000000000bb800003c6982508145454ce325ddbe47a25d4ec3d23119330061a80001f4000000000000000000000000" @@ -764,6 +762,61 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(balancerAfter - balancerBefore, 97191013220606467325121599); } + function testUSV4IntegrationInputETH() public { + // Test created with calldata from our router encoder. + + // Performs a single swap from ETH to PEPE without wrapping or unwrapping + // + // ETH ───(USV4)──> PEPE + // + deal(ALICE, 1 ether); + uint256 balancerBefore = IERC20(PEPE_ADDR).balanceOf(ALICE); + + // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_in` + // and ensuring that the encoded executor address is the one in this test + // `f62849f9a0b5bf2913b396098f7c7019b51a820a` + (bool success,) = tychoRouterAddr.call{value: 1 ether}( + hex"d499aa880000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d23119330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000067def8e900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067b772f100000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000004192bf6f59a6e114588b4b5fb00f3acae3eb2dd18b673924b9cf27d1414be469b70113e4ceef228e11c91d178fea26673d9edcd013dee23fa3c45abdfcb573c9371c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007a00780001000000f62849f9a0b5bf2913b396098f7c7019b51a820abd0625ab00000000000000000000000000000000000000006982508145454ce325ddbe47a25d4ec3d231193301f62849f9a0b5bf2913b396098f7c7019b51a820a91dd73466982508145454ce325ddbe47a25d4ec3d23119330061a80001f4000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = IERC20(PEPE_ADDR).balanceOf(ALICE); + + assertTrue(success, "Call Failed"); + assertEq(balancerAfter - balancerBefore, 242373460199848577067005852); + } + + function testUSV4IntegrationOutputETH() public { + // Test created with calldata from our router encoder. + + // Performs a single swap from ETH to PEPE without wrapping or unwrapping + // + // USDC ───(USV4)──> ETH + // + deal(USDC_ADDR, ALICE, 3000_000000); + uint256 balancerBefore = ALICE.balance; + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(USDC_ADDR).approve(address(permit2Address), type(uint256).max); + + // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_in` + // and ensuring that the encoded executor address is the one in this test + // `f62849f9a0b5bf2913b396098f7c7019b51a820a` + (bool success,) = tychoRouterAddr.call( + hex"d499aa8800000000000000000000000000000000000000000000000000000000b2d05e00000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b2d05e000000000000000000000000000000000000000000000000000000000067df206000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067b79a68000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041d603720f916c45405d4655476fc8f5d5e93e561d1bc1bbd944f865ac2b53638e28fa06fde0c1097d688029c85940a53ba54902b42d17378159ae4affb8b958b01b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007a00780001000000f62849f9a0b5bf2913b396098f7c7019b51a820abd0625aba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000f62849f9a0b5bf2913b396098f7c7019b51a820a91dd73460000000000000000000000000000000000000000000bb800003c000000000000" + ); + + vm.stopPrank(); + + uint256 balancerAfter = ALICE.balance; + + assertTrue(success, "Call Failed"); + console.logUint(balancerAfter - balancerBefore); + assertEq(balancerAfter - balancerBefore, 1117254495486192350); + } + function testSingleSwapWithWrapIntegration() 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 6983268..3b6a883 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -1006,8 +1006,129 @@ mod tests { )); let hex_calldata = encode(&calldata); - println!("{}", hex_calldata); assert_eq!(hex_calldata[..520], expected_input); assert_eq!(hex_calldata[1288..], expected_swaps); } + + #[test] + fn test_split_encoding_strategy_usv4_eth_in() { + // Performs a single swap from ETH to PEPE using a USV4 pool + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + // + // ETH ───(USV4)──> PEPE + // + // Set up a mock private key for signing (Alice's pk in our router tests) + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let eth = eth(); + let pepe = Bytes::from_str("0x6982508145454Ce325dDbE47a25d4ec3d2311933").unwrap(); + + let pool_fee_eth_pepe = Bytes::from(BigInt::from(25000).to_signed_bytes_be()); + let tick_spacing_eth_pepe = Bytes::from(BigInt::from(500).to_signed_bytes_be()); + let mut static_attributes_eth_pepe: HashMap = HashMap::new(); + static_attributes_eth_pepe.insert("fee".into(), pool_fee_eth_pepe); + static_attributes_eth_pepe.insert("tick_spacing".into(), tick_spacing_eth_pepe); + + let swap_eth_pepe = Swap { + component: ProtocolComponent { + id: "0xecd73ecbf77219f21f129c8836d5d686bbc27d264742ddad620500e3e548e2c9" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_eth_pepe, + ..Default::default() + }, + token_in: eth.clone(), + token_out: pepe.clone(), + split: 0f64, + }; + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + SplitSwapStrategyEncoder::new(private_key, eth_chain(), swap_encoder_registry).unwrap(); + + let solution = Solution { + exact_out: false, + given_token: eth, + given_amount: BigUint::from_str("1_000000000000000000").unwrap(), + checked_token: pepe, + expected_amount: Some(BigUint::from_str("300_000_000000000000000000").unwrap()), + checked_amount: None, + slippage: None, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + swaps: vec![swap_eth_pepe], + ..Default::default() + }; + + let (calldata, _, _) = encoder + .encode_strategy(solution) + .unwrap(); + let hex_calldata = encode(&calldata); + + println!("{}", hex_calldata); + } + #[test] + fn test_split_encoding_strategy_usv4_eth_out() { + // Performs a single swap from USDC to ETH using a USV4 pool + // Note: This test does not assert anything. It is only used to obtain integration test + // data for our router solidity test. + // + // USDC ───(USV4)──> ETH + // + // Set up a mock private key for signing (Alice's pk in our router tests) + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let eth = eth(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + // Fee and tick spacing information for this test is obtained by querying the + // USV4 Position Manager contract: 0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e + // Using the poolKeys function with the first 25 bytes of the pool id + let pool_fee_usdc_eth = Bytes::from(BigInt::from(3000).to_signed_bytes_be()); + let tick_spacing_usdc_eth = Bytes::from(BigInt::from(60).to_signed_bytes_be()); + let mut static_attributes_usdc_eth: HashMap = HashMap::new(); + static_attributes_usdc_eth.insert("fee".into(), pool_fee_usdc_eth); + static_attributes_usdc_eth.insert("tick_spacing".into(), tick_spacing_usdc_eth); + + let swap_usdc_eth = Swap { + component: ProtocolComponent { + id: "0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d" + .to_string(), + protocol_system: "uniswap_v4".to_string(), + static_attributes: static_attributes_usdc_eth, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: eth.clone(), + split: 0f64, + }; + + let swap_encoder_registry = get_swap_encoder_registry(); + let encoder = + SplitSwapStrategyEncoder::new(private_key, eth_chain(), swap_encoder_registry).unwrap(); + let solution = Solution { + exact_out: false, + given_token: usdc, + given_amount: BigUint::from_str("3000_000000").unwrap(), + checked_token: eth, + expected_amount: Some(BigUint::from_str("1_000000000000000000").unwrap()), + checked_amount: None, + slippage: None, + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + swaps: vec![swap_usdc_eth], + ..Default::default() + }; + + let (calldata, _, _) = encoder + .encode_strategy(solution) + .unwrap(); + + let hex_calldata = encode(&calldata); + println!("{}", hex_calldata); + } } From c42421718b1d16b90bad2004048b987590293a90 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Fri, 21 Feb 2025 09:44:21 -0500 Subject: [PATCH 2/2] docs: Fix typos in code comments --- foundry/test/TychoRouter.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 5396d93..a42d2ff 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -790,7 +790,7 @@ contract TychoRouterTest is TychoRouterTestSetup { function testUSV4IntegrationOutputETH() public { // Test created with calldata from our router encoder. - // Performs a single swap from ETH to PEPE without wrapping or unwrapping + // Performs a single swap from USDC to ETH without wrapping or unwrapping // // USDC ───(USV4)──> ETH // @@ -801,7 +801,7 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.startPrank(ALICE); IERC20(USDC_ADDR).approve(address(permit2Address), type(uint256).max); - // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_in` + // Encoded solution generated using `test_split_encoding_strategy_usv4_eth_out` // and ensuring that the encoded executor address is the one in this test // `f62849f9a0b5bf2913b396098f7c7019b51a820a` (bool success,) = tychoRouterAddr.call(