From 7822c4f9132b6d64a1281f6e54a8515cb0d242d3 Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Tue, 28 Jan 2025 19:34:58 -0500 Subject: [PATCH 1/4] feat: USV3 verification --- .gitmodules | 3 ++ foundry/lib/v3-periphery | 1 + foundry/src/TychoRouter.sol | 42 ++++++++++++++++++++++++++- foundry/test/TychoRouterTestSetup.sol | 7 +++-- 4 files changed, 50 insertions(+), 3 deletions(-) create mode 160000 foundry/lib/v3-periphery diff --git a/.gitmodules b/.gitmodules index b573165..5d16ef5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "foundry/lib/v2-core"] path = foundry/lib/v2-core url = https://github.com/uniswap/v2-core +[submodule "foundry/lib/v3-periphery"] + path = foundry/lib/v3-periphery + url = https://github.com/Uniswap/v3-periphery diff --git a/foundry/lib/v3-periphery b/foundry/lib/v3-periphery new file mode 160000 index 0000000..80f26c8 --- /dev/null +++ b/foundry/lib/v3-periphery @@ -0,0 +1 @@ +Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index bac2f47..3ae63b0 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -59,10 +59,13 @@ contract TychoRouter is ); event FeeSet(uint256 indexed oldFee, uint256 indexed newFee); - constructor(address _permit2, address weth) { + address private immutable _usv3Factory; + + constructor(address _permit2, address weth, address usv3Factory) { permit2 = IAllowanceTransfer(_permit2); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _weth = IWETH(weth); + _usv3Factory = usv3Factory; } /** @@ -340,4 +343,41 @@ contract TychoRouter is * @dev Allows this contract to receive native token */ receive() external payable {} + + /** + * @dev Called by UniswapV3 pool when swapping on it. + * See in IUniswapV3SwapCallback for documentation. + */ + function uniswapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata msgData + ) external { + (uint256 amountOwed, address tokenOwed) = + _verifyUSV3Callback(amount0Delta, amount1Delta, msgData); + IERC20(tokenOwed).safeTransfer(msg.sender, amountOwed); + } + + function _verifyUSV3Callback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) + internal + pure + returns (uint256 amountOwed, address tokenOwed) + { + address tokenIn = address(bytes20(data[0:20])); + address tokenOut = address(bytes20(data[20:40])); + uint24 fee = uint24(bytes3(data[40:43])); + + CallbackValidationV2.verifyCallback( + _usv3Factory, tokenIn, tokenOut, fee + ); + + amountOwed = + amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta); + + return (amountOwed, tokenOwed); + } } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 91edbf1..53b74b9 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -8,7 +8,9 @@ import "@src/TychoRouter.sol"; import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol"; contract TychoRouterExposed is TychoRouter { - constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {} + constructor(address _permit2, address weth, address usv3Factory) + TychoRouter(_permit2, weth, usv3Factory) + {} function wrapETH(uint256 amount) external payable { return _wrapETH(amount); @@ -39,7 +41,8 @@ contract TychoRouterTestSetup is Test, Constants { vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.startPrank(ADMIN); - tychoRouter = new TychoRouterExposed(permit2Address, WETH_ADDR); + tychoRouter = + new TychoRouterExposed(permit2Address, WETH_ADDR, address(0)); tychoRouterAddr = address(tychoRouter); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER); From 3b9f7dbb52caf404a1ecf07c231ad1ee4798cd2c Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Wed, 29 Jan 2025 10:11:32 -0500 Subject: [PATCH 2/4] chore: update gitmodules --- foundry/lib/openzeppelin-contracts | 2 +- foundry/lib/v2-core | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/foundry/lib/openzeppelin-contracts b/foundry/lib/openzeppelin-contracts index 840c974..acd4ff7 160000 --- a/foundry/lib/openzeppelin-contracts +++ b/foundry/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 840c974028316f3c8172c1b8e5ed67ad95e255ca +Subproject commit acd4ff74de833399287ed6b31b4debf6b2b35527 diff --git a/foundry/lib/v2-core b/foundry/lib/v2-core index ee547b1..4dd5906 160000 --- a/foundry/lib/v2-core +++ b/foundry/lib/v2-core @@ -1 +1 @@ -Subproject commit ee547b17853e71ed4e0101ccfd52e70d5acded58 +Subproject commit 4dd59067c76dea4a0e8e4bfdda41877a6b16dedc From 96af5429232a851a7e7144b8a30843a3e6dc980e Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Wed, 29 Jan 2025 10:20:24 -0500 Subject: [PATCH 3/4] feat: fixed USV3 Verification - We cannot use the regular v3-periphery library because of outdated solc versions. I've copied the v3-updated library which we have used in our previous project --- .gitmodules | 3 + foundry/foundry.toml | 1 + foundry/lib/v3-core | 1 + foundry/lib/v3-periphery | 1 - .../lib/v3-updated/CallbackValidationV2.sol | 39 ++++++++++++ foundry/lib/v3-updated/PoolAddressV2.sol | 59 +++++++++++++++++++ foundry/remappings.txt | 4 +- foundry/src/TychoRouter.sol | 24 ++++---- foundry/test/TychoRouterTestSetup.sol | 2 +- 9 files changed, 121 insertions(+), 13 deletions(-) create mode 160000 foundry/lib/v3-core delete mode 160000 foundry/lib/v3-periphery create mode 100644 foundry/lib/v3-updated/CallbackValidationV2.sol create mode 100644 foundry/lib/v3-updated/PoolAddressV2.sol diff --git a/.gitmodules b/.gitmodules index 61440eb..608f2c8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "foundry/lib/balancer-v2-monorepo"] path = foundry/lib/balancer-v2-monorepo url = https://github.com/balancer/balancer-v2-monorepo +[submodule "foundry/lib/v3-core"] + path = foundry/lib/v3-core + url = https://github.com/Uniswap/v3-core diff --git a/foundry/foundry.toml b/foundry/foundry.toml index a3c8d7b..b1f3da6 100644 --- a/foundry/foundry.toml +++ b/foundry/foundry.toml @@ -3,6 +3,7 @@ src = 'src' out = 'out' libs = ['lib'] solc = "0.8.28" +auto_detect_solc = true # Allows per-file version detection evm_version = 'shanghai' optimizer = true optimizer_runs = 1000 diff --git a/foundry/lib/v3-core b/foundry/lib/v3-core new file mode 160000 index 0000000..d8b1c63 --- /dev/null +++ b/foundry/lib/v3-core @@ -0,0 +1 @@ +Subproject commit d8b1c635c275d2a9450bd6a78f3fa2484fef73eb diff --git a/foundry/lib/v3-periphery b/foundry/lib/v3-periphery deleted file mode 160000 index 80f26c8..0000000 --- a/foundry/lib/v3-periphery +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 diff --git a/foundry/lib/v3-updated/CallbackValidationV2.sol b/foundry/lib/v3-updated/CallbackValidationV2.sol new file mode 100644 index 0000000..b673dd2 --- /dev/null +++ b/foundry/lib/v3-updated/CallbackValidationV2.sol @@ -0,0 +1,39 @@ +// 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 + ) internal view returns (IUniswapV3Pool pool) { + return verifyCallback( + factory, PoolAddressV2.getPoolKey(tokenA, tokenB, fee) + ); + } + + /// @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 + ) internal view returns (IUniswapV3Pool pool) { + pool = IUniswapV3Pool(PoolAddressV2.computeAddress(factory, poolKey)); + require(msg.sender == address(pool), "CV"); + } +} diff --git a/foundry/lib/v3-updated/PoolAddressV2.sol b/foundry/lib/v3-updated/PoolAddressV2.sol new file mode 100644 index 0000000..c63aae7 --- /dev/null +++ b/foundry/lib/v3-updated/PoolAddressV2.sol @@ -0,0 +1,59 @@ +// 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 { + bytes32 internal constant POOL_INIT_CODE_HASH = + 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; + + /// @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) + 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) + ), + POOL_INIT_CODE_HASH + ) + ) + ) + ) + ); + } +} diff --git a/foundry/remappings.txt b/foundry/remappings.txt index ce5c70e..2d2e7e6 100644 --- a/foundry/remappings.txt +++ b/foundry/remappings.txt @@ -3,4 +3,6 @@ @permit2/=lib/permit2/ @src/=src/ @uniswap-v2/=lib/v2-core/ -@balancer-labs/v2-interfaces=lib/balancer-v2-monorepo/pkg/interfaces \ No newline at end of file +@balancer-labs/v2-interfaces=lib/balancer-v2-monorepo/pkg/interfaces +@uniswap/v3-updated/=lib/v3-updated/ +@uniswap/v3-core/=lib/v3-core/ \ No newline at end of file diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 3ae63b0..52b00e1 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -10,6 +10,7 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@permit2/src/interfaces/IAllowanceTransfer.sol"; +import "@uniswap/v3-updated/CallbackValidationV2.sol"; import "./ExecutionDispatcher.sol"; import "./CallbackVerificationDispatcher.sol"; import {LibSwap} from "../lib/LibSwap.sol"; @@ -65,6 +66,10 @@ contract TychoRouter is permit2 = IAllowanceTransfer(_permit2); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _weth = IWETH(weth); + + if (usv3Factory == address(0)) { + revert TychoRouter__AddressZero(); + } _usv3Factory = usv3Factory; } @@ -131,7 +136,9 @@ contract TychoRouter is bytes calldata signature, bytes calldata swaps ) external payable whenNotPaused nonReentrant returns (uint256 amountOut) { - require(receiver != address(0), "Invalid receiver address"); + if (receiver == address(0)) { + revert TychoRouter__AddressZero(); + } // For native ETH, assume funds already in our router. Else, transfer and handle approval. if (wrapEth) { @@ -173,8 +180,8 @@ contract TychoRouter is { uint256 currentAmountIn; uint256 currentAmountOut; - uint8 tokenInIndex; - uint8 tokenOutIndex; + uint8 tokenInIndex = 0; + uint8 tokenOutIndex = 0; uint24 split; bytes calldata swapData; @@ -362,17 +369,14 @@ contract TychoRouter is int256 amount0Delta, int256 amount1Delta, bytes calldata data - ) - internal - pure - returns (uint256 amountOwed, address tokenOwed) - { + ) internal view returns (uint256 amountOwed, address tokenOwed) { address tokenIn = address(bytes20(data[0:20])); address tokenOut = address(bytes20(data[20:40])); - uint24 fee = uint24(bytes3(data[40:43])); + uint24 poolFee = uint24(bytes3(data[40:43])); + // slither-disable-next-line unused-return CallbackValidationV2.verifyCallback( - _usv3Factory, tokenIn, tokenOut, fee + _usv3Factory, tokenIn, tokenOut, poolFee ); amountOwed = diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 53b74b9..a4404c0 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -42,7 +42,7 @@ contract TychoRouterTestSetup is Test, Constants { vm.startPrank(ADMIN); tychoRouter = - new TychoRouterExposed(permit2Address, WETH_ADDR, address(0)); + new TychoRouterExposed(permit2Address, WETH_ADDR, address(1)); tychoRouterAddr = address(tychoRouter); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER); From d3f1136486d5c44e654306592ae4d8850d973fed Mon Sep 17 00:00:00 2001 From: TAMARA LIPOWSKI Date: Wed, 29 Jan 2025 14:05:56 -0500 Subject: [PATCH 4/4] chore: Remove v3-periphery from gitmodules - We aren't using this anymore since the solc version is outdated --- .gitmodules | 3 --- foundry/foundry.toml | 1 - 2 files changed, 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index 608f2c8..0c139fa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,9 +7,6 @@ [submodule "foundry/lib/v2-core"] path = foundry/lib/v2-core url = https://github.com/uniswap/v2-core -[submodule "foundry/lib/v3-periphery"] - path = foundry/lib/v3-periphery - url = https://github.com/Uniswap/v3-periphery [submodule "foundry/lib/balancer-v2-monorepo"] path = foundry/lib/balancer-v2-monorepo url = https://github.com/balancer/balancer-v2-monorepo diff --git a/foundry/foundry.toml b/foundry/foundry.toml index b1f3da6..a3c8d7b 100644 --- a/foundry/foundry.toml +++ b/foundry/foundry.toml @@ -3,7 +3,6 @@ src = 'src' out = 'out' libs = ['lib'] solc = "0.8.28" -auto_detect_solc = true # Allows per-file version detection evm_version = 'shanghai' optimizer = true optimizer_runs = 1000