Merge branch 'refs/heads/main' into feat/bebop-rfq-encoder-and-executor

# Conflicts:
#	config/executor_addresses.json
#	foundry/scripts/deploy-executors.js
#	foundry/test/TychoRouterSequentialSwap.t.sol
#	foundry/test/assets/calldata.txt
#	src/encoding/models.rs
#	tests/common/mod.rs

Took 21 minutes
This commit is contained in:
Diana Carvalho
2025-08-08 14:40:03 +01:00
54 changed files with 5428 additions and 659 deletions

View File

@@ -44,7 +44,7 @@ module.exports = {
},
tenderly: {
project: "project",
project: "tycho",
username: "tvinagre",
privateVerification: false,
},

View File

@@ -2,15 +2,15 @@
pragma solidity ^0.8.26;
import {IFlashAccountant} from "./IFlashAccountant.sol";
import {EkuboPoolKey} from "../types/poolKey.sol";
import {PoolKey} from "../types/poolKey.sol";
import {SqrtRatio} from "../types/sqrtRatio.sol";
interface ICore is IFlashAccountant {
function swap_611415377(
EkuboPoolKey memory poolKey,
PoolKey memory poolKey,
int128 amount,
bool isToken1,
SqrtRatio sqrtRatioLimit,
uint256 skipAhead
) external payable returns (int128 delta0, int128 delta1);
}
}

View File

@@ -10,7 +10,17 @@ interface IPayer {
}
interface IFlashAccountant {
// Forward the lock from the current locker to the given address
// Any additional calldata is also passed through to the forwardee, with no additional encoding
// In addition, any data returned from IForwardee#forwarded is also returned from this function exactly as is, i.e. with no additional encoding or decoding
// Reverts are also bubbled up
function forward(address to) external;
// Withdraws a token amount from the accountant to the given recipient.
// The contract must be locked, as it tracks the withdrawn amount against the current locker's delta.
function withdraw(address token, address recipient, uint128 amount) external;
function withdraw(
address token,
address recipient,
uint128 amount
) external;
}

View File

@@ -1,12 +1,21 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;
using {extension} for Config global;
// address (20 bytes) | fee (8 bytes) | tickSpacing (4 bytes)
type Config is bytes32;
// Each pool has its own state associated with this key
struct EkuboPoolKey {
struct PoolKey {
address token0;
address token1;
Config config;
}
function extension(Config config) pure returns (address e) {
// slither-disable-next-line assembly
assembly ("memory-safe") {
e := shr(96, config)
}
}

3793
foundry/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,9 @@
},
"dependencies": {
"@nomicfoundation/hardhat-foundry": "^1.1.3",
"ethers": "^5.0.0",
"@safe-global/api-kit": "^1.1.0",
"@safe-global/protocol-kit": "^1.0.1",
"ethers": "^5.8.0",
"prompt-sync": "^4.2.0"
}
}

View File

@@ -53,4 +53,36 @@ For each of the following, you must select one of `tenderly_ethereum`, `tenderly
1. If you set a new executor for the same protocol, you need to remove the old one.
2. Run: `npx hardhat run scripts/remove-executor.js --network NETWORK`
3. There will be a prompt for you to insert the executor address you want to remove.
3. There will be a prompt for you to insert the executor address you want to remove.
### Revoke roles
1. If you wish to revoke a role for a certain address, run: `npx hardhat run scripts/revoke-role.js --network NETWORK`
2. There will be a prompt for you to insert the role hash and the address you want to revoke it for.
### Safe wallet
1. If the wallet that has the role, is a Gnosis Safe, you need to set the `SAFE_ADDRESS` env var.
2. The scripts deploy-executors, remove-executor, set-roles and revoke-role all support this.
1. If `SAFE_ADDRESS` is set, then it will propose a transaction to the safe wallet and later on it needs to be
approved in their UI to execute on chain.
2. If it's not set, it will submit the transaction directly to the chain.
## Deploy Uniswap X filler
The current script deploys an Uniswap X filler and verifies it in the corresponding blockchain explorer.
Make sure to run `unset HISTFILE` in your terminal before setting the private key. This will prevent the private key
from being stored in the shell history.
1. Set the following environment variables:
```
export RPC_URL=<chain-rpc-url>
export PRIVATE_KEY=<deploy-wallet-private-key>
export BLOCKCHAIN_EXPLORER_API_KEY=<blockchain-explorer-api-key>
```
2. Confirm that the variables `tychoRouter`, `uniswapXReactor` and `nativeToken` are correctly set in the script. Make
sure that the Uniswap X Reactor address matches the reactor you are targeting.
3. Run `npx hardhat run scripts/deploy-uniswap-x-filler.js --network NETWORK`.

View File

@@ -57,10 +57,11 @@ const executors_to_deploy = {
},
// Args: Permit2
{exchange: "BalancerV2Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]},
// Args: Ekubo core contract, Permit2
// Args: Ekubo core contract, mev resist, Permit2
{
exchange: "EkuboExecutor", args: [
"0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444",
"0x553a2EFc570c9e104942cEC6aC1c18118e54C091",
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
]
},
@@ -78,6 +79,8 @@ const executors_to_deploy = {
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
]
},
// Args: Permit2
{exchange: "BalancerV3Executor", args: ["0x000000000022D473030F116dDEE9F6B43aC78BA3"]},
// Args: Bebop Settlement contract, Permit2
{
exchange: "BebopExecutor",

View File

@@ -0,0 +1,63 @@
require('dotenv').config();
const {ethers} = require("hardhat");
const hre = require("hardhat");
const path = require("path");
const fs = require("fs");
async function main() {
const network = hre.network.name;
let uniswapXReactor;
let nativeToken;
if (network === "ethereum") {
uniswapXReactor = "0x00000011F84B9aa48e5f8aA8B9897600006289Be";
nativeToken = "0x0000000000000000000000000000000000000000";
} else if (network === "base") {
uniswapXReactor = "0x000000001Ec5656dcdB24D90DFa42742738De729";
nativeToken = "0x0000000000000000000000000000000000000000";
} else if (network === "unichain") {
uniswapXReactor = "0x00000006021a6Bce796be7ba509BBBA71e956e37";
nativeToken = "0x0000000000000000000000000000000000000000";
} else {
throw new Error(`Unsupported network: ${network}`);
}
const routerAddressesFilePath = path.join(__dirname, "../../config/router_addresses.json");
const tychoRouter = JSON.parse(fs.readFileSync(routerAddressesFilePath, "utf8"))[network];
console.log(`Deploying Uniswap X filler to ${network} with:`);
console.log(`- Tycho router: ${tychoRouter}`);
console.log(`- Uniswap X reactor: ${uniswapXReactor}`);
console.log(`- Native token: ${nativeToken}`);
const [deployer] = await ethers.getSigners();
console.log(`Deploying with account: ${deployer.address}`);
console.log(`Account balance: ${ethers.utils.formatEther(await deployer.getBalance())} ETH`);
const UniswapXFiller = await ethers.getContractFactory("UniswapXFiller");
const filler = await UniswapXFiller.deploy(tychoRouter, uniswapXReactor, nativeToken);
await filler.deployed();
console.log(`Uniswap X Filler deployed to: ${filler.address}`);
console.log("Waiting for 1 minute before verifying the contract on the blockchain explorer...");
await new Promise(resolve => setTimeout(resolve, 60000));
// Verify on Etherscan
try {
await hre.run("verify:verify", {
address: filler.address,
constructorArguments: [tychoRouter, uniswapXReactor, nativeToken],
});
console.log(`Uniswap X filler verified successfully on blockchain explorer!`);
} catch (error) {
console.error(`Error during blockchain explorer verification:`, error);
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error("Deployment failed:", error);
process.exit(1);
});

View File

@@ -1,16 +1,22 @@
require('dotenv').config();
const {ethers} = require("hardhat");
const hre = require("hardhat");
const {proposeOrSendTransaction} = require("./utils");
const prompt = require('prompt-sync')();
async function main() {
const network = hre.network.name;
const routerAddress = process.env.ROUTER_ADDRESS;
console.log(`Removing executors on TychoRouter at ${routerAddress} on ${network}`);
const safeAddress = process.env.SAFE_ADDRESS;
if (!routerAddress) {
throw new Error("Missing ROUTER_ADDRESS");
}
const [deployer] = await ethers.getSigners();
console.log(`Removing executors with account: ${deployer.address}`);
console.log(`Account balance: ${ethers.utils.formatEther(await deployer.getBalance())} ETH`);
console.log(`Removing executor on TychoRouter at ${routerAddress} on ${network}`);
const [signer] = await ethers.getSigners();
console.log(`Removing executors with account: ${signer.address}`);
console.log(`Account balance: ${ethers.utils.formatEther(await signer.getBalance())} ETH`);
const TychoRouter = await ethers.getContractFactory("TychoRouter");
const router = TychoRouter.attach(routerAddress);
@@ -22,12 +28,15 @@ async function main() {
process.exit(1);
}
// Remove executor
const tx = await router.removeExecutor(executorAddress, {
const txData = {
to: router.address,
data: router.interface.encodeFunctionData("removeExecutor", [executorAddress]),
value: "0",
gasLimit: 50000
});
await tx.wait(); // Wait for the transaction to be mined
console.log(`Executor removed at transaction: ${tx.hash}`);
};
const txHash = await proposeOrSendTransaction(safeAddress, txData, signer, "removeExecutor");
console.log(`TX hash: ${txHash}`);
}
main()

View File

@@ -0,0 +1,51 @@
require('dotenv').config();
const {ethers} = require("hardhat");
const path = require('path');
const fs = require('fs');
const hre = require("hardhat");
const {proposeOrSendTransaction} = require("./utils");
const prompt = require('prompt-sync')();
async function main() {
const network = hre.network.name;
const routerAddress = process.env.ROUTER_ADDRESS;
const safeAddress = process.env.SAFE_ADDRESS;
if (!routerAddress) {
throw new Error("Missing ROUTER_ADDRESS");
}
console.log(`Revoking role on TychoRouter at ${routerAddress} on ${network}`);
const [signer] = await ethers.getSigners();
console.log(`Setting roles with account: ${signer.address}`);
console.log(`Account balance: ${ethers.utils.formatEther(await signer.getBalance())} ETH`);
const TychoRouter = await ethers.getContractFactory("TychoRouter");
const router = TychoRouter.attach(routerAddress);
const roleHash = prompt("Enter role hash to be removed: ");
const address = prompt("Enter the address to remove: ");
if (!roleHash || !address) {
console.error("Please provide the executorAddress as an argument.");
process.exit(1);
}
console.log(`Revoking ${roleHash} to the following address:`, address);
const txData = {
to: router.address,
data: router.interface.encodeFunctionData("revokeRole", [roleHash, address]),
value: "0",
};
const txHash = await proposeOrSendTransaction(safeAddress, txData, signer, "revokeRole");
console.log(`TX hash: ${txHash}`);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error("Error setting roles:", error);
process.exit(1);
});

View File

@@ -1,44 +1,50 @@
{
"ethereum": {
"EXECUTOR_SETTER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0x06e580B872a37402764f909FCcAb0Eb5bb38fe23"
],
"PAUSER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0xB279A562C726F9F3011c1945c9c23Fe1FB631B59",
"0xAC3649A6DFBBB230632604f2fc43773977ec6E67"
],
"UNPAUSER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0xB279A562C726F9F3011c1945c9c23Fe1FB631B59",
"0xAC3649A6DFBBB230632604f2fc43773977ec6E67"
],
"FUND_RESCUER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0xF621770E96bcf1335150faecf77D757faf7ca4A9"
]
},
"base": {
"EXECUTOR_SETTER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0x06e580B872a37402764f909FCcAb0Eb5bb38fe23"
],
"PAUSER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0xB279A562C726F9F3011c1945c9c23Fe1FB631B59",
"0xAC3649A6DFBBB230632604f2fc43773977ec6E67"
],
"UNPAUSER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0xB279A562C726F9F3011c1945c9c23Fe1FB631B59",
"0xAC3649A6DFBBB230632604f2fc43773977ec6E67"
],
"FUND_RESCUER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0xF621770E96bcf1335150faecf77D757faf7ca4A9"
]
},
"unichain": {
"EXECUTOR_SETTER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0x06e580B872a37402764f909FCcAb0Eb5bb38fe23"
],
"PAUSER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0xB279A562C726F9F3011c1945c9c23Fe1FB631B59",
"0xAC3649A6DFBBB230632604f2fc43773977ec6E67"
],
"UNPAUSER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0xB279A562C726F9F3011c1945c9c23Fe1FB631B59",
"0xAC3649A6DFBBB230632604f2fc43773977ec6E67"
],
"FUND_RESCUER_ROLE": [
"0x58Dc7Bf9eD1f4890A7505D5bE4E4252978eAF655"
"0xF621770E96bcf1335150faecf77D757faf7ca4A9"
]
}
}

View File

@@ -1,18 +1,25 @@
require('dotenv').config();
const {ethers} = require("hardhat");
const path = require('path');
const fs = require('fs');
const hre = require("hardhat");
const path = require('path');
const {proposeOrSendTransaction} = require("./utils");
const prompt = require('prompt-sync')();
async function main() {
const network = hre.network.name;
const routerAddress = process.env.ROUTER_ADDRESS;
const safeAddress = process.env.SAFE_ADDRESS;
if (!routerAddress) {
throw new Error("Missing ROUTER_ADDRESS");
}
console.log(`Setting executors on TychoRouter at ${routerAddress} on ${network}`);
const [deployer] = await ethers.getSigners();
console.log(`Setting executors with account: ${deployer.address}`);
console.log(`Account balance: ${ethers.utils.formatEther(await deployer.getBalance())} ETH`);
const [signer] = await ethers.getSigners();
const balance = await signer.getBalance();
console.log(`Using signer: ${signer.address}`);
console.log(`Account balance: ${ethers.utils.formatEther(balance)} ETH`);
const TychoRouter = await ethers.getContractFactory("TychoRouter");
const router = TychoRouter.attach(routerAddress);
@@ -48,13 +55,16 @@ async function main() {
return;
}
// Set executors
const executorAddresses = executorsToSet.map(executor => executor.executor);
const tx = await router.setExecutors(executorAddresses, {
gasLimit: 300000 // should be around 50k per executor
});
await tx.wait(); // Wait for the transaction to be mined
console.log(`Executors set at transaction: ${tx.hash}`);
const executorAddresses = executorsToSet.map(({executor}) => executor);
const txData = {
to: router.address,
data: router.interface.encodeFunctionData("setExecutors", [executorAddresses]),
value: "0",
gasLimit: 300000
};
const txHash = await proposeOrSendTransaction(safeAddress, txData, signer, "setExecutors");
console.log(`TX hash: ${txHash}`);
}
main()

View File

@@ -3,15 +3,21 @@ const {ethers} = require("hardhat");
const path = require('path');
const fs = require('fs');
const hre = require("hardhat");
const {proposeOrSendTransaction} = require("./utils");
async function main() {
const network = hre.network.name;
const routerAddress = process.env.ROUTER_ADDRESS;
const safeAddress = process.env.SAFE_ADDRESS;
if (!routerAddress) {
throw new Error("Missing ROUTER_ADDRESS");
}
console.log(`Setting roles on TychoRouter at ${routerAddress} on ${network}`);
const [deployer] = await ethers.getSigners();
console.log(`Setting roles with account: ${deployer.address}`);
console.log(`Account balance: ${ethers.utils.formatEther(await deployer.getBalance())} ETH`);
const [signer] = await ethers.getSigners();
console.log(`Setting roles with account: ${signer.address}`);
console.log(`Account balance: ${ethers.utils.formatEther(await signer.getBalance())} ETH`);
const TychoRouter = await ethers.getContractFactory("TychoRouter");
const router = TychoRouter.attach(routerAddress);
@@ -20,7 +26,6 @@ async function main() {
const roles = {
EXECUTOR_SETTER_ROLE: "0x6a1dd52dcad5bd732e45b6af4e7344fa284e2d7d4b23b5b09cb55d36b0685c87",
FEE_SETTER_ROLE: "0xe6ad9a47fbda1dc18de1eb5eeb7d935e5e81b4748f3cfc61e233e64f88182060",
PAUSER_ROLE: "0x65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a",
UNPAUSER_ROLE: "0x427da25fe773164f88948d3e215c94b6554e2ed5e5f203a821c9f2f6131cf75a",
FUND_RESCUER_ROLE: "0x912e45d663a6f4cc1d0491d8f046e06c616f40352565ea1cdb86a0e1aaefa41b"
@@ -31,9 +36,15 @@ async function main() {
const addresses = rolesDict[network][roleName];
if (addresses && addresses.length > 0) {
console.log(`Granting ${roleName} to the following addresses:`, addresses);
const tx = await router.batchGrantRole(roleHash, addresses);
await tx.wait(); // Wait for the transaction to be mined
console.log(`Role ${roleName} granted at transaction: ${tx.hash}`);
const txData = {
to: router.address,
data: router.interface.encodeFunctionData("batchGrantRole", [roleHash, addresses]),
value: "0",
};
const txHash = await proposeOrSendTransaction(safeAddress, txData, signer, "batchGrantRole");
console.log(`Role ${roleName} granted at TX hash: ${txHash}`);
} else {
console.log(`No addresses found for role ${roleName}`);
}

66
foundry/scripts/utils.js Normal file
View File

@@ -0,0 +1,66 @@
const {ethers} = require("hardhat");
const Safe = require('@safe-global/protocol-kit').default;
const {EthersAdapter} = require('@safe-global/protocol-kit');
const {default: SafeApiKit} = require("@safe-global/api-kit");
const txServiceUrls = {
ethereum: "https://safe-transaction-mainnet.safe.global",
base: "https://safe-transaction-base.safe.global",
unichain: "https://safe-transaction-unichain.safe.global",
};
const txServiceUrl = txServiceUrls[hre.network.name];
async function proposeOrSendTransaction(safeAddress, txData, signer, methodName) {
if (safeAddress) {
return proposeTransaction(safeAddress, txData, signer, methodName);
} else {
console.log(`Executing the transaction directly`);
const tx = await signer.sendTransaction(txData);
await tx.wait();
return tx.hash;
}
}
async function proposeTransaction(safeAddress, txData, signer, methodName) {
const signerAddress = await signer.getAddress();
console.log(`Proposing transaction to Safe: ${safeAddress} with account: ${signerAddress}`);
const ethAdapter = new EthersAdapter({
ethers,
signerOrProvider: signer,
});
const safeService = new SafeApiKit({txServiceUrl, ethAdapter});
const safeSdk = await Safe.create({
ethAdapter,
safeAddress,
});
let next_nonce = await safeService.getNextNonce(safeAddress);
const safeTransaction = await safeSdk.createTransaction({
safeTransactionData: {
...txData,
nonce: next_nonce
}
});
const safeTxHash = await safeSdk.getTransactionHash(safeTransaction);
const senderSignature = await safeSdk.signTransactionHash(safeTxHash);
const proposeArgs = {
safeAddress,
safeTransactionData: safeTransaction.data,
safeTxHash,
senderAddress: signerAddress,
senderSignature: senderSignature.data,
origin: `Proposed from hardhat: ${methodName}`,
nonce: next_nonce,
};
await safeService.proposeTransaction(proposeArgs);
return safeTxHash;
}
module.exports = {
proposeOrSendTransaction
}

View File

@@ -9,8 +9,12 @@ import {ILocker, IPayer} from "@ekubo/interfaces/IFlashAccountant.sol";
import {NATIVE_TOKEN_ADDRESS} from "@ekubo/math/constants.sol";
import {SafeTransferLib} from "@solady/utils/SafeTransferLib.sol";
import {LibBytes} from "@solady/utils/LibBytes.sol";
import {Config, EkuboPoolKey} from "@ekubo/types/poolKey.sol";
import {MAX_SQRT_RATIO, MIN_SQRT_RATIO} from "@ekubo/types/sqrtRatio.sol";
import {Config, PoolKey} from "@ekubo/types/poolKey.sol";
import {
MAX_SQRT_RATIO,
MIN_SQRT_RATIO,
SqrtRatio
} from "@ekubo/types/sqrtRatio.sol";
import {RestrictTransferFrom} from "../RestrictTransferFrom.sol";
import "@openzeppelin/contracts/utils/Address.sol";
@@ -21,11 +25,13 @@ contract EkuboExecutor is
ICallback,
RestrictTransferFrom
{
error EkuboExecutor__AddressZero();
error EkuboExecutor__InvalidDataLength();
error EkuboExecutor__CoreOnly();
error EkuboExecutor__UnknownCallback();
ICore immutable core;
address immutable mevResist;
uint256 constant POOL_DATA_OFFSET = 57;
uint256 constant HOP_BYTE_LEN = 52;
@@ -33,12 +39,19 @@ contract EkuboExecutor is
bytes4 constant LOCKED_SELECTOR = 0xb45a3c0e; // locked(uint256)
bytes4 constant PAY_CALLBACK_SELECTOR = 0x599d0714; // payCallback(uint256,address)
uint256 constant SKIP_AHEAD = 0;
using SafeERC20 for IERC20;
constructor(address _core, address _permit2)
constructor(address _core, address _mevResist, address _permit2)
RestrictTransferFrom(_permit2)
{
core = ICore(_core);
if (_mevResist == address(0)) {
revert EkuboExecutor__AddressZero();
}
mevResist = _mevResist;
}
function swap(uint256 amountIn, bytes calldata data)
@@ -141,19 +154,40 @@ contract EkuboExecutor is
Config poolConfig =
Config.wrap(LibBytes.loadCalldata(swapData, offset + 20));
(address token0, address token1, bool isToken1) = nextTokenIn
> nextTokenOut
? (nextTokenOut, nextTokenIn, true)
: (nextTokenIn, nextTokenOut, false);
(
address token0,
address token1,
bool isToken1,
SqrtRatio sqrtRatioLimit
) = nextTokenIn > nextTokenOut
? (nextTokenOut, nextTokenIn, true, MAX_SQRT_RATIO)
: (nextTokenIn, nextTokenOut, false, MIN_SQRT_RATIO);
// slither-disable-next-line calls-loop
(int128 delta0, int128 delta1) = core.swap_611415377(
EkuboPoolKey(token0, token1, poolConfig),
nextAmountIn,
isToken1,
isToken1 ? MAX_SQRT_RATIO : MIN_SQRT_RATIO,
0
);
PoolKey memory pk = PoolKey(token0, token1, poolConfig);
int128 delta0;
int128 delta1;
if (poolConfig.extension() == mevResist) {
(delta0, delta1) = abi.decode(
_forward(
mevResist,
abi.encode(
pk,
nextAmountIn,
isToken1,
sqrtRatioLimit,
SKIP_AHEAD
)
),
(int128, int128)
);
} else {
// slither-disable-next-line calls-loop
(delta0, delta1) = core.swap_611415377(
pk, nextAmountIn, isToken1, sqrtRatioLimit, SKIP_AHEAD
);
}
nextTokenIn = nextTokenOut;
nextAmountIn = -(isToken1 ? delta0 : delta1);
@@ -166,6 +200,45 @@ contract EkuboExecutor is
return nextAmountIn;
}
function _forward(address to, bytes memory data)
internal
returns (bytes memory result)
{
address target = address(core);
// slither-disable-next-line assembly
assembly ("memory-safe") {
// We will store result where the free memory pointer is now, ...
result := mload(0x40)
// But first use it to store the calldata
// Selector of forward(address)
mstore(result, shl(224, 0x101e8952))
mstore(add(result, 4), to)
// We only copy the data, not the length, because the length is read from the calldata size
let len := mload(data)
mcopy(add(result, 36), add(data, 32), len)
// If the call failed, pass through the revert
if iszero(call(gas(), target, 0, result, add(36, len), 0, 0)) {
returndatacopy(result, 0, returndatasize())
revert(result, returndatasize())
}
// Copy the entire return data into the space where the result is pointing
mstore(result, returndatasize())
returndatacopy(add(result, 32), 0, returndatasize())
// Update the free memory pointer to be after the end of the data, aligned to the next 32 byte word
mstore(
0x40,
and(add(add(result, add(32, returndatasize())), 31), not(31))
)
}
}
function _pay(address token, uint128 amount, TransferType transferType)
internal
{

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,164 @@
// 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";
import {TychoRouter} from "../TychoRouter.sol";
error UniswapXFiller__AddressZero();
error UniswapXFiller__BatchExecutionNotSupported();
contract UniswapXFiller is AccessControl, IReactorCallback {
using SafeERC20 for IERC20;
// UniswapX V2DutchOrder Reactor
IReactor public immutable reactor;
address public immutable tychoRouter;
address public immutable nativeAddress;
// 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,
address _native_address
) {
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);
// slither-disable-next-line missing-zero-check
nativeAddress = _native_address;
}
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) {
require(
resolvedOrders.length == 1,
UniswapXFiller__BatchExecutionNotSupported()
);
ResolvedOrder memory order = resolvedOrders[0];
bool tokenInApprovalNeeded = bool(uint8(callbackData[0]) == 1);
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
(bool success, bytes memory result) = tychoRouter.call(tychoCalldata);
if (!success) {
revert(
string(
result.length > 0
? result
: abi.encodePacked("Execution failed")
)
);
}
if (tokenOutApprovalNeeded) {
// Multiple outputs are possible when taking fees - but token itself should
// not change.
OutputToken memory output = order.outputs[0];
if (output.token != nativeAddress) {
IERC20 token = IERC20(output.token);
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);
}
}
}
/**
* @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
*/
// slither-disable-next-line locked-ether
receive() external payable {
require(msg.sender.code.length != 0);
}
}

View File

@@ -78,6 +78,7 @@ contract Constants is Test, BaseConstants {
// Uniswap v3
address DAI_WETH_USV3 = 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8;
address DAI_USDT_USV3 = 0x48DA0965ab2d2cbf1C17C09cFB5Cbe67Ad5B1406;
address USDC_WETH_USV3 = 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640; // 0.05% fee
address USDC_WETH_USV3_2 = 0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8; // 0.3% fee

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

@@ -493,6 +493,27 @@ contract TychoRouterSequentialSwapTest is TychoRouterTestSetup {
assertEq(IERC20(WETH_ADDR).balanceOf(tychoRouterAddr), 0);
}
function testSequentialSwapWithUnwrapIntegration() public {
// Performs a sequential swap from USDC to ETH through WBTC using USV2 pools and unwrapping in
// the end
deal(USDC_ADDR, ALICE, 3_000_000_000);
uint256 balanceBefore = ALICE.balance;
// Approve permit2
vm.startPrank(ALICE);
IERC20(USDC_ADDR).approve(PERMIT2_ADDRESS, type(uint256).max);
bytes memory callData =
loadCallDataFromFile("test_sequential_swap_strategy_encoder_unwrap");
(bool success,) = tychoRouterAddr.call(callData);
vm.stopPrank();
uint256 balanceAfter = ALICE.balance;
assertTrue(success, "Call Failed");
assertEq(balanceAfter - balanceBefore, 1404194006633772805);
}
function testUSV3BebopIntegration() public {
// Performs a sequential swap from WETH to ONDO through USDC using USV3 and Bebop RFQ
//

View File

@@ -116,6 +116,7 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
bytes32 initCodePancakeV3 = PANCAKEV3_POOL_CODE_INIT_HASH;
address poolManagerAddress = 0x000000000004444c5dc75cB358380D2e3dE08A90;
address ekuboCore = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444;
address ekuboMevResist = 0x553a2EFc570c9e104942cEC6aC1c18118e54C091;
IPoolManager poolManager = IPoolManager(poolManagerAddress);
usv2Executor =
@@ -127,7 +128,8 @@ contract TychoRouterTestSetup is Constants, Permit2TestHelper, TestUtils {
factoryPancakeV3, initCodePancakeV3, PERMIT2_ADDRESS
);
balancerv2Executor = new BalancerV2Executor(PERMIT2_ADDRESS);
ekuboExecutor = new EkuboExecutor(ekuboCore, PERMIT2_ADDRESS);
ekuboExecutor =
new EkuboExecutor(ekuboCore, ekuboMevResist, PERMIT2_ADDRESS);
curveExecutor = new CurveExecutor(ETH_ADDR_FOR_CURVE, PERMIT2_ADDRESS);
maverickv2Executor =
new MaverickV2Executor(MAVERICK_V2_FACTORY, PERMIT2_ADDRESS);

File diff suppressed because one or more lines are too long

View File

@@ -19,22 +19,29 @@ contract EkuboExecutorTest is Constants, TestUtils {
IERC20 USDT = IERC20(USDT_ADDR);
address constant CORE_ADDRESS = 0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444;
address constant MEV_RESIST_ADDRESS =
0x553a2EFc570c9e104942cEC6aC1c18118e54C091;
bytes32 constant ORACLE_CONFIG =
0x51d02a5948496a67827242eabc5725531342527c000000000000000000000000;
function setUp() public {
vm.createSelectFork(vm.rpcUrl("mainnet"), 22082754);
// 0.01% fee and 0.02% tick spacing
bytes32 constant MEV_RESIST_POOL_CONFIG =
0x553a2EFc570c9e104942cEC6aC1c18118e54C09100068db8bac710cb000000c8;
modifier setUpFork(uint256 blockNumber) {
vm.createSelectFork(vm.rpcUrl("mainnet"), blockNumber);
deployCodeTo(
"executors/EkuboExecutor.sol",
abi.encode(CORE_ADDRESS, PERMIT2_ADDRESS),
abi.encode(CORE_ADDRESS, MEV_RESIST_ADDRESS, PERMIT2_ADDRESS),
EXECUTOR_ADDRESS
);
executor = EkuboExecutor(payable(EXECUTOR_ADDRESS));
_;
}
function testSingleSwapEth() public {
function testSingleSwapEth() public setUpFork(22722989) {
uint256 amountIn = 1 ether;
deal(address(executor), amountIn);
@@ -71,7 +78,7 @@ contract EkuboExecutorTest is Constants, TestUtils {
);
}
function testSingleSwapERC20() public {
function testSingleSwapERC20() public setUpFork(22722989) {
uint256 amountIn = 1_000_000_000;
deal(USDC_ADDR, address(executor), amountIn);
@@ -108,6 +115,43 @@ contract EkuboExecutorTest is Constants, TestUtils {
);
}
function testMevResist() public setUpFork(22722989) {
uint256 amountIn = 1_000_000_000;
deal(USDC_ADDR, address(executor), amountIn);
uint256 usdcBalanceBeforeCore = USDC.balanceOf(CORE_ADDRESS);
uint256 usdcBalanceBeforeExecutor = USDC.balanceOf(address(executor));
uint256 ethBalanceBeforeCore = CORE_ADDRESS.balance;
uint256 ethBalanceBeforeExecutor = address(executor).balance;
bytes memory data = abi.encodePacked(
uint8(RestrictTransferFrom.TransferType.Transfer), // transferNeeded (transfer from executor to core)
address(executor), // receiver
USDC_ADDR, // tokenIn
NATIVE_TOKEN_ADDRESS, // tokenOut
MEV_RESIST_POOL_CONFIG // config
);
uint256 gasBefore = gasleft();
uint256 amountOut = executor.swap(amountIn, data);
console.log(gasBefore - gasleft());
console.log(amountOut);
assertEq(USDC.balanceOf(CORE_ADDRESS), usdcBalanceBeforeCore + amountIn);
assertEq(
USDC.balanceOf(address(executor)),
usdcBalanceBeforeExecutor - amountIn
);
assertEq(CORE_ADDRESS.balance, ethBalanceBeforeCore - amountOut);
assertEq(
address(executor).balance, ethBalanceBeforeExecutor + amountOut
);
}
// Expects input that encodes the same test case as swap_encoder::tests::ekubo::test_encode_swap_multi
function multiHopSwap(bytes memory data) internal {
uint256 amountIn = 1 ether;
@@ -139,7 +183,7 @@ contract EkuboExecutorTest is Constants, TestUtils {
}
// Same test case as in swap_encoder::tests::ekubo::test_encode_swap_multi
function testMultiHopSwap() public {
function testMultiHopSwap() public setUpFork(22082754) {
bytes memory data = abi.encodePacked(
uint8(RestrictTransferFrom.TransferType.Transfer), // transferNeeded (transfer from executor to core)
address(executor), // receiver
@@ -155,7 +199,7 @@ contract EkuboExecutorTest is Constants, TestUtils {
}
// Data is generated by test case in swap_encoder::tests::ekubo::test_encode_swap_multi
function testMultiHopSwapIntegration() public {
function testMultiHopSwapIntegration() public setUpFork(22082754) {
multiHopSwap(loadCallDataFromFile("test_ekubo_encode_swap_multi"));
}
}

View File

@@ -0,0 +1,302 @@
// 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 = address(0xCe79b081c0c924cb67848723ed3057234d10FC6b);
address REACTOR = address(0x00000011F84B9aa48e5f8aA8B9897600006289Be);
UniswapXFiller filler;
address fillerAddr;
event CallbackVerifierSet(address indexed callbackVerifier);
event Withdrawal(
address indexed token, uint256 amount, address indexed receiver
);
function getForkBlock() public pure override returns (uint256) {
return 22880493;
}
function fillerSetup() public {
vm.startPrank(ADMIN);
filler = new UniswapXFiller(tychoRouterAddr, REACTOR, address(0));
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, address(0));
}
function testTychoAddressZeroReactor() public {
vm.expectRevert(UniswapXFiller__AddressZero.selector);
filler = new UniswapXFiller(tychoRouterAddr, address(0), address(0));
}
function testCallback() public {
fillerSetup();
uint256 amountIn = 10 ** 18;
uint256 amountOut = 1847751195973566072891;
bool zeroForOne = false;
bytes memory protocolData = abi.encodePacked(
WETH_ADDR,
WETH_DAI_POOL,
address(filler),
zeroForOne,
RestrictTransferFrom.TransferType.TransferFrom
);
bytes memory swap =
encodeSingleSwap(address(usv2Executor), protocolData);
bytes memory tychoRouterData = abi.encodeWithSelector(
tychoRouter.singleSwap.selector,
amountIn,
WETH_ADDR,
DAI_ADDR,
2008817438608734439722,
false,
false,
address(filler),
true,
swap
);
bytes memory callbackData =
abi.encodePacked(true, true, tychoRouterData);
deal(WETH_ADDR, address(filler), amountIn);
ResolvedOrder[] memory orders = new ResolvedOrder[](1);
OutputToken[] memory outputs = new OutputToken[](1);
outputs[0] = OutputToken({
token: address(DAI_ADDR),
amount: 1847751195973566072891,
recipient: BOB
});
// Irrelevant fields for this test - we only need token output
// info for the sake of testing.
orders[0] = ResolvedOrder({
info: OrderInfo({
reactor: address(0),
swapper: address(0),
nonce: 0,
deadline: 0,
additionalValidationContract: address(0),
additionalValidationData: ""
}),
input: InputToken({
token: address(WETH_ADDR),
amount: amountIn,
maxAmount: amountIn
}),
outputs: outputs,
sig: "",
hash: ""
});
vm.startPrank(REACTOR);
filler.reactorCallback(orders, callbackData);
vm.stopPrank();
// Check that the funds are in the filler at the end of the function call
uint256 finalBalance = IERC20(DAI_ADDR).balanceOf(address(filler));
assertGe(finalBalance, amountOut);
// Check that the proper approval was set
vm.startPrank(REACTOR);
IERC20(DAI_ADDR).transferFrom(address(filler), BOB, amountOut);
vm.stopPrank();
assertGe(IERC20(DAI_ADDR).balanceOf(BOB), amountOut);
}
function testExecuteIntegration() public {
fillerSetup();
// Set to time with no more penalty for not being exclusive filler
vm.warp(1752050415);
deal(
DAI_ADDR,
address(0xD213e6F6dCB2DBaC03FA28b893F6dA1BD822e852),
2000 ether
);
uint256 amountIn = 2000000000000000000000;
vm.startPrank(address(0xD213e6F6dCB2DBaC03FA28b893F6dA1BD822e852));
// Approve Permit2
IERC20(DAI_ADDR).approve(
address(0x000000000022D473030F116dDEE9F6B43aC78BA3), amountIn
);
vm.stopPrank();
// Tx 0x005d7b150017ba1b59d2f99395ccae7bda9b739938ade4e509817e32760aaf9d
// Calldata generated using rust test `test_sequential_swap_usx`
SignedOrder memory order = SignedOrder({
order: hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001000000000000000000000000004449cd34d1eb1fedcf02a1be3834ffde8e6a61800000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000006c6b935b8bbd40000000000000000000000000000000000000000000000000006c6b935b8bbd40000000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000011f84b9aa48e5f8aa8b9897600006289be000000000000000000000000d213e6f6dcb2dbac03fa28b893f6da1bd822e8520468320351debb1ddbfb032a239d699e3d54e3ce2b6e1037cd836a784c80b60100000000000000000000000000000000000000000000000000000000686e2bf9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000076f9f4870000000000000000000000000000000000000000000000000000000076566300000000000000000000000000d213e6f6dcb2dbac03fa28b893f6da1bd822e85200000000000000000000000000000000000000000000000000000000686e2aee00000000000000000000000000000000000000000000000000000000686e2b2a000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000007727b5f40000000000000000000000000000000000000000000000000000000000000041a2d261cd4c8930428260f18b55e3036024bac68d58cb2ee6161e6395b0984b827104158713d44ddc4e14d852b48d93d95a4e60b8d5be1ef431c1e82d2f76a4111b00000000000000000000000000000000000000000000000000000000000000",
sig: hex"f4cc5734820e4ee08519045c83a25b75687756053b3d6c0fda2141380dfa6ef17b40f64d9279f237e96982c6ba53a202e01a4358fd66e027c9bdf200d5626f441c"
});
bytes memory callbackData =
loadCallDataFromFile("test_sequential_swap_usx");
vm.startPrank(EXECUTOR);
filler.execute(order, callbackData);
vm.stopPrank();
}
function testExecute() public {
fillerSetup();
// Set to time with no more penalty for not being exclusive filler
vm.warp(1752050415);
// tx: 0x005d7b150017ba1b59d2f99395ccae7bda9b739938ade4e509817e32760aaf9d
// DAI ──> USDT
SignedOrder memory order = SignedOrder({
order: hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001000000000000000000000000004449cd34d1eb1fedcf02a1be3834ffde8e6a61800000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000006c6b935b8bbd40000000000000000000000000000000000000000000000000006c6b935b8bbd40000000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000011f84b9aa48e5f8aa8b9897600006289be000000000000000000000000d213e6f6dcb2dbac03fa28b893f6da1bd822e8520468320351debb1ddbfb032a239d699e3d54e3ce2b6e1037cd836a784c80b60100000000000000000000000000000000000000000000000000000000686e2bf9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000000000000000000000000000000000000076f9f4870000000000000000000000000000000000000000000000000000000076566300000000000000000000000000d213e6f6dcb2dbac03fa28b893f6da1bd822e85200000000000000000000000000000000000000000000000000000000686e2aee00000000000000000000000000000000000000000000000000000000686e2b2a000000000000000000000000ce79b081c0c924cb67848723ed3057234d10fc6b0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000007727b5f40000000000000000000000000000000000000000000000000000000000000041a2d261cd4c8930428260f18b55e3036024bac68d58cb2ee6161e6395b0984b827104158713d44ddc4e14d852b48d93d95a4e60b8d5be1ef431c1e82d2f76a4111b00000000000000000000000000000000000000000000000000000000000000",
sig: hex"f4cc5734820e4ee08519045c83a25b75687756053b3d6c0fda2141380dfa6ef17b40f64d9279f237e96982c6ba53a202e01a4358fd66e027c9bdf200d5626f441c"
});
uint256 amountIn = 2000000000000000000000;
bool zeroForOne = true;
uint24 fee = 100;
bytes memory protocolData = abi.encodePacked(
DAI_ADDR,
USDT_ADDR,
fee,
fillerAddr,
DAI_USDT_USV3,
zeroForOne,
RestrictTransferFrom.TransferType.TransferFrom
);
bytes memory swap =
encodeSingleSwap(address(usv3Executor), protocolData);
bytes memory tychoRouterData = abi.encodeWithSelector(
tychoRouter.singleSwap.selector,
amountIn,
DAI_ADDR,
USDT_ADDR,
1,
false,
false,
fillerAddr,
true,
swap
);
bytes memory callbackData = abi.encodePacked(
true, // tokenIn approval needed
true, // tokenOut approval needed
tychoRouterData
);
vm.startPrank(address(filler));
IERC20(WBTC_ADDR).approve(tychoRouterAddr, amountIn);
vm.stopPrank();
vm.startPrank(EXECUTOR);
filler.execute(order, callbackData);
vm.stopPrank();
}
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();
}
}