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:
@@ -44,7 +44,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
tenderly: {
|
||||
project: "project",
|
||||
project: "tycho",
|
||||
username: "tvinagre",
|
||||
privateVerification: false,
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
3793
foundry/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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",
|
||||
|
||||
63
foundry/scripts/deploy-uniswap-x-filler.js
Normal file
63
foundry/scripts/deploy-uniswap-x-filler.js
Normal 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);
|
||||
});
|
||||
@@ -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()
|
||||
|
||||
51
foundry/scripts/revoke-role.js
Normal file
51
foundry/scripts/revoke-role.js
Normal 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);
|
||||
});
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
66
foundry/scripts/utils.js
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
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;
|
||||
}
|
||||
164
foundry/src/uniswap_x/UniswapXFiller.sol
Normal file
164
foundry/src/uniswap_x/UniswapXFiller.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -96,7 +96,6 @@ contract TychoRouterTest is TychoRouterTestSetup {
|
||||
}
|
||||
|
||||
vm.startPrank(FUND_RESCUER);
|
||||
|
||||
tychoRouter.withdraw(tokens, FUND_RESCUER);
|
||||
|
||||
// Check balances after withdrawing
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
302
foundry/test/uniswap_x/UniswapXFiller.t.sol
Normal file
302
foundry/test/uniswap_x/UniswapXFiller.t.sol
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user