Merge branch 'main' into router/dc/ENG-4611-update-executors-scripts-safe
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,3 +1,16 @@
|
|||||||
|
## [0.104.0](https://github.com/propeller-heads/tycho-execution/compare/0.103.0...0.104.0) (2025-07-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Take reactor address as input to UniswapXFiller ([00f22d6](https://github.com/propeller-heads/tycho-execution/commit/00f22d62c1f978f27eb5a922bc7b36af2a0b806b))
|
||||||
|
* UniswapXFiller skeleton ([ce1fe1d](https://github.com/propeller-heads/tycho-execution/commit/ce1fe1dd94a4cc68e4695902c80ee30d30d7fd5e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Make slither happy ([d61469e](https://github.com/propeller-heads/tycho-execution/commit/d61469ea67f2c8d0f28e5185da4099260c2b69ea))
|
||||||
|
|
||||||
## [0.103.0](https://github.com/propeller-heads/tycho-execution/compare/0.102.0...0.103.0) (2025-07-07)
|
## [0.103.0](https://github.com/propeller-heads/tycho-execution/compare/0.102.0...0.103.0) (2025-07-07)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4658,7 +4658,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tycho-execution"
|
name = "tycho-execution"
|
||||||
version = "0.103.0"
|
version = "0.104.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy",
|
"alloy",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tycho-execution"
|
name = "tycho-execution"
|
||||||
version = "0.103.0"
|
version = "0.104.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors."
|
description = "Provides tools for encoding and executing swaps against Tycho router and protocol executors."
|
||||||
repository = "https://github.com/propeller-heads/tycho-execution"
|
repository = "https://github.com/propeller-heads/tycho-execution"
|
||||||
|
|||||||
31
foundry/src/uniswap_x/IReactor.sol
Normal file
31
foundry/src/uniswap_x/IReactor.sol
Normal 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;
|
||||||
|
}
|
||||||
16
foundry/src/uniswap_x/IReactorCallback.sol
Normal file
16
foundry/src/uniswap_x/IReactorCallback.sol
Normal 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;
|
||||||
|
}
|
||||||
114
foundry/src/uniswap_x/IStructs.sol
Normal file
114
foundry/src/uniswap_x/IStructs.sol
Normal 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;
|
||||||
|
}
|
||||||
109
foundry/src/uniswap_x/UniswapXFiller.sol
Normal file
109
foundry/src/uniswap_x/UniswapXFiller.sol
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,7 +96,6 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vm.startPrank(FUND_RESCUER);
|
vm.startPrank(FUND_RESCUER);
|
||||||
|
|
||||||
tychoRouter.withdraw(tokens, FUND_RESCUER);
|
tychoRouter.withdraw(tokens, FUND_RESCUER);
|
||||||
|
|
||||||
// Check balances after withdrawing
|
// Check balances after withdrawing
|
||||||
|
|||||||
127
foundry/test/uniswap_x/UniswapXFiller.t.sol
Normal file
127
foundry/test/uniswap_x/UniswapXFiller.t.sol
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user