* UniswapV3 VIP (#237) * `@0x/contracts-zero-ex`: Add UniswapV3Feature * `@0x/contracts-zero-ex`: Add UniswapV3 VIP `@0x/contract-artifacts`: Regenerate. `@0x/contract-wrappers`: Regenerate. `@0x/asset-swapper`: Add UniswapV3 VIP support. * address review comments and appease linter * `@0x/contracts-zero-ex`: Add UniswapV3Feature tests * Multiplex UniswapV3 (#241) * Add UniswapV3 support to Multiplex batchFill * Add AssetSwapper support for Multiplex UniswapV3 * fix repo scripts that use PKG= env var (#242) Co-authored-by: Lawrence Forman <me@merklejerk.com> * `@0x/asset-swapper`: Adjust uniswap gas overhead Co-authored-by: Lawrence Forman <me@merklejerk.com> Co-authored-by: mzhu25 <mchl.zhu.96@gmail.com> * OTC orders feature (#244) * Add OTC orders feature contracts * Address PR feedback * Remove partial fills for takerSigned variant * Add function to query the min valid nonce * Add ETH support * Tightly pack expiry, nonceBucket, and nonce * Address PR feedback * OTC orders unit tests * Bump prettier version * Skip unnecessary math if takerTokenFillAmount == order.takerAmount * appease CI * Update contract-artifacts and contract-wrappers and CHANGELOGs * `@0x/contracts-zero-ex`: Address spot check feedback * `regen wrappers * prettier * `@0x/asset-swapper`: prettier and tweak gas schedule slightly for uni3 Co-authored-by: Lawrence Forman <me@merklejerk.com> Co-authored-by: mzhu25 <mchl.zhu.96@gmail.com>
		
			
				
	
	
		
			429 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			429 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<ProxyFunctionEntity[]> {
 | 
						|
    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<boolean> {
 | 
						|
    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<void> {
 | 
						|
    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<void> {
 | 
						|
    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<void> {
 | 
						|
    const { fnSelector } = await prompts({
 | 
						|
        type: 'autocomplete',
 | 
						|
        name: 'fnSelector',
 | 
						|
        message: 'Enter the selector or name of the function:',
 | 
						|
        choices: [
 | 
						|
            ...proxyFunctions.map(fn => ({
 | 
						|
                title: fn.id,
 | 
						|
                value: fn.id,
 | 
						|
                description: selectorToSignature[fn.id] || '(function signature not found)',
 | 
						|
            })),
 | 
						|
            ...proxyFunctions.map(fn => ({
 | 
						|
                title: selectorToSignature[fn.id] || '(function signature not found)',
 | 
						|
                value: fn.id,
 | 
						|
                description: fn.id,
 | 
						|
            })),
 | 
						|
        ],
 | 
						|
    });
 | 
						|
    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<void> {
 | 
						|
    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<void> {
 | 
						|
    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)',
 | 
						|
                },
 | 
						|
                ...(previousImpl !== constants.NULL_ADDRESS
 | 
						|
                    ? [
 | 
						|
                          {
 | 
						|
                              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<void> {
 | 
						|
    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<wrappers.IZeroExContract> {
 | 
						|
    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);
 | 
						|
});
 |