UNPKG

tenderly-wizard-v6

Version:

A tool for managing virtual testnets using Tenderly

400 lines (358 loc) 12.2 kB
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { BigNumberish, Contract, id, zeroPadValue, AbiCoder, PreparedTransactionRequest, JsonRpcProvider } from "ethers"; import { MetaTransaction, encodeMulti } from "ethers-multisend"; import fs from "fs"; import path from "path"; // @ts-ignore import { ethers, network } from "hardhat"; import { Project, ClassDeclaration, SyntaxKind } from "ts-morph"; import config from "../env-config"; import { calculateProxyAddress, ContractAddresses, ContractFactories, KnownContracts, RolesV1, RolesV2, SupportedNetworks } from "@gnosis-guild/zodiac"; import { RolesVersion } from "./types"; export const SALT = "0x0000000000000000000000000000000000000000000000000000000000000000"; export enum OperationType { Call, DelegateCall, } export enum ExecutionOptions { None, Send, DelegateCall, Both, } export interface MetaTransactionData { to: string; value: string; data: string; operation?: OperationType; } /** * Encodes multiple transactions so we can then use a Safe with multisend for atomic transacting * @param {PreparedTransactionRequest[]} populatedTxs - Array of populated transactions * @param {string} multisendAddr - Address of the multisend contract * @returns {MetaTransaction} Encoded multi-transaction */ export function createMultisendTx( populatedTxs: PreparedTransactionRequest[], multisendAddr: string ): MetaTransaction { const safeTransactionData: MetaTransactionData[] = populatedTxs.map( popTx => ({ to: popTx.to as string, value: popTx.value ? popTx.value.toString() : "0", data: popTx.data as string, }) ); return encodeMulti(safeTransactionData, multisendAddr); } /** * Generates pre-validated signatures for a single owner on a safe * @param {string} from - Address of the signer * @param {string} [initialString="0x"] - Initial string to prepend * @returns {string} Pre-validated signature string */ export const getPreValidatedSignatures = ( from: string, initialString = "0x" ): string => { return `${initialString}000000000000000000000000${from.replace( "0x", "" )}000000000000000000000000000000000000000000000000000000000000000001`; }; /** * Helper function to scope targets in roles contract v1 * @param {string[]} targetAddrs - Array of target addresses * @param {number} roleId - Role ID * @param {Contract} roles - Roles contract instance * @returns {Promise<PreparedTransactionRequest[]>} Array of populated transactions */ export async function scopeTargetsV1( targetAddrs: string[], roleId: number, roles: Contract ) { const scopeTargetTxs = await Promise.all( targetAddrs.map(async target => { //Before granular function/parameter whitelisting can occur, you need to bring a target contract into 'scope' via scopeTarget const tx = await (roles as any).scopeTarget.populateTransaction(ethers.toBeHex(roleId, 32), target); return tx; }) ); return scopeTargetTxs; } /** * Helper function to scope targets in roles contract v2 * @param {string[]} targetAddrs - Array of target addresses * @param {`0x${string}`} roleId - Role ID * @param {Contract} roles - Roles contract instance * @returns {Promise<PreparedTransactionRequest[]>} Array of populated transactions */ export async function scopeTargetsV2( targetAddrs: string[], roleId: `0x${string}`, roles: RolesV2 ) { const scopeTargetTxs = await Promise.all( targetAddrs.map(async target => { const tx = await roles.scopeTarget.populateTransaction(ethers.toBeHex(roleId, 32), target); return tx; }) ); return scopeTargetTxs; } /** * Encodes a string to bytes32 format * @param {string} text - String to encode * @returns {`0x${string}`} Bytes32 representation of the string */ export const encodeBytes32String = ethers.encodeBytes32String; /** * Sets ERC20 token balance for a single token and recipient * @param {string} token - Token address * @param {string} address - Recipient address * @param {BigNumberish} amount - Amount to set * @returns {Promise<void>} */ export const setERC20TokenBalance = async ( token: string, address: string, amount: BigNumberish ) => { const value = ethers.hexlify(amount); await network.provider.request({ method: "tenderly_setErc20Balance", params: [token, address, value.replace("0x0", "0x")], }); }; /** * Sets gas balance for multiple addresses * @returns {Promise<void>} */ export async function setGas() { const { VIRTUAL_MAINNET_RPC } = config; let caller: SignerWithAddress; let manager: SignerWithAddress; let dummyOwnerOne: SignerWithAddress; let dummyOwnerTwo: SignerWithAddress; let dummyOwnerThree: SignerWithAddress; let security: SignerWithAddress; [caller, manager, dummyOwnerOne, dummyOwnerTwo, dummyOwnerThree, security] = await ethers.getSigners(); const provider = new JsonRpcProvider(VIRTUAL_MAINNET_RPC); await provider.send("tenderly_setBalance", [ caller.address, "0x8AC7230489E80000", ]); await provider.send("tenderly_setBalance", [ manager.address, "0x8AC7230489E80000", ]); await provider.send("tenderly_setBalance", [ security.address, "0x8AC7230489E80000", ]); await provider.send("tenderly_setBalance", [ dummyOwnerOne.address, "0x8AC7230489E80000", ]); await provider.send("tenderly_setBalance", [ dummyOwnerTwo.address, "0xDE0B6B3A7640000", ]); await provider.send("tenderly_setBalance", [ dummyOwnerThree.address, "0xDE0B6B3A7640000", ]); } /** * Finds all 'permissions.ts' files in a directory and its subdirectories * @param {string} dir - Directory to search * @returns {string[]} Array of file paths */ export function findPermissionsFiles(whitelistDir: string): string[] { if (!fs.existsSync(whitelistDir)) { throw new Error(`The directory ${whitelistDir} does not exist.`); } let results: string[] = []; // Get absolute path of whitelistDir const absoluteWhitelistDir = path.resolve(process.cwd(), whitelistDir); const files = fs.readdirSync(absoluteWhitelistDir); for (const file of files) { const filePath = path.join(absoluteWhitelistDir, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { // Recursively search subdirectories by calling findPermissionsFiles again results = results.concat(findPermissionsFiles(filePath)); } else if (file === "permissions.ts") { results.push(filePath); } } return results; } /** * Finds all classes that extend Whitelist in a directory * @param {string} whitelistDir - Directory to search * @returns {{ path: string, className: string }[]} Array of objects containing path and class name */ export function findWhitelistClasses( whitelistDir: string ): { path: string; className: string }[] { const project = new Project(); const getAllTsFiles = (dir: string): string[] => { let results: string[] = []; const list = fs.readdirSync(dir); list.forEach(file => { const filePath = path.join(dir, file); const stat = fs.statSync(filePath); if (stat && stat.isDirectory()) { results = results.concat(getAllTsFiles(filePath)); } else if (file.endsWith(".ts")) { results.push(filePath); } }); return results; }; getAllTsFiles(whitelistDir).forEach(file => { project.addSourceFileAtPath(file); }); const whitelistExtensions: { path: string; className: string }[] = []; project.getSourceFiles().forEach(sourceFile => { const classes = sourceFile.getDescendantsOfKind( SyntaxKind.ClassDeclaration ); classes.forEach((classDeclaration: ClassDeclaration) => { const heritage = classDeclaration.getHeritageClauses(); if ( heritage.some(clause => clause.getTypeNodes().some(node => { const text = node.getText(); return ( text.includes("Whitelist") || text.includes("AbstractPendleWhitelist") ); }) ) ) { const absolutePath = sourceFile.getFilePath(); whitelistExtensions.push({ path: absolutePath, className: classDeclaration.getName() ?? "", }); } }); }); // Filter out AccessControllerWhitelist classes let filteredExtensions = whitelistExtensions.filter( extension => !extension.className.includes("AccessControllerWhitelist") ); whitelistExtensions.length = 0; whitelistExtensions.push(...filteredExtensions); return whitelistExtensions; } /** * Checks if required environment variables are present * @param {string[]} requiredVariables - Array of required variable names * @returns {boolean} True if all required variables are present, false otherwise */ export function checkRequiredEnvVariables(requiredVariables: string[]) { const missingVariables = requiredVariables.filter( variable => !(variable in config) ); if (missingVariables.length > 0) { console.log( `Missing required environment variables: ${missingVariables.join(", ")}` ); return false; } console.log("All required environment variables are present."); return true; } export async function predictRolesModAddress( signer: any, owner: string, avatar: string, target: string, rolesVersion: RolesVersion ) { const chainId = Number(await signer.provider.getNetwork().then((n: { chainId: number }) => n.chainId)) as SupportedNetworks; const encodedInitParams = AbiCoder.defaultAbiCoder().encode( ["address", "address", "address"], [owner, avatar, target] ); const rolesContract = rolesVersion == "v1" ? KnownContracts.ROLES_V1 : KnownContracts.ROLES_V2; const moduleSetupData = ContractFactories[rolesContract] .createInterface() .encodeFunctionData("setUp", [encodedInitParams]); return calculateProxyAddress( ContractFactories[KnownContracts.FACTORY].connect( ContractAddresses[chainId][KnownContracts.FACTORY], signer ) as any, ContractAddresses[chainId][rolesContract], moduleSetupData, SALT ); } /** * Predicts the address of a Safe proxy that will be deployed through a Safe proxy factory * @param {Contract} safeProxyFactory - Instance of the Safe proxy factory contract * @param {string} safeMasterCopy - Address of the Safe master copy implementation * @param {string} data - Initialization data for the Safe proxy * @param {number} saltNonce - Nonce used as salt for address calculation * @returns {Promise<string>} The predicted address of the Safe proxy * @description Uses the proxy factory's calculateCreateProxyWithNonceAddress method to predict * the deterministic address where a Safe proxy will be deployed based on the initialization parameters */ export async function predictSafeAddress( safeProxyFactory: Contract, safeMasterCopy: string, data: string, saltNonce: number ) { return await safeProxyFactory.calculateCreateProxyWithNonceAddress( safeMasterCopy, data, saltNonce, { gasLimit: ethers.hexlify(3000000), } ); } /** * Sets the current block number to a target block number by increasing blocks * @param {number} targetBlock - The desired block number to reach * @returns {Promise<void>} A promise that resolves when blocks have been increased * @description This function is useful for testing scenarios that require being at a specific block number. * It calculates the difference between current and target block numbers and increases blocks accordingly. */ export async function setUniformBlockNumber(targetBlock: number) { const currentBlock = await network.provider.send("eth_blockNumber"); const currentBlockDecimal = parseInt(currentBlock, 16); const blocksToIncrease = targetBlock - currentBlockDecimal; console.log("targetBlock:", targetBlock); console.log("currentBlock:", currentBlockDecimal); console.log("blocksToIncrease:", blocksToIncrease); await network.provider.request({ method: "evm_increaseBlocks", params: [ethers.hexlify(blocksToIncrease)], }); }