From 3d3797dbc300ea24c1b758ab5b0a59ba45ed5aa0 Mon Sep 17 00:00:00 2001 From: Marcin Wachulski <94049172+marcin-trust@users.noreply.github.com> Date: Wed, 26 Jan 2022 11:57:10 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A1=20Multisig=20leveraging=20dry-runs?= =?UTF-8?q?=20(#85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Dry-run like multisig - config and signer * Simplify multisig Leverage 1) dry-run 2) tx batch replication to safe * Log text cleanup, colors Removing unnecessary code * Updating addresses of deployed contracts * Fix logging tests * Fix example project - logic * Fix checking for if contract code changed * Remove code not needed after simplification * Fix linting * Add changeset * Rename fn after vanruch remark --- .changeset/gorgeous-moons-deny.md | 5 + .gitignore | 1 + packages/example/deployments.json | 81 +++-------- packages/example/package.json | 6 +- packages/example/src/multisig.ts | 78 +++------- packages/mars/deployments.json | 10 ++ packages/mars/src/actions.ts | 13 -- packages/mars/src/context.ts | 5 +- packages/mars/src/deploy.ts | 15 +- packages/mars/src/execute/bytecode.ts | 3 + packages/mars/src/execute/execute.ts | 128 +++++++---------- packages/mars/src/execute/sendTransaction.ts | 11 +- packages/mars/src/logging.ts | 14 +- packages/mars/src/multisig/index.ts | 3 +- packages/mars/src/multisig/multisig.ts | 125 +++++----------- packages/mars/src/multisig/multisigConfig.ts | 3 + packages/mars/src/multisig/multisigState.ts | 55 -------- packages/mars/src/options/Options.ts | 4 + packages/mars/src/options/config.ts | 51 ++++--- packages/mars/src/symbols.ts | 1 + packages/mars/src/syntax/artifact.ts | 5 +- packages/mars/src/syntax/contract.ts | 2 - packages/mars/src/syntax/createProxy.ts | 38 ++--- packages/mars/src/syntax/multisig.ts | 141 ------------------- packages/mars/test/e2e/multisig.e2e.ts | 43 +----- packages/mars/test/syntax/contract.test.ts | 34 +---- packages/mars/test/syntax/log.test.ts | 33 +++-- packages/mars/test/utils/testDeploy.ts | 1 + 28 files changed, 260 insertions(+), 649 deletions(-) create mode 100644 .changeset/gorgeous-moons-deny.md delete mode 100644 packages/mars/src/multisig/multisigState.ts delete mode 100644 packages/mars/src/syntax/multisig.ts diff --git a/.changeset/gorgeous-moons-deny.md b/.changeset/gorgeous-moons-deny.md new file mode 100644 index 0000000..ad1e8e5 --- /dev/null +++ b/.changeset/gorgeous-moons-deny.md @@ -0,0 +1,5 @@ +--- +'ethereum-mars': patch +--- + +Rework multisig on dry-run foundation diff --git a/.gitignore b/.gitignore index 3f8a747..06760f7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ docs/source/_build .vscode cache .nyc_output +tsconfig.tsbuildinfo diff --git a/packages/example/deployments.json b/packages/example/deployments.json index da82708..5cf2b6b 100644 --- a/packages/example/deployments.json +++ b/packages/example/deployments.json @@ -1,84 +1,39 @@ { - "kovan": { - "dai": { - "txHash": "0x7c058a68e08739b173f5acace0e6fa29dd95aafc5601733fef46bfe4de37ca6f", - "address": "0x0a3BDD55AbA03BE21365A164bac541845f2412AF" - }, - "btc": { - "multisig": true, - "txHash": "0x1234abcd....", - "address": "0x28562C8c670BdA5AA186156Fff49F07C2Ff0124F" - }, - "market": { - "multisig": true, - "txHash": "0x1234abcd....", - "address": "0x51Ef1C90C02bE44d8609EAc9A3Fe30b7B907fB9c" - }, - "dai_proxy": { - "txHash": "0xe2b80e23d3a90278e0f0e623d74802c00084872e15ad5950480d2f0004c4f6fc", - "address": "0xD6c9fb590c15EA0B8FF016934488459E1578C66c" - }, - "btc_proxy": { - "txHash": "0x28e6d21a3e8f224967ba3a7d09c9a1298f81f8cad450085949530826399727a9", - "address": "0x92146cB5fCC0fCA0d1A0E24f28Ebc447D73983E1" - } - }, "rinkeby": { - "preProxied": { - "address": "0x4e2A5FBcc6F4D9084078DD22572F68508db58cde", - "multisig": true - }, - "preProxied_proxy": { - "address": "0x6477449B2DEe45061A07f50D1ADf4F14745250e2", - "multisig": true - }, - "firstImpl": { - "address": "0x1f9b25e82411d6833258E12299D415E30070349b", - "multisig": true - }, - "secondImpl": { - "address": "0x2dAF1283F1fec9430c7B2BE3aC6de91316e2c13F", - "multisig": true - }, "firstImpl_proxy": { - "address": "0xD16e947D18aa823891c364Ca6C3859dcB810F94f", + "txHash": "0x1ea5d560f381f1e01dff325155a890028f3e78cc03eb8e17809abf924d9873ea", + "address": "0xf303e0312e5bC76B88CC4128768F0c7CdA9854c2", "multisig": true }, "secondImpl_proxy": { - "address": "0xd5BfA8d7aE023c7a400c75F438C44CD0298E1B66", + "txHash": "0x719616a5ecbc1e25b1b9daaab17aaf888790d341125136fdd48e9c82e960b47b", + "address": "0x4D800bd33970537c6BE38267FAc39b30a49b3098", "multisig": true }, "firstBare": { - "address": "0x7d78C83E8D44282644443585DF17777a897a1eCc", + "txHash": "0xb5280556a3364f0e0522684e090c5ce5df46f58547a907804df748ca39a47ece", + "address": "0x606726A56294a0f3b6dFDDaCA45F1bb2a0769449", "multisig": true }, "secondBare": { - "address": "0xd08E76c8bE999577af1a51DA63D7c5be1407038f", + "txHash": "0xba6dfc4447d35eb2190aab14c0467609230bc61b32893d6cef760e98bccfe301", + "address": "0x5bE964d739e9934b7BdF76D00f6A28d491d8BEb1", "multisig": true }, - "firstMarket": { - "address": "0x2AD49DfB0F0F2Eb12722cc9F4De662bDE1720fA7", + "market": { + "txHash": "0x43742bc4f33a033ef409bb498d83c4b6410f93dc4f62fc64acaf8d26019fbb3d", + "address": "0x2B39d9F9173A0BD90f4b91895140788d5789995F", "multisig": true }, - "secondMarket": { - "address": "0x1D68D6dBf3e1905c5e60cFe75EEb372349425ca2", + "firstImpl": { + "txHash": "0x2ab7fe540a49d0fca122b80ad1243b8eae907e5b6e4aef2343cc1bc5f48b2471", + "address": "0x7E080543BBCBB2549CA9D8C3A7f8cec82D635D86", "multisig": true }, - "_multisig": { - "Contract creation, proxying and initialization": { - "id": "0xa3347af848a3ba5824a3b22fd9d57c64787999091c77a4b7b7cf2e0fc933a2dd", - "state": "EXECUTED", - "txHash": "0xde1973d64ea04fd7047c9801cc18e19fd97bf7e6f05e49251878053eacdd88b8" - }, - "Conditional initialization": { - "id": "0x99f9ab9fe81f6e1d2a2ac124597dce3a2feed808a78d00e945af8a5a8181c522", - "state": "EXECUTED", - "txHash": "0x8abc9b1742e9315965bbde886a9e6920dea9fd1026b17a0efac2ca65a70bdd23" - }, - "Cross-dependant initialization multisig": { - "id": "0xca09e75db41176d9e5795c1b3a85b5e848ccfbc9294d93a8495ef72270d6f9cf", - "state": "PROPOSED" - } + "secondImpl": { + "txHash": "0xe6d469862e639df2e5d81546f86b3444a5a5e7f032c531ffa73adceaf134618a", + "address": "0x676DE87a94113a956bf4F08CCa1F430969B49e64", + "multisig": true } } } diff --git a/packages/example/package.json b/packages/example/package.json index 6f7bd18..acf8112 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -11,11 +11,11 @@ "lint:fix": "yarn lint:prettier --write && yarn lint:eslint --fix", "lint:eslint": "eslint './{src,test}/**/*.ts'", "lint:prettier": "prettier './{src,test}/**/*.ts'", - "build": "yarn waffle && mars", + "build": "yarn waffle && mars && tsc", "deploy": "ts-node src/index.ts", "deploy:multisig": "ts-node src/multisig.ts", - "test": "echo tests", - "cover": "echo tests" + "test": "echo no tests here", + "cover": "echo no tests here" }, "dependencies": { "ethereum-mars": "0.x" diff --git a/packages/example/src/multisig.ts b/packages/example/src/multisig.ts index 984d4e5..9875993 100644 --- a/packages/example/src/multisig.ts +++ b/packages/example/src/multisig.ts @@ -1,46 +1,24 @@ import { contract, createProxy, debug, deploy, Options, runIf } from 'ethereum-mars' import { Market, Token, UpgradeabilityProxy } from '../build/artifacts' -import ganache from 'ganache-core' -import { Wallet } from 'ethers' -import { multisig } from 'ethereum-mars/build/src/syntax/multisig' import { logConfig } from 'ethereum-mars/build/src/logging' -const inMemoryRunOptions = ((): Options => { - const wallet = Wallet.createRandom() - const provider = ganache.provider({ - locked: true, - gasPrice: '0', - accounts: [{ secretKey: wallet.privateKey, balance: '100000000000000000000' }], - }) - return { - dryRun: true, - network: provider, - fromAddress: wallet.address, - } -})() - -const options = process.argv.indexOf('--network') < 0 ? inMemoryRunOptions : {} -options.multisigGnosisSafe = '0x8772CD484C059EC5c61459a0abb5A45ece16701f' -options.multisigGnosisServiceUri = 'https://safe-transaction.rinkeby.gnosis.io' -logConfig.mode.console = true +const options = { + network: 'rinkeby', + logFile: 'tx.log', + noConfirm: true, + multisig: true, + multisigGnosisSafe: '0x8772CD484C059EC5c61459a0abb5A45ece16701f', + multisigGnosisServiceUri: 'https://safe-transaction.rinkeby.gnosis.io', +} as Options +logConfig.mode.console = false // based upon https://github.com/trusttoken/smart-contracts/blob/main/deploy/truefi2.ts // to reproduce complexity level of the standard deployment script -deploy(options, (deployer, config) => { +deploy(options, (deployer) => { debug(`Deployer is ${deployer}`) - const isRinkeby = config.networkName === 'rinkeby' - const useMultisig = isRinkeby - const proxyCreationPhase = true - - const creationMultisig = useMultisig ? multisig('Contract creation, proxying and initialization') : undefined - const proxy = createProxy(UpgradeabilityProxy) - // existing contracts, already deployed - const wellKnown = isRinkeby ? '0x124BCA8F86a1eC3b84d68BEDB0Cc640D301C3eEF' : contract('wellKnown', Token) - const preProxied = proxy(contract('preProxied', Token), { noImplUpgrade: proxyCreationPhase }) - // new contract implementations const firstImpl = contract('firstImpl', Token) const secondImpl = contract('secondImpl', Token) @@ -49,20 +27,15 @@ deploy(options, (deployer, config) => { const firstProxied = proxy(firstImpl, { onInitialize: 'initialize', params: [112233], - noImplUpgrade: proxyCreationPhase, }) - const secondProxied = proxy(secondImpl, { noImplUpgrade: proxyCreationPhase }) + const secondProxied = proxy(secondImpl) // new bare contracts const firstBare = contract('firstBare', Token) const secondBare = contract('secondBare', Token) // contracts that depend on previous deployments in order to construct - const firstMarket = contract('firstMarket', Market, [wellKnown, preProxied]) - const secondMarket = contract('secondMarket', Market, [firstProxied, secondBare]) - - creationMultisig?.propose() - const conditionalInitMultisig = useMultisig ? multisig('Conditional initialization') : undefined + const market = contract('market', Market, [firstProxied, secondBare]) // contract initialization runIf(firstProxied.isInitialized().not(), () => { @@ -77,24 +50,17 @@ deploy(options, (deployer, config) => { runIf(secondBare.val().equals(0), () => { secondBare.initialize(222) }) - // the following one is to show initialization has not happened in the multisig yet - // we need to move the below to a separate multisig group runIf(secondBare.val().equals(222), () => { secondBare.initialize(333) }) - - conditionalInitMultisig?.propose() - const crossDependantInitializationMultisig = useMultisig - ? multisig('Cross-dependant initialization multisig') - : undefined - - // to show dependencies on initialization of other contracts - firstProxied.approve(secondMarket, 50000) - secondBare.approve(secondMarket, 50000) - secondMarket.supply(11111, 22222) - runIf(firstMarket.xToken().equals(0).not().and(secondBare.val().equals(0).not()), () => { - secondMarket.supply(33333, 22222) - }) - - crossDependantInitializationMultisig?.propose() + debug('Balance in firstProxied', firstProxied.balanceOf(deployer)) + debug('Balance in secondBare', secondBare.balanceOf(deployer)) + firstProxied.approve(market, 11111) + secondBare.approve(market, 22222) + runIf( + firstProxied.allowance(deployer, market).gte(11111).and(secondBare.allowance(deployer, market).gte(22222)), + () => { + market.supply(11111, 22222) + } + ) }).then() diff --git a/packages/mars/deployments.json b/packages/mars/deployments.json index e49f37e..61bb513 100644 --- a/packages/mars/deployments.json +++ b/packages/mars/deployments.json @@ -1,4 +1,14 @@ { "rinkeby": { + "impl_proxy": { + "txHash": "0xccdb5085057d2c66c42d5c48ebe011149eeee1ed7ca06c4202fd9d58d88d01ee", + "address": "0xCBF89991D0FF2b4CfB971a1F2604143225068072", + "multisig": true + }, + "impl": { + "txHash": "0xd394543f4e0cb585ec7a0571b08ee87a2d331598565aeb3ed01b56f4b36a8e62", + "address": "0x030dA9f941AfB71646F25B3768DB3527cF1fCc9C", + "multisig": true + } } } diff --git a/packages/mars/src/actions.ts b/packages/mars/src/actions.ts index 7d30538..1bdfaa0 100644 --- a/packages/mars/src/actions.ts +++ b/packages/mars/src/actions.ts @@ -2,7 +2,6 @@ import { AbiConstructorEntry, AbiFunctionEntry } from './abi' import { ArtifactFrom } from './syntax/artifact' import { BooleanLike, Future } from './values' import { TransactionOptions } from './execute/sendTransaction' -import { MultisigBuilder } from './multisig' export type Action = | DeployAction @@ -12,8 +11,6 @@ export type Action = | StartConditionalAction | EndConditionalAction | DebugAction - | MultisigActionStart - | MultisigActionEnd | GetStorageAction export interface DeployAction { @@ -25,7 +22,6 @@ export interface DeployAction { options: Partial resolve: (address: string) => void skipUpgrade: boolean - multisig?: MultisigBuilder } export interface StartConditionalAction { @@ -53,7 +49,6 @@ export interface TransactionAction { params: any[] options: Partial resolve: (value: any) => void - multisig?: MultisigBuilder } export interface EncodeAction { @@ -68,14 +63,6 @@ export interface DebugAction { messages: any[] } -export interface MultisigActionStart { - type: 'MULTISIG_START' -} - -export interface MultisigActionEnd { - type: 'MULTISIG_END' -} - export interface GetStorageAction { type: 'GET_STORAGE_AT' address: Future diff --git a/packages/mars/src/context.ts b/packages/mars/src/context.ts index 90a300f..d7a7bc8 100644 --- a/packages/mars/src/context.ts +++ b/packages/mars/src/context.ts @@ -1,5 +1,5 @@ import { Action } from './actions' -import { MultisigContext } from './syntax/multisig' +import { MultisigTxDispatcher } from './multisig' export const context = { enabled: false, @@ -11,6 +11,5 @@ export const context = { actions: [] as Action[], // Counts depth of conditional transactions after the failed one conditionalDepth: 0, - // TODO extract GlobalContext class and remove undefined - multisig: undefined as MultisigContext | undefined, + multisig: undefined as MultisigTxDispatcher | undefined, } diff --git a/packages/mars/src/deploy.ts b/packages/mars/src/deploy.ts index bb9aca5..67251c4 100644 --- a/packages/mars/src/deploy.ts +++ b/packages/mars/src/deploy.ts @@ -1,7 +1,8 @@ import { context } from './context' import { getConfig, Options } from './options' import { execute, ExecuteOptions } from './execute/execute' -import { MultisigContext } from './syntax/multisig' +import { MultisigTxDispatcher } from './multisig' +import { log } from './logging' export async function deploy( options: Options, @@ -11,9 +12,19 @@ export async function deploy( context.enabled = true context.actions = [] - context.multisig = config.multisig ? new MultisigContext(config.multisig) : undefined + context.multisig = config.multisig ? new MultisigTxDispatcher(config.multisig) : undefined const result = callback(await config.signer.getAddress(), config) context.enabled = false await execute(context.actions, config) + + // Refactor -> extract to multisig extension + if (config.multisig && context.multisig) { + if (context.multisig.txBatch.length > 0) { + const multisigId = await context.multisig.propose() + await context.multisig.approve(multisigId) + } else { + log('Multisig batch empty. Nothing to process.') + } + } return { result, config } } diff --git a/packages/mars/src/execute/bytecode.ts b/packages/mars/src/execute/bytecode.ts index eddc81d..5dfe080 100644 --- a/packages/mars/src/execute/bytecode.ts +++ b/packages/mars/src/execute/bytecode.ts @@ -1,4 +1,7 @@ export function isBytecodeEqual(a: string, b: string) { + if (a === undefined) throw new Error('left-side operand undefined') + if (b === undefined) throw new Error('right-side operand undefined') + return removeHashes(normalize(a)) === removeHashes(normalize(b)) } diff --git a/packages/mars/src/execute/execute.ts b/packages/mars/src/execute/execute.ts index 3a2ed95..e981cca 100644 --- a/packages/mars/src/execute/execute.ts +++ b/packages/mars/src/execute/execute.ts @@ -10,16 +10,15 @@ import { TransactionAction, } from '../actions' import { BigNumber, Contract, providers, utils } from 'ethers' -import { AbiSymbol, Address, ArtifactSymbol, Bytecode, Name } from '../symbols' +import { AbiSymbol, Address, ArtifactSymbol, Bytecode, DeployedBytecode, Name } from '../symbols' import { Future, resolveBytesLike } from '../values' import { getDeployTx } from './getDeployTx' -import { sendTransaction, TransactionOptions, withGas } from './sendTransaction' +import { sendTransaction, TransactionOptions } from './sendTransaction' import { read, save, SaveEntry } from './save' import { isBytecodeEqual } from './bytecode' import { JsonInputs, verify, verifySingleFile } from '../verification' import { context } from '../context' -import { MultisigConfig } from '../multisig/multisigConfig' -import { log } from '../logging' +import { MultisigConfig } from '../multisig' export type TransactionOverrides = Partial & { skipUpgrade?: boolean @@ -41,20 +40,10 @@ export interface ExecuteOptions extends TransactionOptions { export async function execute(actions: Action[], options: ExecuteOptions) { for (const action of actions) { - // TODO: improve action logging details - log('โš™๏ธ EXE ' + action.type) - const result = await executeAction(action, options) - if (result && !result.continue) break + await executeAction(action, options) } } -interface ActionResult { - /** - * Whether to continue the pipeline. If false, then all consequent actions are not going to be executed in this run. - */ - continue: boolean -} - async function executeGetStorageAt( { address: futureAddress, storageAddress, resolve }: GetStorageAction, options: ExecuteOptions @@ -64,7 +53,7 @@ async function executeGetStorageAt( resolve(storageValue ?? '0x') } -async function executeAction(action: Action, options: ExecuteOptions): Promise { +async function executeAction(action: Action, options: ExecuteOptions): Promise { if (context.conditionalDepth > 0) { if (action.type === 'CONDITIONAL_START') { context.conditionalDepth++ @@ -87,10 +76,6 @@ async function executeAction(action: Action, options: ExecuteOptions): Promise { - const existing = read(options.deploymentsFile, options.networkName, name) - if (!existing) return +function getDeployedAddress(fileName: string, networkName: string, localContractName: string): string | undefined { + const localEntry = read(fileName, networkName, localContractName) + return localEntry ? localEntry.address : undefined +} - if (existing.multisig) { - // TODO: multisig improvement candidate; for now we do not look for internal ex contract deployment data - return existing.address - } else if (existing.txHash) { - const [existingTx, receipt] = await Promise.all([ - // TODO: support abstract signers where no provider exists - options.signer.provider!.getTransaction(existing.txHash), - options.signer.provider!.getTransactionReceipt(existing.txHash), - ]) - if (existingTx && receipt && shouldSkipUpgrade) { - return existing.address - } - if (existingTx && receipt) { - if ( - tx.data && - isBytecodeEqual(existingTx.data, tx.data.toString()) && - receipt.contractAddress.toLowerCase() === existing.address.toLowerCase() - ) { - return existing.address - } - } - } +async function isDeployedContractSameAsLocal( + provider: providers.Provider, + address: string, + localContractBytecode: string +): Promise { + const networkBytecode = await provider.getCode(address) + return networkBytecode !== undefined && isBytecodeEqual(networkBytecode, localContractBytecode) +} + +async function isNewDeploymentNeeded( + localAddress: string | undefined, + provider: providers.Provider, + localBytecode: string, + skipEqualityCheck: boolean +): Promise { + if (!localAddress) return true + + const contractsAreEqual = + skipEqualityCheck || (await isDeployedContractSameAsLocal(provider, localAddress, localBytecode)) + + return !contractsAreEqual } async function executeDeploy(action: DeployAction, globalOptions: ExecuteOptions) { const options = { ...globalOptions, ...action.options } const params = action.params.map((param) => resolveValue(param)) - const tx = getDeployTx(action.artifact[AbiSymbol], action.artifact[Bytecode], params) - const existingAddress = await getExistingDeployment(tx, action.name, action.skipUpgrade, options) + let tx = getDeployTx(action.artifact[AbiSymbol], action.artifact[Bytecode], params) + const existingAddress = getDeployedAddress(options.deploymentsFile, options.networkName, action.name) let address: string, txHash: string | undefined - if (existingAddress) { + if ( + !(await isNewDeploymentNeeded( + existingAddress, + options.provider, + action.artifact[DeployedBytecode], + action.skipUpgrade + )) + ) { console.log(`Skipping deployment ${action.name} - ${existingAddress}`) - address = existingAddress + address = existingAddress } else { - if (action.multisig) { - address = await action.multisig.addContractDeployment(tx) + if (context.multisig) { + // eslint-disable-next-line no-extra-semi,@typescript-eslint/no-extra-semi + ;({ transaction: tx, address } = await context.multisig.addContractDeployment(tx)) + ;({ txHash } = await sendTransaction(`Deploy ${action.name}`, options, tx)) } else { // eslint-disable-next-line no-extra-semi,@typescript-eslint/no-extra-semi ;({ txHash, address } = await sendTransaction(`Deploy ${action.name}`, options, tx)) } + if (!options.dryRun) { - const multisig = !!action.multisig + const multisig = !!context.multisig save(options.deploymentsFile, options.networkName, action.name, { txHash, address, multisig }) } } @@ -197,23 +187,13 @@ async function executeTransaction(action: TransactionAction, globalOptions: Exec to: resolveValue(action.address), data: new utils.Interface([action.method]).encodeFunctionData(action.method.name, params), } - - if (action.multisig) { - let txToAdd: providers.TransactionRequest - try { - txToAdd = await withGas(transaction, options.gasLimit, options.gasPrice, options.signer) - } catch (e) { - txToAdd = transaction - } - await action.multisig.addContractInteraction(txToAdd) - } else { - const { txHash } = await sendTransaction( - `${action.name}.${action.method.name}(${printableTransactionParams(params)})`, - options, - transaction - ) - action.resolve(resolveBytesLike(txHash)) - } + const { txHash, txWithGas } = await sendTransaction( + `${action.name}.${action.method.name}(${printableTransactionParams(params)})`, + options, + transaction + ) + context.multisig?.addContractInteraction(txWithGas) + action.resolve(resolveBytesLike(txHash)) } function printableTransactionParams(params: unknown[]) { diff --git a/packages/mars/src/execute/sendTransaction.ts b/packages/mars/src/execute/sendTransaction.ts index 5d3b509..80d7e08 100644 --- a/packages/mars/src/execute/sendTransaction.ts +++ b/packages/mars/src/execute/sendTransaction.ts @@ -2,14 +2,16 @@ import { BigNumber, constants, providers, Signer, utils } from 'ethers' import readline from 'readline' import { getEthPriceUsd } from './getEthPriceUsd' import chalk from 'chalk' -import { logTx } from '../logging' +import { log } from '../logging' export interface TransactionOptions { signer: Signer + provider: providers.Provider gasPrice: BigNumber gasLimit?: number | BigNumber noConfirm: boolean logFile: string + dryRun: boolean } export async function withGas( @@ -24,7 +26,7 @@ export async function withGas( export async function sendTransaction( name: string, - { signer, gasPrice, noConfirm, gasLimit: overwrittenGasLimit }: TransactionOptions, + { signer, gasPrice, noConfirm, gasLimit: overwrittenGasLimit, dryRun }: TransactionOptions, transaction: providers.TransactionRequest ) { const txWithGas = await withGas(transaction, overwrittenGasLimit, gasPrice, signer) @@ -36,7 +38,7 @@ export async function sendTransaction( const balance = utils.formatEther(await signer.getBalance()) const balanceInUsd = (parseFloat(balance) * price).toFixed(2) - console.log(chalk.yellow('Transaction:'), name) + console.log(chalk.yellow('๐Ÿš€ ' + (dryRun ? '[DRYRUN] ' : '') + 'Transaction:'), name) console.log(chalk.blue(' Fee:'), `$${feeInUsd}, ฮž${fee}`) console.log(chalk.blue(' Balance:'), `$${balanceInUsd}, ฮž${balance}`) if (!noConfirm) { @@ -52,11 +54,12 @@ export async function sendTransaction( } console.log() - logTx(name, tx) + log(`๐Ÿš€ Transaction: '${name}' Hash: ${tx.hash} Hex data: ${tx.data}`) return { txHash: receipt.transactionHash, address: receipt.contractAddress || constants.AddressZero, + txWithGas, } } diff --git a/packages/mars/src/logging.ts b/packages/mars/src/logging.ts index a438a2a..b32c89e 100644 --- a/packages/mars/src/logging.ts +++ b/packages/mars/src/logging.ts @@ -1,5 +1,4 @@ import fs from 'fs' -import { providers } from 'ethers' export type LogMode = { console: boolean @@ -11,19 +10,8 @@ export const logConfig = { filepath: '', } -export interface TxLogData { - hash?: string - from: string - to: string - data: string -} - -export function logTx(txName: string, tx: providers.TransactionRequest | providers.TransactionResponse | TxLogData) { - log(`๐Ÿ“• Transaction: '${txName}' Hash: ${(tx as any).hash} From: ${tx.from} To: ${tx.to} Hex data: ${tx.data} `) -} - export function log(...args: string[]) { - const argsJoined = args.join('\n') + const argsJoined = args.join('\n') + '\n' if (logConfig.mode.console) { console.log(argsJoined) diff --git a/packages/mars/src/multisig/index.ts b/packages/mars/src/multisig/index.ts index 8554cf7..0949f3a 100644 --- a/packages/mars/src/multisig/index.ts +++ b/packages/mars/src/multisig/index.ts @@ -1,3 +1,2 @@ -export { MultisigBuilder, MultisigExecutable } from './multisig' +export { MultisigTxDispatcher } from './multisig' export { MultisigConfig } from './multisigConfig' -export { MultisigState, Unknown, Proposed, Executed, readSavedMultisig } from './multisigState' diff --git a/packages/mars/src/multisig/multisig.ts b/packages/mars/src/multisig/multisig.ts index e91db79..aad79db 100644 --- a/packages/mars/src/multisig/multisig.ts +++ b/packages/mars/src/multisig/multisig.ts @@ -1,48 +1,41 @@ import { ethers, providers, Signer } from 'ethers' -import { ContractDeployer } from './gnosis/contractDeployer' +import { ContractDeployer, DeterministicDeployment } from './gnosis/contractDeployer' import { MultisigConfig } from './multisigConfig' import { SafeTransactionDataPartial } from '@gnosis.pm/safe-core-sdk-types' import Safe, { EthersAdapter } from '@gnosis.pm/safe-core-sdk' import SafeServiceClient from '@gnosis.pm/safe-service-client' -import { Executed, MultisigState, Proposed } from './multisigState' -import { log, logTx } from '../logging' +import { log } from '../logging' +import chalk from 'chalk' -/** - * Builds multisig parts and provides construction of multisig executable. - * This split responsibility of multisig definition building and its execution at later state. - * - * Encapsulates the multisig implementation details (multisig vendor/service) from all the other logic. - */ -export class MultisigBuilder { +export class MultisigTxDispatcher { private _contractDeployer: ContractDeployer + private _config: MultisigConfig + private readonly _signer: Signer + private _safe?: Safe + private _safeServiceClient: SafeServiceClient - public name: string public txBatch: providers.TransactionRequest[] - /** - * Creates a multisig builder instance. - * - * @param name name of the multisig to differentiate from other multisig batches in the deployment (if many) - * @param networkChainId chain id of the network, needed in order to locate auxiliary contract deployment contracts - */ - constructor(name: string, networkChainId: number) { - this.name = name + constructor(config: MultisigConfig) { + this._config = config + this._signer = config.multisigSigner + this._safeServiceClient = new SafeServiceClient(config.gnosisServiceUri) + this._config = config this.txBatch = [] - this._contractDeployer = new ContractDeployer(networkChainId) + this._contractDeployer = new ContractDeployer(config.networkChainId) } /** * Adds a contract deployment transaction as a multisig batch part. * * @param tx contract deployment transaction - * @returns the address of the contract to be deployed to. Deterministic, i.e. known before deployment transaction - * is finalized and unchanged after that. + * @returns deterministic deployment data in order to be able replicate the transaction later on */ - public async addContractDeployment(tx: providers.TransactionRequest): Promise { - const { transaction: wrappedTx, address } = await this._contractDeployer.createDeploymentTx(tx) - this.txBatch.push(wrappedTx) + public async addContractDeployment(tx: providers.TransactionRequest): Promise { + const deployment = await this._contractDeployer.createDeploymentTx(tx) + this.txBatch.push(deployment.transaction) - return address + return deployment } /** @@ -50,42 +43,10 @@ export class MultisigBuilder { * * @param tx contract deployment transaction */ - public async addContractInteraction(tx: providers.TransactionRequest): Promise { + public addContractInteraction(tx: providers.TransactionRequest): void { this.txBatch.push(tx) } - /** - * Creates execution orchestrator of the multisig - * - * @param signer signs multisig transactions - * @param config multisig configuration - */ - public buildExecutable(signer: Signer, config: MultisigConfig): MultisigExecutable { - return new MultisigExecutable(this.name, signer, config) - } -} - -/** - * Multisig state-changing operations. - * - * Encapsulates the multisig implementation details (multisig vendor/service) from all the other logic. - */ -// TODO: extract gnosis specifics from here -export class MultisigExecutable { - private _signer: Signer - private _safe?: Safe - private _config: MultisigConfig - private _safeServiceClient: SafeServiceClient - - public name: string - - constructor(name: string, signer: Signer, config: MultisigConfig) { - this.name = name - this._config = config - this._signer = signer - this._safeServiceClient = new SafeServiceClient(config.gnosisServiceUri) - } - /** * Registers a multisig transaction in the multisig system for multisig participants to approve and execute later on. * @@ -93,25 +54,18 @@ export class MultisigExecutable { * leveraged off-chain. It depends on the particular multisig service in use. * It is guaranteed though that the final execution of the multisig must be transacted and finalized in the network. * - * @param tx either a single transaction request or a batch of many * @returns unique id of the multisig transaction */ - public async propose(tx: providers.TransactionRequest | providers.TransactionRequest[]): Promise { + public async propose(): Promise { const safe = await this.ensureSafe() - const txs = Array.isArray(tx) ? tx : [tx] - const safeMultisigParts: SafeTransactionDataPartial[] = txs.map((tx) => { - const part = { - to: tx.to, - data: tx.data, - value: tx.value?.toString() ?? '0', - } as SafeTransactionDataPartial - logTx(`[MULTISIG-PART] ${this.name}`, { - from: '', - to: tx.to, - data: tx.data, - }) - return part - }) + const safeMultisigParts: SafeTransactionDataPartial[] = this.txBatch.map( + (tx) => + ({ + to: tx.to, + data: tx.data, + value: tx.value?.toString() ?? '0', + } as SafeTransactionDataPartial) + ) const safeMultisigTx = await safe.createTransaction(safeMultisigParts) const safeMultisigTxHash = await safe.getTransactionHash(safeMultisigTx) const senderAddress = await this._signer.getAddress() @@ -121,7 +75,11 @@ export class MultisigExecutable { safeTransaction: safeMultisigTx, senderAddress, }) - logTx(`[MULTISIG] ${this.name}`, { hash: safeMultisigTxHash, from: senderAddress, to: safe.getAddress() }) + log( + chalk.yellow( + `๐Ÿคน Multisig batch has been proposed (${safeMultisigParts.length} transactions) to the queue. Batch ID = ${safeMultisigTxHash}` + ) + ) return safeMultisigTxHash } @@ -134,20 +92,7 @@ export class MultisigExecutable { const safe = await this.ensureSafe() const confirmationSignature = await safe.signTransactionHash(id) await this._safeServiceClient.confirmTransaction(id, confirmationSignature.data) - log(`[MULTISIG] Approved ${id} by ${await this._signer.getAddress()}`) - } - - /** - * Returns info about the state of the multisig. - * @param id multisig identifier - */ - public async checkState(id: string): Promise { - const response = await this._safeServiceClient.getTransaction(id) - const state = response.isExecuted - ? ({ kind: 'EXECUTED', txHash: response.transactionHash } as Executed) - : ({ kind: 'PROPOSED' } as Proposed) - log(`๐Ÿ” Checking multisig (ID=${id}). State: ${state.kind}.`) - return state + log(chalk.yellow(`๐ŸŽฏ Multisig batch ${id} approved by ${await this._signer.getAddress()}`)) } private async ensureSafe(): Promise { diff --git a/packages/mars/src/multisig/multisigConfig.ts b/packages/mars/src/multisig/multisigConfig.ts index 5008162..557162a 100644 --- a/packages/mars/src/multisig/multisigConfig.ts +++ b/packages/mars/src/multisig/multisigConfig.ts @@ -1,7 +1,10 @@ +import { Signer } from 'ethers' + export interface MultisigConfig { networkChainId: number gnosisSafeAddress: string gnosisServiceUri: string + multisigSigner: Signer } /** diff --git a/packages/mars/src/multisig/multisigState.ts b/packages/mars/src/multisig/multisigState.ts deleted file mode 100644 index 844a670..0000000 --- a/packages/mars/src/multisig/multisigState.ts +++ /dev/null @@ -1,55 +0,0 @@ -// TODO: consider if State pattern could be useful with regard to MultisigBuilder and MultisigExecutable -import { read, save } from '../execute/save' - -export type MultisigState = Unknown | Proposed | Executed - -export type Unknown = { - kind: 'UNKNOWN' -} - -export type Proposed = { - kind: 'PROPOSED' -} - -export type Executed = { - kind: 'EXECUTED' - txHash: string -} - -/** - * A multisig section in deployments file - */ -interface SavedMultisigSection { - [name: string]: SavedMultisigEntry -} - -/** - * A single entry in the multisig section of deployments file - */ -export interface SavedMultisigEntry { - id: string - state: string - txHash?: string -} - -const multisigDeploymentFileSection = '_multisig' - -function readMultisigSection(file: string, network: string): SavedMultisigSection | undefined { - const allMultisigsSection = read(file, network, multisigDeploymentFileSection) - - if (!allMultisigsSection) return undefined - else return allMultisigsSection -} - -export function readSavedMultisig(file: string, network: string, multisigName: string): SavedMultisigEntry | undefined { - const allMultisigsSection = readMultisigSection(file, network) - - return allMultisigsSection ? allMultisigsSection[multisigName] : undefined -} - -export function saveMultisig(file: string, network: string, name: string, entry: SavedMultisigEntry): void { - const allMultisigsSection = readMultisigSection(file, network) ?? {} - allMultisigsSection[name] = entry - - save(file, network, multisigDeploymentFileSection, allMultisigsSection) -} diff --git a/packages/mars/src/options/Options.ts b/packages/mars/src/options/Options.ts index 34e5d3f..4ccc0dd 100644 --- a/packages/mars/src/options/Options.ts +++ b/packages/mars/src/options/Options.ts @@ -40,6 +40,10 @@ export interface Options { sources?: string waffleConfig?: string dataPrintMode?: boolean + /** + * Enables multisig mode + */ + multisig?: boolean /** * Gnosis Safe contract address to be used in multisig deployments. * See: https://gnosis-safe.io/ diff --git a/packages/mars/src/options/config.ts b/packages/mars/src/options/config.ts index 2252a66..8ae6dd9 100644 --- a/packages/mars/src/options/config.ts +++ b/packages/mars/src/options/config.ts @@ -1,4 +1,5 @@ -import { providers, Wallet } from 'ethers' +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { providers, Signer, Wallet } from 'ethers' import Ganache from 'ganache-core' import { ExecuteOptions } from '../execute/execute' import { createJsonInputs } from '../verification' @@ -32,19 +33,18 @@ export async function getConfig(options: Options): Promise { } } - const { signer, networkName } = await getSigner(merged) + const { signer, networkName, multisigSigner } = await getSigner(merged) const gasPrice = merged.gasPrice ?? (await signer.getGasPrice()) - const multisig = ensureMultisigConfig( - // multisig not supported in dry run scenario - merged.dryRun - ? {} - : { - networkChainId: (await signer.provider.getNetwork()).chainId, - gnosisSafeAddress: merged.multisigGnosisSafe, - gnosisServiceUri: merged.multisigGnosisServiceUri, - } - ) + const multisig = merged.multisig + ? ensureMultisigConfig({ + // @typescript-eslint/no-non-null-assertion + networkChainId: (await multisigSigner!.provider!.getNetwork()).chainId, + gnosisSafeAddress: merged.multisigGnosisSafe, + gnosisServiceUri: merged.multisigGnosisServiceUri, + multisigSigner: multisigSigner, + }) + : undefined logConfig.mode.file = !!merged.logFile logConfig.filepath = merged.logFile ?? '' @@ -53,6 +53,7 @@ export async function getConfig(options: Options): Promise { gasPrice, noConfirm: !!merged.noConfirm, signer, + provider: signer.provider, networkName: networkName, dryRun: !!merged.dryRun, logFile: merged.logFile ?? '', @@ -66,13 +67,15 @@ function isNetworkProvider(network: string | Ganache.Provider): network is Ganac return !!network && typeof network === 'object' && (network as Ganache.Provider).send !== undefined } +// Refactoring candidate - https://github.com/EthWorks/Mars/issues/50 +// signer returned here has non-empty provider async function getSigner(options: Options) { - const { network, infuraApiKey, alchemyApiKey, dryRun, fromAddress, privateKey } = options + const { network, infuraApiKey, alchemyApiKey, dryRun, fromAddress, privateKey, multisig } = options if (network === undefined) { throw new Error('No network specified. This should never happen.') } let rpcUrl: string | undefined - let provider: providers.JsonRpcProvider + let provider: providers.JsonRpcProvider | undefined if (isNetworkProvider(network)) { // this causes 'MaxListenersExceededWarning: Possible EventEmitter memory leak detected.' when many contracts in use // details at https://github.com/ChainSafe/web3.js/issues/1648 @@ -87,8 +90,20 @@ async function getSigner(options: Options) { throw new Error('Cannot construct rpc url. This should never happen.') } - let signer - if (dryRun) { + let signer: Signer + let multisigSigner: Signer | undefined + if (multisig) { + if (privateKey === undefined) { + exit('No private key specified. In dry-run multisig a private key must be provided') + } + const multisigProvider = provider ?? new providers.JsonRpcProvider(rpcUrl) + multisigSigner = new Wallet(privateKey, multisigProvider) + const ganache = Ganache.provider({ + fork: rpcUrl, + }) + provider = new providers.Web3Provider(ganache as any) + signer = new Wallet(privateKey, provider) + } else if (dryRun) { const randomWallet = Wallet.createRandom() const ganache = Ganache.provider({ fork: network ?? rpcUrl, @@ -106,6 +121,6 @@ async function getSigner(options: Options) { } const networkName = - isNetworkProvider(network) || network.startsWith('http') ? (await signer.provider.getNetwork()).name : network - return { signer: signer, networkName } + isNetworkProvider(network) || network.startsWith('http') ? (await provider.getNetwork()).name : network + return { signer, networkName, multisigSigner } } diff --git a/packages/mars/src/symbols.ts b/packages/mars/src/symbols.ts index 8cea002..321dce3 100644 --- a/packages/mars/src/symbols.ts +++ b/packages/mars/src/symbols.ts @@ -6,3 +6,4 @@ export const Type = Symbol('Type') export const Address = Symbol('Address') export const AbiSymbol = Symbol('Abi') export const Bytecode = Symbol('Bytecode') +export const DeployedBytecode = Symbol('DeployedBytecode') diff --git a/packages/mars/src/syntax/artifact.ts b/packages/mars/src/syntax/artifact.ts index 7aa7626..ba9d2ec 100644 --- a/packages/mars/src/syntax/artifact.ts +++ b/packages/mars/src/syntax/artifact.ts @@ -1,17 +1,19 @@ import { Abi } from '../abi' import { context } from '../context' -import { AbiSymbol, Bytecode, Name, Type } from '../symbols' +import { AbiSymbol, Bytecode, DeployedBytecode, Name, Type } from '../symbols' import { Future } from '../values' export interface ArtifactJSON { abi: Abi bytecode: string + evm: { deployedBytecode: { object: string } } } export type ArtifactFrom = { [Name]: string [AbiSymbol]: Abi [Bytecode]: string + [DeployedBytecode]: string [Type]: T } & { [K in keyof T]: T[K] extends (...args: infer A) => any ? (...args: A) => string : never @@ -22,6 +24,7 @@ export function createArtifact(name: string, json: ArtifactJSON): ArtifactFro [Name]: name, [AbiSymbol]: json.abi, [Bytecode]: json.bytecode, + [DeployedBytecode]: json.evm.deployedBytecode.object, } for (const entry of json.abi) { if (entry.type === 'function') { diff --git a/packages/mars/src/syntax/contract.ts b/packages/mars/src/syntax/contract.ts index a6f465f..581ceca 100644 --- a/packages/mars/src/syntax/contract.ts +++ b/packages/mars/src/syntax/contract.ts @@ -67,7 +67,6 @@ export function contract(...args: any[]): any { options, resolve: resolveAddress, skipUpgrade: !!options.skipUpgrade, - multisig: context.multisig?.current(), }) return makeContractInstance(name, artifact, address) @@ -121,7 +120,6 @@ export function makeContractInstance(name: string, artifact: ArtifactFrom, params, options, resolve: resolveResult, - multisig: context.multisig?.current(), }) const type = entry.outputs?.[0]?.type const length = entry.outputs?.length diff --git a/packages/mars/src/syntax/createProxy.ts b/packages/mars/src/syntax/createProxy.ts index 2a25808..3663e6f 100644 --- a/packages/mars/src/syntax/createProxy.ts +++ b/packages/mars/src/syntax/createProxy.ts @@ -23,17 +23,6 @@ type ProxyOptionals = { * Params for the initialization routine */ params?: Params - /** - * If set to true, it prevents proxied contract from being redeployed when e.g. ctor argument containing initial - * implementation address changes. - */ - noRedeploy?: boolean - /** - * If set to true, it prevents implementation upgrade in proxy contracts. Useful in multisig scenario where proxy - * does not exist and there is no possibility to check existing implementation behind proxy in order to reason about - * implementation upgrading. - */ - noImplUpgrade?: boolean } /** @@ -110,17 +99,14 @@ export function createProxy(...args: any[]): any { const onUpgrade: any = args[onUpgradeIndex] ?? 'upgradeTo' return (...args: any[]) => { - const [name, implementation, onInitialize, noRedeploy, noImplUpgrade] = parseProxyArgs(...args) + const [name, implementation, onInitialize] = parseProxyArgs(...args) const proxy = contract<{ new (...args: any): void implementation(): Future - }>(name ?? `${implementation[Name]}_proxy`, artifact as any, params, { skipUpgrade: noRedeploy }) - let currentImplementation: Future | undefined - if (!noImplUpgrade) { - currentImplementation = getImplementation(proxy) - const normalizedOnUpgrade = normalizeCall(proxy, onUpgrade, [implementation]) - runIf(currentImplementation.equals(implementation[Address]).not(), () => normalizedOnUpgrade(proxy)) - } + }>(name ?? `${implementation[Name]}_proxy`, artifact as any, params) + const currentImplementation = getImplementation(proxy) + const normalizedOnUpgrade = normalizeCall(proxy, onUpgrade, [implementation]) + runIf(currentImplementation.equals(implementation[Address]).not(), () => normalizedOnUpgrade(proxy)) const contractBehindProxy = makeContractInstance( implementation[Name], @@ -128,20 +114,14 @@ export function createProxy(...args: any[]): any { proxy[Address] ) - if (!noImplUpgrade && currentImplementation) - runIf( - currentImplementation.equals(constants.AddressZero), - () => onInitialize && onInitialize(contractBehindProxy) - ) + runIf(currentImplementation.equals(constants.AddressZero), () => onInitialize && onInitialize(contractBehindProxy)) return contractBehindProxy } } // refactoring: provide a proxy instance with params convergence function -function parseProxyArgs( - ...args: any[] -): [string, Contract, ((contract: Contract) => unknown) | undefined, boolean, boolean] { +function parseProxyArgs(...args: any[]): [string, Contract, ((contract: Contract) => unknown) | undefined] { const hasObjectParam = args.length == 2 && typeof args[1] !== 'function' && typeof args[1] !== 'string' const objectParam = (hasObjectParam ? args[1] : {}) as ProxyOptionals const withName = typeof args[0] === 'string' @@ -150,9 +130,7 @@ function parseProxyArgs( const onInitialize = hasObjectParam ? objectParam.onInitialize : args[withName ? 2 : 1] const onInitializeParams = (hasObjectParam ? objectParam.params : args[withName ? 3 : 2]) ?? [] const onInitializeNormalized = onInitialize ? normalizeCall(contract, onInitialize, onInitializeParams) : undefined - const noRedeploy = objectParam.noRedeploy ?? false - const noImplUpgrade = objectParam.noImplUpgrade ?? false - return [name, contract, onInitializeNormalized, noRedeploy, noImplUpgrade] + return [name, contract, onInitializeNormalized] } function normalizeCall( diff --git a/packages/mars/src/syntax/multisig.ts b/packages/mars/src/syntax/multisig.ts deleted file mode 100644 index 6d9c5e2..0000000 --- a/packages/mars/src/syntax/multisig.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { context } from '../context' -import { MultisigBuilder, MultisigConfig, readSavedMultisig } from '../multisig' -import { ExecuteOptions } from '../execute/execute' -import { SavedMultisigEntry, saveMultisig } from '../multisig/multisigState' -import { log } from '../logging' - -/*** - * Designates a wrapping block of syntax statements that are to be executed as a single multisig transaction batch. - * Use 'propose()' to terminate the block. - */ -export interface MultisigBlock { - name: string - - propose(): void -} - -/*** - * Opens a multisig block. All subsequent contract creations and interactions are queued in a multisig transaction. - * @param name name of the multisig. It must be unique per deployment i.e. all multisig blocks in a deployment must - * have unique names. - */ -export function multisig(name: string): MultisigBlock { - context.ensureEnabled() - const multisig = context.multisig - - if (!multisig) throw new Error('Multisig context does not exist. Ensure you configured it for the deployment.') - - multisig.defineStart(name) - context.actions.push({ - type: 'MULTISIG_START', - }) - - return { - name, - propose() { - context.ensureEnabled() - - multisig.defineEnd() - context.actions.push({ - type: 'MULTISIG_END', - }) - }, - } -} - -/*** - * Multisig context of deployment process. - * - * A global overview of all the multisigs in the deployment. Manages them globally. - */ -export class MultisigContext { - private _all: MultisigBuilder[] = [] - private _config: MultisigConfig - private _current?: MultisigBuilder - - constructor(config: MultisigConfig) { - this._config = config - } - - public defineStart(name: string): void { - if (this.isActive()) - throw new Error("Multisig block already opened. Make sure you proposed the previous multisig with 'propose'.") - - if (this.contains(multisig.name)) throw new Error(`Multisig name ${multisig.name} already defined.`) - - const builder = new MultisigBuilder(name, this._config.networkChainId) - this._current = builder - this._all.push(builder) - } - - public defineEnd(): void { - if (!this.isActive()) - throw new Error('Multisig block has not been opened. Ensure you created a multisig block first.') - - this._current = undefined - } - - public async executeStart(): Promise { - if (this._all.length == 0) - throw new Error('There are no multisig elements to process. This indicates a bug in code.') - - this._current = this._all[0] - this._all = this._all.slice(1) - } - - // TODO: refactor -> decouple state management from this - public async executeEnd(options: ExecuteOptions): Promise<{ continue: boolean }> { - const current = this.ensureCurrent() - const executable = current.buildExecutable(options.signer, this._config) - const multisigData = readSavedMultisig(options.deploymentsFile, options.networkName, executable.name) - let state: SavedMultisigEntry - if (!multisigData || multisigData.state == 'UNKNOWN') { - if (current.txBatch.length == 0) { - log(`Skipping multisig '${executable.name}' due to empty tx set.'`) - return { continue: true } - } - // two separate calls - not perfect, in order to optimize see: https://github.com/gnosis/safe-core-sdk/issues/130 - const multisigId = await executable.propose(current.txBatch) - await executable.approve(multisigId) - state = { - id: multisigId, - state: 'PROPOSED', - } - saveMultisig(options.deploymentsFile, options.networkName, executable.name, state) - return { continue: false } - } else if (multisigData.state == 'PROPOSED') { - const checkedState = await executable.checkState(multisigData.id) - if (checkedState.kind == 'EXECUTED') { - state = { - id: multisigData.id, - state: 'EXECUTED', - txHash: checkedState.txHash, - } - saveMultisig(options.deploymentsFile, options.networkName, executable.name, state) - return { continue: true } - } - return { continue: false } - } - - this._current = undefined - return { continue: true } - } - - public isActive(): boolean { - return this._current !== undefined - } - - public current(): MultisigBuilder | undefined { - return this._current - } - - private contains(name: string) { - return this._all.map((m) => m.name).indexOf(name) >= 0 - } - - private ensureCurrent(): MultisigBuilder { - if (!this._current) - throw new Error('Current builder is undefined. You are outside the multisig block trying to do multisig') - return this._current - } -} diff --git a/packages/mars/test/e2e/multisig.e2e.ts b/packages/mars/test/e2e/multisig.e2e.ts index ffe6919..6302af6 100644 --- a/packages/mars/test/e2e/multisig.e2e.ts +++ b/packages/mars/test/e2e/multisig.e2e.ts @@ -1,16 +1,12 @@ import { UpgradeabilityProxy, UpgradeableContract } from '../fixtures/exampleArtifacts' import { logConfig } from '../../src/logging' import { contract, createProxy, debug, deploy, Options, runIf } from '../../src' -import { multisig } from '../../src/syntax/multisig' -import SafeServiceClient from '@gnosis.pm/safe-service-client' -import { ethers, providers } from 'ethers' -import Safe, { EthersAdapter } from '@gnosis.pm/safe-core-sdk' -import { expect } from 'chai' const options = { network: 'rinkeby', privateKey: process.env.PRIVATE_KEY, infuraApiKey: process.env.INFURA_KEY, + multisig: true, multisigGnosisSafe: '0x8772CD484C059EC5c61459a0abb5A45ece16701f', multisigGnosisServiceUri: 'https://safe-transaction.rinkeby.gnosis.io', disableCommandLineOptions: true, @@ -19,52 +15,21 @@ const options = { logConfig.mode.console = true describe('Multisig', () => { - it('Executes multisigs in separate runs', async () => { - await deploy(options, (deployer, config) => { + it('Dry-runs transactions, collects them as multisig batch and proposes to Gnosis Safe', async () => { + await deploy(options, (deployer) => { debug(`Deployer is ${deployer}`) - const useMultisig = config.networkName === 'rinkeby' - // this is no beauty and indicates our definition and execution pipelines lack information passing, to be improv. - const proxyCreationPhase = true - - // CREATION Multisig - const creationMultisig = useMultisig ? multisig('Contract creation, proxying and initialization') : undefined const proxy = createProxy(UpgradeabilityProxy) const impl = contract('impl', UpgradeableContract) const proxied = proxy(impl, { onInitialize: 'initialize', params: [112233], - noImplUpgrade: proxyCreationPhase, }) - creationMultisig?.propose() - // CONDITIONAL INIT Multisig - const conditionalInitMultisig = useMultisig ? multisig('Conditional init') : undefined - debug(`X value: ${proxied.x()}`) + debug('Proxied value:', proxied.x()) runIf(proxied.x().equals(112233), () => { proxied.resetTo(102030) }) - conditionalInitMultisig?.propose() }) }) - - it('Approves off-chain a proposed Safe transaction', async () => { - const safeTxHashToApprove = '0xca09e75db41176d9e5795c1b3a85b5e848ccfbc9294d93a8495ef72270d6f9cf' - - const safeServiceClient = new SafeServiceClient(options.multisigGnosisServiceUri!) - const web3Provider = new providers.InfuraProvider(options.network!.toString(), options!.infuraApiKey) - const signer = new ethers.Wallet(options.privateKey!, web3Provider) - const safe = await Safe.create({ - ethAdapter: new EthersAdapter({ ethers, signer }), - safeAddress: options.multisigGnosisSafe!, - }) - - const confirmationSignature = await safe.signTransactionHash(safeTxHashToApprove) - const confirmationResponse = await safeServiceClient.confirmTransaction( - safeTxHashToApprove, - confirmationSignature.data - ) - - expect(confirmationResponse.signature).not.null - }) }) diff --git a/packages/mars/test/syntax/contract.test.ts b/packages/mars/test/syntax/contract.test.ts index 846c754..e38732a 100644 --- a/packages/mars/test/syntax/contract.test.ts +++ b/packages/mars/test/syntax/contract.test.ts @@ -14,7 +14,6 @@ import { UpgradeableContract2, } from '../fixtures/exampleArtifacts' import { BigNumber } from 'ethers' -import { Contract, NoParams } from '../../src/syntax/contract' describe('Contract', () => { const getDeployResult = () => JSON.parse(fs.readFileSync('./test/deployments.json').toString()) @@ -124,14 +123,14 @@ describe('Contract', () => { expect(await provider.getBlockNumber()).to.equal(2) }) - it('redeploys contract with different constructor args', async () => { + it('does not redeploy contract if different constructor args only', async () => { const { result: firstCall, provider } = await testDeploy(() => contract(ComplexContract, [10, 'test'])) const { result: secondCall } = await testDeploy(() => contract(ComplexContract, [11, 'test']), { injectProvider: provider, saveDeploy: true, }) - expect(firstCall[Address].resolve()).to.not.equal(secondCall[Address].resolve()) - expect(await provider.getBlockNumber()).to.equal(2) + expect(firstCall[Address].resolve()).to.equal(secondCall[Address].resolve()) + expect(await provider.getBlockNumber()).to.equal(1) }) it('redeploys contract if bytecode has changed', async () => { @@ -253,33 +252,6 @@ describe('Contract', () => { expectFuture(xAfterUpdate, BigNumber.from(420)) }) - it('does not redeploy existing proxy when intentionally configured not to', async () => { - const { result: proxyDeploymentCall } = await testDeploy(() => { - // First iteration of proxy creation - let upgradeable = contract('upgradeable', UpgradeableContract) as Contract - let proxy = createProxy( - OpenZeppelinProxy, - [upgradeable, '0xfe4b84df0000000000000000000000000000000000000000000000000000000000002710'], - 'upgradeTo' - ) - proxy(upgradeable, { noRedeploy: true }) - - // Second iteration of proxy creation - upgradeable = contract('upgradeable', UpgradeableContract2) - proxy = createProxy( - OpenZeppelinProxy, - [upgradeable, '0xfe4b84df0000000000000000000000000000000000000000000000000000000000002710'], - 'upgradeTo' - ) - - const proxied = proxy(upgradeable, { noRedeploy: true }) - return proxied - }) - - const proxyAddress = proxyDeploymentCall[Address].resolve() - expect(getDeployResult().test.upgradeable_proxy.address).to.equal(proxyAddress) - }) - afterEach(() => { fs.unlinkSync('./test/deployments.json') }) diff --git a/packages/mars/test/syntax/log.test.ts b/packages/mars/test/syntax/log.test.ts index 400466e..f9d961c 100644 --- a/packages/mars/test/syntax/log.test.ts +++ b/packages/mars/test/syntax/log.test.ts @@ -7,20 +7,35 @@ import { SimpleContract } from '../fixtures/exampleArtifacts' describe('Log', () => { const logPath = 'test.log' - it('logs deployment', async () => { - expect(fs.existsSync(logPath)).to.be.false + it('logs deployment transaction', async () => { + await deploySomething() + const text = readLog() + expect(text).to.match(/Transaction: (.*) Hash: (.*) Hex data: (.*)/) + }) - await testDeploy(() => contract(SimpleContract), { - saveDeploy: true, - logFile: 'test.log', - }) - const text = fs.readFileSync(logPath).toString() - expect(text.split('\n').length).to.eq(1) - expect(text).to.match(/Transaction: (.*) Hash: (.*) From: (.*) To: (.*) Hex data: (.*)/) + it('separates log entries with new lines', async () => { + await deploySomething() + const text = readLog() + expect(text.split('\n').length).to.eq(2) + }) + + beforeEach(async () => { + expect(fs.existsSync(logPath)).to.be.false }) afterEach(async () => { fs.unlinkSync(logPath) fs.unlinkSync('./test/deployments.json') }) + + async function deploySomething() { + await testDeploy(() => contract(SimpleContract), { + saveDeploy: true, + logFile: logPath, + }) + } + + function readLog(): string { + return fs.readFileSync(logPath).toString() + } }) diff --git a/packages/mars/test/utils/testDeploy.ts b/packages/mars/test/utils/testDeploy.ts index 0826849..3d15ca4 100644 --- a/packages/mars/test/utils/testDeploy.ts +++ b/packages/mars/test/utils/testDeploy.ts @@ -27,6 +27,7 @@ export async function testDeploy( logFile: options.logFile ?? '', deploymentsFile: './test/deployments.json', signer: provider.getSigner(0), + provider: provider, gasPrice: BigNumber.from(0), } context.enabled = true