diff --git a/foundry/interfaces/ICallback.sol b/foundry/interfaces/ICallback.sol new file mode 100644 index 0000000..408a5fe --- /dev/null +++ b/foundry/interfaces/ICallback.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +interface ICallback { + /** + * @notice Handles callback data from a protocol or contract interaction. + * @dev This method processes callback data and returns a result. Implementations + * should handle the specific callback logic required by the protocol. + * + * @param data The encoded callback data to be processed. + * @return result The encoded result of the callback processing. + */ + function handleCallback( + bytes calldata data + ) external returns (bytes memory result); + + /** + * @notice Verifies the validity of callback data. + * @dev This view function checks if the provided callback data is valid according + * to the protocol's requirements. It should revert if the data is invalid. + * + * @param data The encoded callback data to verify. + */ + function verifyCallback(bytes calldata data) external view; +} diff --git a/foundry/interfaces/ICallbackVerifier.sol b/foundry/interfaces/ICallbackVerifier.sol deleted file mode 100644 index 612279c..0000000 --- a/foundry/interfaces/ICallbackVerifier.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -interface ICallbackVerifier { - error UnauthorizedCaller(string exchange, address sender); - - /** - * @dev This method should revert if the sender is not a verified sender of the exchange. - */ - function verifyCallback( - address sender, - bytes calldata data - ) external returns (uint256 amountOwed, address tokenOwed); -} diff --git a/foundry/lib/v3-updated/CallbackValidationV2.sol b/foundry/lib/v3-updated/CallbackValidationV2.sol index b673dd2..a3c7a7f 100644 --- a/foundry/lib/v3-updated/CallbackValidationV2.sol +++ b/foundry/lib/v3-updated/CallbackValidationV2.sol @@ -20,9 +20,11 @@ library CallbackValidationV2 { address tokenB, uint24 fee ) internal view returns (IUniswapV3Pool pool) { - return verifyCallback( - factory, PoolAddressV2.getPoolKey(tokenA, tokenB, fee) - ); + return + verifyCallback( + factory, + PoolAddressV2.getPoolKey(tokenA, tokenB, fee) + ); } /// @notice Returns the address of a valid Uniswap V3 Pool diff --git a/foundry/src/CallbackVerificationDispatcher.sol b/foundry/src/CallbackVerificationDispatcher.sol deleted file mode 100644 index 8df5848..0000000 --- a/foundry/src/CallbackVerificationDispatcher.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -import "@interfaces/ICallbackVerifier.sol"; - -error CallbackVerificationDispatcher__UnapprovedVerifier(); -error CallbackVerificationDispatcher__NonContractVerifier(); - -/** - * @title Dispatch callback verification to external contracts - * @author PropellerHeads Devs - * @dev Provides the ability call external contracts to perform callback - * verification. This allows dynamically adding new supported protocols - * without needing to upgrade any contracts. - * - * Note: Verifier contracts need to implement the ICallbackVerifier interface - */ -contract CallbackVerificationDispatcher { - mapping(address => bool) public callbackVerifiers; - - event CallbackVerifierSet(address indexed callbackVerifier); - event CallbackVerifierRemoved(address indexed callbackVerifier); - - /** - * @dev Adds or replaces an approved callback verifier contract address if it is a - * contract. - * @param target address of the callback verifier contract - */ - function _setCallbackVerifier(address target) internal { - if (target.code.length == 0) { - revert CallbackVerificationDispatcher__NonContractVerifier(); - } - callbackVerifiers[target] = true; - emit CallbackVerifierSet(target); - } - - /** - * @dev Removes an approved callback verifier contract address - * @param target address of the callback verifier contract - */ - function _removeCallbackVerifier(address target) internal { - delete callbackVerifiers[target]; - emit CallbackVerifierRemoved(target); - } - - /** - * @dev Calls a callback verifier. This should revert if the callback verification fails. - */ - // slither-disable-next-line dead-code - function _callVerifyCallback(bytes calldata data) - internal - view - returns (uint256 amountOwed, address tokenOwed) - { - address verifier; - bytes4 decodedSelector; - bytes memory verifierData; - - (verifier, decodedSelector, verifierData) = - _decodeVerifierAndSelector(data); - - if (!callbackVerifiers[verifier]) { - revert CallbackVerificationDispatcher__UnapprovedVerifier(); - } - - bytes4 selector = decodedSelector == bytes4(0) - ? ICallbackVerifier.verifyCallback.selector - : decodedSelector; - - address sender = msg.sender; - - // slither-disable-next-line low-level-calls - (bool success, bytes memory result) = verifier.staticcall( - abi.encodeWithSelector(selector, sender, verifierData) - ); - - if (!success) { - if (result.length > 0) { - revert(string(result)); - } else { - revert("Callback verification failed"); - } - } - - (amountOwed, tokenOwed) = abi.decode(result, (uint256, address)); - } - - // slither-disable-next-line dead-code - function _decodeVerifierAndSelector(bytes calldata data) - internal - pure - returns (address verifier, bytes4 selector, bytes memory verifierData) - { - require(data.length >= 20, "Invalid data length"); - verifier = address(uint160(bytes20(data[:20]))); - selector = bytes4(data[20:24]); - verifierData = data[24:]; - } -} diff --git a/foundry/src/ExecutionDispatcher.sol b/foundry/src/Dispatcher.sol similarity index 66% rename from foundry/src/ExecutionDispatcher.sol rename to foundry/src/Dispatcher.sol index 73bd554..03292be 100644 --- a/foundry/src/ExecutionDispatcher.sol +++ b/foundry/src/Dispatcher.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.26; import "@interfaces/IExecutor.sol"; +import "@interfaces/ICallback.sol"; -error ExecutionDispatcher__UnapprovedExecutor(); -error ExecutionDispatcher__NonContractExecutor(); +error Dispatcher__UnapprovedExecutor(); +error Dispatcher__NonContractExecutor(); +error Dispatcher__InvalidDataLength(); /** - * @title ExecutionDispatcher - Dispatch execution to external contracts + * @title Dispatcher - Dispatch execution to external contracts * @author PropellerHeads Devs * @dev Provides the ability to delegate execution of swaps to external * contracts. This allows dynamically adding new supported protocols @@ -18,7 +20,7 @@ error ExecutionDispatcher__NonContractExecutor(); * Note: Executor contracts need to implement the IExecutor interface unless * an alternate selector is specified. */ -contract ExecutionDispatcher { +contract Dispatcher { mapping(address => bool) public executors; event ExecutorSet(address indexed executor); @@ -31,7 +33,7 @@ contract ExecutionDispatcher { */ function _setExecutor(address target) internal { if (target.code.length == 0) { - revert ExecutionDispatcher__NonContractExecutor(); + revert Dispatcher__NonContractExecutor(); } executors[target] = true; emit ExecutorSet(target); @@ -58,7 +60,7 @@ contract ExecutionDispatcher { bytes calldata data ) internal returns (uint256 calculatedAmount) { if (!executors[executor]) { - revert ExecutionDispatcher__UnapprovedExecutor(); + revert Dispatcher__UnapprovedExecutor(); } selector = selector == bytes4(0) ? IExecutor.swap.selector : selector; @@ -79,4 +81,29 @@ contract ExecutionDispatcher { calculatedAmount = abi.decode(result, (uint256)); } + + function _handleCallback(bytes calldata data) internal { + bytes4 selector = bytes4(data[data.length - 4:]); + address executor = address(uint160(bytes20(data[data.length - 24:]))); + + if (!executors[executor]) { + revert Dispatcher__UnapprovedExecutor(); + } + + selector = + selector == bytes4(0) ? ICallback.handleCallback.selector : selector; + // slither-disable-next-line controlled-delegatecall,low-level-calls + (bool success, bytes memory result) = + executor.delegatecall(abi.encodeWithSelector(selector, data)); + + if (!success) { + revert( + string( + result.length > 0 + ? result + : abi.encodePacked("Callback failed") + ) + ); + } + } } diff --git a/foundry/src/TychoRouter.sol b/foundry/src/TychoRouter.sol index 844507e..9e16b61 100644 --- a/foundry/src/TychoRouter.sol +++ b/foundry/src/TychoRouter.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; import "../lib/IWETH.sol"; import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol"; -import "./CallbackVerificationDispatcher.sol"; + import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -11,12 +11,9 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/utils/Address.sol"; import "@permit2/src/interfaces/IAllowanceTransfer.sol"; -import "@uniswap/v3-updated/CallbackValidationV2.sol"; -import "./ExecutionDispatcher.sol"; -import "./CallbackVerificationDispatcher.sol"; +import "./Dispatcher.sol"; import {LibSwap} from "../lib/LibSwap.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; -import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol"; error TychoRouter__WithdrawalFailed(); error TychoRouter__AddressZero(); @@ -24,15 +21,9 @@ error TychoRouter__EmptySwaps(); error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount); error TychoRouter__AmountInNotFullySpent(uint256 leftoverAmount); error TychoRouter__MessageValueMismatch(uint256 value, uint256 amount); +error TychoRouter__InvalidDataLength(); -contract TychoRouter is - AccessControl, - ExecutionDispatcher, - CallbackVerificationDispatcher, - Pausable, - ReentrancyGuard, - SafeCallback -{ +contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard { IAllowanceTransfer public immutable permit2; IWETH private immutable _weth; @@ -66,24 +57,13 @@ contract TychoRouter is ); event FeeSet(uint256 indexed oldFee, uint256 indexed newFee); - address private immutable _usv3Factory; - - constructor( - IPoolManager _poolManager, - address _permit2, - address weth, - address usv3Factory - ) SafeCallback(_poolManager) { - if ( - _permit2 == address(0) || weth == address(0) - || usv3Factory == address(0) - ) { + constructor(address _permit2, address weth) { + if (_permit2 == address(0) || weth == address(0)) { revert TychoRouter__AddressZero(); } permit2 = IAllowanceTransfer(_permit2); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _weth = IWETH(weth); - _usv3Factory = usv3Factory; } /** @@ -236,21 +216,9 @@ contract TychoRouter is /** * @dev We use the fallback function to allow flexibility on callback. - * This function will static call a verifier contract and should revert if the - * caller is not a pool. */ fallback() external { - _executeGenericCallback(msg.data); - } - - /** - * @dev Check if the sender is correct and executes callback actions. - * @param msgData encoded data. It must includes data for the verification. - */ - function _executeGenericCallback(bytes calldata msgData) internal { - (uint256 amountOwed, address tokenOwed) = _callVerifyCallback(msgData); - - IERC20(tokenOwed).safeTransfer(msg.sender, amountOwed); + _handleCallback(msg.data); } /** @@ -303,28 +271,6 @@ contract TychoRouter is _removeExecutor(target); } - /** - * @dev Entrypoint to add or replace an approved callback verifier contract address - * @param target address of the callback verifier contract - */ - function setCallbackVerifier(address target) - external - onlyRole(EXECUTOR_SETTER_ROLE) - { - _setCallbackVerifier(target); - } - - /** - * @dev Entrypoint to remove an approved callback verifier contract address - * @param target address of the callback verifier contract - */ - function removeCallbackVerifier(address target) - external - onlyRole(EXECUTOR_SETTER_ROLE) - { - _removeCallbackVerifier(target); - } - /** * @dev Allows setting the fee receiver. */ @@ -414,55 +360,30 @@ contract TychoRouter is * 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, + int256, /* amount0Delta */ + int256, /* amount1Delta */ bytes calldata data - ) internal view returns (uint256 amountIn, address tokenIn) { - tokenIn = address(bytes20(data[0:20])); - address tokenOut = address(bytes20(data[20:40])); - uint24 poolFee = uint24(bytes3(data[40:43])); + ) 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])); - // slither-disable-next-line unused-return - CallbackValidationV2.verifyCallback( - _usv3Factory, tokenIn, tokenOut, poolFee - ); - - amountIn = - amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta); - - return (amountIn, tokenIn); + bytes calldata fullData = msg.data[4:dataOffset + 32 + dataLength]; + _handleCallback(fullData); } - function _unlockCallback(bytes calldata data) - internal - override + /** + * @dev Called by UniswapV4 pool manager after achieving unlock state. + */ + function unlockCallback(bytes calldata data) + external returns (bytes memory) { - require(data.length >= 20, "Invalid data length"); - bytes4 selector = bytes4(data[data.length - 4:]); - address executor = - address(uint160(bytes20(data[data.length - 24:data.length - 4]))); - bytes memory protocolData = data[:data.length - 24]; - - if (!executors[executor]) { - revert ExecutionDispatcher__UnapprovedExecutor(); - } - - // slither-disable-next-line controlled-delegatecall,low-level-calls - (bool success,) = executor.delegatecall( - abi.encodeWithSelector(selector, protocolData) - ); - require(success, "delegatecall to uniswap v4 callback failed"); + if (data.length < 24) revert TychoRouter__InvalidDataLength(); + _handleCallback(data); return ""; } } diff --git a/foundry/src/executors/UniswapV3Executor.sol b/foundry/src/executors/UniswapV3Executor.sol index ac986c5..d1719fe 100644 --- a/foundry/src/executors/UniswapV3Executor.sol +++ b/foundry/src/executors/UniswapV3Executor.sol @@ -4,14 +4,30 @@ 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(); +error UniswapV3Executor__InvalidFactory(); + +contract UniswapV3Executor is IExecutor, ICallback { + using SafeERC20 for IERC20; -contract UniswapV3Executor is IExecutor { uint160 private constant MIN_SQRT_RATIO = 4295128739; uint160 private constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; + address public immutable factory; + address private immutable self; + + constructor(address _factory) { + if (_factory == address(0)) { + revert UniswapV3Executor__InvalidFactory(); + } + factory = _factory; + self = address(this); + } + // slither-disable-next-line locked-ether function swap(uint256 amountIn, bytes calldata data) external @@ -50,6 +66,54 @@ contract UniswapV3Executor is IExecutor { } } + function handleCallback(bytes calldata msgData) + public + returns (bytes memory result) + { + // The data has the following layout: + // - amount0Delta (32 bytes) + // - amount1Delta (32 bytes) + // - dataOffset (32 bytes) + // - dataLength (32 bytes) + // - protocolData (variable length) + + (int256 amount0Delta, int256 amount1Delta) = + abi.decode(msgData[:64], (int256, int256)); + + address tokenIn = address(bytes20(msgData[128:148])); + + verifyCallback(msgData[128:]); + + uint256 amountOwed = + amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta); + + IERC20(tokenIn).safeTransfer(msg.sender, amountOwed); + return abi.encode(amountOwed, tokenIn); + } + + function verifyCallback(bytes calldata data) public view { + 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(factory, tokenIn, tokenOut, poolFee); + } + + function uniswapV3SwapCallback( + int256, /* amount0Delta */ + int256, /* amount1Delta */ + bytes calldata /* data */ + ) external { + 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); + } + function _decodeData(bytes calldata data) internal pure @@ -75,9 +139,11 @@ contract UniswapV3Executor is IExecutor { function _makeV3CallbackData(address tokenIn, address tokenOut, uint24 fee) internal - pure + view returns (bytes memory) { - return abi.encodePacked(tokenIn, tokenOut, fee); + return abi.encodePacked( + tokenIn, tokenOut, fee, self, ICallback.handleCallback.selector + ); } } diff --git a/foundry/test/CallbackVerificationDispatcher.t.sol b/foundry/test/CallbackVerificationDispatcher.t.sol deleted file mode 100644 index fd2577e..0000000 --- a/foundry/test/CallbackVerificationDispatcher.t.sol +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.26; - -import "@src/CallbackVerificationDispatcher.sol"; -import "./TychoRouterTestSetup.sol"; - -contract CallbackVerificationDispatcherExposed is - CallbackVerificationDispatcher -{ - function exposedCallVerifier(bytes calldata data) - external - view - returns (uint256 amountOwed, address tokenOwed) - { - return _callVerifyCallback(data); - } - - function exposedDecodeVerifierAndSelector(bytes calldata data) - external - pure - returns (address executor, bytes4 selector, bytes memory protocolData) - { - return _decodeVerifierAndSelector(data); - } - - function exposedSetCallbackVerifier(address target) external { - _setCallbackVerifier(target); - } - - function exposedRemoveCallbackVerifier(address target) external { - _removeCallbackVerifier(target); - } -} - -contract CallbackVerificationDispatcherTest is Constants { - CallbackVerificationDispatcherExposed dispatcherExposed; - - event CallbackVerifierSet(address indexed callbackVerifier); - event CallbackVerifierRemoved(address indexed callbackVerifier); - - function setUp() public { - uint256 forkBlock = 20673900; - vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - dispatcherExposed = new CallbackVerificationDispatcherExposed(); - deal(WETH_ADDR, address(dispatcherExposed), 15 ether); - deployDummyContract(); - } - - function testSetValidVerifier() public { - vm.expectEmit(); - // Define the event we expect to be emitted at the next step - emit CallbackVerifierSet(DUMMY); - dispatcherExposed.exposedSetCallbackVerifier(DUMMY); - assert(dispatcherExposed.callbackVerifiers(DUMMY) == true); - } - - function testRemoveVerifier() public { - dispatcherExposed.exposedSetCallbackVerifier(DUMMY); - vm.expectEmit(); - // Define the event we expect to be emitted at the next step - emit CallbackVerifierRemoved(DUMMY); - dispatcherExposed.exposedRemoveCallbackVerifier(DUMMY); - assert(dispatcherExposed.callbackVerifiers(DUMMY) == false); - } - - function testRemoveUnSetVerifier() public { - dispatcherExposed.exposedRemoveCallbackVerifier(BOB); - assert(dispatcherExposed.callbackVerifiers(BOB) == false); - } - - function testSetVerifierNonContract() public { - vm.expectRevert( - abi.encodeWithSelector( - CallbackVerificationDispatcher__NonContractVerifier.selector - ) - ); - dispatcherExposed.exposedSetCallbackVerifier(BOB); - } - - function testCallVerifierSuccess() public { - // For this test, we can use any callback verifier and any calldata that we - // know works for this verifier. We don't care about which calldata/executor, - // since we are only testing the functionality of the staticcall and not - // the inner verifier. - // Thus, this test case designed from scratch using previously-deployed - // Maverick callback verifier. Looking at the code, we can easily design - // passing calldata. - dispatcherExposed.exposedSetCallbackVerifier( - address(0x2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e8) - ); - bytes memory data = - hex"2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e876b20f8a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; - vm.startPrank(address(0xD0b2F5018B5D22759724af6d4281AC0B13266360)); - (uint256 amountOwed, address tokenOwed) = - dispatcherExposed.exposedCallVerifier(data); - vm.stopPrank(); - - // The specific values returned are not important for this test. - // The goal is to ensure correct calling of the Maverick verifier's. - // Since the verifier's output format has changed, the asserted values may not be meaningful. - // Full validation of the functionality will be covered in the integration tests. - assert(amountOwed == 1); - assert(tokenOwed == address(0x0000000000000000000000000000000000000001)); - } - - function testCallVerifierNoSelector() public { - // This test is exactly the same as testCallVerifierSuccess, except that the - // fn selector is not explicitly passed. The test should still pass using the - // default selector. - dispatcherExposed.exposedSetCallbackVerifier( - address(0x2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e8) - ); - - // Pass all-zero selector. This should default to the verifyCallback selector - bytes memory data = - hex"2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e8000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; - vm.startPrank(address(0xD0b2F5018B5D22759724af6d4281AC0B13266360)); - (uint256 amountOwed, address tokenOwed) = - dispatcherExposed.exposedCallVerifier(data); - vm.stopPrank(); - - // The specific values returned are not important for this test. - // The goal is to ensure correct calling of the Maverick verifier's. - // Since the verifier's output format has changed, the asserted values may not be meaningful. - // Full validation of the functionality will be covered in the integration tests. - assert(amountOwed == 1); - assert(tokenOwed == address(0x0000000000000000000000000000000000000001)); - } - - function testCallVerifierBadSelector() public { - // A bad selector is provided to an approved executor - causing the call - // itself to fail. Make sure this actually reverts. - dispatcherExposed.exposedSetCallbackVerifier( - address(0x2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e8) - ); - vm.startPrank(address(0xD0b2F5018B5D22759724af6d4281AC0B13266360)); - bytes memory data = - hex"2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e8aa0000000000"; - vm.expectRevert(bytes("Callback verification failed")); - dispatcherExposed.exposedCallVerifier(data); - vm.stopPrank(); - } - - function testCallVerifierParseRevertMessage() public { - // Verification should fail because caller is not a Maverick pool - // Check that we correctly parse the revert message - dispatcherExposed.exposedSetCallbackVerifier( - address(0x2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e8) - ); - bytes memory data = - hex"2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e8000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; - vm.expectRevert( - abi.encodeWithSignature( - "Error(string)", "Must call from a Maverick Factory Pool" - ) - ); - dispatcherExposed.exposedCallVerifier(data); - } - - function testCallVerifierUnapprovedVerifier() public { - bytes memory data = - hex"5d622C9053b8FFB1B3465495C8a42E603632bA70aabbccdd1111111111111111"; - vm.expectRevert(); - dispatcherExposed.exposedCallVerifier(data); - } - - function testDecodeVerifierAndSelector() public view { - bytes memory data = - hex"2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e876b20f8aA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; - (address executor, bytes4 selector, bytes memory verifierData) = - dispatcherExposed.exposedDecodeVerifierAndSelector(data); - assert(executor == address(0x2C960bD1CFE09A26105ad3C351bEa0a3fAD0F8e8)); - assert(selector == bytes4(0x76b20f8a)); - // Direct bytes comparison not supported - must use keccak - assert( - keccak256(verifierData) - == keccak256(hex"A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") - ); - } -} diff --git a/foundry/test/Constants.sol b/foundry/test/Constants.sol index ba8fbef..3b742f0 100644 --- a/foundry/test/Constants.sol +++ b/foundry/test/Constants.sol @@ -38,6 +38,7 @@ contract Constants is Test { address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416; // uniswap v3 + address USV3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8; /** diff --git a/foundry/test/ExecutionDispatcher.t.sol b/foundry/test/ExecutionDispatcher.t.sol index 7f2fb81..4f12df0 100644 --- a/foundry/test/ExecutionDispatcher.t.sol +++ b/foundry/test/ExecutionDispatcher.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.26; -import "@src/ExecutionDispatcher.sol"; +import "@src/Dispatcher.sol"; import "./TychoRouterTestSetup.sol"; -contract ExecutionDispatcherExposed is ExecutionDispatcher { +contract DispatcherExposed is Dispatcher { function exposedCallExecutor( address executor, bytes4 selector, @@ -23,8 +23,8 @@ contract ExecutionDispatcherExposed is ExecutionDispatcher { } } -contract ExecutionDispatcherTest is Constants { - ExecutionDispatcherExposed dispatcherExposed; +contract DispatcherTest is Constants { + DispatcherExposed dispatcherExposed; event ExecutorSet(address indexed executor); event ExecutorRemoved(address indexed executor); @@ -32,7 +32,7 @@ contract ExecutionDispatcherTest is Constants { function setUp() public { uint256 forkBlock = 20673900; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - dispatcherExposed = new ExecutionDispatcherExposed(); + dispatcherExposed = new DispatcherExposed(); deal(WETH_ADDR, address(dispatcherExposed), 15 ether); deployDummyContract(); } @@ -61,9 +61,7 @@ contract ExecutionDispatcherTest is Constants { function testSetExecutorNonContract() public { vm.expectRevert( - abi.encodeWithSelector( - ExecutionDispatcher__NonContractExecutor.selector - ) + abi.encodeWithSelector(Dispatcher__NonContractExecutor.selector) ); dispatcherExposed.exposedSetExecutor(BOB); } diff --git a/foundry/test/TychoRouter.t.sol b/foundry/test/TychoRouter.t.sol index 28eed5c..d5787b5 100644 --- a/foundry/test/TychoRouter.t.sol +++ b/foundry/test/TychoRouter.t.sol @@ -5,6 +5,7 @@ import "@src/executors/UniswapV4Executor.sol"; import {TychoRouter} from "@src/TychoRouter.sol"; import "./TychoRouterTestSetup.sol"; import "./executors/UniswapV4Utils.sol"; +import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol"; contract TychoRouterTest is TychoRouterTestSetup { bytes32 public constant EXECUTOR_SETTER_ROLE = @@ -63,31 +64,6 @@ contract TychoRouterTest is TychoRouterTestSetup { tychoRouter.setExecutors(executors); } - function testSetVerifierValidRole() public { - vm.startPrank(EXECUTOR_SETTER); - tychoRouter.setCallbackVerifier(DUMMY); - vm.stopPrank(); - assert(tychoRouter.callbackVerifiers(DUMMY) == true); - } - - function testRemoveVerifierValidRole() public { - vm.startPrank(EXECUTOR_SETTER); - tychoRouter.setCallbackVerifier(DUMMY); - tychoRouter.removeCallbackVerifier(DUMMY); - vm.stopPrank(); - assert(tychoRouter.callbackVerifiers(DUMMY) == false); - } - - function testRemoveVerifierMissingSetterRole() public { - vm.expectRevert(); - tychoRouter.removeCallbackVerifier(BOB); - } - - function testSetVerifierMissingSetterRole() public { - vm.expectRevert(); - tychoRouter.setCallbackVerifier(DUMMY); - } - function testWithdrawNative() public { vm.startPrank(FUND_RESCUER); // Send 100 ether to tychoRouter @@ -626,24 +602,6 @@ contract TychoRouterTest is TychoRouterTestSetup { vm.stopPrank(); } - function testUSV3Callback() public { - uint24 poolFee = 3000; - uint256 amountOwed = 1000000000000000000; - deal(WETH_ADDR, tychoRouterAddr, amountOwed); - uint256 initialPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3); - - vm.startPrank(DAI_WETH_USV3); - tychoRouter.uniswapV3SwapCallback( - -2631245338449998525223, - int256(amountOwed), - abi.encodePacked(WETH_ADDR, DAI_ADDR, poolFee) - ); - vm.stopPrank(); - - uint256 finalPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3); - assertEq(finalPoolReserve - initialPoolReserve, amountOwed); - } - function testSwapSingleUSV3() public { // Trade 1 WETH for DAI with 1 swap on Uniswap V3 // 1 WETH -> DAI diff --git a/foundry/test/TychoRouterTestSetup.sol b/foundry/test/TychoRouterTestSetup.sol index b0bbf2e..9fb1f44 100644 --- a/foundry/test/TychoRouterTestSetup.sol +++ b/foundry/test/TychoRouterTestSetup.sol @@ -12,12 +12,7 @@ import {WETH} from "../lib/permit2/lib/solmate/src/tokens/WETH.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; contract TychoRouterExposed is TychoRouter { - constructor( - IPoolManager _poolManager, - address _permit2, - address weth, - address usv3Factory - ) TychoRouter(_poolManager, _permit2, weth, usv3Factory) {} + constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {} function wrapETH(uint256 amount) external payable { return _wrapETH(amount); @@ -50,12 +45,10 @@ contract TychoRouterTestSetup is Test, Constants { vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); vm.startPrank(ADMIN); - address factoryV3 = address(0x1F98431c8aD98523631AE4a59f267346ea31F984); + address factoryV3 = USV3_FACTORY; address poolManagerAddress = 0x000000000004444c5dc75cB358380D2e3dE08A90; IPoolManager poolManager = IPoolManager(poolManagerAddress); - tychoRouter = new TychoRouterExposed( - poolManager, permit2Address, WETH_ADDR, factoryV3 - ); + tychoRouter = new TychoRouterExposed(permit2Address, WETH_ADDR); tychoRouterAddr = address(tychoRouter); tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER); tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER); @@ -68,7 +61,7 @@ contract TychoRouterTestSetup is Test, Constants { vm.stopPrank(); usv2Executor = new UniswapV2Executor(); - usv3Executor = new UniswapV3Executor(); + usv3Executor = new UniswapV3Executor(factoryV3); usv4Executor = new UniswapV4Executor(poolManager); vm.startPrank(EXECUTOR_SETTER); address[] memory executors = new address[](3); diff --git a/foundry/test/executors/UniswapV3Executor.t.sol b/foundry/test/executors/UniswapV3Executor.t.sol index 4c9c1e9..86b999b 100644 --- a/foundry/test/executors/UniswapV3Executor.t.sol +++ b/foundry/test/executors/UniswapV3Executor.t.sol @@ -6,6 +6,8 @@ import {Test} from "../../lib/forge-std/src/Test.sol"; import {Constants} from "../Constants.sol"; contract UniswapV3ExecutorExposed is UniswapV3Executor { + constructor(address _factory) UniswapV3Executor(_factory) {} + function decodeData(bytes calldata data) external pure @@ -22,7 +24,7 @@ contract UniswapV3ExecutorExposed is UniswapV3Executor { } } -contract UniswapV3ExecutorTest is UniswapV3ExecutorExposed, Test, Constants { +contract UniswapV3ExecutorTest is Test, Constants { using SafeERC20 for IERC20; UniswapV3ExecutorExposed uniswapV3Exposed; @@ -32,7 +34,8 @@ contract UniswapV3ExecutorTest is UniswapV3ExecutorExposed, Test, Constants { function setUp() public { uint256 forkBlock = 17323404; vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock); - uniswapV3Exposed = new UniswapV3ExecutorExposed(); + + uniswapV3Exposed = new UniswapV3ExecutorExposed(USV3_FACTORY); } function testDecodeParams() public view { @@ -65,4 +68,61 @@ contract UniswapV3ExecutorTest is UniswapV3ExecutorExposed, Test, Constants { vm.expectRevert(UniswapV3Executor__InvalidDataLength.selector); uniswapV3Exposed.decodeData(invalidParams); } + + function testUSV3Callback() public { + uint24 poolFee = 3000; + uint256 amountOwed = 1000000000000000000; + deal(WETH_ADDR, address(uniswapV3Exposed), amountOwed); + uint256 initialPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3); + + vm.startPrank(DAI_WETH_USV3); + bytes memory protocolData = + abi.encodePacked(WETH_ADDR, DAI_ADDR, poolFee); + uint256 dataOffset = 3; // some offset + uint256 dataLength = protocolData.length; + + bytes memory callbackData = abi.encodePacked( + int256(amountOwed), // amount0Delta + int256(0), // amount1Delta + dataOffset, + dataLength, + protocolData + ); + uniswapV3Exposed.handleCallback(callbackData); + vm.stopPrank(); + + uint256 finalPoolReserve = IERC20(WETH_ADDR).balanceOf(DAI_WETH_USV3); + assertEq(finalPoolReserve - initialPoolReserve, amountOwed); + } + + function testSwapIntegration() public { + uint256 amountIn = 10 ** 18; + deal(WETH_ADDR, address(uniswapV3Exposed), amountIn); + + uint256 expAmountOut = 1205_128428842122129186; //Swap 1 WETH for 1205.12 DAI + bool zeroForOne = false; + + bytes memory data = encodeUniswapV3Swap( + WETH_ADDR, DAI_ADDR, address(this), DAI_WETH_USV3, zeroForOne + ); + + uint256 amountOut = uniswapV3Exposed.swap(amountIn, data); + + assertGe(amountOut, expAmountOut); + assertEq(IERC20(WETH_ADDR).balanceOf(address(uniswapV3Exposed)), 0); + assertGe(IERC20(DAI_ADDR).balanceOf(address(this)), expAmountOut); + } + + function encodeUniswapV3Swap( + address tokenIn, + address tokenOut, + address receiver, + address target, + bool zero2one + ) internal view returns (bytes memory) { + IUniswapV3Pool pool = IUniswapV3Pool(target); + return abi.encodePacked( + tokenIn, tokenOut, pool.fee(), receiver, target, zero2one + ); + } }