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