Merge pull request #234 from propeller-heads/usx/tnl/ENG-4666-filler-skeleton

feat: UniswapXFiller skeleton
This commit is contained in:
Tamara
2025-07-08 10:29:27 -04:00
committed by GitHub
6 changed files with 397 additions and 1 deletions

View File

@@ -0,0 +1,31 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import {SignedOrder} from "./IStructs.sol";
import {IReactorCallback} from "./IReactorCallback.sol";
/// @notice Interface for order execution reactors
interface IReactor {
/// @notice Execute a single order
/// @param order The order definition and valid signature to execute
function execute(SignedOrder calldata order) external payable;
/// @notice Execute a single order using the given callback data
/// @param order The order definition and valid signature to execute
function executeWithCallback(
SignedOrder calldata order,
bytes calldata callbackData
) external payable;
/// @notice Execute the given orders at once
/// @param orders The order definitions and valid signatures to execute
function executeBatch(SignedOrder[] calldata orders) external payable;
/// @notice Execute the given orders at once using a callback with the given callback data
/// @param orders The order definitions and valid signatures to execute
/// @param callbackData The callbackData to pass to the callback
function executeBatchWithCallback(
SignedOrder[] calldata orders,
bytes calldata callbackData
) external payable;
}

View File

@@ -0,0 +1,16 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "./IStructs.sol";
/// @notice Callback for executing orders through a reactor.
interface IReactorCallback {
/// @notice Called by the reactor during the execution of an order
/// @param resolvedOrders Has inputs and outputs
/// @param fillData The fillData specified for an order execution
/// @dev Must have approved each token and amount in outputs to the msg.sender
function reactorCallback(
ResolvedOrder[] memory resolvedOrders,
bytes memory fillData
) external;
}

View File

@@ -0,0 +1,114 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
/// @dev external struct including a generic encoded order and swapper signature
/// The order bytes will be parsed and mapped to a ResolvedOrder in the concrete reactor contract
struct SignedOrder {
bytes order;
bytes sig;
}
struct OrderInfo {
// The address of the reactor that this order is targeting
// Note that this must be included in every order so the swapper
// signature commits to the specific reactor that they trust to fill their order properly
address reactor;
// The address of the user which created the order
// Note that this must be included so that order hashes are unique by swapper
address swapper;
// The nonce of the order, allowing for signature replay protection and cancellation
uint256 nonce;
// The timestamp after which this order is no longer valid
uint256 deadline;
// Custom validation contract
address additionalValidationContract;
// Encoded validation params for additionalValidationContract
bytes additionalValidationData;
}
/// @dev tokens that need to be sent from the swapper in order to satisfy an order
struct InputToken {
address token;
uint256 amount;
// Needed for dutch decaying inputs
uint256 maxAmount;
}
/// @dev tokens that need to be received by the recipient in order to satisfy an order
struct OutputToken {
address token;
uint256 amount;
address recipient;
}
/// @dev generic concrete order that specifies exact tokens which need to be sent and received
struct ResolvedOrder {
OrderInfo info;
InputToken input;
OutputToken[] outputs;
bytes sig;
bytes32 hash;
}
struct DutchOutput {
address token;
uint256 startAmount;
uint256 endAmount;
address recipient;
}
struct DutchInput {
address token;
uint256 startAmount;
uint256 endAmount;
}
struct ExclusiveDutchOrder {
OrderInfo info;
uint256 decayStartTime;
uint256 decayEndTime;
address exclusiveFiller;
uint256 exclusivityOverrideBps;
DutchInput input;
DutchOutput[] outputs;
}
struct DutchOrder {
OrderInfo info;
uint256 decayStartTime;
uint256 decayEndTime;
address exclusiveFiller;
uint256 exclusivityOverrideBps;
DutchInput input;
DutchOutput[] outputs;
}
struct CosignerData {
// The time at which the DutchOutputs start decaying
uint256 decayStartTime;
// The time at which price becomes static
uint256 decayEndTime;
// The address who has exclusive rights to the order until decayStartTime
address exclusiveFiller;
// The amount in bps that a non-exclusive filler needs to improve the outputs by to be able to fill the order
uint256 exclusivityOverrideBps;
// The tokens that the swapper will provide when settling the order
uint256 inputAmount;
// The tokens that must be received to satisfy the order
uint256[] outputAmounts;
}
struct V2DutchOrder {
// generic order information
OrderInfo info;
// The address which must cosign the full order
address cosigner;
// The tokens that the swapper will provide when settling the order
DutchInput baseInput;
// The tokens that must be received to satisfy the order
DutchOutput[] baseOutputs;
// signed over by the cosigner
CosignerData cosignerData;
// signature from the cosigner over (orderHash || cosignerData)
bytes cosignature;
}

View File

@@ -0,0 +1,109 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "./IReactor.sol";
import "./IReactorCallback.sol";
import {
SafeERC20,
IERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Address.sol";
error UniswapXFiller__AddressZero();
contract UniswapXFiller is AccessControl, IReactorCallback {
using SafeERC20 for IERC20;
// UniswapX V2DutchOrder Reactor
IReactor public immutable reactor;
address public immutable tychoRouter;
// keccak256("NAME_OF_ROLE") : save gas on deployment
bytes32 public constant REACTOR_ROLE =
0x39dd1d7269516fc1f719706a5e9b05cdcb1644978808b171257d9a8eab55dd57;
bytes32 public constant EXECUTOR_ROLE =
0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63;
event Withdrawal(
address indexed token, uint256 amount, address indexed receiver
);
constructor(address _tychoRouter, address _reactor) {
if (_tychoRouter == address(0)) revert UniswapXFiller__AddressZero();
if (_reactor == address(0)) revert UniswapXFiller__AddressZero();
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(REACTOR_ROLE, address(reactor));
tychoRouter = _tychoRouter;
reactor = IReactor(_reactor);
}
function execute(SignedOrder calldata order, bytes calldata callbackData)
external
onlyRole(EXECUTOR_ROLE)
{
reactor.executeWithCallback(order, callbackData);
}
function reactorCallback(
ResolvedOrder[] calldata resolvedOrders,
bytes calldata callbackData
) external onlyRole(REACTOR_ROLE) {
// TODO
}
/**
* @dev Allows granting roles to multiple accounts in a single call.
*/
function batchGrantRole(bytes32 role, address[] memory accounts)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
for (uint256 i = 0; i < accounts.length; i++) {
_grantRole(role, accounts[i]);
}
}
/**
* @dev Allows withdrawing any ERC20 funds.
*/
function withdraw(IERC20[] memory tokens, address receiver)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
if (receiver == address(0)) revert UniswapXFiller__AddressZero();
for (uint256 i = 0; i < tokens.length; i++) {
// slither-disable-next-line calls-loop
uint256 tokenBalance = tokens[i].balanceOf(address(this));
if (tokenBalance > 0) {
emit Withdrawal(address(tokens[i]), tokenBalance, receiver);
tokens[i].safeTransfer(receiver, tokenBalance);
}
}
}
/**
* @dev Allows withdrawing any NATIVE funds.
*/
function withdrawNative(address receiver)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
if (receiver == address(0)) revert UniswapXFiller__AddressZero();
uint256 amount = address(this).balance;
if (amount > 0) {
emit Withdrawal(address(0), amount, receiver);
Address.sendValue(payable(receiver), amount);
}
}
/**
* @dev Allows this contract to receive native token with empty msg.data from contracts
*/
receive() external payable {
require(msg.sender.code.length != 0);
}
}

View File

@@ -96,7 +96,6 @@ contract TychoRouterTest is TychoRouterTestSetup {
}
vm.startPrank(FUND_RESCUER);
tychoRouter.withdraw(tokens, FUND_RESCUER);
// Check balances after withdrawing

View File

@@ -0,0 +1,127 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.26;
import "forge-std/Test.sol";
import "@src/uniswap_x/UniswapXFiller.sol";
import "../TychoRouterTestSetup.sol";
contract UniswapXFillerTest is Test, TychoRouterTestSetup {
address EXECUTOR = makeAddr("executor");
address REACTOR = address(0x00000011F84B9aa48e5f8aA8B9897600006289Be);
UniswapXFiller filler;
address fillerAddr;
event CallbackVerifierSet(address indexed callbackVerifier);
event Withdrawal(
address indexed token, uint256 amount, address indexed receiver
);
function fillerSetup() public {
vm.startPrank(ADMIN);
filler = new UniswapXFiller(tychoRouterAddr, REACTOR);
fillerAddr = address(filler);
filler.grantRole(keccak256("EXECUTOR_ROLE"), EXECUTOR);
vm.stopPrank();
}
function testTychoAddressZeroTychoRouter() public {
vm.expectRevert(UniswapXFiller__AddressZero.selector);
filler = new UniswapXFiller(address(0), REACTOR);
}
function testTychoAddressZeroReactor() public {
vm.expectRevert(UniswapXFiller__AddressZero.selector);
filler = new UniswapXFiller(tychoRouterAddr, address(0));
}
function testWithdrawNative() public {
fillerSetup();
vm.startPrank(ADMIN);
// Send 100 ether to filler
assertEq(fillerAddr.balance, 0);
assertEq(ADMIN.balance, 0);
vm.deal(fillerAddr, 100 ether);
vm.expectEmit();
emit Withdrawal(address(0), 100 ether, ADMIN);
filler.withdrawNative(ADMIN);
assertEq(fillerAddr.balance, 0);
assertEq(ADMIN.balance, 100 ether);
vm.stopPrank();
}
function testWithdrawNativeAddressZero() public {
fillerSetup();
vm.deal(fillerAddr, 100 ether);
vm.startPrank(ADMIN);
vm.expectRevert(UniswapXFiller__AddressZero.selector);
filler.withdrawNative(address(0));
vm.stopPrank();
}
function testWithdrawNativeMissingRole() public {
fillerSetup();
vm.deal(fillerAddr, 100 ether);
// Not role ADMIN
vm.startPrank(BOB);
vm.expectRevert();
filler.withdrawNative(ADMIN);
vm.stopPrank();
}
function testWithdrawERC20Tokens() public {
fillerSetup();
IERC20[] memory tokens = new IERC20[](2);
tokens[0] = IERC20(WETH_ADDR);
tokens[1] = IERC20(USDC_ADDR);
for (uint256 i = 0; i < tokens.length; i++) {
deal(address(tokens[i]), fillerAddr, 100 ether);
}
vm.startPrank(ADMIN);
filler.withdraw(tokens, ADMIN);
// Check balances after withdrawing
for (uint256 i = 0; i < tokens.length; i++) {
// slither-disable-next-line calls-loop
assertEq(tokens[i].balanceOf(fillerAddr), 0);
// slither-disable-next-line calls-loop
assertEq(tokens[i].balanceOf(ADMIN), 100 ether);
}
vm.stopPrank();
}
function testWithdrawERC20TokensAddressZero() public {
fillerSetup();
IERC20[] memory tokens = new IERC20[](2);
tokens[0] = IERC20(WETH_ADDR);
tokens[1] = IERC20(USDC_ADDR);
for (uint256 i = 0; i < tokens.length; i++) {
deal(address(tokens[i]), fillerAddr, 100 ether);
}
vm.startPrank(ADMIN);
vm.expectRevert(UniswapXFiller__AddressZero.selector);
filler.withdraw(tokens, address(0));
vm.stopPrank();
}
function testWithdrawERC20TokensAddressMissingRole() public {
fillerSetup();
IERC20[] memory tokens = new IERC20[](2);
tokens[0] = IERC20(WETH_ADDR);
tokens[1] = IERC20(USDC_ADDR);
for (uint256 i = 0; i < tokens.length; i++) {
deal(address(tokens[i]), fillerAddr, 100 ether);
}
// Not role ADMIN
vm.startPrank(BOB);
vm.expectRevert();
filler.withdraw(tokens, ADMIN);
vm.stopPrank();
}
}