Merge pull request #66 from propeller-heads/router/hr/ENG-4237-refactor-usv3-callback
feat: refactor usv3 callback
This commit is contained in:
25
foundry/interfaces/ICallback.sol
Normal file
25
foundry/interfaces/ICallback.sol
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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:];
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ contract Constants is Test {
|
||||
address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416;
|
||||
|
||||
// uniswap v3
|
||||
address USV3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
|
||||
address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user