Merge pull request #66 from propeller-heads/router/hr/ENG-4237-refactor-usv3-callback

feat: refactor usv3 callback
This commit is contained in:
Harsh Vardhan Roy
2025-02-18 20:39:02 +05:30
committed by GitHub
13 changed files with 231 additions and 473 deletions

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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:];
}
}

View File

@@ -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")
)
);
}
}
}

View File

@@ -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 "";
}
}

View File

@@ -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
);
}
}

View File

@@ -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")
);
}
}

View File

@@ -38,6 +38,7 @@ contract Constants is Test {
address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416;
// uniswap v3
address USV3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
/**

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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
);
}
}