feat: (WIP) Handle approvals in UniswapXFiller

Reasons for using regular approvals instead of permit2:
- More simplicity, simpler setup
- Unfortunately we won't be able to "expire" approvals, which is a risk we may be okay with taking - since one advantage of this is that we will be able save on gas by not approving the reactor every single time (is this our intention?)

TODO:
- Do we really need input tokens to pass through our router?
- Handle native ETH (double check how this is transferred in the reactor)
This commit is contained in:
TAMARA LIPOWSKI
2025-07-08 18:15:26 -04:00
parent 808202df8c
commit e243667f9e
2 changed files with 49 additions and 10 deletions

View File

@@ -9,6 +9,7 @@ import {
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import {TychoRouter} from "../TychoRouter.sol";
error UniswapXFiller__AddressZero();
error UniswapXFiller__BatchExecutionNotSupported();
@@ -58,12 +59,34 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
ResolvedOrder memory order = resolvedOrders[0];
(
bool tokenInApprovalNeeded,
bool tokenOutApprovalNeeded,
bytes memory tychoCalldata
) = abi.decode(callbackData, (bool, bool, bytes));
// The TychoRouter will take the input tokens from the filler
if (tokenInApprovalNeeded) {
// TODO only if ERC20
IERC20(order.input.token).forceApprove(
tychoRouter, order.input.maxAmount
);
} else {
// The filler will manually transfer into the TychoRouter
// Note: We are using the balance of our filler instead of the order
// input amount to avoid having to do a decay calculation in the filler.
IERC20 inputToken = IERC20(order.input.token);
inputToken.transfer(
tychoRouter, inputToken.balanceOf(address(this))
);
}
// TODO properly handle native in and out tokens
uint256 ethValue = 0;
// slither-disable-next-line low-level-calls
(bool success, bytes memory result) =
tychoRouter.call{value: ethValue}(callbackData);
tychoRouter.call{value: ethValue}(tychoCalldata);
if (!success) {
revert(
@@ -75,10 +98,13 @@ contract UniswapXFiller is AccessControl, IReactorCallback {
);
}
// Multiple outputs are possible when taking fees - but token itself should
// not change.
IERC20 token = IERC20(order.outputs[0].token);
token.forceApprove(address(reactor), type(uint256).max);
if (tokenOutApprovalNeeded) {
// Multiple outputs are possible when taking fees - but token itself should
// not change.
// TODO only if ERC20
IERC20 token = IERC20(order.outputs[0].token);
token.forceApprove(address(reactor), type(uint256).max);
}
}
/**

View File

@@ -55,7 +55,7 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
bytes memory callbackData = abi.encodeWithSelector(
bytes memory tychoRouterData = abi.encodeWithSelector(
tychoRouter.singleSwap.selector,
amountIn,
WETH_ADDR,
@@ -68,6 +68,8 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
swap
);
bytes memory callbackData = abi.encode(true, true, tychoRouterData);
deal(WETH_ADDR, address(filler), amountIn);
vm.startPrank(address(filler));
IERC20(WETH_ADDR).approve(tychoRouterAddr, amountIn);
@@ -76,11 +78,11 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
OutputToken[] memory outputs = new OutputToken[](1);
outputs[0] = OutputToken({
token: address(DAI_ADDR),
// Irrelevant fields - we only need the token address for approval
amount: 1847751195973566072891,
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({
info: OrderInfo({
reactor: address(0),
@@ -90,7 +92,12 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
additionalValidationContract: address(0),
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,
sig: "",
hash: ""
@@ -133,7 +140,7 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
bytes memory callbackData = abi.encodeWithSelector(
bytes memory tychoRouterData = abi.encodeWithSelector(
tychoRouter.singleSwap.selector,
amountIn,
WBTC_ADDR,
@@ -146,6 +153,12 @@ contract UniswapXFillerTest is Test, TychoRouterTestSetup {
swap
);
bytes memory callbackData = abi.encode(
true, // tokenIn approval needed
true, // tokenOut approval needed
tychoRouterData
);
vm.startPrank(address(filler));
IERC20(WBTC_ADDR).approve(tychoRouterAddr, amountIn);
vm.stopPrank();