diff --git a/package.json b/package.json index bb8a3220..14ed78c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@open-ibc/vibc-core-smart-contracts", - "version": "4.0.6", + "version": "4.0.7", "main": "dist/index.js", "bin": { "verify-vibc-core-smart-contracts": "./dist/scripts/verify-contract-script.js", diff --git a/src/evm/schemas/account.ts b/src/evm/schemas/account.ts index bf60ad0d..f0a42af6 100644 --- a/src/evm/schemas/account.ts +++ b/src/evm/schemas/account.ts @@ -4,7 +4,7 @@ import fs from "fs"; import path from "path"; import { Registry } from "../../utils/registry"; import { renderString } from "../../utils/io"; -import { initializedMultisig } from "./multisig"; +import { initializedMultisigConfig, uninitializedMultisigConfig } from "./multisig"; import { isMnemonic, isPrivateKey, @@ -24,7 +24,7 @@ const keyStore = z.object({ export type KeyStore = z.infer; export const evmAccounts = z.array( - z.union([singleSigAccount, initializedMultisig]) + z.union([singleSigAccount, initializedMultisigConfig, uninitializedMultisigConfig]) ); // Type of account that one can send transactions from export type EvmAccounts = z.infer; export const EvmAccountsConfig = z.union([evmAccounts, keyStore]); @@ -104,7 +104,7 @@ export class SingleSigAccountRegistry extends Registry { // load a Map of { [name: string]: Wallet } from EvmAccountsSchema object export function loadEvmAccounts(config: unknown): Registry { if (!isEvmAccountsConfig(config)) { - throw new Error(`Error parsing schema: ${config}`); + throw new Error(`Error parsing schema: ${config}: \n ${EvmAccountsConfig.safeParse(config).error}`); } const walletMap = new Registry([]); diff --git a/src/evm/schemas/multisig.ts b/src/evm/schemas/multisig.ts index 14a955c4..1a0bceef 100644 --- a/src/evm/schemas/multisig.ts +++ b/src/evm/schemas/multisig.ts @@ -1,37 +1,50 @@ import { z } from 'zod'; -import {singleSigAccount, wallet} from './wallet'; +import { wallet } from './wallet'; -// defined in an account spec, which will be cconvertedi nto an initialized multisig config once we deploy the multisig contract -export const uninitializedMultisigConfig = z.object({ - name: z.string().min(1), - chainId: z.number(), - owners: z.array(z.string().min(1)), - signer: singleSigAccount -}) .strict() +// defined in an account spec, which will be converted into an initialized multisig config once we deploy the multisig contract +export const uninitializedMultisigConfig = z + .object({ + name: z.string().min(1), + privateKey: z.string().min(1), + owners: z.array(z.string().min(1)), + threshold: z.number(), + }) + .strict(); // Defined in an account spec, which is not necessarily in a config -export const initializedMultisigConfig = z.object({ - name: z.string().min(1), - chainId: z.number(), - safeAddress: z.string().min(1), - signer: singleSigAccount -}) .strict() - +export const initializedMultisigConfig = z + .object({ + name: z.string().min(1), + chainId: z.number(), + privateKey: z.string().min(1), + safeAddress: z.string().min(1), + }) + .strict(); // Multisig which is described in an account spec but is not yet initialized. (i.e. multisig contract has not been deployed yet) -export const unInitializedMultisig = z.intersection( - uninitializedMultisigConfig, - z.object({wallet: wallet}) -) +export const unInitializedMultisig = z.object({ + name: z.string().min(1), + privateKey: z.string().min(1), + owners: z.array(z.string().min(1)), + threshold: z.number(), + wallet: wallet, +}); // Multisig which has been deployed & can be used to propose transactions. This is the type that loadEvmAccounts will return for multisig types -export const initializedMultisig = z.intersection( - initializedMultisigConfig, - z.object({wallet: wallet}) -); +export const initializedMultisig = z.object({ + name: z.string().min(1), + chainId: z.number(), + privateKey: z.string().min(1), + safeAddress: z.string().min(1), + wallet: wallet, +}); -export type UninitializedMultisigConfig = z.infer; -export type InitializedMultisigConfig = z.infer; +export type UninitializedMultisigConfig = z.infer< + typeof uninitializedMultisigConfig +>; +export type InitializedMultisigConfig = z.infer< + typeof initializedMultisigConfig +>; export type UninitializedMultisig = z.infer; export type InitializedMultisig = z.infer; @@ -41,7 +54,7 @@ export const isUninitializedMultisigConfig = ( ): account is UninitializedMultisigConfig => { return uninitializedMultisigConfig.safeParse(account).success; }; - + export const isUninitializedMultisig = ( account: unknown ): account is UninitializedMultisig => { @@ -60,10 +73,17 @@ export const isInitializedMultisig = ( return initializedMultisig.safeParse(account).success; }; -export const isMultisig = (account: unknown): account is InitializedMultisig | UninitializedMultisig => { +export const isMultisig = ( + account: unknown +): account is InitializedMultisig | UninitializedMultisig => { return isInitializedMultisig(account) || isUninitializedMultisig(account); }; -export const isMultisigConfig = (account: unknown): account is InitializedMultisigConfig | UninitializedMultisigConfig => { - return isInitializedMultisigConfig(account) || isUninitializedMultisigConfig(account); -} +export const isMultisigConfig = ( + account: unknown +): account is InitializedMultisigConfig | UninitializedMultisigConfig => { + return ( + isInitializedMultisigConfig(account) || + isUninitializedMultisigConfig(account) + ); +}; diff --git a/src/evm/schemas/sendingAccount.ts b/src/evm/schemas/sendingAccount.ts index da3da850..9a110b90 100644 --- a/src/evm/schemas/sendingAccount.ts +++ b/src/evm/schemas/sendingAccount.ts @@ -2,6 +2,7 @@ import { fs } from "zx"; import { Registry } from "../../utils/registry"; import { createWallet, + EvmAccountsConfig, isEvmAccounts, isEvmAccountsConfig, isKeyStore, @@ -9,10 +10,9 @@ import { import { isPrivateKey, isSingleSigAccount, Wallet } from "./wallet"; import { InitializedMultisig, - isInitializedMultisig, isMultisig, isMultisigConfig, - isUninitializedMultisig, + unInitializedMultisig, UninitializedMultisig, } from "./multisig"; import path from "path"; @@ -33,7 +33,10 @@ export class SendingAccountRegistry extends Registry { } static loadMultiple(registryItems: { name: string; registry: any }[]) { - const result = new Registry([] as SendingAccountRegistry[], { + const result = new Registry< + SendingAccountRegistry, + { name: string; registry: any } + >([], { toObj: (t) => { return { name: t.name, registry: t.serialize() }; }, @@ -78,20 +81,17 @@ export class SendingAccountRegistry extends Registry { const account = this.mustGet(accountName); if (isSingleSigAccount(account) && isPrivateKey(account)) { return account.privateKey; - } else if ( - isInitializedMultisig(account) || - isUninitializedMultisig(account) - ) { + } else if (isMultisig(account)) { return account.wallet.privateKey; } throw new Error( `Can't find private key for ${accountName} in this registry` ); }; + // Connect all accounts to the provider public connectProviderAccounts = (rpc: string) => { const provider = ethers.getDefaultProvider(rpc); - // const newAccounts = this.subset([]); for (const [name, account] of this.entries()) { if (isMultisig(account)) { const newMultisigWallet = { @@ -107,10 +107,15 @@ export class SendingAccountRegistry extends Registry { }; } -// Load a map of evm accounts from a config through connecting wallets, can either take in sending accounts or not +// Load a map of evm accounts from a config through connecting wallets, can either take in sending accounts a single sig account +// This will convert either from MultisigAccountConfig -> MultisigAccount or SingleSigAccountConfig -> Wallet export function loadSendingAccounts(config: unknown): Registry { if (!isEvmAccountsConfig(config)) { - throw new Error(`Error parsing schema: ${config}`); + throw new Error( + `Error parsing schema: ${config} \n ${ + EvmAccountsConfig.safeParse(config).error + }` + ); } const walletMap = new Registry([]); @@ -118,10 +123,13 @@ export function loadSendingAccounts(config: unknown): Registry { if (isEvmAccounts(config)) { for (const account of config) { if (isMultisigConfig(account)) { - const wallet = createWallet(account.signer); - const multisigAccount: InitializedMultisig = { + const wallet = createWallet({ + name: account.name, + privateKey: account.privateKey, + }); + const multisigAccount: InitializedMultisig | UninitializedMultisig = { ...account, - wallet: wallet, + wallet, }; walletMap.set(account.name, multisigAccount); } else if (isSingleSigAccount(account)) { diff --git a/src/multisig/safe.ts b/src/multisig/safe.ts index f83c590b..67f87ec3 100644 --- a/src/multisig/safe.ts +++ b/src/multisig/safe.ts @@ -17,8 +17,6 @@ export const newSafeFromOwner = async ( owners: string[], threshold: number ) => { - // TODO: check owners is indeed an array and not a string (for edge case of one address) - const safeFactory = await SafeFactory.init({ provider: RPC_URL, signer: ownerKey, diff --git a/src/scripts/deploy-multisig.ts b/src/scripts/deploy-multisig.ts index 368ec039..f1e528b6 100644 --- a/src/scripts/deploy-multisig.ts +++ b/src/scripts/deploy-multisig.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { ethers } from "ethers"; +import { ethers, toBigInt } from "ethers"; import { SingleSigAccountRegistry, parseObjFromFile } from ".."; import { newSafeFromOwner } from "../multisig/safe"; @@ -7,9 +7,11 @@ import { parseMultisigInitArgsFromCLI, saveMultisigAddressToAccountsSpec, } from "../utils/io"; +import { SendingAccountRegistry } from "../evm/schemas/sendingAccount"; +import { isUninitializedMultisig } from "../evm/schemas/multisig"; async function main() { - const { rpcUrl, owners, initiator, accountsSpecPath, threshold } = + const { rpcUrl, initiator, accountsSpecPath, chainId } = await parseMultisigInitArgsFromCLI(); const accountConfigFromYaml = { @@ -17,30 +19,43 @@ async function main() { registry: parseObjFromFile(accountsSpecPath), }; - const accounts = SingleSigAccountRegistry.loadMultiple([ + const accounts = SendingAccountRegistry.loadMultiple([ accountConfigFromYaml, ]).mustGet("multisig-accounts"); + const multisigAccount = accounts.mustGet(initiator); + + if (!isUninitializedMultisig(multisigAccount)) { + throw new Error( + "Account read from yaml but isn't a multisig account that needs to be initialized." + ); + } + const senderPrivateKey = accounts.getSinglePrivateKeyFromAccount(initiator); if (!senderPrivateKey) { throw new Error(`Could not find private key for owner ${initiator}`); } + const provider = new ethers.JsonRpcProvider(rpcUrl); + const providerChainId = (await provider.getNetwork()).chainId; + if (!providerChainId || providerChainId !== toBigInt(chainId)) { + throw new Error( + `Chain id mismatch between multisig account and rpc url. ${chainId} is specified in accounts spec, but ${providerChainId} is the chain id of the rpc url` + ); + } + const newSafeAddress = await newSafeFromOwner( rpcUrl, senderPrivateKey, - owners, - threshold + multisigAccount.owners, + multisigAccount.threshold ); - const provider = new ethers.JsonRpcProvider(rpcUrl); - const chainId = (await provider.getNetwork()).chainId; - await saveMultisigAddressToAccountsSpec( newSafeAddress, accountsSpecPath, - initiator, - chainId + chainId, + initiator ); } diff --git a/src/scripts/execute-multisig-tx.ts b/src/scripts/execute-multisig-tx.ts index 2a4fe92d..df9c7473 100644 --- a/src/scripts/execute-multisig-tx.ts +++ b/src/scripts/execute-multisig-tx.ts @@ -1,23 +1,22 @@ #!/usr/bin/env node -import { AccountRegistry, parseObjFromFile } from ".."; -import { executeMultisigTx} from "../multisig/safe"; +import { parseObjFromFile } from ".."; +import { executeMultisigTx } from "../multisig/safe"; -import { - parseExecuteMultisigTxArgsFromCLI, -} from "../utils/io"; -import { isParsedMultiSigWallet } from "../evm/schemas/account"; +import { parseExecuteMultisigTxArgsFromCLI } from "../utils/io"; +import { isInitializedMultisig } from "../evm/schemas/multisig"; +import { SendingAccountRegistry } from "../evm/schemas/sendingAccount"; async function main() { const { executor, rpcUrl, txIndex, accountsSpecPath } = await parseExecuteMultisigTxArgsFromCLI(); - const accounts = AccountRegistry.load( + const accounts = SendingAccountRegistry.load( parseObjFromFile(accountsSpecPath), "multisig-accounts" ); const multisigAccount = accounts.mustGet(executor); - if (!isParsedMultiSigWallet(multisigAccount)) { + if (!isInitializedMultisig(multisigAccount)) { throw new Error("Can only execute transactions on a multisig wallet"); } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 98d8ae35..6d95de8b 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -44,8 +44,8 @@ export const UPDATE_SPECS_PATH = process.env.UPDATE_SPECS_PATH ? process.env.UPDATE_SPECS_PATH : path.resolve(SPECS_BASE_PATH, "update.spec.yaml"); -export const ACCOUNTS_SPECS_PATH = process.env.ACCOUNTS_SPECS_PATH - ? process.env.ACCOUNTS_SPECS_PATH +export const ACCOUNT_SPECS_PATH = process.env.ACCOUNT_SPECS_PATH + ? process.env.ACCOUNT_SPECS_PATH : path.resolve(SPECS_BASE_PATH, "evm.accounts.yaml"); export const EXTRA_BINDINGS_PATH = process.env.EXTRA_BINDINGS_PATH; diff --git a/src/utils/io.ts b/src/utils/io.ts index ef9bebb5..9b09e705 100644 --- a/src/utils/io.ts +++ b/src/utils/io.ts @@ -11,7 +11,7 @@ import { Chain, ChainConfigSchema, ChainFolderSchema } from '../evm/chain'; import { DEPLOYMENTS_PATH, ARTIFACTS_PATH, - ACCOUNTS_SPECS_PATH, + ACCOUNT_SPECS_PATH, CHAIN_NAME, CHAIN_ID, RPC_URL, @@ -140,10 +140,10 @@ export function parseZodSchema( `parsing ${className} failed. ${zErr.issues .map((i) => i.path) .join(', ')}: ${zErr.message}\nconfig obj:\n${JSON.stringify( - config, - null, - 2 - )}` + config, + null, + 2 + )}` ); } else { throw e; @@ -273,7 +273,7 @@ export async function writeDeployedContractToFile( // Read existing accounts into env export async function readAccountsIntoEnv( env: any, - accountRegistry: SingleSigAccountRegistry| SendingAccountRegistry + accountRegistry: SingleSigAccountRegistry | SendingAccountRegistry ) { accountRegistry.keys().forEach((accountName) => { env[accountName] = accountRegistry.mustGet(accountName); @@ -389,7 +389,7 @@ export async function parseArgsFromCLI() { const deploymentEnvironment = argv1.DEPLOYMENT_ENVIRONMENT || DEPLOYMENT_ENVIRONMENT; const accountSpecs = - (argv1.ACCOUNT_SPECS_PATH as string) || ACCOUNTS_SPECS_PATH; + (argv1.ACCOUNT_SPECS_PATH as string) || ACCOUNT_SPECS_PATH; const updateSpecs = (argv1.UPDATE_SPECS_PATH as string) || UPDATE_SPECS_PATH; const anvilPort = (argv1.ANVIL_PORT as string) || ANVIL_PORT; @@ -434,27 +434,18 @@ export async function parseArgsFromCLI() { } export const parseMultisigInitArgsFromCLI = async () => { - const argv1 = await yargs(hideBin(process.argv)).option('OWNERS', { - alias: 'o', - description: 'Owners to init multisig safe with', - type: 'array', - string: true, - }).argv; + const argv1 = await yargs(hideBin(process.argv)).argv; const rpcUrl = (argv1.RPC_URL as string) || RPC_URL; - const owners = argv1.OWNERS as string[]; - const threshold = argv1.THRESHOLD as number; const initiator = argv1.INITIATOR as string; const accountsSpecPath = - (argv1.ACCOUNTS_SPECS_PATH as string) || ACCOUNTS_SPECS_PATH; - - // TODO: validate args + (argv1.ACCOUNT_SPECS_PATH as string) || ACCOUNT_SPECS_PATH; + const chainId = (argv1.CHAIN_ID as number) || CHAIN_ID; return { rpcUrl, - owners, initiator, accountsSpecPath, - threshold, + chainId, }; }; @@ -464,7 +455,7 @@ export async function parseExecuteMultisigTxArgsFromCLI() { const TX_INDEX = argv1.TX_INDEX as number; const rpcUrl = (argv1.RPC_URL as string) || RPC_URL; const accountsSpecPath = - (argv1.ACCOUNTS_SPECS_PATH as string) || ACCOUNTS_SPECS_PATH; + (argv1.ACCOUNT_SPECS_PATH as string) || ACCOUNT_SPECS_PATH; if (!executor || !TX_INDEX) { throw new Error('executor and txIndex must be provided'); @@ -523,23 +514,29 @@ export const parseVerifyArgsFromCLI = async () => { export const saveMultisigAddressToAccountsSpec = async ( newSafeAddress: string, accountsSpecPath: string, - ownerName: string, // Used to find which owner to write to - chainId: BigNumberish + chainId: number, + ownerName: string // Used to find which owner to write to, + ) => { // TODO: Currently this yaml lib doesn't include comments - we need to figure out a way to preserve comments / whitespaces, etc const yamlFile = readYamlFile(accountsSpecPath); - const owner = yamlFile.find((account: any) => account.name === ownerName); - if (!owner) { - throw new Error(`Could not find owner ${ownerName} in accounts spec`); - } - - yamlFile.push({ - ...owner, - safeAddress: newSafeAddress, - chainId: chainId, - name: `${ownerName}_MULTISIG`, + let matchFound = false; + const newYamlFile = yamlFile.map((account: any) => { + if (account.name !== ownerName) { + return account; + } + matchFound = true; + return { + name: `${account.name}_MULTISIG`, + chainId, + safeAddress: newSafeAddress, + privateKey: account.privateKey, + }; }); - await writeYamlFile(accountsSpecPath, yamlFile); + if (!matchFound) { + throw new Error(`Could not find owner ${ownerName} in accounts spec`); + } + await writeYamlFile(accountsSpecPath, newYamlFile); };