UNPKG

tenderly-wizard-v6

Version:

A tool for managing virtual testnets using Tenderly

365 lines (341 loc) 11 kB
import SAFE_MASTER_COPY_ABI from "../contracts/safe_master_copy_v1.json"; import SAFE_MODULE_PROXY_FACTORY_ABI from "../contracts/safe_module_proxy_factory_v1.json"; import ROLES_V1_MASTER_COPY_ABI from "../contracts/roles_v1.json"; import { AccessControllerWhitelistV1 } from "../whitelist/acs/scope-access-controller-v1"; import colors from "colors"; import { addSafeSigners, deploySafeV1, removeDeployerAsOwner, } from "./deploy-safe-v1"; import { tx, SAFE_OPERATION_DELEGATECALL, MANAGER_ROLE_ID_V1, SECURITY_ROLE_ID_V1, SALTS, } from "../utils/constants"; // @ts-ignore import { ethers, network } from "hardhat"; import { createMultisendTx, getPreValidatedSignatures, predictRolesModAddress, SALT, setUniformBlockNumber, } from "../utils/util"; import { ChainConfig } from "../utils/types"; import { getChainConfig } from "../utils/roles-chain-config"; import { ChainId } from "zodiac-roles-sdk/."; import { Contract, ZeroAddress } from "ethers"; import { ContractAddresses, ContractFactories, KnownContracts, calculateProxyAddress, deployAndSetUpModule, } from "@gnosis-guild/zodiac"; //@dev note that hardhat struggles with nested contracts. When we call a Safe to interact with Roles, only events from the Safe can be detected. export async function deployRoles( owner: string, avatar: string, target: string, proxied: boolean, chainId: ChainId, chainConfig: ChainConfig["v1"] ) { const [caller] = await ethers.getSigners(); if (proxied) { // get expected Module Address and transaction const { expectedModuleAddress, transaction } = await deployAndSetUpModule( KnownContracts.ROLES_V1, { types: ["address", "address", "address"], values: [owner, avatar, target], }, caller.provider, chainId, SALT ); const predictedRolesAddress = await predictRolesModAddress( caller, owner, avatar, target, "v1" ); console.log(`prediected roles address: ${predictedRolesAddress}`); // check if address is matching predicted address before processing transaction if (expectedModuleAddress !== predictedRolesAddress) { throw new Error( `Roles mod address deployment unexpected, expected ${predictRolesModAddress}, actual: ${expectedModuleAddress}` ); } // const rolesMaster = new Contract( // chainConfig.ROLES_MASTER_COPY_ADDR, // ROLES_V1_MASTER_COPY_ABI, // caller // ); // const initParams = await rolesMaster.populateTransaction.setUp(encoded); // const tsSalt = new Date().getTime(); // salt must be the same // const safeModuleProxyFactory = new Contract( // chainConfig.SAFE_MODULE_PROXY_FACTORY_ADDR, // SAFE_MODULE_PROXY_FACTORY_ABI, // caller // ); // const deployModTx = await safeModuleProxyFactory.deployModule( // chainConfig.ROLES_MASTER_COPY_ADDR, // initParams.data as string, // tsSalt // ); // const txReceipt = await deployModTx.wait(); // const txData = txReceipt.events?.find( // (x: any) => x.event == "ModuleProxyCreation" // ); // const rolesModAddress = txData?.args?.proxy; try { await caller.sendTransaction(transaction); console.info( colors.green( `✅ Roles was deployed via proxy factory to ${expectedModuleAddress}` ) ); return expectedModuleAddress; } catch (e: any) { console.error(e); throw new Error(`Roles mod address deployment failed: ${e}`); } } const Permissions = await ethers.getContractFactory("Permissions"); const permissions = await Permissions.deploy(); const Roles = await ethers.getContractFactory("Roles", { libraries: { Permissions: permissions.address, }, }); const roles = await Roles.deploy(owner, avatar, target); console.info("Modifier deployed to:", roles.address, "\n"); return roles.address; } //If the roles module is not already enabled on Safe, enable it export async function enableRolesModifier(safeAddr: string, rolesAddr: string) { const [caller] = await ethers.getSigners(); const signature = getPreValidatedSignatures(caller.address); const invSafe = new Contract(safeAddr, SAFE_MASTER_COPY_ABI, caller); const enabled = await invSafe.isModuleEnabled(rolesAddr); console.log( `ℹ️ Roles modifier: ${rolesAddr} is enabled on safe: ${safeAddr} ${enabled}` ); if (!enabled) { const enable = await invSafe.enableModule.populateTransaction(rolesAddr); const enableTx = await invSafe.execTransaction( safeAddr, tx.zeroValue, enable.data ?? "", tx.operation, tx.avatarTxGas, tx.baseGas, tx.gasPrice, tx.gasToken, tx.refundReceiver, signature ); const txReceipt = await enableTx.wait(); const txData = txReceipt.events?.find( (x: any) => x.event == "EnabledModule" ); const moduleEnabledFromEvent = txData?.args?.module; console.info( colors.blue( `ℹ️ Roles modifier: ${moduleEnabledFromEvent} has been enabled on safe: ${safeAddr}` ) ); } else { console.info( `Roles modifier: ${rolesAddr} was already enabled on safe: ${safeAddr}` ); } } // sets the address of the multisend contract export async function setRolesMultisend( safeAddr: string, rolesAddr: string, chainConfig: ChainConfig["v1"] ) { const [caller] = await ethers.getSigners(); const roles = new Contract(rolesAddr, ROLES_V1_MASTER_COPY_ABI, caller); const multisendOnRecord = await roles.multisend(); //If no MS on record, submit a tx to write one on record if (multisendOnRecord === ZeroAddress) { const setMsPopTx = await roles.setMultisend.populateTransaction( chainConfig.MULTISEND_ADDR ); const safe = new Contract(safeAddr, SAFE_MASTER_COPY_ABI, caller); const signature = getPreValidatedSignatures(caller.address); await safe.execTransaction( rolesAddr, tx.zeroValue, setMsPopTx.data, tx.operation, tx.avatarTxGas, tx.baseGas, tx.gasPrice, tx.gasToken, tx.refundReceiver, signature ); console.info( colors.blue( `ℹ️ Multisend has been set to: ${chainConfig.MULTISEND_ADDR}` ) ); } else { console.info( `Multisend has already been previously set to: ${multisendOnRecord}` ); } } // assign a role to a array of members addresses attached to a role id policy export async function assignRoles( safeAddr: string, rolesAddr: string, memberAddrs: string[], roleId: number, chainConfig: ChainConfig["v1"] ) { const [caller] = await ethers.getSigners(); // assign manager a role (becomes a member of role:manager_role_id) const roles = new Contract(rolesAddr, ROLES_V1_MASTER_COPY_ABI, caller); const signature = getPreValidatedSignatures(caller.address); const acSafe = new Contract(safeAddr, SAFE_MASTER_COPY_ABI, caller); const assignRolesPopTx = await Promise.all( memberAddrs.map(async memberAddr => { return await roles.assignRoles.populateTransaction( memberAddr, [roleId], [true] ); }) ); const metaTxs = createMultisendTx( assignRolesPopTx, chainConfig.MULTISEND_ADDR ); await acSafe.execTransaction( chainConfig.MULTISEND_ADDR, tx.zeroValue, metaTxs.data, SAFE_OPERATION_DELEGATECALL, tx.avatarTxGas, tx.baseGas, tx.gasPrice, tx.gasToken, tx.refundReceiver, signature ); console.info( colors.blue( `Role member: ${memberAddrs.toString()} has been assigned role id: ${roleId}` ) ); } // this will deploy the entire system from scratch, WITHOUT any investment manager permissions export const deployAccessControlSystemV1 = async ( chainId: ChainId, options: { proxied: boolean; sysAdminAddresses: string[]; acSafeThreshold: number; invSafeThreshold: number; securityEOAs: string[]; managerEOAs: string[]; }, deployed?: { acSafeAddr: string | null; invSafeAddr: string | null; invRolesAddr: string | null; acRolesAddr: string | null; } ) => { // get chain config for multichain deploy const chainConfig = getChainConfig(chainId, "v1"); //Deploy both safes const accessControlSafeAddr = deployed?.acSafeAddr || (await deploySafeV1(chainConfig, SALTS.safes.accessControl)); const investmentSafeAddr = deployed?.invSafeAddr || (await deploySafeV1(chainConfig, SALTS.safes.investment)); // //Deploy and enable a Roles modifier on the investment safe const invRolesAddr = deployed?.invRolesAddr || (await deployRoles( accessControlSafeAddr, investmentSafeAddr, investmentSafeAddr, options.proxied, chainId, chainConfig )); await enableRolesModifier(investmentSafeAddr, invRolesAddr); //Set the multisend address on roles so that manager can send multisend txs later on await setRolesMultisend(accessControlSafeAddr, invRolesAddr, chainConfig); //Deploy and enable a Roles modifier on the access control safe const acRolesAddr = deployed?.acRolesAddr || (await deployRoles( accessControlSafeAddr, accessControlSafeAddr, accessControlSafeAddr, options.proxied, chainId, chainConfig )); await enableRolesModifier(accessControlSafeAddr, acRolesAddr); //Set the multisend address on roles so that manager can send multisend txs later on await setRolesMultisend(accessControlSafeAddr, acRolesAddr, chainConfig); //Grant an access controller role to Security EOA's await assignRoles( accessControlSafeAddr, acRolesAddr, options.securityEOAs, SECURITY_ROLE_ID_V1, chainConfig ); // Populate this role for Security so they can call whitelisting related functions on investment roles const [caller] = await ethers.getSigners(); const accessControllerWhitelist = new AccessControllerWhitelistV1( acRolesAddr, caller ); await accessControllerWhitelist.execute(invRolesAddr, accessControlSafeAddr); //Grant a role to the investment managers EOAs //the idea would be that each strategy would be transacted on by 1 EOA await assignRoles( accessControlSafeAddr, invRolesAddr, options.managerEOAs, MANAGER_ROLE_ID_V1, chainConfig ); // Add signers await addSafeSigners( investmentSafeAddr, options.sysAdminAddresses, chainConfig ); await addSafeSigners( accessControlSafeAddr, options.sysAdminAddresses, chainConfig ); //Remove the deployer address as owner and rewrite signing threshold await removeDeployerAsOwner(investmentSafeAddr, options.invSafeThreshold); await removeDeployerAsOwner(accessControlSafeAddr, options.acSafeThreshold); return { acSafe: accessControlSafeAddr, invSafe: investmentSafeAddr, invRoles: invRolesAddr, acRoles: acRolesAddr, }; };