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 ccfb7ea..d611e99 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -735,8 +735,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 @@ -750,8 +749,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" @@ -765,6 +763,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 USDC to ETH 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_out` + // 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 3749e0f..dd2989d 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -1034,7 +1034,6 @@ mod tests { )); let hex_calldata = encode(&calldata); - println!("{}", hex_calldata); assert_eq!(hex_calldata[..520], expected_input); assert_eq!(hex_calldata[1288..], expected_swaps); } @@ -1118,4 +1117,129 @@ mod tests { assert_eq!(hex_calldata, expected_input); println!("{}", hex_calldata); } + + #[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(eth_chain(), swap_encoder_registry, Some(private_key)) + .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(eth_chain(), swap_encoder_registry, Some(private_key)) + .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); + } }