Add tooling for deploying new contract implementations (#22)

* Add tooling for deploying new contract implementations

* Address comments

* update package lock

* Don't skip checking verification input

* Fix build
pull/29/head
Asa Oines 3 years ago committed by GitHub
parent d63e38123d
commit 49f930acf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3213
      typescript/optics-deploy/package-lock.json
  2. 8
      typescript/optics-deploy/package.json
  3. 2
      typescript/optics-deploy/src/bridge/BridgeDeploy.ts
  4. 15
      typescript/optics-deploy/src/bridge/TestBridgeDeploy.ts
  5. 12
      typescript/optics-deploy/src/checks.ts
  6. 1
      typescript/optics-deploy/src/core/CoreDeploy.ts
  7. 26
      typescript/optics-deploy/src/core/checks.ts
  8. 27
      typescript/optics-deploy/src/core/index.ts
  9. 66
      typescript/optics-deploy/src/proxyUtils.ts
  10. 103
      typescript/optics-deploy/src/upgrade.ts
  11. 32
      typescript/optics-deploy/src/utils.ts

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
{ {
"devDependencies": { "devDependencies": {
"@types/chai": "^4.2.21", "@types/chai": "^4.2.21",
"ethers": "^5.4.4", "ethers": "^5.4.7",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"ts-node": "^10.1.0", "ts-node": "^10.1.0",
"typechain": "^5.0.0", "typechain": "^5.0.0",
@ -22,11 +22,11 @@
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"dependencies": { "dependencies": {
"@ethersproject/experimental": "^5.3.0", "@ethersproject/experimental": "^5.3.0",
"@optics-xyz/multi-provider": "^0.0.4", "@optics-xyz/ts-interface": "1.1.0",
"@optics-xyz/ts-interface": "^1.0.9",
"@types/node": "^16.9.1", "@types/node": "^16.9.1",
"axios": "^0.21.3", "axios": "^0.21.3",
"chai": "^4.3.4", "chai": "^4.3.4",
"dotenv": "^10.0.0" "dotenv": "^10.0.0",
"optics-multi-provider-community": "0.1.22"
} }
} }

@ -12,7 +12,6 @@ export class BridgeDeploy extends Deploy<BridgeContracts> {
readonly config: BridgeConfig; readonly config: BridgeConfig;
readonly coreDeployPath: string; readonly coreDeployPath: string;
readonly coreContractAddresses: CoreContractAddresses; readonly coreContractAddresses: CoreContractAddresses;
readonly test: boolean;
constructor( constructor(
chain: Chain, chain: Chain,
@ -29,7 +28,6 @@ export class BridgeDeploy extends Deploy<BridgeContracts> {
chain.config.name, chain.config.name,
'contracts', 'contracts',
); );
this.test = test;
} }
get ubcAddress(): string | undefined { get ubcAddress(): string | undefined {

@ -12,12 +12,13 @@ import {
MockWeth, MockWeth,
MockWeth__factory, MockWeth__factory,
} from '@optics-xyz/ts-interface/dist/optics-xapps'; } from '@optics-xyz/ts-interface/dist/optics-xapps';
import { ContractVerificationInput } from '../deploy';
import { BridgeContracts } from './BridgeContracts'; import { BridgeContracts } from './BridgeContracts';
import * as process from '.'; import * as process from '.';
import { Chain } from '../chain'; import { Chain } from '../chain';
import { Deploy } from '../deploy';
import { TokenIdentifier } from '@optics-xyz/multi-provider/dist/optics/tokens';
import { TokenIdentifier } from 'optics-multi-provider-community/dist/optics/tokens'
import { CoreConfig } from '../core/CoreDeploy'; import { CoreConfig } from '../core/CoreDeploy';
function toBytes32(address: string): string { function toBytes32(address: string): string {
@ -67,16 +68,12 @@ export async function getTestChain(
// router's `handle` function. The test signer is pre-authorized. Messages the // router's `handle` function. The test signer is pre-authorized. Messages the
// router dispatches will be logged in the `Enqueue` event on the `MockCore` // router dispatches will be logged in the `Enqueue` event on the `MockCore`
// contract. // contract.
export default class TestBridgeDeploy { export default class TestBridgeDeploy extends Deploy<BridgeContracts> {
signer: Signer; signer: Signer;
ubc: UpgradeBeaconController; ubc: UpgradeBeaconController;
mockCore: MockCore; mockCore: MockCore;
mockWeth: MockWeth; mockWeth: MockWeth;
contracts: BridgeContracts;
verificationInput: ContractVerificationInput[];
localDomain: number; localDomain: number;
chain: Chain;
test: boolean = true;
constructor( constructor(
signer: Signer, signer: Signer,
@ -91,15 +88,13 @@ export default class TestBridgeDeploy {
if (!callerKnowsWhatTheyAreDoing) { if (!callerKnowsWhatTheyAreDoing) {
throw new Error("Don't instantiate via new."); throw new Error("Don't instantiate via new.");
} }
super(chain, contracts, true)
this.signer = signer; this.signer = signer;
this.ubc = ubc; this.ubc = ubc;
this.mockCore = mockCore; this.mockCore = mockCore;
this.mockWeth = mockWeth; this.mockWeth = mockWeth;
this.contracts = contracts;
this.verificationInput = [];
this.localDomain = domain; this.localDomain = domain;
this.config.weth = mockWeth.address; this.config.weth = mockWeth.address;
this.chain = chain;
} }
static async deploy(ethers: any, signer: Signer): Promise<TestBridgeDeploy> { static async deploy(ethers: any, signer: Signer): Promise<TestBridgeDeploy> {

@ -59,7 +59,13 @@ export class InvariantViolationCollector {
} }
// Declare method this way to retain scope // Declare method this way to retain scope
handleViolation = (violation: InvariantViolation) => { handleViolation = (v: InvariantViolation) => {
this.violations.push(violation) const duplicateIndex = this.violations.findIndex((m: InvariantViolation) =>
m.domain === v.domain &&
m.actualImplementationAddress === v.actualImplementationAddress &&
m.expectedImplementationAddress === v.expectedImplementationAddress
)
if (duplicateIndex !== -1)
this.violations.push(v);
} }
} }

@ -19,6 +19,7 @@ type Governor = {
domain: number; domain: number;
address: Address; address: Address;
}; };
export type CoreConfig = { export type CoreConfig = {
environment: DeployEnvironment; environment: DeployEnvironment;
updater: Address; updater: Address;

@ -1,10 +1,9 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { Contract, ethers } from 'ethers'; import { Contract, ethers } from 'ethers';
import { CoreDeploy as Deploy } from './CoreDeploy'; import { Deploy } from '../deploy';
import { BridgeDeploy } from '../bridge/BridgeDeploy'; import { CoreDeploy } from './CoreDeploy';
import { BeaconProxy } from '../proxyUtils'; import { BeaconProxy } from '../proxyUtils';
import TestBridgeDeploy from '../bridge/TestBridgeDeploy';
import { UpgradeBeaconController } from '@optics-xyz/ts-interface/dist/optics-core'; import { UpgradeBeaconController } from '@optics-xyz/ts-interface/dist/optics-core';
import { import {
assertInvariantViolation, assertInvariantViolation,
@ -51,18 +50,18 @@ export async function checkBeaconProxyImplementation(
} }
export function checkVerificationInput( export function checkVerificationInput(
deploy: Deploy | BridgeDeploy | TestBridgeDeploy, deploy: Deploy<any>,
name: string, name: string,
addr: string, addr: string,
) { ) {
const inputAddr = deploy.verificationInput.filter( const match = deploy.verificationInput.find(
(contract) => contract.name == name, (contract) => contract.name == name && contract.address === addr
)[0].address; )
expect(inputAddr).to.equal(addr); expect(match).to.not.be.undefined;
} }
export async function checkCoreDeploy( export async function checkCoreDeploy(
deploy: Deploy, deploy: CoreDeploy,
remoteDomains: number[], remoteDomains: number[],
governorDomain: number, governorDomain: number,
invariantViolationHandler: InvariantViolationHandler = assertInvariantViolation, invariantViolationHandler: InvariantViolationHandler = assertInvariantViolation,
@ -167,7 +166,14 @@ export async function checkCoreDeploy(
expect(beaconOwner).to.equal(governorAddr); expect(beaconOwner).to.equal(governorAddr);
expect(homeOwner).to.equal(governorAddr); expect(homeOwner).to.equal(governorAddr);
// check verification addresses checkCoreVerificationInput(deploy, remoteDomains)
}
function checkCoreVerificationInput(
deploy: CoreDeploy,
remoteDomains: number[],
) {
// Checks that verification input is consistent with deployed contracts.
checkVerificationInput( checkVerificationInput(
deploy, deploy,
'UpgradeBeaconController', 'UpgradeBeaconController',

@ -6,28 +6,7 @@ import * as proxyUtils from '../proxyUtils';
import { CoreDeploy } from './CoreDeploy'; import { CoreDeploy } from './CoreDeploy';
import * as contracts from '@optics-xyz/ts-interface/dist/optics-core'; import * as contracts from '@optics-xyz/ts-interface/dist/optics-core';
import { checkCoreDeploy } from './checks'; import { checkCoreDeploy } from './checks';
import { toBytes32 } from '../utils'; import { log, warn, toBytes32 } from '../utils';
function log(isTest: boolean, str: string) {
if (!isTest) {
console.log(str);
}
}
function warn(text: string, padded: boolean = false) {
if (padded) {
const padding = '*'.repeat(text.length + 8);
console.log(
`
${padding}
*** ${text.toUpperCase()} ***
${padding}
`,
);
} else {
console.log(`**** ${text.toUpperCase()} ****`);
}
}
export async function deployUpgradeBeaconController(deploy: CoreDeploy) { export async function deployUpgradeBeaconController(deploy: CoreDeploy) {
let factory = new contracts.UpgradeBeaconController__factory(deploy.deployer); let factory = new contracts.UpgradeBeaconController__factory(deploy.deployer);
@ -657,9 +636,9 @@ export function writePartials(dir: string) {
* *
* @param deploys - The array of chain deploys * @param deploys - The array of chain deploys
*/ */
export function writeDeployOutput(deploys: CoreDeploy[]) { export function writeDeployOutput(deploys: CoreDeploy[], writeDir?: string) {
log(deploys[0].test, `Have ${deploys.length} deploys`); log(deploys[0].test, `Have ${deploys.length} deploys`);
const dir = `../../rust/config/${Date.now()}`; const dir = writeDir ? writeDir : `../../rust/config/${Date.now()}`;
for (const local of deploys) { for (const local of deploys) {
// get remotes // get remotes
const remotes = deploys const remotes = deploys

@ -56,7 +56,7 @@ export async function deployProxy<T extends ethers.Contract>(
// we cast here because Factories don't have associated types // we cast here because Factories don't have associated types
// this is unsafe if the specified typevar doesn't match the factory output // this is unsafe if the specified typevar doesn't match the factory output
// :( // :(
const implementation = await factory.deploy(...deployArgs, deploy.overrides); const implementation = await _deployImplementation(deploy, factory, deployArgs);
const beacon = await _deployBeacon(deploy, implementation); const beacon = await _deployBeacon(deploy, implementation);
const proxy = await _deployProxy(deploy, beacon, initData); const proxy = await _deployProxy(deploy, beacon, initData);
@ -64,7 +64,7 @@ export async function deployProxy<T extends ethers.Contract>(
// due to nonce ordering // due to nonce ordering
await proxy.deployTransaction.wait(deploy.chain.confirmations); await proxy.deployTransaction.wait(deploy.chain.confirmations);
// add UpgradeBeacon to Etherscan verification // add Implementation to Etherscan verification
deploy.verificationInput.push({ deploy.verificationInput.push({
name: `${name} Implementation`, name: `${name} Implementation`,
address: implementation!.address, address: implementation!.address,
@ -123,6 +123,68 @@ export async function duplicate<T extends ethers.Contract>(
); );
} }
/**
* Deploys an Implementation for a given contract, updates the deploy with the
* implementation verification info, and returns the implementation contract.
*
* @param T - The contract
*/
export async function deployImplementation<T extends ethers.Contract>(
name: ProxyNames,
deploy: Deploy,
factory: ethers.ContractFactory,
...deployArgs: any[]
): Promise<T> {
const implementation = await _deployImplementation(deploy, factory, deployArgs)
await implementation.deployTransaction.wait(deploy.chain.confirmations);
// add Implementation to Etherscan verification
deploy.verificationInput.push({
name: `${name} Implementation`,
address: implementation!.address,
constructorArguments: deployArgs,
});
return implementation as T;
}
/**
* Given an existing BeaconProxy, returns a new BeaconProxy with a different implementation.
*
* @param T - The contract
*/
export function overrideBeaconProxyImplementation<T extends ethers.Contract>(
implementation: T,
deploy: CoreDeploy,
factory: ethers.ContractFactory,
beaconProxy: BeaconProxy<T>
): BeaconProxy<T> {
const beacon = contracts.UpgradeBeacon__factory.connect(beaconProxy.beacon.address, deploy.provider);
return new BeaconProxy(
implementation as T,
factory.attach(beaconProxy.proxy.address) as T,
beacon,
);
}
/**
* Returns an UNWAITED implementation
*
* @dev The TX to deploy may still be in-flight
* @dev We set manual gas here to suppress ethers's preflight checks
*
* @param deploy - The deploy
* @param factory - The implementation factory object
* @param deployArgs - The arguments to pass to the implementation constructor
*/
async function _deployImplementation<T extends ethers.Contract>(
deploy: Deploy,
factory: ethers.ContractFactory,
deployArgs: any[]
): Promise<T> {
const implementation = await factory.deploy(...deployArgs, deploy.overrides);
return implementation as T
}
/** /**
* Returns an UNWAITED beacon * Returns an UNWAITED beacon
* *

@ -0,0 +1,103 @@
import * as proxyUtils from './proxyUtils';
import { CoreDeploy } from './core/CoreDeploy';
import { writeDeployOutput } from './core';
import * as contracts from '@optics-xyz/ts-interface/dist/optics-core';
import { log, warn } from './utils';
/**
* Deploys a Home implementation on the chain of the given deploy and updates
* the deploy instance with the new contract.
*
* @param deploy - The deploy instance
*/
export async function deployHomeImplementation(deploy: CoreDeploy) {
const isTestDeploy: boolean = deploy.test;
if (isTestDeploy) warn('deploying test Home');
const homeFactory = isTestDeploy
? contracts.TestHome__factory
: contracts.Home__factory;
const implementation = await proxyUtils.deployImplementation<contracts.Home>(
'Home',
deploy,
new homeFactory(deploy.deployer),
deploy.chain.domain
);
deploy.contracts.home = proxyUtils.overrideBeaconProxyImplementation<contracts.Home>(
implementation,
deploy,
new homeFactory(deploy.deployer),
deploy.contracts.home!
);
}
/**
* Deploys a Replica implementation on the chain of the given deploy and updates
* the deploy instance with the new contracts.
*
* @param deploy - The deploy instance
*/
export async function deployReplicaImplementation(deploy: CoreDeploy) {
const isTestDeploy: boolean = deploy.test;
if (isTestDeploy) warn('deploying test Replica');
const replicaFactory = isTestDeploy
? contracts.TestReplica__factory
: contracts.Replica__factory;
const implementation = await proxyUtils.deployImplementation<contracts.Replica>(
'Replica',
deploy,
new replicaFactory(deploy.deployer),
deploy.chain.domain,
deploy.config.processGas,
deploy.config.reserveGas,
);
for (const domain in deploy.contracts.replicas) {
deploy.contracts.replicas[domain] = proxyUtils.overrideBeaconProxyImplementation<contracts.Replica>(
implementation,
deploy,
new replicaFactory(deploy.deployer),
deploy.contracts.replicas[domain]
);
}
}
/**
* Deploy a new contract implementation to each chain in the deploys
* array.
*
* @dev The first chain in the array will be the governing chain
*
* @param deploys - An array of chain deploys
* @param deployImplementation - A function that deploys a new implementation
*/
export async function deployImplementations(dir: string, deploys: CoreDeploy[], deployImplementation: (d: CoreDeploy) => void) {
if (deploys.length == 0) {
throw new Error('Must pass at least one deploy config');
}
// there exists any chain marked test
const isTestDeploy: boolean = deploys.filter((c) => c.test).length > 0;
log(isTestDeploy, `Beginning ${deploys.length} Chain deploy process`);
log(isTestDeploy, `Deploy env is ${deploys[0].config.environment}`);
log(isTestDeploy, `${deploys[0].chain.name} is governing`);
log(isTestDeploy, 'awaiting provider ready');
await Promise.all([
deploys.map(async (deploy) => {
await deploy.ready();
}),
]);
log(isTestDeploy, 'done readying');
// Do it sequentially
for (const deploy of deploys) {
await deployImplementation(deploy)
}
// write config outputs again, should write under a different dir
if (!isTestDeploy) {
writeDeployOutput(deploys, dir);
}
}

@ -1,4 +1,6 @@
import { exec } from 'child_process' import { exec } from 'child_process'
import fs from 'fs';
import path from 'path';
/* /*
* Converts address to Bytes32 * Converts address to Bytes32
@ -83,4 +85,32 @@ export const ensure0x = (hexstr: string) => (hexstr.startsWith('0x') ? hexstr :
export const strip0x = (hexstr: string) => (hexstr.startsWith('0x') ? hexstr.slice(2) : hexstr) export const strip0x = (hexstr: string) => (hexstr.startsWith('0x') ? hexstr.slice(2) : hexstr)
export function includeConditionally(condition: boolean, data: any) { export function includeConditionally(condition: boolean, data: any) {
return condition ? data : {}; return condition ? data : {};
} }
export function log(isTest: boolean, str: string) {
if (!isTest) {
console.log(str);
}
}
export function warn(text: string, padded: boolean = false) {
if (padded) {
const padding = '*'.repeat(text.length + 8);
console.log(
`
${padding}
*** ${text.toUpperCase()} ***
${padding}
`,
);
} else {
console.log(`**** ${text.toUpperCase()} ****`);
}
}
export function writeJSON(directory: string, filename: string, obj: any) {
fs.writeFileSync(
path.join(directory, filename),
JSON.stringify(obj, null, 2),
);
}

Loading…
Cancel
Save