diff --git a/contracts/zero-ex/.npmignore b/contracts/zero-ex/.npmignore index bdf2b8acbe..4ceb415266 100644 --- a/contracts/zero-ex/.npmignore +++ b/contracts/zero-ex/.npmignore @@ -8,3 +8,4 @@ # Blacklist tests in lib /lib/test/* # Package specific ignore +/lib/scripts/* diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 1a62991061..9bd49c9cfb 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -37,7 +37,8 @@ "compile:truffle": "truffle compile", "docs:md": "ts-doc-gen --sourceDir='$PROJECT_FILES' --output=$MD_FILE_DIR --fileExtension=mdx --tsconfig=./typedoc-tsconfig.json", "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES", - "publish:private": "yarn build && gitpkg publish" + "publish:private": "yarn build && gitpkg publish", + "rollback": "node ./lib/scripts/rollback.js" }, "config": { "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature,ILiquidityProviderFeature,NativeOrdersFeature,INativeOrdersFeature,FeeCollectorController,FeeCollector", @@ -55,6 +56,7 @@ "homepage": "https://github.com/0xProject/protocol/tree/main/contracts/zero-ex", "devDependencies": { "@0x/abi-gen": "^5.4.13", + "@0x/contract-addresses": "^5.6.0", "@0x/contracts-erc20": "^3.2.12", "@0x/contracts-gen": "^2.0.24", "@0x/contracts-test-utils": "^5.3.15", @@ -62,11 +64,15 @@ "@0x/sol-compiler": "^4.4.1", "@0x/ts-doc-gen": "^0.0.28", "@0x/tslint-config": "^4.1.3", + "@types/isomorphic-fetch": "^0.0.35", "@types/lodash": "4.14.104", "@types/mocha": "^5.2.7", + "@types/prompts": "^2.0.9", + "isomorphic-fetch": "^3.0.0", "lodash": "^4.17.11", "mocha": "^6.2.0", "npm-run-all": "^4.1.2", + "prompts": "^2.4.0", "shx": "^0.2.2", "solhint": "^1.4.1", "truffle": "^5.0.32", diff --git a/contracts/zero-ex/scripts/rollback.ts b/contracts/zero-ex/scripts/rollback.ts new file mode 100644 index 0000000000..6d014fa88c --- /dev/null +++ b/contracts/zero-ex/scripts/rollback.ts @@ -0,0 +1,420 @@ +import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; +import { constants } from '@0x/contracts-test-utils'; +import { RPCSubprovider, SupportedProvider, Web3ProviderEngine } from '@0x/subproviders'; +import { AbiEncoder, BigNumber, logUtils, providerUtils } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { MethodAbi } from 'ethereum-types'; +import * as fetch from 'isomorphic-fetch'; +import * as _ from 'lodash'; +import * as prompts from 'prompts'; + +import * as wrappers from '../src/wrappers'; + +const SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/mzhu25/zeroex-migrations'; + +const ownableFeature = new wrappers.OwnableFeatureContract(constants.NULL_ADDRESS, new Web3ProviderEngine()); +const simpleFunctionRegistryFeature = new wrappers.SimpleFunctionRegistryFeatureContract( + constants.NULL_ADDRESS, + new Web3ProviderEngine(), +); +const DO_NOT_ROLLBACK = [ + ownableFeature.getSelector('migrate'), + ownableFeature.getSelector('transferOwnership'), + simpleFunctionRegistryFeature.getSelector('rollback'), + simpleFunctionRegistryFeature.getSelector('extend'), +]; + +const governorEncoder = AbiEncoder.create('(bytes[], address[], uint256[])'); + +const selectorToSignature: { [selector: string]: string } = {}; +for (const wrapper of Object.values(wrappers)) { + if (typeof wrapper === 'function') { + const contract = new wrapper(constants.NULL_ADDRESS, new Web3ProviderEngine()); + contract.abi + .filter(abiDef => abiDef.type === 'function') + .map(method => { + const methodName = (method as MethodAbi).name; + const selector = contract.getSelector(methodName); + const signature = contract.getFunctionSignature(methodName); + selectorToSignature[selector] = signature; + }); + } +} + +interface ProxyFunctionEntity { + id: string; + currentImpl: string; + fullHistory: Array<{ impl: string; timestamp: string }>; +} + +interface Deployment { + time: string; + updates: Array<{ selector: string; signature?: string; previousImpl: string; newImpl: string }>; +} + +async function querySubgraphAsync(): Promise { + const query = ` + { + proxyFunctions { + id + currentImpl + fullHistory { + impl + timestamp + } + } + } + `; + + const response = await fetch(SUBGRAPH_URL, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + }), + }); + const { + data: { proxyFunctions }, + } = await response.json(); + // Sort the history in chronological order + proxyFunctions.map((fn: ProxyFunctionEntity) => + fn.fullHistory.sort((a, b) => Number.parseInt(a.timestamp, 10) - Number.parseInt(b.timestamp, 10)), + ); + return proxyFunctions; +} + +function reconstructDeployments(proxyFunctions: ProxyFunctionEntity[]): Deployment[] { + const deploymentsByTimestamp: { [timestamp: string]: Deployment } = {}; + proxyFunctions.map(fn => { + fn.fullHistory.map((update, i) => { + const { updates } = (deploymentsByTimestamp[update.timestamp] = deploymentsByTimestamp[ + update.timestamp + ] || { time: timestampToUTC(update.timestamp), updates: [] }); + updates.push({ + selector: fn.id, + signature: selectorToSignature[fn.id], + previousImpl: i > 0 ? fn.fullHistory[i - 1].impl : constants.NULL_ADDRESS, + newImpl: update.impl, + }); + }); + }); + return Object.keys(deploymentsByTimestamp) + .sort() + .map(timestamp => deploymentsByTimestamp[timestamp]); +} + +function timestampToUTC(timestamp: string): string { + return new Date(Number.parseInt(timestamp, 10) * 1000).toUTCString(); +} + +enum CommandLineActions { + History = 'History', + Function = 'Function', + Current = 'Current', + Rollback = 'Rollback', + Emergency = 'Emergency', + Exit = 'Exit', +} + +async function confirmRollbackAsync( + rollbackTargets: { [selector: string]: string }, + proxyFunctions: ProxyFunctionEntity[], +): Promise { + const { confirmed } = await prompts({ + type: 'confirm', + name: 'confirmed', + message: `Are these the correct rollbacks?\n${Object.entries(rollbackTargets) + .map( + ([selector, target]) => + `[${selector}] ${selectorToSignature[selector] || '(function signature not found)'} \n ${ + proxyFunctions.find(fn => fn.id === selector)!.currentImpl + } => ${target}`, + ) + .join('\n')}`, + }); + return confirmed; +} + +async function printRollbackCalldataAsync( + rollbackTargets: { [selector: string]: string }, + zeroEx: wrappers.IZeroExContract, +): Promise { + const numRollbacks = Object.keys(rollbackTargets).length; + const { numTxns } = await prompts({ + type: 'number', + name: 'numTxns', + message: + 'To avoid limitations on calldata size, the full rollback can be split into multiple transactions. How many transactions would you like to split it into?', + initial: 1, + style: 'default', + min: 1, + max: numRollbacks, + }); + for (let i = 0; i < numTxns; i++) { + const startIndex = i * Math.trunc(numRollbacks / numTxns); + const endIndex = startIndex + Math.trunc(numRollbacks / numTxns) + (i < numRollbacks % numTxns ? 1 : 0); + const rollbacks = Object.entries(rollbackTargets).slice(startIndex, endIndex); + const rollbackCallData = governorEncoder.encode([ + rollbacks.map(([selector, target]) => zeroEx.rollback(selector, target).getABIEncodedTransactionData()), + new Array(rollbacks.length).fill(zeroEx.address), + new Array(rollbacks.length).fill(constants.ZERO_AMOUNT), + ]); + if (numTxns > 1) { + logUtils.log(`======================== Governor Calldata #${i + 1} ========================`); + } + logUtils.log(rollbackCallData); + } +} + +async function deploymentHistoryAsync(deployments: Deployment[], proxyFunctions: ProxyFunctionEntity[]): Promise { + const { index } = await prompts({ + type: 'select', + name: 'index', + message: 'Choose a deployment:', + choices: deployments.map((deployment, i) => ({ + title: deployment.time, + value: i, + })), + }); + + const { action } = await prompts({ + type: 'select', + name: 'action', + message: 'What would you like to do?', + choices: [ + { title: 'Deployment info', value: 'info' }, + { title: 'Rollback this deployment', value: 'rollback' }, + ], + }); + + if (action === 'info') { + logUtils.log( + deployments[index].updates.map(update => ({ + selector: update.selector, + signature: update.signature || '(function signature not found)', + update: `${update.previousImpl} => ${update.newImpl}`, + })), + ); + } else { + const zeroEx = await getMainnetContractAsync(); + const rollbackTargets: { [selector: string]: string } = {}; + for (const update of deployments[index].updates) { + rollbackTargets[update.selector] = update.previousImpl; + const rollbackLength = (await zeroEx.getRollbackLength(update.selector).callAsync()).toNumber(); + for (let i = rollbackLength - 1; i >= 0; i--) { + const entry = await zeroEx.getRollbackEntryAtIndex(update.selector, new BigNumber(i)).callAsync(); + if (entry === update.previousImpl) { + break; + } else if (i === 0) { + logUtils.log( + 'Cannot rollback this deployment. The following update from this deployment cannot be rolled back:', + ); + logUtils.log(`\t[${update.selector}] ${update.signature || '(function signature not found)'}`); + logUtils.log(`\t${update.previousImpl} => ${update.newImpl}`); + logUtils.log( + `Cannot find ${ + update.previousImpl + } in the selector's rollback history. It itself may have been previously rolled back.`, + ); + return; + } + } + } + const isConfirmed = await confirmRollbackAsync(rollbackTargets, proxyFunctions); + if (isConfirmed) { + await printRollbackCalldataAsync(rollbackTargets, zeroEx); + } + } +} + +async function functionHistoryAsync(proxyFunctions: ProxyFunctionEntity[]): Promise { + const { fnSelector } = await prompts({ + type: 'autocomplete', + name: 'fnSelector', + message: 'Enter the selector or name of the function:', + choices: [ + ..._.flatMap(Object.entries(selectorToSignature), ([selector, signature]) => [ + { title: selector, value: selector, description: signature }, + { title: signature, value: selector, description: selector }, + ]), + ...proxyFunctions + .filter(fn => !Object.keys(selectorToSignature).includes(fn.id)) + .map(fn => ({ title: fn.id, value: fn.id, description: '(function signature not found)' })), + ], + }); + const functionEntity = proxyFunctions.find(fn => fn.id === fnSelector); + if (functionEntity === undefined) { + logUtils.log(`Couldn't find deployment history for selector ${fnSelector}`); + } else { + logUtils.log( + functionEntity.fullHistory.map(update => ({ + date: timestampToUTC(update.timestamp), + impl: update.impl, + })), + ); + } +} + +async function currentFunctionsAsync(proxyFunctions: ProxyFunctionEntity[]): Promise { + const currentFunctions: { + [selector: string]: { signature: string; impl: string; lastUpdated: string }; + } = {}; + proxyFunctions + .filter(fn => fn.currentImpl !== constants.NULL_ADDRESS) + .map(fn => { + currentFunctions[fn.id] = { + signature: selectorToSignature[fn.id] || '(function signature not found)', + impl: fn.currentImpl, + lastUpdated: timestampToUTC(fn.fullHistory.slice(-1)[0].timestamp), + }; + }); + logUtils.log(currentFunctions); +} + +async function generateRollbackAsync(proxyFunctions: ProxyFunctionEntity[]): Promise { + const zeroEx = await getMainnetContractAsync(); + const { selected } = await prompts({ + type: 'autocompleteMultiselect', + name: 'selected', + message: 'Select the functions to rollback:', + choices: _.flatMap(proxyFunctions.filter(fn => fn.currentImpl !== constants.NULL_ADDRESS), fn => [ + { + title: [ + `[${fn.id}]`, + `Implemented @ ${fn.currentImpl}`, + selectorToSignature[fn.id] || '(function signature not found)', + ].join('\n\t\t\t\t'), + value: fn.id, + }, + ]), + }); + const rollbackTargets: { [selector: string]: string } = {}; + for (const selector of selected) { + const rollbackLength = (await zeroEx.getRollbackLength(selector).callAsync()).toNumber(); + const rollbackHistory = await Promise.all( + _.range(rollbackLength).map(async i => + zeroEx.getRollbackEntryAtIndex(selector, new BigNumber(i)).callAsync(), + ), + ); + const fullHistory = proxyFunctions.find(fn => fn.id === selector)!.fullHistory; + const previousImpl = rollbackHistory[rollbackLength - 1]; + const { target } = await prompts({ + type: 'select', + name: 'target', + message: 'Select the implementation to rollback to', + hint: `[${selector}] ${selectorToSignature[selector] || '(function signature not found)'}`, + choices: [ + { + title: 'DISABLE', + value: constants.NULL_ADDRESS, + description: 'Rolls back to address(0)', + }, + { + title: 'PREVIOUS', + value: previousImpl, + description: `${previousImpl} (${timestampToUTC( + _.findLast(fullHistory, update => update.impl === previousImpl)!.timestamp, + )})`, + }, + ...[...new Set(rollbackHistory)] + .filter(impl => impl !== constants.NULL_ADDRESS) + .map(impl => ({ + title: impl, + value: impl, + description: timestampToUTC(_.findLast(fullHistory, update => update.impl === impl)!.timestamp), + })), + ], + }); + rollbackTargets[selector] = target; + } + + const isConfirmed = await confirmRollbackAsync(rollbackTargets, proxyFunctions); + if (isConfirmed) { + await printRollbackCalldataAsync(rollbackTargets, zeroEx); + } +} + +async function generateEmergencyRollbackAsync(proxyFunctions: ProxyFunctionEntity[]): Promise { + const zeroEx = new wrappers.IZeroExContract( + getContractAddressesForChainOrThrow(1).exchangeProxy, + new Web3ProviderEngine(), + ); + const allSelectors = proxyFunctions + .filter(fn => fn.currentImpl !== constants.NULL_ADDRESS && !DO_NOT_ROLLBACK.includes(fn.id)) + .map(fn => fn.id); + await printRollbackCalldataAsync( + _.zipObject(allSelectors, new Array(allSelectors.length).fill(constants.NULL_ADDRESS)), + zeroEx, + ); +} + +let provider: SupportedProvider | undefined = process.env.RPC_URL ? createWeb3Provider(process.env.RPC_URL) : undefined; + +function createWeb3Provider(rpcUrl: string): SupportedProvider { + const providerEngine = new Web3ProviderEngine(); + providerEngine.addProvider(new RPCSubprovider(rpcUrl)); + providerUtils.startProviderEngine(providerEngine); + return providerEngine; +} + +async function getMainnetContractAsync(): Promise { + if (provider === undefined) { + const { rpcUrl } = await prompts({ + type: 'text', + name: 'rpcUrl', + message: 'Enter an RPC endpoint:', + }); + provider = createWeb3Provider(rpcUrl); + } + const chainId = await new Web3Wrapper(provider).getChainIdAsync(); + const { exchangeProxy } = getContractAddressesForChainOrThrow(chainId); + return new wrappers.IZeroExContract(exchangeProxy, provider); +} + +(async () => { + const proxyFunctions = await querySubgraphAsync(); + const deployments = reconstructDeployments(proxyFunctions); + + while (true) { + const { action } = await prompts({ + type: 'select', + name: 'action', + message: 'What would you like to do?', + choices: [ + { title: '🚒 Deployment history', value: CommandLineActions.History }, + { title: 'πŸ“œ Function history', value: CommandLineActions.Function }, + { title: 'πŸ—ΊοΈ Currently registered functions', value: CommandLineActions.Current }, + { title: 'πŸ”™ Generate rollback calldata', value: CommandLineActions.Rollback }, + { title: '🚨 Emergency shutdown calldata', value: CommandLineActions.Emergency }, + { title: 'πŸ‘‹ Exit', value: CommandLineActions.Exit }, + ], + }); + + switch (action) { + case CommandLineActions.History: + await deploymentHistoryAsync(deployments, proxyFunctions); + break; + case CommandLineActions.Function: + await functionHistoryAsync(proxyFunctions); + break; + case CommandLineActions.Current: + await currentFunctionsAsync(proxyFunctions); + break; + case CommandLineActions.Rollback: + await generateRollbackAsync(proxyFunctions); + break; + case CommandLineActions.Emergency: + await generateEmergencyRollbackAsync(proxyFunctions); + break; + case CommandLineActions.Exit: + default: + process.exit(0); + } + } +})().catch(err => { + logUtils.log(err); + process.exit(1); +}); diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index ed9e9567b2..df7fc62881 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig", "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, - "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], + "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*", "./scripts/**/*"], "files": [ "generated-artifacts/AffiliateFeeTransformer.json", "generated-artifacts/BridgeAdapter.json", diff --git a/yarn.lock b/yarn.lock index e7e25036b3..864d6ce698 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2569,6 +2569,11 @@ dependencies: "@types/node" "*" +"@types/isomorphic-fetch@^0.0.35": + version "0.0.35" + resolved "https://registry.yarnpkg.com/@types/isomorphic-fetch/-/isomorphic-fetch-0.0.35.tgz#c1c0d402daac324582b6186b91f8905340ea3361" + integrity sha512-DaZNUvLDCAnCTjgwxgiL1eQdxIKEpNLOlTNtAgnZc50bG2copGhRrFN9/PxPBuJe+tZVLCbQ7ls0xveXVRPkvw== + "@types/js-combinatorics@^0.5.29": version "0.5.32" resolved "https://registry.yarnpkg.com/@types/js-combinatorics/-/js-combinatorics-0.5.32.tgz#befa3c2b6ea10c45fd8d672f7aa477a79a2601ed" @@ -2625,6 +2630,13 @@ dependencies: "@types/node" "*" +"@types/prompts@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@types/prompts/-/prompts-2.0.9.tgz#19f419310eaa224a520476b19d4183f6a2b3bd8f" + integrity sha512-TORZP+FSjTYMWwKadftmqEn6bziN5RnfygehByGsjxoK5ydnClddtv6GikGWPvCm24oI+YBwck5WDxIIyNxUrA== + dependencies: + "@types/node" "*" + "@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" @@ -7846,6 +7858,14 @@ isomorphic-fetch@2.2.1, isomorphic-fetch@^2.2.1: node-fetch "^1.0.1" whatwg-fetch ">=0.10.0" +isomorphic-fetch@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" + integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== + dependencies: + node-fetch "^2.6.1" + whatwg-fetch "^3.4.1" + isstream@0.1.x, isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -8138,6 +8158,11 @@ klaw@^1.0.0: optionalDependencies: graceful-fs "^4.1.9" +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + lazy@~1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/lazy/-/lazy-1.0.11.tgz#daa068206282542c088288e975c297c1ae77b690" @@ -10300,6 +10325,14 @@ prompt@^1.0.0: utile "0.3.x" winston "2.1.x" +prompts@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" + integrity sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + promzard@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee" @@ -11333,6 +11366,11 @@ sinon@^4.0.0: supports-color "^5.1.0" type-detect "^4.0.5" +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -13439,6 +13477,11 @@ whatwg-fetch@>=0.10.0: version "3.4.1" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz#e5f871572d6879663fa5674c8f833f15a8425ab3" +whatwg-fetch@^3.4.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.5.0.tgz#605a2cd0a7146e5db141e29d1c62ab84c0c4c868" + integrity sha512-jXkLtsR42xhXg7akoDKvKWE40eJeI+2KZqcp2h3NsOrRnDvtWX36KcKl30dy+hxECivdk2BVUHVNrPtoMBUx6A== + whatwg-url@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"