feat: Upgrade scripts to submit to Safe wallet

Also add revoke-role.js script

Took 4 hours 0 minutes
This commit is contained in:
Diana Carvalho
2025-07-08 12:23:41 +01:00
parent 4e49b3b99b
commit 2733bb0072
8 changed files with 3619 additions and 386 deletions

3793
foundry/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,9 @@
}, },
"dependencies": { "dependencies": {
"@nomicfoundation/hardhat-foundry": "^1.1.3", "@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" "prompt-sync": "^4.2.0"
} }
} }

View File

@@ -53,4 +53,17 @@ 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. 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` 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.

View File

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

View File

@@ -3,15 +3,21 @@ const {ethers} = require("hardhat");
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const hre = require("hardhat"); const hre = require("hardhat");
const {proposeOrSendTransaction} = require("./utils");
async function main() { async function main() {
const network = hre.network.name; const network = hre.network.name;
const routerAddress = process.env.ROUTER_ADDRESS; 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}`); console.log(`Setting roles on TychoRouter at ${routerAddress} on ${network}`);
const [deployer] = await ethers.getSigners(); const [signer] = await ethers.getSigners();
console.log(`Setting roles with account: ${deployer.address}`); console.log(`Setting roles with account: ${signer.address}`);
console.log(`Account balance: ${ethers.utils.formatEther(await deployer.getBalance())} ETH`); console.log(`Account balance: ${ethers.utils.formatEther(await signer.getBalance())} ETH`);
const TychoRouter = await ethers.getContractFactory("TychoRouter"); const TychoRouter = await ethers.getContractFactory("TychoRouter");
const router = TychoRouter.attach(routerAddress); const router = TychoRouter.attach(routerAddress);
@@ -27,13 +33,21 @@ async function main() {
}; };
// Iterate through roles and grant them to the corresponding addresses // Iterate through roles and grant them to the corresponding addresses
let nonceOffset = 0
for (const [roleName, roleHash] of Object.entries(roles)) { for (const [roleName, roleHash] of Object.entries(roles)) {
const addresses = rolesDict[network][roleName]; const addresses = rolesDict[network][roleName];
if (addresses && addresses.length > 0) { if (addresses && addresses.length > 0) {
console.log(`Granting ${roleName} to the following addresses:`, addresses); 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 const txData = {
console.log(`Role ${roleName} granted at transaction: ${tx.hash}`); to: router.address,
data: router.interface.encodeFunctionData("batchGrantRole", [roleHash, addresses]),
value: "0",
};
const txHash = await proposeOrSendTransaction(safeAddress, txData, signer, "batchGrantRole", nonceOffset);
nonceOffset += 1
console.log(`Role ${roleName} granted at TX hash: ${txHash}`);
} else { } else {
console.log(`No addresses found for role ${roleName}`); console.log(`No addresses found for role ${roleName}`);
} }

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

@@ -0,0 +1,55 @@
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 txServiceUrl = 'https://safe-transaction-mainnet.safe.global';
async function proposeOrSendTransaction(safeAddress, txData, signer, methodName, nonceOffset = 0) {
if (safeAddress) {
return proposeTransaction(safeAddress, txData, signer, methodName, nonceOffset);
} 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, nonceOffset = 0) {
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,
});
const safeTransaction = await safeSdk.createTransaction({safeTransactionData: txData});
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: await safeService.getNextNonce(safeAddress) + nonceOffset,
};
await safeService.proposeTransaction(proposeArgs);
return safeTxHash;
}
module.exports = {
proposeOrSendTransaction
}