Merge pull request #238 from propeller-heads/usx/tnl/ENG-4669-handle-approvals

feat: Handle approvals in UniswapXFiller
This commit is contained in:
Tamara
2025-07-10 10:36:39 -04:00
committed by GitHub
2 changed files with 57 additions and 17 deletions

View File

@@ -9,6 +9,7 @@ import {
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Address.sol"; import "@openzeppelin/contracts/utils/Address.sol";
import {TychoRouter} from "../TychoRouter.sol";
error UniswapXFiller__AddressZero(); error UniswapXFiller__AddressZero();
error UniswapXFiller__BatchExecutionNotSupported(); error UniswapXFiller__BatchExecutionNotSupported();
@@ -19,6 +20,7 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
// UniswapX V2DutchOrder Reactor // UniswapX V2DutchOrder Reactor
IReactor public immutable reactor; IReactor public immutable reactor;
address public immutable tychoRouter; address public immutable tychoRouter;
address public immutable nativeAddress;
// keccak256("NAME_OF_ROLE") : save gas on deployment // keccak256("NAME_OF_ROLE") : save gas on deployment
bytes32 public constant REACTOR_ROLE = bytes32 public constant REACTOR_ROLE =
@@ -30,7 +32,11 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
address indexed token, uint256 amount, address indexed receiver address indexed token, uint256 amount, address indexed receiver
); );
constructor(address _tychoRouter, address _reactor) { constructor(
address _tychoRouter,
address _reactor,
address _native_address
) {
if (_tychoRouter == address(0)) revert UniswapXFiller__AddressZero(); if (_tychoRouter == address(0)) revert UniswapXFiller__AddressZero();
if (_reactor == address(0)) revert UniswapXFiller__AddressZero(); if (_reactor == address(0)) revert UniswapXFiller__AddressZero();
@@ -38,6 +44,9 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
_grantRole(REACTOR_ROLE, address(_reactor)); _grantRole(REACTOR_ROLE, address(_reactor));
tychoRouter = _tychoRouter; tychoRouter = _tychoRouter;
reactor = IReactor(_reactor); reactor = IReactor(_reactor);
// slither-disable-next-line missing-zero-check
nativeAddress = _native_address;
} }
function execute(SignedOrder calldata order, bytes calldata callbackData) function execute(SignedOrder calldata order, bytes calldata callbackData)
@@ -58,12 +67,20 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
ResolvedOrder memory order = resolvedOrders[0]; ResolvedOrder memory order = resolvedOrders[0];
// TODO properly handle native in and out tokens bool tokenInApprovalNeeded = bool(uint8(callbackData[0]) == 1);
uint256 ethValue = 0; bool tokenOutApprovalNeeded = bool(uint8(callbackData[1]) == 1);
bytes calldata tychoCalldata = bytes(callbackData[2:]);
// The TychoRouter will take the input tokens from the filler
if (tokenInApprovalNeeded) {
// Native ETH input is not supported by UniswapX
IERC20(order.input.token).forceApprove(
tychoRouter, type(uint256).max
);
}
// slither-disable-next-line low-level-calls // slither-disable-next-line low-level-calls
(bool success, bytes memory result) = (bool success, bytes memory result) = tychoRouter.call(tychoCalldata);
tychoRouter.call{value: ethValue}(callbackData);
if (!success) { if (!success) {
revert( revert(
@@ -75,10 +92,19 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
); );
} }
if (tokenOutApprovalNeeded) {
// Multiple outputs are possible when taking fees - but token itself should // Multiple outputs are possible when taking fees - but token itself should
// not change. // not change.
IERC20 token = IERC20(order.outputs[0].token); OutputToken memory output = order.outputs[0];
if (output.token != nativeAddress) {
IERC20 token = IERC20(output.token);
token.forceApprove(address(reactor), type(uint256).max); token.forceApprove(address(reactor), type(uint256).max);
} else {
// With native ETH - the filler is responsible for transferring back
// to the reactor.
Address.sendValue(payable(address(reactor)), output.amount);
}
}
} }
/** /**

View File

@@ -23,7 +23,7 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
function fillerSetup() public { function fillerSetup() public {
vm.startPrank(ADMIN); vm.startPrank(ADMIN);
filler = new UniswapXFiller(tychoRouterAddr, REACTOR); filler = new UniswapXFiller(tychoRouterAddr, REACTOR, address(0));
fillerAddr = address(filler); fillerAddr = address(filler);
filler.grantRole(keccak256("EXECUTOR_ROLE"), EXECUTOR); filler.grantRole(keccak256("EXECUTOR_ROLE"), EXECUTOR);
vm.stopPrank(); vm.stopPrank();
@@ -31,12 +31,12 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
function testTychoAddressZeroTychoRouter() public { function testTychoAddressZeroTychoRouter() public {
vm.expectRevert(UniswapXFiller__AddressZero.selector); vm.expectRevert(UniswapXFiller__AddressZero.selector);
filler = new UniswapXFiller(address(0), REACTOR); filler = new UniswapXFiller(address(0), REACTOR, address(0));
} }
function testTychoAddressZeroReactor() public { function testTychoAddressZeroReactor() public {
vm.expectRevert(UniswapXFiller__AddressZero.selector); vm.expectRevert(UniswapXFiller__AddressZero.selector);
filler = new UniswapXFiller(tychoRouterAddr, address(0)); filler = new UniswapXFiller(tychoRouterAddr, address(0), address(0));
} }
function testCallback() public { function testCallback() public {
@@ -55,7 +55,7 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
bytes memory swap = bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData); encodeSingleSwap(address(usv2Executor), protocolData);
bytes memory callbackData = abi.encodeWithSelector( bytes memory tychoRouterData = abi.encodeWithSelector(
tychoRouter.singleSwap.selector, tychoRouter.singleSwap.selector,
amountIn, amountIn,
WETH_ADDR, WETH_ADDR,
@@ -68,6 +68,9 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
swap swap
); );
bytes memory callbackData =
abi.encodePacked(true, true, tychoRouterData);
deal(WETH_ADDR, address(filler), amountIn); deal(WETH_ADDR, address(filler), amountIn);
vm.startPrank(address(filler)); vm.startPrank(address(filler));
IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn); IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn);
@@ -76,11 +79,11 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
OutputToken[] memory outputs = new OutputToken[](1); OutputToken[] memory outputs = new OutputToken[](1);
outputs[0] = OutputToken({ outputs[0] = OutputToken({
token: address(DAI_ADDR), token: address(DAI_ADDR),
// Irrelevant fields - we only need the token address for approval
amount: 1847751195973566072891, amount: 1847751195973566072891,
recipient: BOB recipient: BOB
}); });
// All irrelevant fields for this test - we only need the output token address // Mostly irrelevant fields for this test - we only need token input and outputs
// info for the sake of testing.
orders[0] = ResolvedOrder({ orders[0] = ResolvedOrder({
info: OrderInfo({ info: OrderInfo({
reactor: address(0), reactor: address(0),
@@ -90,7 +93,12 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
additionalValidationContract: address(0), additionalValidationContract: address(0),
additionalValidationData: "" additionalValidationData: ""
}), }),
input: InputToken({token: address(WETH_ADDR), amount: 0, maxAmount: 0}), input: InputToken({
token: address(WETH_ADDR),
amount: amountIn,
// We need the proper maxAmount for our approval to work
maxAmount: amountIn
}),
outputs: outputs, outputs: outputs,
sig: "", sig: "",
hash: "" hash: ""
@@ -133,7 +141,7 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
bytes memory swap = bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData); encodeSingleSwap(address(usv2Executor), protocolData);
bytes memory callbackData = abi.encodeWithSelector( bytes memory tychoRouterData = abi.encodeWithSelector(
tychoRouter.singleSwap.selector, tychoRouter.singleSwap.selector,
amountIn, amountIn,
WBTC_ADDR, WBTC_ADDR,
@@ -146,6 +154,12 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
swap swap
); );
bytes memory callbackData = abi.encodePacked(
true, // tokenIn approval needed
true, // tokenOut approval needed
tychoRouterData
);
vm.startPrank(address(filler)); vm.startPrank(address(filler));
IERC20(WBTC_ADDR).approve(tychoRouterAddr, amountIn); IERC20(WBTC_ADDR).approve(tychoRouterAddr, amountIn);
vm.stopPrank(); vm.stopPrank();