UNPKG

tenderly-wizard-v6

Version:

A tool for managing virtual testnets using Tenderly

232 lines (212 loc) 6.71 kB
import { Contract, ContractTransactionReceipt, Provider } from "ethers"; import ROLES_V1_MASTER_COPY_ABI from "../contracts/roles_v1.json"; import ROLES_V2_MASTER_COPY_ABI from "../contracts/roles_v2.json"; import { LedgerSigner } from "@anders-t/ethers-ledger"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; // @ts-ignore import { ethers } from "hardhat"; import { ChainId, Permission, applyTargets, processPermissions, } from "zodiac-roles-sdk"; import { encodeMulti } from "ethers-multisend"; import { ANY, APPROVAL_SIG, EMPTY_BYTES, EQUAL_TO, GAS_LIMIT, MANAGER_ROLE_ID_V2, OPTIONS_SEND, SAFE_OPERATION_DELEGATECALL, SECURITY_ROLE_ID_V2, TYPE_STATIC, ZERO_VALUE, } from "../utils/constants"; import { getChainConfig } from "../utils/roles-chain-config"; import { RolesVersion } from "../utils/types"; import config from "../env-config"; import { RolesV1, RolesV2 } from "@gnosis-guild/zodiac"; export enum ExecutionOptions { None, Send, DelegateCall, Both, } // Make LedgerSigner compatible with ContractRunner by ensuring it has a provider type CompatibleSigner = SignerWithAddress | (LedgerSigner & { provider: Provider | null }); export class Whitelist { roles: RolesV2 | RolesV1; caller: CompatibleSigner; constructor( rolesAddr: string, rolesVersion: RolesVersion, caller: CompatibleSigner ) { if (rolesVersion === "v1") { this.roles = new Contract( rolesAddr, ROLES_V1_MASTER_COPY_ABI, caller as any ) as unknown as RolesV1; } else { this.roles = new Contract( rolesAddr, ROLES_V2_MASTER_COPY_ABI, caller as any ) as unknown as RolesV2; } this.caller = caller; } // roles.scopeTarget helper function async scopeTargets(targetAddrs: string[], roleId: number) { 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 (this.roles as RolesV1).scopeTarget.populateTransaction( roleId, target ); return tx; }) ); return scopeTargetTxs; } // Helper to allows function calls without param scoping async scopeAllowFunctionsV1(target: string, sigs: string[], roleId: number) { const scopeFuncsTxs = await Promise.all( sigs.map(async sig => { // scopeAllowFunction on Roles allows a role member to call the function in question with no paramter scoping const tx = await (this.roles as RolesV1).scopeAllowFunction.populateTransaction( roleId, target, sig, ExecutionOptions.Both ); return tx; }) ); return scopeFuncsTxs; } // Helper to allows function calls without param scoping async scopeAllowFunctionsV2( target: string, sigs: string[], roleId: `0x${string}` ) { const scopeFuncsTxs = await Promise.all( sigs.map(async sig => { // allowFunction on Roles allows a role member to call the function in question with no paramter scoping const tx = await (this.roles as RolesV2).allowFunction.populateTransaction( roleId, target, sig, ExecutionOptions.Both ); return tx; }) ); return scopeFuncsTxs; } // Helper for crafting erc20 approve related permissions async scopeFunctionERC20Approval( contractAddr: string, approvedSpender: string ) { const scopedApproveFunctionTx = await (this.roles as RolesV1).scopeFunction.populateTransaction( MANAGER_ROLE_ID_V2, contractAddr, APPROVAL_SIG, [true, false], [TYPE_STATIC, TYPE_STATIC], [EQUAL_TO, ANY], [approvedSpender, EMPTY_BYTES], OPTIONS_SEND ); return scopedApproveFunctionTx; } } // // Helper to assert a string is a hex address // export function asHexString(address: string): `0x${string}` { // if (!address.startsWith("0x")) throw new Error("Address must start with 0x"); // if (address.length !== 42) throw new Error("Address must be 42 characters"); // return address as `0x${string}`; // } /** * Executes whitelist permissions for a specific chain. * @note This function will work as long as it's connected to a Hardhat Tenderly testnet. * @param permissions Array of Permission objects to be executed * @param chainId The chain ID where the permissions will be applied * @returns Transaction receipt from the execution * */ export async function executeWhitelistV2( permissions: Permission[], chainId: ChainId, rolesVersion: RolesVersion ) { const [ caller, manager, dummyOwnerOne, dummyOwnerTwo, dummyOwnerThree, security, ] = await ethers.getSigners(); // get chain config const chainConfig = getChainConfig(chainId, rolesVersion); // Process any promises in the permissions array const processedPermissions = (await Promise.all( permissions.map(async (permission) => { if (permission instanceof Promise) { return await permission; } return permission; }) )).flat(); // Process the permissions const { targets } = processPermissions(processedPermissions); // Apply the targets const calls = await applyTargets( MANAGER_ROLE_ID_V2 as `0x${string}`, targets, { chainId, address: config.ACCESS_CONTROL_ROLES_ADDRESS! as `0x${string}`, mode: "replace", // or "extend" or "remove" log: console.debug, currentTargets: [], }); console.log(`${calls.length} permissions to execute`); const multiSendTx = encodeMulti( calls.map((data) => { return { to: config.INVESTMENT_ROLES_ADDRESS! as `0x${string}`, value: "0", data: data as `0x${string}`, }; }) ); // Security needs to indirectly execute this bundle via acRoles if (!config.ACCESS_CONTROL_ROLES_ADDRESS) throw new Error("ACCESS_CONTROL_ROLES_ADDRESS is undefined"); const acRoles = new Contract( config.ACCESS_CONTROL_ROLES_ADDRESS! as `0x${string}`, ROLES_V2_MASTER_COPY_ABI, security ); // role members wishing to transact as the Safe will always have to call via execTransactionWithRole const tx = await acRoles.execTransactionWithRole( chainConfig.MULTISEND_ADDR, ZERO_VALUE, multiSendTx.data, SAFE_OPERATION_DELEGATECALL, SECURITY_ROLE_ID_V2, true, { gasLimit: GAS_LIMIT, } ); const receipt: ContractTransactionReceipt = await tx.wait(); console.log("Whitelist executed successfully", receipt.hash); }