From d582543057665b737cc0aab5243ccc22db1f0a13 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Fri, 21 Mar 2025 11:26:41 -0400 Subject: [PATCH 1/3] feat: Pancakeswap V3 support --- .../lib/v3-updated/CallbackValidationV2.sol | 11 ++++--- foundry/lib/v3-updated/PoolAddressV2.sol | 6 ++-- foundry/scripts/deploy-executors.js | 11 ++++++- foundry/src/TychoRouter.sol | 19 +++++++++++ foundry/src/executors/UniswapV3Executor.sol | 4 ++- foundry/test/TychoRouter.t.sol | 33 +++++++++++++++++++ foundry/test/TychoRouterTestSetup.sol | 10 ++++-- 7 files changed, 82 insertions(+), 12 deletions(-) diff --git a/foundry/lib/v3-updated/CallbackValidationV2.sol b/foundry/lib/v3-updated/CallbackValidationV2.sol index a3c7a7f..0a24242 100644 --- a/foundry/lib/v3-updated/CallbackValidationV2.sol +++ b/foundry/lib/v3-updated/CallbackValidationV2.sol @@ -18,12 +18,14 @@ library CallbackValidationV2 { address factory, address tokenA, address tokenB, - uint24 fee + uint24 fee, + bytes32 initCode ) internal view returns (IUniswapV3Pool pool) { return verifyCallback( factory, - PoolAddressV2.getPoolKey(tokenA, tokenB, fee) + PoolAddressV2.getPoolKey(tokenA, tokenB, fee), + initCode ); } @@ -33,9 +35,10 @@ library CallbackValidationV2 { /// @return pool The V3 pool contract address function verifyCallback( address factory, - PoolAddressV2.PoolKey memory poolKey + PoolAddressV2.PoolKey memory poolKey, + bytes32 initCode ) internal view returns (IUniswapV3Pool pool) { - pool = IUniswapV3Pool(PoolAddressV2.computeAddress(factory, poolKey)); + pool = IUniswapV3Pool(PoolAddressV2.computeAddress(factory, poolKey, initCode)); require(msg.sender == address(pool), "CV"); } } diff --git a/foundry/lib/v3-updated/PoolAddressV2.sol b/foundry/lib/v3-updated/PoolAddressV2.sol index c63aae7..d30eedb 100644 --- a/foundry/lib/v3-updated/PoolAddressV2.sol +++ b/foundry/lib/v3-updated/PoolAddressV2.sol @@ -5,8 +5,6 @@ pragma solidity >=0.5.0; /// @title Provides functions for deriving a pool address from the factory, tokens, and the fee library PoolAddressV2 { - bytes32 internal constant POOL_INIT_CODE_HASH = - 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; /// @notice The identifying key of the pool struct PoolKey { @@ -33,7 +31,7 @@ library PoolAddressV2 { /// @param factory The Uniswap V3 factory contract address /// @param key The PoolKey /// @return pool The contract address of the V3 pool - function computeAddress(address factory, PoolKey memory key) + function computeAddress(address factory, PoolKey memory key, bytes32 initCode) internal pure returns (address pool) @@ -49,7 +47,7 @@ library PoolAddressV2 { keccak256( abi.encode(key.token0, key.token1, key.fee) ), - POOL_INIT_CODE_HASH + initCode ) ) ) diff --git a/foundry/scripts/deploy-executors.js b/foundry/scripts/deploy-executors.js index 77e9dbb..c8809b7 100644 --- a/foundry/scripts/deploy-executors.js +++ b/foundry/scripts/deploy-executors.js @@ -25,6 +25,11 @@ const executors_to_deploy = { "0x1F98431c8aD98523631AE4a59f267346ea31F984", "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" ]}, + // PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash + {exchange: "UniswapV3Executor", args: [ + "0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9", + "0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2" + ]}, // Args: Pool manager {exchange: "UniswapV4Executor", args: ["0x000000000004444c5dc75cB358380D2e3dE08A90"]}, {exchange: "BalancerV2Executor", args: []}, @@ -50,11 +55,15 @@ const executors_to_deploy = { "0x33128a8fC17869897dcE68Ed026d694621f6FDfD", "0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54" ]}, + // PANCAKESWAP V3 - Args: Deployer, Pool Init Code Hash + {exchange: "UniswapV3Executor", args: [ + "0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9", + "0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2" + ]}, // Args: Pool manager {exchange: "UniswapV4Executor", args: ["0x498581ff718922c3f8e6a244956af099b2652b2b"]}, {exchange: "BalancerV2Executor", args: []}, ], - } async function main() { diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 47d6d14..2f29f6e 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -518,6 +518,25 @@ contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { _handleCallback(fullData); } + /** + * @dev Called by PancakeV3 pool when swapping on it. + */ + function pancakeV3SwapCallback( + int256, /* amount0Delta */ + int256, /* amount1Delta */ + bytes calldata data + ) external { + if (data.length < 24) revert TychoRouter__InvalidDataLength(); + // We are taking advantage of the fact that the data we need is already encoded in the correct format inside msg.data + // This way we preserve the bytes calldata (and don't need to convert it to bytes memory) + uint256 dataOffset = 4 + 32 + 32 + 32; // Skip selector + 2 ints + data_offset + uint256 dataLength = + uint256(bytes32(msg.data[dataOffset:dataOffset + 32])); + + bytes calldata fullData = msg.data[4:dataOffset + 32 + dataLength]; + _handleCallback(fullData); + } + /** * @dev Called by UniswapV4 pool manager after achieving unlock state. */ diff --git a/foundry/src/executors/UniswapV3Executor.sol b/foundry/src/executors/UniswapV3Executor.sol index f5878a8..050bb83 100644 --- a/foundry/src/executors/UniswapV3Executor.sol +++ b/foundry/src/executors/UniswapV3Executor.sol @@ -107,7 +107,9 @@ contract UniswapV3Executor is IExecutor, ICallback { uint24 poolFee = uint24(bytes3(data[40:43])); // slither-disable-next-line unused-return - CallbackValidationV2.verifyCallback(factory, tokenIn, tokenOut, poolFee); + CallbackValidationV2.verifyCallback( + factory, tokenIn, tokenOut, poolFee, initCode + ); } function uniswapV3SwapCallback( diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 3801a2e..befc8b5 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -753,6 +753,39 @@ contract TychoRouterTest is TychoRouterTestSetup { assertGe(finalBalance, expAmountOut); } + function testSwapSinglePancakeV3() public { + // Trade 1 WETH for USDT with 1 swap on Pancakeswap V3 + // 1 WETH -> USDT + // (PancakeV3) + uint256 amountIn = 10 ** 18; + deal(WETH_ADDR, tychoRouterAddr, amountIn); + + uint256 expAmountOut = 2659_567519; //Swap 1 WETH for 1205.12 DAI + bool zeroForOne = true; + bytes memory protocolData = encodeUniswapV3Swap( + WETH_ADDR, + USDT_ADDR, + tychoRouterAddr, + PANCAKESWAPV3_WETH_USDT_POOL, + zeroForOne + ); + bytes memory swap = encodeSwap( + uint8(0), + uint8(1), + uint24(0), + address(pancakev3Executor), + protocolData + ); + + bytes[] memory swaps = new bytes[](1); + swaps[0] = swap; + + tychoRouter.exposedSwap(amountIn, 2, pleEncode(swaps)); + + uint256 finalBalance = IERC20(USDT_ADDR).balanceOf(tychoRouterAddr); + assertGe(finalBalance, expAmountOut); + } + function testSwapSingleUSV3Permit2() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V3 using Permit2 // 1 WETH -> DAI diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index f6eb0ef..1e7b70e 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -36,6 +36,7 @@ contract TychoRouterTestSetup is Test, Constants { address tychoRouterAddr; UniswapV2Executor public usv2Executor; UniswapV3Executor public usv3Executor; + UniswapV3Executor public pancakev3Executor; UniswapV4Executor public usv4Executor; MockERC20[] tokens; @@ -46,8 +47,10 @@ contract TychoRouterTestSetup is Test, Constants { vm.startPrank(ADMIN); address factoryV2 = USV2_FACTORY_ETHEREUM; address factoryV3 = USV3_FACTORY_ETHEREUM; + address factoryPancakeV3 = PANCAKESWAPV3_DEPLOYER_ETHEREUM; bytes32 initCodeV2 = USV2_POOL_CODE_INIT_HASH; bytes32 initCodeV3 = USV3_POOL_CODE_INIT_HASH; + bytes32 initCodePancakeV3 = PANCAKEV3_POOL_CODE_INIT_HASH; address poolManagerAddress = 0x000000000004444c5dc75cB358380D2e3dE08A90; IPoolManager poolManager = IPoolManager(poolManagerAddress); tychoRouter = new TychoRouterExposed(PERMIT2_ADDRESS, WETH_ADDR); @@ -65,11 +68,14 @@ contract TychoRouterTestSetup is Test, Constants { usv2Executor = new UniswapV2Executor(factoryV2, initCodeV2); usv3Executor = new UniswapV3Executor(factoryV3, initCodeV3); usv4Executor = new UniswapV4Executor(poolManager); + pancakev3Executor = + new UniswapV3Executor(factoryPancakeV3, initCodePancakeV3); vm.startPrank(EXECUTOR_SETTER); - address[] memory executors = new address[](3); + address[] memory executors = new address[](4); executors[0] = address(usv2Executor); executors[1] = address(usv3Executor); - executors[2] = address(usv4Executor); + executors[2] = address(pancakev3Executor); + executors[3] = address(usv4Executor); tychoRouter.setExecutors(executors); vm.stopPrank(); From c361e1515df25a746ac4f2821e9e243926ff0c8e Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 25 Mar 2025 12:43:00 +0100 Subject: [PATCH 2/3] refactor: remove duplicate code The callback verification was already implemented in _verifyPairAddress. --- .../lib/v3-updated/CallbackValidationV2.sol | 44 -------------- foundry/lib/v3-updated/PoolAddressV2.sol | 57 ------------------- foundry/src/executors/UniswapV3Executor.sol | 6 +- 3 files changed, 1 insertion(+), 106 deletions(-) delete mode 100644 foundry/lib/v3-updated/CallbackValidationV2.sol delete mode 100644 foundry/lib/v3-updated/PoolAddressV2.sol diff --git a/foundry/lib/v3-updated/CallbackValidationV2.sol b/foundry/lib/v3-updated/CallbackValidationV2.sol deleted file mode 100644 index 0a24242..0000000 --- a/foundry/lib/v3-updated/CallbackValidationV2.sol +++ /dev/null @@ -1,44 +0,0 @@ -// Updated v3 lib to solidity >=0.7.6 - -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.6; - -import "./PoolAddressV2.sol"; -import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; - -/// @notice Provides validation for callbacks from Uniswap V3 Pools -library CallbackValidationV2 { - /// @notice Returns the address of a valid Uniswap V3 Pool - /// @param factory The contract address of the Uniswap V3 factory - /// @param tokenA The contract address of either token0 or token1 - /// @param tokenB The contract address of the other token - /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip - /// @return pool The V3 pool contract address - function verifyCallback( - address factory, - address tokenA, - address tokenB, - uint24 fee, - bytes32 initCode - ) internal view returns (IUniswapV3Pool pool) { - return - verifyCallback( - factory, - PoolAddressV2.getPoolKey(tokenA, tokenB, fee), - initCode - ); - } - - /// @notice Returns the address of a valid Uniswap V3 Pool - /// @param factory The contract address of the Uniswap V3 factory - /// @param poolKey The identifying key of the V3 pool - /// @return pool The V3 pool contract address - function verifyCallback( - address factory, - PoolAddressV2.PoolKey memory poolKey, - bytes32 initCode - ) internal view returns (IUniswapV3Pool pool) { - pool = IUniswapV3Pool(PoolAddressV2.computeAddress(factory, poolKey, initCode)); - require(msg.sender == address(pool), "CV"); - } -} diff --git a/foundry/lib/v3-updated/PoolAddressV2.sol b/foundry/lib/v3-updated/PoolAddressV2.sol deleted file mode 100644 index d30eedb..0000000 --- a/foundry/lib/v3-updated/PoolAddressV2.sol +++ /dev/null @@ -1,57 +0,0 @@ -// Updated v3 lib to solidity >=0.7.6 - -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.5.0; - -/// @title Provides functions for deriving a pool address from the factory, tokens, and the fee -library PoolAddressV2 { - - /// @notice The identifying key of the pool - struct PoolKey { - address token0; - address token1; - uint24 fee; - } - - /// @notice Returns PoolKey: the ordered tokens with the matched fee levels - /// @param tokenA The first token of a pool, unsorted - /// @param tokenB The second token of a pool, unsorted - /// @param fee The fee level of the pool - /// @return Poolkey The pool details with ordered token0 and token1 assignments - function getPoolKey(address tokenA, address tokenB, uint24 fee) - internal - pure - returns (PoolKey memory) - { - if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); - return PoolKey({token0: tokenA, token1: tokenB, fee: fee}); - } - - /// @notice Deterministically computes the pool address given the factory and PoolKey - /// @param factory The Uniswap V3 factory contract address - /// @param key The PoolKey - /// @return pool The contract address of the V3 pool - function computeAddress(address factory, PoolKey memory key, bytes32 initCode) - internal - pure - returns (address pool) - { - require(key.token0 < key.token1); - pool = address( - uint160( - uint256( - keccak256( - abi.encodePacked( - hex"ff", - factory, - keccak256( - abi.encode(key.token0, key.token1, key.fee) - ), - initCode - ) - ) - ) - ) - ); - } -} diff --git a/foundry/src/executors/UniswapV3Executor.sol b/foundry/src/executors/UniswapV3Executor.sol index 050bb83..1d46c08 100644 --- a/foundry/src/executors/UniswapV3Executor.sol +++ b/foundry/src/executors/UniswapV3Executor.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; -import "@uniswap/v3-updated/CallbackValidationV2.sol"; import "@interfaces/ICallback.sol"; error UniswapV3Executor__InvalidDataLength(); @@ -106,10 +105,7 @@ contract UniswapV3Executor is IExecutor, ICallback { address tokenOut = address(bytes20(data[20:40])); uint24 poolFee = uint24(bytes3(data[40:43])); - // slither-disable-next-line unused-return - CallbackValidationV2.verifyCallback( - factory, tokenIn, tokenOut, poolFee, initCode - ); + _verifyPairAddress(tokenIn, tokenOut, poolFee, msg.sender); } function uniswapV3SwapCallback( From fa024a4a6702c7809af31a6ba392338d9368c6d2 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 25 Mar 2025 17:49:33 +0100 Subject: [PATCH 3/3] feat: Add PancakeSwapV3 support to encoding - Add deployed executor address to json - Build proper encoder for pancake v3 - Increase gas limit when setting executors (costs more since we set one more) - Fix json filename in scripts/README.md --- config/executor_addresses.json | 1 + foundry/scripts/README.md | 2 +- foundry/scripts/set-executors.js | 2 +- src/encoding/evm/swap_encoder/builder.rs | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/executor_addresses.json b/config/executor_addresses.json index 1ec0465..2079fcf 100644 --- a/config/executor_addresses.json +++ b/config/executor_addresses.json @@ -4,6 +4,7 @@ "sushiswap_v2": "0x8ABd4075cF54E0A9C52D18e6951b969AF3249cF9", "pancakeswap_v2": "0x9fC9e63cCf5F773A8bC79DcfA38c581B0DEa1d11", "uniswap_v3": "0xdD8559c917393FC8DD2b4dD289c52Ff445fDE1B0", + "pancakeswap_v3": "0x4929B619A8F0D9c06ed0FfD497636580D823F65d", "uniswap_v4": "0x042C0ebBEAb9d9987c2f64Ee05f2B3aeB86eAf70", "vm:balancer_v2": "0x00BE8EfAE40219Ff76287b0F9b9e497942f5BC91" }, diff --git a/foundry/scripts/README.md b/foundry/scripts/README.md index 15fb38d..aa93ee2 100644 --- a/foundry/scripts/README.md +++ b/foundry/scripts/README.md @@ -47,4 +47,4 @@ For each of the following, you must select one of `tenderly_ethereum`, `tenderly 1. In `scripts/deploy-executors.js` define the executors to be deployed 2. Deploy executors: `npx hardhat run scripts/deploy-executors.js --network NETWORK` -3. Fill in the executor addresses in `config/executors.json` +3. Fill in the executor addresses in `config/executor_addresses.json` diff --git a/foundry/scripts/set-executors.js b/foundry/scripts/set-executors.js index a1b0d3c..9455af6 100644 --- a/foundry/scripts/set-executors.js +++ b/foundry/scripts/set-executors.js @@ -51,7 +51,7 @@ async function main() { // Set executors const executorAddresses = executorsToSet.map(executor => executor.executor); const tx = await router.setExecutors(executorAddresses, { - gasLimit: 200000 // should be around 50k per executor + gasLimit: 300000 // should be around 50k per executor }); await tx.wait(); // Wait for the transaction to be mined console.log(`Executors set at transaction: ${tx.hash}`); diff --git a/src/encoding/evm/swap_encoder/builder.rs b/src/encoding/evm/swap_encoder/builder.rs index 40ede0f..6918b0a 100644 --- a/src/encoding/evm/swap_encoder/builder.rs +++ b/src/encoding/evm/swap_encoder/builder.rs @@ -27,6 +27,7 @@ impl SwapEncoderBuilder { "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))), _ => Err(EncodingError::FatalError(format!( "Unknown protocol system: {}",