Skip to content

Commit

Permalink
Implement contract upgrades compatibility in ContractKit (#8308)
Browse files Browse the repository at this point in the history
### 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
  • Loading branch information
yorhodes committed Jul 20, 2021
1 parent 27e2213 commit 255044d
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 41 deletions.
6 changes: 2 additions & 4 deletions packages/protocol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
6 changes: 1 addition & 5 deletions packages/protocol/scripts/bash/release-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 37 additions & 29 deletions packages/protocol/scripts/build.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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' })
}
Expand All @@ -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?`
Expand All @@ -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()
Expand All @@ -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)
})
8 changes: 5 additions & 3 deletions packages/sdk/contractkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
19 changes: 19 additions & 0 deletions packages/sdk/contractkit/src/versions.ts
Original file line number Diff line number Diff line change
@@ -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<ContractVersion['toRaw']>) =>
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)
6 changes: 6 additions & 0 deletions packages/sdk/contractkit/src/wrappers/Accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,6 +36,8 @@ interface AccountSummary {
* Contract for handling deposits needed for voting.
*/
export class AccountsWrapper extends BaseWrapper<Accounts> {
private RELEASE_4_VERSION = newContractVersion(1, 1, 2, 0)

/**
* Creates an account.
*/
Expand Down Expand Up @@ -284,6 +287,7 @@ export class AccountsWrapper extends BaseWrapper<Accounts> {
}

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(),
Expand All @@ -308,13 +312,15 @@ export class AccountsWrapper extends BaseWrapper<Accounts> {
}

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))
)
}

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))
Expand Down
55 changes: 55 additions & 0 deletions packages/sdk/contractkit/src/wrappers/BaseWrapper.test.ts
Original file line number Diff line number Diff line change
@@ -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<any> => ({
call: async () => mockVersion.toRaw(),
})

class TestWrapper extends BaseWrapper<ICeloVersionedContract> {
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()
})
})
})
})
21 changes: 21 additions & 0 deletions packages/sdk/contractkit/src/wrappers/BaseWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<I extends any[], O> = (...args: I) => CeloTxObject<O>
Expand All @@ -24,13 +26,32 @@ type EventsEnum<T extends Contract> = {

/** Base ContractWrapper */
export abstract class BaseWrapper<T extends Contract> {
protected _version?: T['methods'] extends ICeloVersionedContract['methods']
? ContractVersion
: never

constructor(protected readonly kit: ContractKit, protected readonly contract: T) {}

/** Contract address */
get address(): string {
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<T>, options: PastEventOptions): Promise<EventLog[]> {
return this.contract.getPastEvents(event as string, options)
Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 255044d

Please sign in to comment.