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,
|
address tokenB,
|
||||||
uint24 fee
|
uint24 fee
|
||||||
) internal view returns (IUniswapV3Pool pool) {
|
) internal view returns (IUniswapV3Pool pool) {
|
||||||
return verifyCallback(
|
return
|
||||||
factory, PoolAddressV2.getPoolKey(tokenA, tokenB, fee)
|
verifyCallback(
|
||||||
);
|
factory,
|
||||||
|
PoolAddressV2.getPoolKey(tokenA, tokenB, fee)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @notice Returns the address of a valid Uniswap V3 Pool
|
/// @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;
|
pragma solidity ^0.8.26;
|
||||||
|
|
||||||
import "@interfaces/IExecutor.sol";
|
import "@interfaces/IExecutor.sol";
|
||||||
|
import "@interfaces/ICallback.sol";
|
||||||
|
|
||||||
error ExecutionDispatcher__UnapprovedExecutor();
|
error Dispatcher__UnapprovedExecutor();
|
||||||
error ExecutionDispatcher__NonContractExecutor();
|
error Dispatcher__NonContractExecutor();
|
||||||
|
error Dispatcher__InvalidDataLength();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title ExecutionDispatcher - Dispatch execution to external contracts
|
* @title Dispatcher - Dispatch execution to external contracts
|
||||||
* @author PropellerHeads Devs
|
* @author PropellerHeads Devs
|
||||||
* @dev Provides the ability to delegate execution of swaps to external
|
* @dev Provides the ability to delegate execution of swaps to external
|
||||||
* contracts. This allows dynamically adding new supported protocols
|
* 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
|
* Note: Executor contracts need to implement the IExecutor interface unless
|
||||||
* an alternate selector is specified.
|
* an alternate selector is specified.
|
||||||
*/
|
*/
|
||||||
contract ExecutionDispatcher {
|
contract Dispatcher {
|
||||||
mapping(address => bool) public executors;
|
mapping(address => bool) public executors;
|
||||||
|
|
||||||
event ExecutorSet(address indexed executor);
|
event ExecutorSet(address indexed executor);
|
||||||
@@ -31,7 +33,7 @@ contract ExecutionDispatcher {
|
|||||||
*/
|
*/
|
||||||
function _setExecutor(address target) internal {
|
function _setExecutor(address target) internal {
|
||||||
if (target.code.length == 0) {
|
if (target.code.length == 0) {
|
||||||
revert ExecutionDispatcher__NonContractExecutor();
|
revert Dispatcher__NonContractExecutor();
|
||||||
}
|
}
|
||||||
executors[target] = true;
|
executors[target] = true;
|
||||||
emit ExecutorSet(target);
|
emit ExecutorSet(target);
|
||||||
@@ -58,7 +60,7 @@ contract ExecutionDispatcher {
|
|||||||
bytes calldata data
|
bytes calldata data
|
||||||
) internal returns (uint256 calculatedAmount) {
|
) internal returns (uint256 calculatedAmount) {
|
||||||
if (!executors[executor]) {
|
if (!executors[executor]) {
|
||||||
revert ExecutionDispatcher__UnapprovedExecutor();
|
revert Dispatcher__UnapprovedExecutor();
|
||||||
}
|
}
|
||||||
|
|
||||||
selector = selector == bytes4(0) ? IExecutor.swap.selector : selector;
|
selector = selector == bytes4(0) ? IExecutor.swap.selector : selector;
|
||||||
@@ -79,4 +81,29 @@ contract ExecutionDispatcher {
|
|||||||
|
|
||||||
calculatedAmount = abi.decode(result, (uint256));
|
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/IWETH.sol";
|
||||||
import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol";
|
import "../lib/bytes/LibPrefixLengthEncodedByteArray.sol";
|
||||||
import "./CallbackVerificationDispatcher.sol";
|
|
||||||
import "@openzeppelin/contracts/access/AccessControl.sol";
|
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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/Pausable.sol";
|
||||||
import "@openzeppelin/contracts/utils/Address.sol";
|
import "@openzeppelin/contracts/utils/Address.sol";
|
||||||
import "@permit2/src/interfaces/IAllowanceTransfer.sol";
|
import "@permit2/src/interfaces/IAllowanceTransfer.sol";
|
||||||
import "@uniswap/v3-updated/CallbackValidationV2.sol";
|
import "./Dispatcher.sol";
|
||||||
import "./ExecutionDispatcher.sol";
|
|
||||||
import "./CallbackVerificationDispatcher.sol";
|
|
||||||
import {LibSwap} from "../lib/LibSwap.sol";
|
import {LibSwap} from "../lib/LibSwap.sol";
|
||||||
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.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__WithdrawalFailed();
|
||||||
error TychoRouter__AddressZero();
|
error TychoRouter__AddressZero();
|
||||||
@@ -24,15 +21,9 @@ error TychoRouter__EmptySwaps();
|
|||||||
error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount);
|
error TychoRouter__NegativeSlippage(uint256 amount, uint256 minAmount);
|
||||||
error TychoRouter__AmountInNotFullySpent(uint256 leftoverAmount);
|
error TychoRouter__AmountInNotFullySpent(uint256 leftoverAmount);
|
||||||
error TychoRouter__MessageValueMismatch(uint256 value, uint256 amount);
|
error TychoRouter__MessageValueMismatch(uint256 value, uint256 amount);
|
||||||
|
error TychoRouter__InvalidDataLength();
|
||||||
|
|
||||||
contract TychoRouter is
|
contract TychoRouter is AccessControl, Dispatcher, Pausable, ReentrancyGuard {
|
||||||
AccessControl,
|
|
||||||
ExecutionDispatcher,
|
|
||||||
CallbackVerificationDispatcher,
|
|
||||||
Pausable,
|
|
||||||
ReentrancyGuard,
|
|
||||||
SafeCallback
|
|
||||||
{
|
|
||||||
IAllowanceTransfer public immutable permit2;
|
IAllowanceTransfer public immutable permit2;
|
||||||
IWETH private immutable _weth;
|
IWETH private immutable _weth;
|
||||||
|
|
||||||
@@ -66,24 +57,13 @@ contract TychoRouter is
|
|||||||
);
|
);
|
||||||
event FeeSet(uint256 indexed oldFee, uint256 indexed newFee);
|
event FeeSet(uint256 indexed oldFee, uint256 indexed newFee);
|
||||||
|
|
||||||
address private immutable _usv3Factory;
|
constructor(address _permit2, address weth) {
|
||||||
|
if (_permit2 == address(0) || weth == address(0)) {
|
||||||
constructor(
|
|
||||||
IPoolManager _poolManager,
|
|
||||||
address _permit2,
|
|
||||||
address weth,
|
|
||||||
address usv3Factory
|
|
||||||
) SafeCallback(_poolManager) {
|
|
||||||
if (
|
|
||||||
_permit2 == address(0) || weth == address(0)
|
|
||||||
|| usv3Factory == address(0)
|
|
||||||
) {
|
|
||||||
revert TychoRouter__AddressZero();
|
revert TychoRouter__AddressZero();
|
||||||
}
|
}
|
||||||
permit2 = IAllowanceTransfer(_permit2);
|
permit2 = IAllowanceTransfer(_permit2);
|
||||||
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
|
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
|
||||||
_weth = IWETH(weth);
|
_weth = IWETH(weth);
|
||||||
_usv3Factory = usv3Factory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -236,21 +216,9 @@ contract TychoRouter is
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev We use the fallback function to allow flexibility on callback.
|
* @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 {
|
fallback() external {
|
||||||
_executeGenericCallback(msg.data);
|
_handleCallback(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -303,28 +271,6 @@ contract TychoRouter is
|
|||||||
_removeExecutor(target);
|
_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.
|
* @dev Allows setting the fee receiver.
|
||||||
*/
|
*/
|
||||||
@@ -414,55 +360,30 @@ contract TychoRouter is
|
|||||||
* See in IUniswapV3SwapCallback for documentation.
|
* See in IUniswapV3SwapCallback for documentation.
|
||||||
*/
|
*/
|
||||||
function uniswapV3SwapCallback(
|
function uniswapV3SwapCallback(
|
||||||
int256 amount0Delta,
|
int256, /* amount0Delta */
|
||||||
int256 amount1Delta,
|
int256, /* amount1Delta */
|
||||||
bytes calldata msgData
|
|
||||||
) external {
|
|
||||||
(uint256 amountOwed, address tokenOwed) =
|
|
||||||
_verifyUSV3Callback(amount0Delta, amount1Delta, msgData);
|
|
||||||
IERC20(tokenOwed).safeTransfer(msg.sender, amountOwed);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _verifyUSV3Callback(
|
|
||||||
int256 amount0Delta,
|
|
||||||
int256 amount1Delta,
|
|
||||||
bytes calldata data
|
bytes calldata data
|
||||||
) internal view returns (uint256 amountIn, address tokenIn) {
|
) external {
|
||||||
tokenIn = address(bytes20(data[0:20]));
|
if (data.length < 24) revert TychoRouter__InvalidDataLength();
|
||||||
address tokenOut = address(bytes20(data[20:40]));
|
// We are taking advantage of the fact that the data we need is already encoded in the correct format inside msg.data
|
||||||
uint24 poolFee = uint24(bytes3(data[40:43]));
|
// 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
|
bytes calldata fullData = msg.data[4:dataOffset + 32 + dataLength];
|
||||||
CallbackValidationV2.verifyCallback(
|
_handleCallback(fullData);
|
||||||
_usv3Factory, tokenIn, tokenOut, poolFee
|
|
||||||
);
|
|
||||||
|
|
||||||
amountIn =
|
|
||||||
amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);
|
|
||||||
|
|
||||||
return (amountIn, tokenIn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _unlockCallback(bytes calldata data)
|
/**
|
||||||
internal
|
* @dev Called by UniswapV4 pool manager after achieving unlock state.
|
||||||
override
|
*/
|
||||||
|
function unlockCallback(bytes calldata data)
|
||||||
|
external
|
||||||
returns (bytes memory)
|
returns (bytes memory)
|
||||||
{
|
{
|
||||||
require(data.length >= 20, "Invalid data length");
|
if (data.length < 24) revert TychoRouter__InvalidDataLength();
|
||||||
bytes4 selector = bytes4(data[data.length - 4:]);
|
_handleCallback(data);
|
||||||
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");
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,30 @@ pragma solidity ^0.8.26;
|
|||||||
import "@interfaces/IExecutor.sol";
|
import "@interfaces/IExecutor.sol";
|
||||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||||
|
import "@uniswap/v3-updated/CallbackValidationV2.sol";
|
||||||
|
import "@interfaces/ICallback.sol";
|
||||||
|
|
||||||
error UniswapV3Executor__InvalidDataLength();
|
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 MIN_SQRT_RATIO = 4295128739;
|
||||||
uint160 private constant MAX_SQRT_RATIO =
|
uint160 private constant MAX_SQRT_RATIO =
|
||||||
1461446703485210103287273052203988822378723970342;
|
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
|
// slither-disable-next-line locked-ether
|
||||||
function swap(uint256 amountIn, bytes calldata data)
|
function swap(uint256 amountIn, bytes calldata data)
|
||||||
external
|
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)
|
function _decodeData(bytes calldata data)
|
||||||
internal
|
internal
|
||||||
pure
|
pure
|
||||||
@@ -75,9 +139,11 @@ contract UniswapV3Executor is IExecutor {
|
|||||||
|
|
||||||
function _makeV3CallbackData(address tokenIn, address tokenOut, uint24 fee)
|
function _makeV3CallbackData(address tokenIn, address tokenOut, uint24 fee)
|
||||||
internal
|
internal
|
||||||
pure
|
view
|
||||||
returns (bytes memory)
|
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;
|
address USDC_WBTC_POOL = 0x004375Dff511095CC5A197A54140a24eFEF3A416;
|
||||||
|
|
||||||
// uniswap v3
|
// uniswap v3
|
||||||
|
address USV3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
|
||||||
address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
|
address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
pragma solidity ^0.8.26;
|
pragma solidity ^0.8.26;
|
||||||
|
|
||||||
import "@src/ExecutionDispatcher.sol";
|
import "@src/Dispatcher.sol";
|
||||||
import "./TychoRouterTestSetup.sol";
|
import "./TychoRouterTestSetup.sol";
|
||||||
|
|
||||||
contract ExecutionDispatcherExposed is ExecutionDispatcher {
|
contract DispatcherExposed is Dispatcher {
|
||||||
function exposedCallExecutor(
|
function exposedCallExecutor(
|
||||||
address executor,
|
address executor,
|
||||||
bytes4 selector,
|
bytes4 selector,
|
||||||
@@ -23,8 +23,8 @@ contract ExecutionDispatcherExposed is ExecutionDispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contract ExecutionDispatcherTest is Constants {
|
contract DispatcherTest is Constants {
|
||||||
ExecutionDispatcherExposed dispatcherExposed;
|
DispatcherExposed dispatcherExposed;
|
||||||
|
|
||||||
event ExecutorSet(address indexed executor);
|
event ExecutorSet(address indexed executor);
|
||||||
event ExecutorRemoved(address indexed executor);
|
event ExecutorRemoved(address indexed executor);
|
||||||
@@ -32,7 +32,7 @@ contract ExecutionDispatcherTest is Constants {
|
|||||||
function setUp() public {
|
function setUp() public {
|
||||||
uint256 forkBlock = 20673900;
|
uint256 forkBlock = 20673900;
|
||||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||||
dispatcherExposed = new ExecutionDispatcherExposed();
|
dispatcherExposed = new DispatcherExposed();
|
||||||
deal(WETH_ADDR, address(dispatcherExposed), 15 ether);
|
deal(WETH_ADDR, address(dispatcherExposed), 15 ether);
|
||||||
deployDummyContract();
|
deployDummyContract();
|
||||||
}
|
}
|
||||||
@@ -61,9 +61,7 @@ contract ExecutionDispatcherTest is Constants {
|
|||||||
|
|
||||||
function testSetExecutorNonContract() public {
|
function testSetExecutorNonContract() public {
|
||||||
vm.expectRevert(
|
vm.expectRevert(
|
||||||
abi.encodeWithSelector(
|
abi.encodeWithSelector(Dispatcher__NonContractExecutor.selector)
|
||||||
ExecutionDispatcher__NonContractExecutor.selector
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
dispatcherExposed.exposedSetExecutor(BOB);
|
dispatcherExposed.exposedSetExecutor(BOB);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import "@src/executors/UniswapV4Executor.sol";
|
|||||||
import {TychoRouter} from "@src/TychoRouter.sol";
|
import {TychoRouter} from "@src/TychoRouter.sol";
|
||||||
import "./TychoRouterTestSetup.sol";
|
import "./TychoRouterTestSetup.sol";
|
||||||
import "./executors/UniswapV4Utils.sol";
|
import "./executors/UniswapV4Utils.sol";
|
||||||
|
import {SafeCallback} from "@uniswap/v4-periphery/src/base/SafeCallback.sol";
|
||||||
|
|
||||||
contract TychoRouterTest is TychoRouterTestSetup {
|
contract TychoRouterTest is TychoRouterTestSetup {
|
||||||
bytes32 public constant EXECUTOR_SETTER_ROLE =
|
bytes32 public constant EXECUTOR_SETTER_ROLE =
|
||||||
@@ -63,31 +64,6 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
|||||||
tychoRouter.setExecutors(executors);
|
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 {
|
function testWithdrawNative() public {
|
||||||
vm.startPrank(FUND_RESCUER);
|
vm.startPrank(FUND_RESCUER);
|
||||||
// Send 100 ether to tychoRouter
|
// Send 100 ether to tychoRouter
|
||||||
@@ -626,24 +602,6 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
|||||||
vm.stopPrank();
|
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 {
|
function testSwapSingleUSV3() public {
|
||||||
// Trade 1 WETH for DAI with 1 swap on Uniswap V3
|
// Trade 1 WETH for DAI with 1 swap on Uniswap V3
|
||||||
// 1 WETH -> DAI
|
// 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";
|
import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol";
|
||||||
|
|
||||||
contract TychoRouterExposed is TychoRouter {
|
contract TychoRouterExposed is TychoRouter {
|
||||||
constructor(
|
constructor(address _permit2, address weth) TychoRouter(_permit2, weth) {}
|
||||||
IPoolManager _poolManager,
|
|
||||||
address _permit2,
|
|
||||||
address weth,
|
|
||||||
address usv3Factory
|
|
||||||
) TychoRouter(_poolManager, _permit2, weth, usv3Factory) {}
|
|
||||||
|
|
||||||
function wrapETH(uint256 amount) external payable {
|
function wrapETH(uint256 amount) external payable {
|
||||||
return _wrapETH(amount);
|
return _wrapETH(amount);
|
||||||
@@ -50,12 +45,10 @@ contract TychoRouterTestSetup is Test, Constants {
|
|||||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||||
|
|
||||||
vm.startPrank(ADMIN);
|
vm.startPrank(ADMIN);
|
||||||
address factoryV3 = address(0x1F98431c8aD98523631AE4a59f267346ea31F984);
|
address factoryV3 = USV3_FACTORY;
|
||||||
address poolManagerAddress = 0x000000000004444c5dc75cB358380D2e3dE08A90;
|
address poolManagerAddress = 0x000000000004444c5dc75cB358380D2e3dE08A90;
|
||||||
IPoolManager poolManager = IPoolManager(poolManagerAddress);
|
IPoolManager poolManager = IPoolManager(poolManagerAddress);
|
||||||
tychoRouter = new TychoRouterExposed(
|
tychoRouter = new TychoRouterExposed(permit2Address, WETH_ADDR);
|
||||||
poolManager, permit2Address, WETH_ADDR, factoryV3
|
|
||||||
);
|
|
||||||
tychoRouterAddr = address(tychoRouter);
|
tychoRouterAddr = address(tychoRouter);
|
||||||
tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER);
|
tychoRouter.grantRole(keccak256("FUND_RESCUER_ROLE"), FUND_RESCUER);
|
||||||
tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER);
|
tychoRouter.grantRole(keccak256("FEE_SETTER_ROLE"), FEE_SETTER);
|
||||||
@@ -68,7 +61,7 @@ contract TychoRouterTestSetup is Test, Constants {
|
|||||||
vm.stopPrank();
|
vm.stopPrank();
|
||||||
|
|
||||||
usv2Executor = new UniswapV2Executor();
|
usv2Executor = new UniswapV2Executor();
|
||||||
usv3Executor = new UniswapV3Executor();
|
usv3Executor = new UniswapV3Executor(factoryV3);
|
||||||
usv4Executor = new UniswapV4Executor(poolManager);
|
usv4Executor = new UniswapV4Executor(poolManager);
|
||||||
vm.startPrank(EXECUTOR_SETTER);
|
vm.startPrank(EXECUTOR_SETTER);
|
||||||
address[] memory executors = new address[](3);
|
address[] memory executors = new address[](3);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {Test} from "../../lib/forge-std/src/Test.sol";
|
|||||||
import {Constants} from "../Constants.sol";
|
import {Constants} from "../Constants.sol";
|
||||||
|
|
||||||
contract UniswapV3ExecutorExposed is UniswapV3Executor {
|
contract UniswapV3ExecutorExposed is UniswapV3Executor {
|
||||||
|
constructor(address _factory) UniswapV3Executor(_factory) {}
|
||||||
|
|
||||||
function decodeData(bytes calldata data)
|
function decodeData(bytes calldata data)
|
||||||
external
|
external
|
||||||
pure
|
pure
|
||||||
@@ -22,7 +24,7 @@ contract UniswapV3ExecutorExposed is UniswapV3Executor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contract UniswapV3ExecutorTest is UniswapV3ExecutorExposed, Test, Constants {
|
contract UniswapV3ExecutorTest is Test, Constants {
|
||||||
using SafeERC20 for IERC20;
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
UniswapV3ExecutorExposed uniswapV3Exposed;
|
UniswapV3ExecutorExposed uniswapV3Exposed;
|
||||||
@@ -32,7 +34,8 @@ contract UniswapV3ExecutorTest is UniswapV3ExecutorExposed, Test, Constants {
|
|||||||
function setUp() public {
|
function setUp() public {
|
||||||
uint256 forkBlock = 17323404;
|
uint256 forkBlock = 17323404;
|
||||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||||
uniswapV3Exposed = new UniswapV3ExecutorExposed();
|
|
||||||
|
uniswapV3Exposed = new UniswapV3ExecutorExposed(USV3_FACTORY);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDecodeParams() public view {
|
function testDecodeParams() public view {
|
||||||
@@ -65,4 +68,61 @@ contract UniswapV3ExecutorTest is UniswapV3ExecutorExposed, Test, Constants {
|
|||||||
vm.expectRevert(UniswapV3Executor__InvalidDataLength.selector);
|
vm.expectRevert(UniswapV3Executor__InvalidDataLength.selector);
|
||||||
uniswapV3Exposed.decodeData(invalidParams);
|
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