From d582543057665b737cc0aab5243ccc22db1f0a13 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Fri, 21 Mar 2025 11:26:41 -0400 Subject: [PATCH] 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();