diff --git a/config/executor_addresses.json b/config/executor_addresses.json index da05b18..96f1ac2 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -1,7 +1,7 @@ { "ethereum": { "uniswap_v2": "0xf6c5be66FFf9DC69962d73da0A617a827c382329", - "uniswap_v3": "0xdD8559c917393FC8DD2b4dD289c52Ff445fDE1B0", + "uniswap_v3": "0x2e234DAe75C793f67A35089C9d99245E1C58470b", "uniswap_v4": "0x042C0ebBEAb9d9987c2f64Ee05f2B3aeB86eAf70", "vm:balancer_v2": "0x00BE8EfAE40219Ff76287b0F9b9e497942f5BC91" }, @@ -19,4 +19,4 @@ "tenderly_base": { "uniswap_v3": "0x7c7E06d7317e620a185078e236879D2a87fC8d22" } -} \ No newline at end of file +} diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 32fb7f5..7a26ff7 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -14,6 +14,7 @@ import "@permit2/src/interfaces/IAllowanceTransfer.sol"; import "./Dispatcher.sol"; import {LibSwap} from "../lib/LibSwap.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import "forge-std/console.sol"; // ✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷ // ✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷✷ @@ -259,7 +260,6 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { : IERC20(tokenIn).balanceOf(address(this)); amountOut = _swap(amountIn, nTokens, swaps); - uint256 currentBalance = tokenIn == address(0) ? address(this).balance : IERC20(tokenIn).balanceOf(address(this)); @@ -323,6 +323,10 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { revert TychoRouter__EmptySwaps(); } + console.log("amountIn", amountIn); + console.log("nTokens", nTokens); + console.logBytes(swaps_); + uint256 currentAmountIn; uint256 currentAmountOut; uint8 tokenInIndex = 0; diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index e86b622..c760122 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -1192,7 +1192,7 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(WBTC_ADDR).balanceOf(tychoRouterAddr), 102718); } - function testCyclicSequentialSwap() public { + function testCyclicSequentialSwap1() public { // This test has start and end tokens that are the same // The flow is: // USDC -> WETH -> USDC using two pools @@ -1332,6 +1332,23 @@ contract TychoRouterTest is TychoRouterTestSetup { assertEq(IERC20(USDC_ADDR).balanceOf(tychoRouterAddr), 99525908); } + function testCyclicSequentialSwapIntegration() public { + deal(USDC_ADDR, ALICE, 100 * 10 ** 6); + + // Approve permit2 + vm.startPrank(ALICE); + IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max); + // Encoded solution generated using `test_cyclic_sequential_swap` + // but manually replacing the executor address + // `dD8559c917393FC8DD2b4dD289c52Ff445fDE1B0` with the one in this test + // `2e234DAe75C793f67A35089C9d99245E1C58470b` + (bool success,) = tychoRouterAddr.call( + hex"d499aa880000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f4308e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000cd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000067f67a8b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ede3eca2a72b3aecc820e955b36f38437d013950000000000000000000000000000000000000000000000000000000067cef493000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000041c07077fc73bb0f5129006061288fa0583c101631307377281d6b8f3feb50aa2d564f9948c92e0e4abc3771d592bd2f22ebb18ccf21b270459b05f272251ce1c71b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d8000000" + ); + + vm.stopPrank(); + } + // Base Network Tests // Make sure to set the RPC_URL to base network function testSwapSingleBase() public { @@ -1354,3 +1371,11 @@ contract TychoRouterTest is TychoRouterTestSetup { assertGt(IERC20(BASE_MAG7).balanceOf(tychoRouterAddr), 1379830606); } } + +//amountIn 100000000 +//nTokens 2 +//0x006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d800 + +//amountIn 100000000 +//nTokens 2 +//0x006d00010000002e234dae75c793f67a35089c9d99245e1c58470ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f43ede3eca2a72b3aecc820e955b36f38437d0139588e6a0c2ddd26feeb64f039a2c41296fcb3f564001006d01000000002e234dae75c793f67a35089c9d99245e1c58470bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000bb83ede3eca2a72b3aecc820e955b36f38437d013958ad599c3a0ff1de082011efddc58f1908eb6e6d800 diff --git a/src/encoding/evm/strategy_encoder/strategy_encoders.rs b/src/encoding/evm/strategy_encoder/strategy_encoders.rs index ea2b191..336bd04 100644 --- a/src/encoding/evm/strategy_encoder/strategy_encoders.rs +++ b/src/encoding/evm/strategy_encoder/strategy_encoders.rs @@ -206,6 +206,11 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { } let encoded_swaps = self.ple_encode(swaps); + let tokens_len = if &solution.given_token == &solution.checked_token { + tokens.len() - 1 + } else { + tokens.len() + }; let method_calldata = if let Some(permit2) = self.permit2.clone() { let (permit, signature) = permit2.get_permit( &solution.router_address, @@ -220,7 +225,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { biguint_to_u256(&min_amount_out), wrap, unwrap, - U256::from(tokens.len()), + U256::from(tokens_len), bytes_to_address(&solution.receiver)?, permit, signature.as_bytes().to_vec(), @@ -235,7 +240,7 @@ impl StrategyEncoder for SplitSwapStrategyEncoder { biguint_to_u256(&min_amount_out), wrap, unwrap, - U256::from(tokens.len()), + U256::from(tokens_len), bytes_to_address(&solution.receiver)?, encoded_swaps, ) @@ -1212,4 +1217,91 @@ mod tests { let hex_calldata = encode(&calldata); println!("{}", hex_calldata); } + + #[test] + fn test_cyclic_sequential_swap() { + // This test has start and end tokens that are the same + // The flow is: + // USDC -> WETH -> USDC using two pools + + // Set up a mock private key for signing (Alice's pk in our router tests) + let private_key = + "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(); + + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + // Create two Uniswap V3 pools for the cyclic swap + // USDC -> WETH (Pool 1) + let swap_usdc_weth = Swap { + component: ProtocolComponent { + id: "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), /* USDC-WETH USV3 + * Pool 1 */ + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(500).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: usdc.clone(), + token_out: weth.clone(), + split: 0f64, + }; + + // WETH -> USDC (Pool 2) + let swap_weth_usdc = Swap { + component: ProtocolComponent { + id: "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8".to_string(), /* USDC-WETH USV3 + * Pool 2 */ + protocol_system: "uniswap_v3".to_string(), + static_attributes: { + let mut attrs = HashMap::new(); + attrs.insert( + "fee".to_string(), + Bytes::from(BigInt::from(3000).to_signed_bytes_be()), + ); + attrs + }, + ..Default::default() + }, + token_in: weth.clone(), + token_out: usdc.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.clone(), + given_amount: BigUint::from_str("100000000").unwrap(), // 100 USDC (6 decimals) + checked_token: usdc.clone(), + expected_amount: None, + checked_amount: Some(BigUint::from_str("99889294").unwrap()), /* Expected output from + * test */ + slippage: None, + swaps: vec![swap_usdc_weth, swap_weth_usdc], + router_address: Bytes::from_str("0x3Ede3eCa2a72B3aeCC820E955B36f38437D01395").unwrap(), + sender: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + receiver: Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2").unwrap(), + ..Default::default() + }; + + let (calldata, _) = encoder + .encode_strategy(solution) + .unwrap(); + + println!("{}", hex::encode(&calldata)); + } + + #[test] + } diff --git a/src/encoding/evm/strategy_encoder/strategy_validators.rs b/src/encoding/evm/strategy_encoder/strategy_validators.rs index 266769d..8ac5628 100644 --- a/src/encoding/evm/strategy_encoder/strategy_validators.rs +++ b/src/encoding/evm/strategy_encoder/strategy_validators.rs @@ -150,6 +150,13 @@ impl SplitSwapValidator { .insert(&swap.token_out); } + // Collect all unique tokens from the swaps + let mut all_tokens = HashSet::new(); + for swap in swaps { + all_tokens.insert(&swap.token_in); + all_tokens.insert(&swap.token_out); + } + // BFS from validation_given let mut visited = HashSet::new(); let mut queue = VecDeque::new(); @@ -160,8 +167,8 @@ impl SplitSwapValidator { continue; } - // Early success check - if token == checked_token && visited.len() == graph.len() + 1 { + // Early success check - if we've reached the checked token and visited all tokens + if token == checked_token && visited.len() == all_tokens.len() { return Ok(()); } @@ -174,6 +181,13 @@ impl SplitSwapValidator { } } + // After BFS completes, check if both conditions are met: + // 1. The checked token is in the visited set + // 2. All unique tokens from the swaps are visited + if visited.contains(checked_token) && visited.len() == all_tokens.len() { + return Ok(()); + } + // If we get here, either checked_token wasn't reached or not all tokens were visited if !visited.contains(checked_token) { Err(EncodingError::InvalidInput( @@ -291,6 +305,41 @@ mod tests { )); } + #[test] + fn test_validate_path_cyclic_swap() { + let validator = SplitSwapValidator; + let eth = Bytes::from_str("0x0000000000000000000000000000000000000000").unwrap(); + let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(); + let usdc = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + + let cyclic_swaps = vec![ + Swap { + component: ProtocolComponent { + id: "pool1".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: usdc.clone(), + token_out: weth.clone(), + split: 0.5, + }, + Swap { + component: ProtocolComponent { + id: "pool2".to_string(), + protocol_system: "uniswap_v2".to_string(), + ..Default::default() + }, + token_in: weth.clone(), + token_out: usdc.clone(), + split: 0.5, + }, + ]; + + // Test with USDC as both given token and checked token + let result = validator.validate_swap_path(&cyclic_swaps, &usdc, &usdc, &None, ð, &weth); + assert_eq!(result, Ok(())); + } + #[test] fn test_validate_path_unreachable_checked_token() { let validator = SplitSwapValidator;