From 255044d0c2ce72014baaf593659bab1699b1842d Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 20 Jul 2021 16:54:54 -0400 Subject: [PATCH] Implement contract upgrades compatibility in ContractKit (#8308) ### Description - Adds version logic to ContractKit base wrapper to support generic version gatekeeping ### Other changes - Modularize and simplify protocol build scripts to only generate artifacts when necessary - Modify contractkit build:gen script to support targeting contract releases ### Tested - Tests added for base wrapper version compatibility ### Related issues - Fixes #8018 ### Backwards compatibility Yes ### Documentation None --- packages/protocol/package.json | 6 +- packages/protocol/scripts/bash/release-lib.sh | 6 +- packages/protocol/scripts/build.ts | 66 +++++++++++-------- packages/sdk/contractkit/package.json | 8 ++- packages/sdk/contractkit/src/versions.ts | 19 ++++++ .../sdk/contractkit/src/wrappers/Accounts.ts | 6 ++ .../src/wrappers/BaseWrapper.test.ts | 55 ++++++++++++++++ .../contractkit/src/wrappers/BaseWrapper.ts | 21 ++++++ yarn.lock | 7 ++ 9 files changed, 153 insertions(+), 41 deletions(-) create mode 100644 packages/sdk/contractkit/src/versions.ts create mode 100644 packages/sdk/contractkit/src/wrappers/BaseWrapper.test.ts diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 73425cbaf0e..df74a71c5ea 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -10,16 +10,15 @@ "lint:sol": "solhint './contracts/**/*.sol'", "lint": "yarn run lint:ts && yarn run lint:sol", "clean": "rm -rf ./types/typechain && rm -rf build/* && rm -rf .0x-artifacts/* && rm -rf migrations/*.js* && rm -rf test/**/*.js* && rm -f lib/*.js* && rm -f lib/**/*.js* && rm -f scripts/*.js*", - "pretest": "yarn run build", "test": "rm test/**/*.js ; node runTests.js", "quicktest": "./scripts/bash/quicktest.sh", "test:coverage": "yarn run test --coverage", "test:devchain-release": "./scripts/bash/release-on-devchain.sh", "test:release-snapshots": "./scripts/bash/release-snapshots.sh", "test:generate-old-devchain-and-build": "./scripts/bash/generate-old-devchain-and-build.sh", + "build:ts": "rm -f migrations/*.js* && ts-node ./scripts/build.ts --truffleTypes ./types/typechain && tsc -b", "gas": "yarn run test --gas", - "build:ts": "rm -f migrations/*.js* && tsc -b", - "build:sol": "ts-node ./scripts/build.ts", + "build:sol": "ts-node ./scripts/build.ts --solidity ${BUILD_DIR:-./build}", "build": "yarn build:sol && yarn build:ts", "migrate": "./scripts/bash/migrate.sh", "set_block_gas_limit": "./scripts/bash/set_block_gas_limit.sh", @@ -40,7 +39,6 @@ "verify-release": "./scripts/bash/verify-release.sh", "invite": "./scripts/bash/invite.sh", "ganache-dev": "./scripts/bash/ganache.sh", - "pretruffle:migrate": "yarn build:ts", "truffle:migrate": "truffle migrate", "devchain": "ts-node scripts/devchain.ts", "set-exchange-rate": "./scripts/bash/set_exchange_rate.sh", diff --git a/packages/protocol/scripts/bash/release-lib.sh b/packages/protocol/scripts/bash/release-lib.sh index 12047cefb5c..9a0f90062dd 100644 --- a/packages/protocol/scripts/bash/release-lib.sh +++ b/packages/protocol/scripts/bash/release-lib.sh @@ -18,16 +18,12 @@ function build_tag() { [ -d contracts ] && rm -r contracts git checkout $BRANCH -- contracts 2>>$LOG_FILE >> $LOG_FILE - [ -d "build/contracts" ] && mv build/contracts build/contracts_tmp if [ ! -d $BUILD_DIR ]; then echo " - Build contract artifacts at $BUILD_DIR" - yarn build:sol >> $LOG_FILE - rm -rf $BUILD_DIR && mkdir -p $BUILD_DIR - mv build/contracts $BUILD_DIR + BUILD_DIR=$BUILD_DIR yarn build:sol >> $LOG_FILE else echo " - Contract artifacts already built at $BUILD_DIR" fi - [ -d "build/contracts_tmp" ] && mv build/contracts_tmp build/contracts [ -d contracts ] && rm -r contracts git checkout - -- contracts 2>>$LOG_FILE >> $LOG_FILE diff --git a/packages/protocol/scripts/build.ts b/packages/protocol/scripts/build.ts index 32053e17cba..a8aa8f99f99 100644 --- a/packages/protocol/scripts/build.ts +++ b/packages/protocol/scripts/build.ts @@ -1,13 +1,12 @@ /* tslint:disable no-console */ import Web3V1Celo from '@celo/typechain-target-web3-v1-celo' import { execSync } from 'child_process' -import fs from 'fs' +import { readJSONSync } from 'fs-extra' import path from 'path' import { tsGenerator } from 'ts-generator' const ROOT_DIR = path.normalize(path.join(__dirname, '../')) -const BUILD_DIR = path.join(ROOT_DIR, 'build') -const CONTRACTKIT_GEN_DIR = path.normalize(path.join(ROOT_DIR, '../sdk/contractkit/src/generated')) +const BUILD_DIR = path.join(ROOT_DIR, process.env.BUILD_DIR ?? './build') export const ProxyContracts = [ 'AccountsProxy', @@ -92,11 +91,6 @@ const Interfaces = ['ICeloToken', 'IERC20', 'ICeloVersionedContract'] export const ImplContracts = OtherContracts.concat(ProxyContracts).concat(CoreContracts) -function getArtifact(contractName: string) { - const file = fs.readFileSync(`${BUILD_DIR}/contracts/${contractName}.json`).toString() - return JSON.parse(file) -} - function exec(cmd: string) { return execSync(cmd, { cwd: ROOT_DIR, stdio: 'inherit' }) } @@ -105,14 +99,14 @@ function hasEmptyBytecode(contract: any) { return contract.bytecode === '0x' } -function compile() { - console.log('Compiling') +function compile(outdir: string) { + console.log(`protocol: Compiling solidity to ${outdir}`) - exec(`yarn run --silent truffle compile --build_directory=${BUILD_DIR}`) + exec(`yarn run --silent truffle compile --build_directory=${outdir}`) for (const contractName of ImplContracts) { try { - const fileStr = getArtifact(contractName) + const fileStr = readJSONSync(`${outdir}/contracts/${contractName}.json`) if (hasEmptyBytecode(fileStr)) { console.error( `${contractName} has empty bytecode. Maybe you forgot to fully implement an interface?` @@ -127,23 +121,20 @@ function compile() { } } -function generateFilesForTruffle() { - console.log('protocol: Generating Truffle Types') - exec(`rm -rf "${ROOT_DIR}/typechain"`) +function generateFilesForTruffle(outdir: string) { + console.log(`protocol: Generating Truffle Types to ${outdir}`) + exec(`rm -rf "${outdir}"`) const globPattern = `${BUILD_DIR}/contracts/*.json` - exec( - `yarn run --silent typechain --target=truffle --outDir "./types/typechain" "${globPattern}" ` - ) + exec(`yarn run --silent typechain --target=truffle --outDir "${outdir}" "${globPattern}" `) } -async function generateFilesForContractKit() { - console.log('contractkit: Generating Types') - exec(`rm -rf ${CONTRACTKIT_GEN_DIR}`) - const relativePath = path.relative(ROOT_DIR, CONTRACTKIT_GEN_DIR) +async function generateFilesForContractKit(outdir: string) { + console.log(`protocol: Generating Web3 Types to ${outdir}`) + exec(`rm -rf ${outdir}`) + const relativePath = path.relative(ROOT_DIR, outdir) const contractKitContracts = CoreContracts.concat('Proxy').concat(Interfaces) - const globPattern = `${BUILD_DIR}/contracts/@(${contractKitContracts.join('|')}).json` const cwd = process.cwd() @@ -158,16 +149,33 @@ async function generateFilesForContractKit() { await tsGenerator({ cwd, loggingLvl: 'info' }, web3Generator) - exec(`yarn --cwd "${ROOT_DIR}/../.." prettier --write "${CONTRACTKIT_GEN_DIR}/**/*.ts"`) + exec(`yarn prettier --write "${outdir}/**/*.ts"`) +} + +const _buildTargets = { + solidity: undefined, + truffleTypes: undefined, + web3Types: undefined, } -async function main() { - compile() - generateFilesForTruffle() - await generateFilesForContractKit() +async function main(buildTargets: typeof _buildTargets) { + if (buildTargets.solidity) { + compile(buildTargets.solidity) + } + if (buildTargets.truffleTypes) { + generateFilesForTruffle(buildTargets.truffleTypes) + } + if (buildTargets.web3Types) { + await generateFilesForContractKit(buildTargets.web3Types) + } } -main().catch((err) => { +const minimist = require('minimist') +const argv = minimist(process.argv.slice(2), { + string: Object.keys(_buildTargets), +}) + +main(argv).catch((err) => { console.error(err) process.exit(1) }) diff --git a/packages/sdk/contractkit/package.json b/packages/sdk/contractkit/package.json index a8e1cd3e6a9..3348dfef974 100644 --- a/packages/sdk/contractkit/package.json +++ b/packages/sdk/contractkit/package.json @@ -14,11 +14,12 @@ "contractkit" ], "scripts": { - "build": "tsc -b .", + "build:ts": "tsc -b .", + "build:gen": "BUILD_DIR=./build/$RELEASE_TAG yarn --cwd ../../protocol ts-node ./scripts/build.ts --web3Types ../sdk/contractkit/src/generated", + "build": "yarn build:gen && yarn build:ts", "clean": "tsc -b . --clean", "clean:all": "yarn clean && rm -rf src/generated", - "build:gen": "yarn --cwd ../../protocol build", - "prepublishOnly": "yarn build:gen && yarn build", + "prepublishOnly": "yarn build", "docs": "typedoc && ts-node ../utils/scripts/linkdocs.ts contractkit", "test:reset": "yarn --cwd ../../protocol devchain generate-tar .tmp/devchain.tar.gz --migration_override ../../dev-utils/src/migration-override.json --upto 25", "test:livechain": "yarn --cwd ../../protocol devchain run-tar .tmp/devchain.tar.gz", @@ -37,6 +38,7 @@ "fp-ts": "2.1.1", "io-ts": "2.0.1", "moment": "^2.29.0", + "semver": "^7.3.5", "web3": "1.3.6" }, "devDependencies": { diff --git a/packages/sdk/contractkit/src/versions.ts b/packages/sdk/contractkit/src/versions.ts new file mode 100644 index 00000000000..814f966da5e --- /dev/null +++ b/packages/sdk/contractkit/src/versions.ts @@ -0,0 +1,19 @@ +const semverGte = require('semver/functions/gte') + +export class ContractVersion { + constructor( + public readonly storage: number | string, + public readonly major: number | string, + public readonly minor: number | string, + public readonly patch: number | string + ) {} + private toSemver = () => `${this.storage}.${this.major}.${this.minor}` + isAtLeast = (other: ContractVersion) => semverGte(this.toSemver(), other.toSemver()) + toString = () => this.toSemver().concat(`.${this.patch}`) + toRaw = () => [this.storage, this.major, this.minor, this.patch] + static fromRaw = (raw: ReturnType) => + new ContractVersion(raw[0], raw[1], raw[2], raw[3]) +} + +export const newContractVersion = (storage: number, major: number, minor: number, patch: number) => + new ContractVersion(storage, major, minor, patch) diff --git a/packages/sdk/contractkit/src/wrappers/Accounts.ts b/packages/sdk/contractkit/src/wrappers/Accounts.ts index 359cdce7532..1ceabd47909 100644 --- a/packages/sdk/contractkit/src/wrappers/Accounts.ts +++ b/packages/sdk/contractkit/src/wrappers/Accounts.ts @@ -10,6 +10,7 @@ import { soliditySha3 } from '@celo/utils/lib/solidity' import { authorizeSigner as buildAuthorizeSignerTypedData } from '@celo/utils/lib/typed-data-constructors' import { keccak256 } from 'web3-utils' import { Accounts } from '../generated/Accounts' +import { newContractVersion } from '../versions' import { BaseWrapper, proxyCall, @@ -35,6 +36,8 @@ interface AccountSummary { * Contract for handling deposits needed for voting. */ export class AccountsWrapper extends BaseWrapper { + private RELEASE_4_VERSION = newContractVersion(1, 1, 2, 0) + /** * Creates an account. */ @@ -284,6 +287,7 @@ export class AccountsWrapper extends BaseWrapper { } async authorizeSigner(signer: Address, role: string) { + await this.onlyVersionOrGreater(this.RELEASE_4_VERSION) const [accounts, chainId, accountsContract] = await Promise.all([ this.kit.connection.getAccounts(), this.kit.connection.chainId(), @@ -308,6 +312,7 @@ export class AccountsWrapper extends BaseWrapper { } async startSignerAuthorization(signer: Address, role: string) { + await this.onlyVersionOrGreater(this.RELEASE_4_VERSION) return toTransactionObject( this.kit.connection, this.contract.methods.authorizeSigner(signer, keccak256(role)) @@ -315,6 +320,7 @@ export class AccountsWrapper extends BaseWrapper { } async completeSignerAuthorization(account: Address, role: string) { + await this.onlyVersionOrGreater(this.RELEASE_4_VERSION) return toTransactionObject( this.kit.connection, this.contract.methods.completeSignerAuthorization(account, keccak256(role)) diff --git a/packages/sdk/contractkit/src/wrappers/BaseWrapper.test.ts b/packages/sdk/contractkit/src/wrappers/BaseWrapper.test.ts new file mode 100644 index 00000000000..247f37bd612 --- /dev/null +++ b/packages/sdk/contractkit/src/wrappers/BaseWrapper.test.ts @@ -0,0 +1,55 @@ +import { NULL_ADDRESS } from '@celo/base' +import { CeloTxObject } from '@celo/connect' +import Web3 from 'web3' +import { + ICeloVersionedContract, + newICeloVersionedContract, +} from '../generated/ICeloVersionedContract' +import { newKitFromWeb3 } from '../kit' +import { ContractVersion, newContractVersion } from '../versions' +import { BaseWrapper } from './BaseWrapper' + +const web3 = new Web3('http://localhost:8545') +const mockContract = newICeloVersionedContract(web3, NULL_ADDRESS) +const mockVersion = newContractVersion(1, 1, 1, 1) +// @ts-ignore +mockContract.methods.getVersionNumber = (): CeloTxObject => ({ + call: async () => mockVersion.toRaw(), +}) + +class TestWrapper extends BaseWrapper { + constructor() { + super(newKitFromWeb3(web3), mockContract) + } + + async protectedFunction(v: ContractVersion) { + await this.onlyVersionOrGreater(v) + } +} + +describe('TestWrapper', () => { + const tw = new TestWrapper() + + describe(`#onlyVersionOrGreater (actual = ${mockVersion})`, () => { + const throwTests = [ + newContractVersion(2, 0, 0, 0), + newContractVersion(1, 2, 0, 0), + newContractVersion(1, 1, 2, 0), + ] + const resolveTests = [newContractVersion(1, 1, 1, 2), newContractVersion(1, 0, 0, 0)] + + throwTests.forEach((v) => { + it(`should throw with incompatible version ${v}`, async () => { + await expect(tw.protectedFunction(v)).rejects.toThrow( + `Bytecode version ${mockVersion} is not compatible with ${v} yet` + ) + }) + }) + + resolveTests.forEach((v) => { + it(`should resolve with compatible version ${v}`, async () => { + await expect(tw.protectedFunction(v)).resolves.not.toThrow() + }) + }) + }) +}) diff --git a/packages/sdk/contractkit/src/wrappers/BaseWrapper.ts b/packages/sdk/contractkit/src/wrappers/BaseWrapper.ts index 2b8984643ce..79a81b8de72 100644 --- a/packages/sdk/contractkit/src/wrappers/BaseWrapper.ts +++ b/packages/sdk/contractkit/src/wrappers/BaseWrapper.ts @@ -11,7 +11,9 @@ import { import { fromFixed, toFixed } from '@celo/utils/lib/fixidity' import BigNumber from 'bignumber.js' import moment from 'moment' +import { ICeloVersionedContract } from '../generated/ICeloVersionedContract' import { ContractKit } from '../kit' +import { ContractVersion } from '../versions' /** Represents web3 native contract Method */ type Method = (...args: I) => CeloTxObject @@ -24,6 +26,10 @@ type EventsEnum = { /** Base ContractWrapper */ export abstract class BaseWrapper { + protected _version?: T['methods'] extends ICeloVersionedContract['methods'] + ? ContractVersion + : never + constructor(protected readonly kit: ContractKit, protected readonly contract: T) {} /** Contract address */ @@ -31,6 +37,21 @@ export abstract class BaseWrapper { return this.contract.options.address } + async version() { + if (!this._version) { + const raw = await this.contract.methods.getVersionNumber().call() + // @ts-ignore conditional type + this._version = ContractVersion.fromRaw(raw) + } + return this._version! + } + + protected async onlyVersionOrGreater(version: ContractVersion) { + if (!(await this.version()).isAtLeast(version)) { + throw new Error(`Bytecode version ${this._version} is not compatible with ${version} yet`) + } + } + /** Contract getPastEvents */ public getPastEvents(event: Events, options: PastEventOptions): Promise { return this.contract.getPastEvents(event as string, options) diff --git a/yarn.lock b/yarn.lock index 8a5a432ab3d..31da6155108 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22994,6 +22994,13 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + semver@~5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"