tenderly-wizard-v6
Version:
A tool for managing virtual testnets using Tenderly
400 lines (358 loc) • 12.2 kB
text/typescript
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)],
});
}