tenderly-wizard-v6
Version:
A tool for managing virtual testnets using Tenderly
285 lines (284 loc) • 13 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setERC20TokenBalance = exports.encodeBytes32String = exports.getPreValidatedSignatures = exports.ExecutionOptions = exports.OperationType = exports.SALT = void 0;
exports.createMultisendTx = createMultisendTx;
exports.scopeTargetsV1 = scopeTargetsV1;
exports.scopeTargetsV2 = scopeTargetsV2;
exports.setGas = setGas;
exports.findPermissionsFiles = findPermissionsFiles;
exports.findWhitelistClasses = findWhitelistClasses;
exports.checkRequiredEnvVariables = checkRequiredEnvVariables;
exports.predictRolesModAddress = predictRolesModAddress;
exports.predictSafeAddress = predictSafeAddress;
exports.setUniformBlockNumber = setUniformBlockNumber;
const ethers_1 = require("ethers");
const ethers_multisend_1 = require("ethers-multisend");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
// @ts-ignore
const hardhat_1 = require("hardhat");
const ts_morph_1 = require("ts-morph");
const env_config_1 = __importDefault(require("../env-config"));
const zodiac_1 = require("@gnosis-guild/zodiac");
exports.SALT = "0x0000000000000000000000000000000000000000000000000000000000000000";
var OperationType;
(function (OperationType) {
OperationType[OperationType["Call"] = 0] = "Call";
OperationType[OperationType["DelegateCall"] = 1] = "DelegateCall";
})(OperationType || (exports.OperationType = OperationType = {}));
var ExecutionOptions;
(function (ExecutionOptions) {
ExecutionOptions[ExecutionOptions["None"] = 0] = "None";
ExecutionOptions[ExecutionOptions["Send"] = 1] = "Send";
ExecutionOptions[ExecutionOptions["DelegateCall"] = 2] = "DelegateCall";
ExecutionOptions[ExecutionOptions["Both"] = 3] = "Both";
})(ExecutionOptions || (exports.ExecutionOptions = ExecutionOptions = {}));
/**
* 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
*/
function createMultisendTx(populatedTxs, multisendAddr) {
const safeTransactionData = populatedTxs.map(popTx => ({
to: popTx.to,
value: popTx.value ? popTx.value.toString() : "0",
data: popTx.data,
}));
return (0, ethers_multisend_1.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
*/
const getPreValidatedSignatures = (from, initialString = "0x") => {
return `${initialString}000000000000000000000000${from.replace("0x", "")}000000000000000000000000000000000000000000000000000000000000000001`;
};
exports.getPreValidatedSignatures = getPreValidatedSignatures;
/**
* 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
*/
async function scopeTargetsV1(targetAddrs, roleId, roles) {
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.scopeTarget.populateTransaction(hardhat_1.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
*/
async function scopeTargetsV2(targetAddrs, roleId, roles) {
const scopeTargetTxs = await Promise.all(targetAddrs.map(async (target) => {
const tx = await roles.scopeTarget.populateTransaction(hardhat_1.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
*/
exports.encodeBytes32String = hardhat_1.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>}
*/
const setERC20TokenBalance = async (token, address, amount) => {
const value = hardhat_1.ethers.hexlify(amount);
await hardhat_1.network.provider.request({
method: "tenderly_setErc20Balance",
params: [token, address, value.replace("0x0", "0x")],
});
};
exports.setERC20TokenBalance = setERC20TokenBalance;
/**
* Sets gas balance for multiple addresses
* @returns {Promise<void>}
*/
async function setGas() {
const { VIRTUAL_MAINNET_RPC } = env_config_1.default;
let caller;
let manager;
let dummyOwnerOne;
let dummyOwnerTwo;
let dummyOwnerThree;
let security;
[caller, manager, dummyOwnerOne, dummyOwnerTwo, dummyOwnerThree, security] =
await hardhat_1.ethers.getSigners();
const provider = new ethers_1.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
*/
function findPermissionsFiles(whitelistDir) {
if (!fs_1.default.existsSync(whitelistDir)) {
throw new Error(`The directory ${whitelistDir} does not exist.`);
}
let results = [];
// Get absolute path of whitelistDir
const absoluteWhitelistDir = path_1.default.resolve(process.cwd(), whitelistDir);
const files = fs_1.default.readdirSync(absoluteWhitelistDir);
for (const file of files) {
const filePath = path_1.default.join(absoluteWhitelistDir, file);
const stat = fs_1.default.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
*/
function findWhitelistClasses(whitelistDir) {
const project = new ts_morph_1.Project();
const getAllTsFiles = (dir) => {
let results = [];
const list = fs_1.default.readdirSync(dir);
list.forEach(file => {
const filePath = path_1.default.join(dir, file);
const stat = fs_1.default.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 = [];
project.getSourceFiles().forEach(sourceFile => {
const classes = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassDeclaration);
classes.forEach((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
*/
function checkRequiredEnvVariables(requiredVariables) {
const missingVariables = requiredVariables.filter(variable => !(variable in env_config_1.default));
if (missingVariables.length > 0) {
console.log(`Missing required environment variables: ${missingVariables.join(", ")}`);
return false;
}
console.log("All required environment variables are present.");
return true;
}
async function predictRolesModAddress(signer, owner, avatar, target, rolesVersion) {
const chainId = Number(await signer.provider.getNetwork().then((n) => n.chainId));
const encodedInitParams = ethers_1.AbiCoder.defaultAbiCoder().encode(["address", "address", "address"], [owner, avatar, target]);
const rolesContract = rolesVersion == "v1" ? zodiac_1.KnownContracts.ROLES_V1 : zodiac_1.KnownContracts.ROLES_V2;
const moduleSetupData = zodiac_1.ContractFactories[rolesContract]
.createInterface()
.encodeFunctionData("setUp", [encodedInitParams]);
return (0, zodiac_1.calculateProxyAddress)(zodiac_1.ContractFactories[zodiac_1.KnownContracts.FACTORY].connect(zodiac_1.ContractAddresses[chainId][zodiac_1.KnownContracts.FACTORY], signer), zodiac_1.ContractAddresses[chainId][rolesContract], moduleSetupData, exports.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
*/
async function predictSafeAddress(safeProxyFactory, safeMasterCopy, data, saltNonce) {
return await safeProxyFactory.calculateCreateProxyWithNonceAddress(safeMasterCopy, data, saltNonce, {
gasLimit: hardhat_1.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.
*/
async function setUniformBlockNumber(targetBlock) {
const currentBlock = await hardhat_1.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 hardhat_1.network.provider.request({
method: "evm_increaseBlocks",
params: [hardhat_1.ethers.hexlify(blocksToIncrease)],
});
}