diff --git a/.gitmodules b/.gitmodules index e830f1a..0c139fa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,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/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-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 bac2f47..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"; @@ -59,10 +60,17 @@ 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); + + if (usv3Factory == address(0)) { + revert TychoRouter__AddressZero(); + } + _usv3Factory = usv3Factory; } /** @@ -128,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) { @@ -170,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; @@ -340,4 +350,38 @@ 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 view returns (uint256 amountOwed, address tokenOwed) { + address tokenIn = address(bytes20(data[0:20])); + address tokenOut = address(bytes20(data[20:40])); + uint24 poolFee = uint24(bytes3(data[40:43])); + + // slither-disable-next-line unused-return + CallbackValidationV2.verifyCallback( + _usv3Factory, tokenIn, tokenOut, poolFee + ); + + amountOwed = + amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta); + + return (amountOwed, tokenOwed); + } } diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index 91edbf1..a4404c0 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(1)); tychoRouterAddr = address(tychoRouter); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER);