Skip to content

Commit

Permalink
feat: add universal router executor (#316)
Browse files Browse the repository at this point in the history
* add universal router executor

* Add tests

* fix import

* nit comments
fix import

* nit: style
  • Loading branch information
zhongeric authored Feb 11, 2025
1 parent 5016e3d commit 1fd0d1c
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 0 deletions.
32 changes: 32 additions & 0 deletions script/DeployUniversalRouterExecutor.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.13;

import "forge-std/console2.sol";
import "forge-std/Script.sol";
import {UniversalRouterExecutor} from "../src/sample-executors/UniversalRouterExecutor.sol";
import {IReactor} from "../src/interfaces/IReactor.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";

contract DeployUniversalRouterExecutor is Script {
function setUp() public {}

function run() public returns (UniversalRouterExecutor executor) {
uint256 privateKey = vm.envUint("FOUNDRY_PRIVATE_KEY");
IReactor reactor = IReactor(vm.envAddress("FOUNDRY_UNIVERSALROUTEREXECUTOR_DEPLOY_REACTOR"));
// can encode with cast abi-encode "foo(address[])" "[addr1, addr2, ...]"
bytes memory encodedAddresses =
vm.envBytes("FOUNDRY_UNIVERSALROUTEREXECUTOR_DEPLOY_WHITELISTED_CALLERS_ENCODED");
address owner = vm.envAddress("FOUNDRY_UNIVERSALROUTEREXECUTOR_DEPLOY_OWNER");
address universalRouter = vm.envAddress("FOUNDRY_UNIVERSALROUTEREXECUTOR_DEPLOY_UNIVERSALROUTER");
IPermit2 permit2 = IPermit2(vm.envAddress("FOUNDRY_UNIVERSALROUTEREXECUTOR_DEPLOY_PERMIT2"));

address[] memory decodedAddresses = abi.decode(encodedAddresses, (address[]));

vm.startBroadcast(privateKey);
executor = new UniversalRouterExecutor{salt: 0x00}(decodedAddresses, reactor, owner, universalRouter, permit2);
vm.stopBroadcast();

console2.log("UniversalRouterExecutor", address(executor));

This comment has been minimized.

Copy link
@Alucar08

Alucar08 Feb 21, 2025

console2.log("owner", executor.owner());
}
}
25 changes: 25 additions & 0 deletions src/external/IUniversalRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

interface IUniversalRouter {
/// @notice Thrown when a required command has failed
error ExecutionFailed(uint256 commandIndex, bytes message);

/// @notice Thrown when attempting to send ETH directly to the contract
error ETHNotAccepted();

/// @notice Thrown when executing commands with an expired deadline
error TransactionDeadlinePassed();

/// @notice Thrown when attempting to execute commands and an incorrect number of inputs are provided
error LengthMismatch();

// @notice Thrown when an address that isn't WETH tries to send ETH to the router without calldata
error InvalidEthSender();

/// @notice Executes encoded commands along with provided inputs. Reverts if deadline has expired.
/// @param commands A set of concatenated commands, each 1 byte in length
/// @param inputs An array of byte strings containing abi encoded inputs for each command
/// @param deadline The deadline by which the transaction must be executed
function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable;
}
123 changes: 123 additions & 0 deletions src/sample-executors/UniversalRouterExecutor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {Owned} from "solmate/src/auth/Owned.sol";
import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol";
import {ERC20} from "solmate/src/tokens/ERC20.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import {IReactorCallback} from "../interfaces/IReactorCallback.sol";
import {IReactor} from "../interfaces/IReactor.sol";
import {CurrencyLibrary} from "../lib/CurrencyLibrary.sol";
import {ResolvedOrder, SignedOrder} from "../base/ReactorStructs.sol";

/// @notice A fill contract that uses UniversalRouter to execute trades
contract UniversalRouterExecutor is IReactorCallback, Owned {
using SafeTransferLib for ERC20;
using CurrencyLibrary for address;

/// @notice thrown if reactorCallback is called with a non-whitelisted filler
error CallerNotWhitelisted();
/// @notice thrown if reactorCallback is called by an address other than the reactor
error MsgSenderNotReactor();

address public immutable universalRouter;
mapping(address => bool) whitelistedCallers;
IReactor public immutable reactor;
IPermit2 public immutable permit2;

modifier onlyWhitelistedCaller() {
if (whitelistedCallers[msg.sender] == false) {
revert CallerNotWhitelisted();
}
_;
}

modifier onlyReactor() {
if (msg.sender != address(reactor)) {
revert MsgSenderNotReactor();
}
_;
}

constructor(
address[] memory _whitelistedCallers,
IReactor _reactor,
address _owner,
address _universalRouter,
IPermit2 _permit2
) Owned(_owner) {
for (uint256 i = 0; i < _whitelistedCallers.length; i++) {
whitelistedCallers[_whitelistedCallers[i]] = true;
}
reactor = _reactor;
universalRouter = _universalRouter;
permit2 = _permit2;
}

/// @notice assume that we already have all output tokens
function execute(SignedOrder calldata order, bytes calldata callbackData) external onlyWhitelistedCaller {
reactor.executeWithCallback(order, callbackData);
}

/// @notice assume that we already have all output tokens
function executeBatch(SignedOrder[] calldata orders, bytes calldata callbackData) external onlyWhitelistedCaller {
reactor.executeBatchWithCallback(orders, callbackData);
}

/// @notice fill UniswapX orders using UniversalRouter
/// @param callbackData It has the below encoded:
/// address[] memory tokensToApproveForUniversalRouter: Max approve these tokens to permit2 and universalRouter
/// address[] memory tokensToApproveForReactor: Max approve these tokens to reactor
/// bytes memory data: execution data
function reactorCallback(ResolvedOrder[] calldata, bytes calldata callbackData) external onlyReactor {
(
address[] memory tokensToApproveForUniversalRouter,
address[] memory tokensToApproveForReactor,
bytes memory data
) = abi.decode(callbackData, (address[], address[], bytes));

unchecked {
for (uint256 i = 0; i < tokensToApproveForUniversalRouter.length; i++) {
// Max approve token to permit2
ERC20(tokensToApproveForUniversalRouter[i]).safeApprove(address(permit2), type(uint256).max);
// Max approve token to universalRouter via permit2
permit2.approve(
tokensToApproveForUniversalRouter[i], address(universalRouter), type(uint160).max, type(uint48).max
);
}

for (uint256 i = 0; i < tokensToApproveForReactor.length; i++) {
ERC20(tokensToApproveForReactor[i]).safeApprove(address(reactor), type(uint256).max);
}
}

(bool success, bytes memory returnData) = universalRouter.call(data);
if (!success) {
assembly {
revert(add(returnData, 32), mload(returnData))
}
}

// transfer any native balance to the reactor
// it will refund any excess
if (address(this).balance > 0) {
CurrencyLibrary.transferNative(address(reactor), address(this).balance);
}
}

/// @notice Transfer all ETH in this contract to the recipient. Can only be called by owner.
/// @param recipient The recipient of the ETH
function withdrawETH(address recipient) external onlyOwner {
SafeTransferLib.safeTransferETH(recipient, address(this).balance);
}

/// @notice Transfer the entire balance of an ERC20 token in this contract to a recipient. Can only be called by owner.
/// @param token The ERC20 token to withdraw
/// @param to The recipient of the tokens
function withdrawERC20(ERC20 token, address to) external onlyOwner {
token.safeTransfer(to, token.balanceOf(address(this)));
}

/// @notice Necessary for this contract to receive ETH
receive() external payable {}
}
156 changes: 156 additions & 0 deletions test/integration/UniversalRouterExecutorIntegration.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol";
import {Test} from "forge-std/Test.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import {ERC20} from "solmate/src/tokens/ERC20.sol";
import {UniversalRouterExecutor} from "../../src/sample-executors/UniversalRouterExecutor.sol";
import {InputToken, OrderInfo, SignedOrder} from "../../src/base/ReactorStructs.sol";
import {OrderInfoBuilder} from "../util/OrderInfoBuilder.sol";
import {DutchOrderReactor, DutchOrder, DutchInput, DutchOutput} from "../../src/reactors/DutchOrderReactor.sol";
import {OutputsBuilder} from "../util/OutputsBuilder.sol";
import {PermitSignature} from "../util/PermitSignature.sol";
import {IReactor} from "../../src/interfaces/IReactor.sol";
import {IUniversalRouter} from "../../src/external/IUniversalRouter.sol";

contract UniversalRouterExecutorIntegrationTest is Test, PermitSignature {
using OrderInfoBuilder for OrderInfo;
using SafeTransferLib for ERC20;

ERC20 constant USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
ERC20 constant USDT = ERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);

uint256 constant USDC_ONE = 1e6;

// UniversalRouter with V4 support
IUniversalRouter universalRouter = IUniversalRouter(0x66a9893cC07D91D95644AEDD05D03f95e1dBA8Af);
IPermit2 permit2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3);

address swapper;
uint256 swapperPrivateKey;
address whitelistedCaller;
address owner;
UniversalRouterExecutor universalRouterExecutor;
DutchOrderReactor reactor;

// UniversalRouter commands
uint256 constant V3_SWAP_EXACT_IN = 0x00;

function setUp() public {
swapperPrivateKey = 0xbeef;
swapper = vm.addr(swapperPrivateKey);
vm.label(swapper, "swapper");
whitelistedCaller = makeAddr("whitelistedCaller");
owner = makeAddr("owner");
// 02-10-2025
vm.createSelectFork(vm.envString("FOUNDRY_RPC_URL"), 21818802);
reactor = new DutchOrderReactor(permit2, address(0));
address[] memory whitelistedCallers = new address[](1);
whitelistedCallers[0] = whitelistedCaller;
universalRouterExecutor = new UniversalRouterExecutor(
whitelistedCallers, IReactor(address(reactor)), owner, address(universalRouter), permit2
);

vm.prank(swapper);
USDC.approve(address(permit2), type(uint256).max);

deal(address(USDC), swapper, 100 * 1e6);
}

function baseTest(DutchOrder memory order) internal {
_baseTest(order, false, "");
}

function _baseTest(DutchOrder memory order, bool expectRevert, bytes memory revertData) internal {
address[] memory tokensToApproveForPermit2AndUniversalRouter = new address[](1);
tokensToApproveForPermit2AndUniversalRouter[0] = address(USDC);

address[] memory tokensToApproveForReactor = new address[](1);
tokensToApproveForReactor[0] = address(USDT);

bytes memory commands = hex"00";
bytes[] memory inputs = new bytes[](1);
// V3 swap USDC -> USDT, with recipient as universalRouterExecutor
inputs[0] =
hex"0000000000000000000000002e234DAe75C793f67A35089C9d99245E1C58470b0000000000000000000000000000000000000000000000000000000000989680000000000000000000000000000000000000000000000000000000000090972200000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002ba0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000064dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000";

bytes memory data =
abi.encodeWithSelector(IUniversalRouter.execute.selector, commands, inputs, uint256(block.timestamp + 1000));

vm.prank(whitelistedCaller);
if (expectRevert) {
vm.expectRevert(revertData);
}
universalRouterExecutor.execute(
SignedOrder(abi.encode(order), signOrder(swapperPrivateKey, address(permit2), order)),
abi.encode(tokensToApproveForPermit2AndUniversalRouter, tokensToApproveForReactor, data)
);
}

function test_universalRouterExecutor() public {
DutchOrder memory order = DutchOrder({
info: OrderInfoBuilder.init(address(reactor)).withSwapper(swapper).withDeadline(block.timestamp + 100),
decayStartTime: block.timestamp - 100,
decayEndTime: block.timestamp + 100,
input: DutchInput(USDC, 10 * USDC_ONE, 10 * USDC_ONE),
outputs: OutputsBuilder.singleDutch(address(USDT), 9 * USDC_ONE, 9 * USDC_ONE, address(swapper))
});

address[] memory tokensToApproveForPermit2AndUniversalRouter = new address[](1);
tokensToApproveForPermit2AndUniversalRouter[0] = address(USDC);

address[] memory tokensToApproveForReactor = new address[](1);
tokensToApproveForReactor[0] = address(USDT);

uint256 swapperInputBalanceBefore = USDC.balanceOf(swapper);
uint256 swapperOutputBalanceBefore = USDT.balanceOf(swapper);

baseTest(order);

assertEq(USDC.balanceOf(swapper), swapperInputBalanceBefore - 10 * USDC_ONE);
assertEq(USDT.balanceOf(swapper), swapperOutputBalanceBefore + 9 * USDC_ONE);
// Expect some USDT to be left in the executor from the swap
assertGe(USDT.balanceOf(address(universalRouterExecutor)), 0);
}

function test_universalRouterExecutor_TooLittleReceived() public {
DutchOrder memory order = DutchOrder({
info: OrderInfoBuilder.init(address(reactor)).withSwapper(swapper).withDeadline(block.timestamp + 100),
decayStartTime: block.timestamp - 100,
decayEndTime: block.timestamp + 100,
input: DutchInput(USDC, 10 * USDC_ONE, 10 * USDC_ONE),
// Too much output
outputs: OutputsBuilder.singleDutch(address(USDT), 11 * USDC_ONE, 11 * USDC_ONE, address(swapper))
});

_baseTest(order, true, bytes("TRANSFER_FROM_FAILED"));
}

function test_universalRouterExecutor_onlyOwner() public {
address nonOwner = makeAddr("nonOwner");
address recipient = makeAddr("recipient");
uint256 recipientBalanceBefore = recipient.balance;
uint256 recipientUSDCBalanceBefore = USDC.balanceOf(recipient);

vm.deal(address(universalRouterExecutor), 1 ether);
deal(address(USDC), address(universalRouterExecutor), 100 * USDC_ONE);

vm.prank(nonOwner);
vm.expectRevert("UNAUTHORIZED");
universalRouterExecutor.withdrawETH(recipient);

vm.prank(nonOwner);
vm.expectRevert("UNAUTHORIZED");
universalRouterExecutor.withdrawERC20(USDC, recipient);

vm.prank(owner);
universalRouterExecutor.withdrawETH(recipient);
assertEq(address(recipient).balance, recipientBalanceBefore + 1 ether);

vm.prank(owner);
universalRouterExecutor.withdrawERC20(USDC, recipient);
assertEq(USDC.balanceOf(recipient), recipientUSDCBalanceBefore + 100 * USDC_ONE);
assertEq(USDC.balanceOf(address(universalRouterExecutor)), 0);
}
}

0 comments on commit 1fd0d1c

Please sign in to comment.