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

@@ -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
}