Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: contract source code verification now integrated into gemforge deploy process #48

Merged
merged 1 commit into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"javascript": {
"formatter": {
"semicolons": "asNeeded"
}
},
"globals": ["describe", "it", "before", "after", "beforeEach", "afterEach"]
},
"linter": {
"enabled": true,
Expand Down
23 changes: 16 additions & 7 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export const command = () =>
info('Deploying facets...')
for (const name of changes.facetsToDeploy) {
info(` Deploying ${name} ...`)
const contract = await deployContract(ctx, name, signer)
const contract = await deployContract(ctx, target, name, signer)
facetContracts[name] = contract
info(` Deployed ${name} at: ${await contract.address}`)
}
Expand All @@ -189,6 +189,7 @@ export const command = () =>
} else {
const { address, data } = await deployAndEncodeInitData(
ctx,
target,
signer,
initContract,
initFunction,
Expand All @@ -204,6 +205,7 @@ export const command = () =>
} else {
const { address, data } = await deployAndEncodeInitData(
ctx,
target,
signer,
args.upgradeInitContract,
args.upgradeInitMethod,
Expand Down Expand Up @@ -261,25 +263,32 @@ export const command = () =>
const deployNewDiamond = async (ctx: Context, signer: Signer, target: Target) => {
info(`Deploying diamond...`)
const { create3Salt } = target.config
const salt32bytes = create3Salt || ethers.keccak256(ethers.hexlify(ethers.randomBytes(32)))
info(` CREATE3 salt: ${salt32bytes}`)
const diamond = await deployContract3(ctx, 'DiamondProxy', signer, salt32bytes, await signer.getAddress())
info(` DiamondProxy deployed at: ${diamond.address}`)
let salt32bytes = create3Salt
if (!salt32bytes) {
salt32bytes = ethers.keccak256(ethers.hexlify(ethers.randomBytes(32)))
info(` CREATE3 salt (randomized): ${salt32bytes}`)
} else {
info(` CREATE3 salt (specified): ${salt32bytes}`)
}
const diamond = await deployContract3(ctx, target, 'DiamondProxy', signer, salt32bytes, await signer.getAddress())
info(` ...deployed at: ${diamond.address}`)
return await getContractAt(ctx, 'IDiamondProxy', signer, diamond.address)
}


const deployAndEncodeInitData = async (
ctx: Context,
target: Target,
signer: Signer,
contractName: string,
methodName: string,
initArgs: any[],
logPrefix: string
): Promise<{ address: string; data: string }> => {
info(`Deploying ${logPrefix} contract: ${contractName} ...`)
const contract = await deployContract(ctx, contractName, signer)
const contract = await deployContract(ctx, target, contractName, signer)
const address = contract.address
info(` ${logPrefix} contract deployed at: ${address}`)
info(` ...deployed at: ${address}`)

const methodSelector = contract.contract.interface.getFunction(methodName)
if (!methodSelector) {
Expand Down
6 changes: 3 additions & 3 deletions src/commands/scaffold.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import semver from 'semver'
import { error, info } from '../shared/log.js'
import { getContext } from '../shared/context.js'
import { $, ensureFolderExistsAndIsEmpty, fileExists, writeTemplate } from '../shared/fs.js'
import { $, ensureFolderExistsAndIsEmpty } from '../shared/fs.js'
import { error, info } from '../shared/log.js'
import { createCommand, logSuccess } from './common.js'

const HARDHAT_GIT_REPO = 'https://github.com/gemstation/contracts-hardhat.git'
Expand All @@ -24,7 +24,7 @@ export const command = () =>
info('Checking for Python...')
try {
await $({ quiet: true })`python --version`
} catch (err) {
} catch (_err) {
try {
await $({ quiet: true })`python3 --version`
} catch (err2) {
Expand Down
85 changes: 68 additions & 17 deletions src/shared/chain.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { glob } from "glob";
import { create } from "node:domain";
import path from "node:path";
import get from "lodash.get";
import { BigVal } from "bigval"
import { Provider } from "ethers";
import { Fragment } from "ethers";
import { Mutex } from "./mutex.js";
import { create } from "node:domain";
import { Context } from "./context.js";
import { error, info, trace } from "./log.js";
import { TransactionReceipt } from "ethers";
import { loadJson, saveJson } from "./fs.js";
import { ErrorFragment, EventFragment } from "ethers";
import { Contract, ethers, Signer, TransactionResponse } from "ethers";
import { MnemonicWalletConfig, PrivateKeyWalletConfig, NetworkConfig, TargetConfig, WalletConfig } from "./config/index.js";
import { Contract, Signer, TransactionResponse, ethers } from "ethers";
import { glob } from "glob";
import get from "lodash.get";
import { MnemonicWalletConfig, NetworkConfig, PrivateKeyWalletConfig, TargetConfig, WalletConfig } from "./config/index.js";
import { Context } from "./context.js";
import { FACTORY_ABI, FACTORY_BYTECODE, FACTORY_DEPLOYED_ADDRESS, FACTORY_DEPLOYER_ADDRESS, FACTORY_GAS_LIMIT, FACTORY_GAS_PRICE, FACTORY_NAME, FACTORY_SIGNED_RAW_TX } from "./create3.js";
import { $, loadJson, saveJson } from "./fs.js";
import { error, info, trace } from "./log.js";
import { Mutex } from "./mutex.js";

interface Network {
config: NetworkConfig,
Expand Down Expand Up @@ -362,7 +362,7 @@ export const getDeploymentRecorderData = () => {
return deploymentRecorder.concat([])
}

export const deployContract = async (ctx: Context, name: string, signer: Signer, ...args: any[]): Promise<OnChainContract> => {
export const deployContract = async (ctx: Context, target: Target, name: string, signer: Signer, ...args: any[]): Promise<OnChainContract> => {
try {
const artifact = loadContractArtifact(ctx, name)
const factory = new ethers.ContractFactory(artifact.abi, artifact.bytecode, signer)
Expand All @@ -373,6 +373,8 @@ export const deployContract = async (ctx: Context, name: string, signer: Signer,
const contract = await tx.waitForDeployment() as Contract
const address = await contract.getAddress()

await verifyContract(ctx, target, artifact, address, args)

deploymentRecorder.push({
name,
fullyQualifiedName: artifact.fullyQualifiedName,
Expand All @@ -396,14 +398,10 @@ export const deployContract = async (ctx: Context, name: string, signer: Signer,

/**
* Deploy a contract using CREATE3.
* @param ctx
* @param name
* @param signer
* @param args
* @returns
*/
export const deployContract3 = async (
ctx: Context,
target: Target,
name: string,
signer: Signer,
create3Salt: string = '',
Expand Down Expand Up @@ -472,6 +470,8 @@ export const deployContract3 = async (

trace(` ...done`)

await verifyContract(ctx, target, _nameArtifact, address, args)

deploymentRecorder.push({
name,
fullyQualifiedName: _nameArtifact.fullyQualifiedName,
Expand All @@ -489,6 +489,55 @@ export const deployContract3 = async (
}
}

export const verifyContract = async (ctx: Context, target: Target, artifact: ContractArtifact, address: string, constructorArgs: any[]) => {
if (ctx.config.networks[target.config.network].contractVerification) {
const verification = ctx.config.networks[target.config.network].contractVerification!

const $$ = $({
cwd: ctx.folder,
quiet: true,
})

trace(` Verifying contract ${artifact.name} deployed at ${address} ...`)

if (ctx.config.artifacts.format === 'foundry') {
let { apiUrl, apiKey } = verification.foundry!
apiUrl = typeof apiUrl === 'function' ? await apiUrl() : apiUrl
apiKey = typeof apiKey === 'function' ? await apiKey() : apiKey

let argStr = '0x'

if (constructorArgs.length) {
argStr = (await $$`cast abi-encode constructor(address) ${constructorArgs.join(' ')}`).stdout
}

trace(` Verifying ${artifact.name} at ${address} with args ${argStr}`)

await $$`forge verify-contract ${address} ${artifact.name} --constructor-args ${argStr} --verifier-url ${apiUrl} --etherscan-api-key ${apiKey} --watch`
} else {
const { networkId } = verification.hardhat!

let argStr = "";

if (constructorArgs.length) {
argStr = constructorArgs.join(", ")
}

trace(` Verifying ${artifact.name} at ${address} with args ${argStr}`)

if (argStr) {
await $$`npx hardhat verify --network ${networkId} --contract ${artifact.fullyQualifiedName} ${address} ${argStr}`
} else {
await $$`npx hardhat verify --network ${networkId} --contract ${artifact.fullyQualifiedName} ${address}`
}
}

trace(` ...verified!`)
} else {
trace(` Contract verification is disabled for network ${target.config.network}, so skipping verification.`)
}
}

export const getContractValue = async <T = any>(contract: OnChainContract, method: string, args: any[], dontExitOnError = false): Promise<T> => {
const label = `${method}() on contract ${contract.artifact.name} deployed at ${contract.address} with args (${args.join(', ')})`

Expand Down Expand Up @@ -574,13 +623,15 @@ const getAllContractArtifactPaths = (ctx: Context): ContractArtifactPath[] => {
let fullyQualifiedName = ''

switch (ctx.config.artifacts.format) {
case 'foundry':
case 'foundry': {
fullyQualifiedName = `${name}.sol:${name}`
break
case 'hardhat':
}
case 'hardhat': {
const filePath = path.relative(ctx.artifactsPath, f)
fullyQualifiedName = `${path.dirname(filePath)}:${name}`
break
}
default:
error(`Unknown artifacts format: ${ctx.config.artifacts.format}`)
}
Expand Down
28 changes: 15 additions & 13 deletions src/shared/config/common.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,45 @@
import get from 'lodash.get'

export const throwError = (msg: string, key: string, val?: any) => {
throw new Error(`${msg} for [${key}]${typeof val !== undefined ? `: ${val}` : ''}`)
type ErrorMsgOptions = { suffix: string }

export const throwError = (msg: string, key: string, val?: any, errorMsgOptions: ErrorMsgOptions = { suffix: '' }) => {
throw new Error(`${msg}${errorMsgOptions.suffix ? `: ${errorMsgOptions.suffix}` : ''}${typeof val !== 'undefined' ? ` for [${key}]: ${val}` : ''}`)
}

export const ensure = (config: object, key: string, isValid: (v: any) => boolean, msg: string = 'Invalid value') => {
export const ensure = (config: object, key: string, isValid: (v: any) => boolean, msg: string = 'Invalid value', errorMsgOptions?: ErrorMsgOptions) => {
const val = get(config, key)
if (!isValid(val)) {
throwError(msg, key, val)
throwError(msg, key, val, errorMsgOptions)
}
}

export const ensureIsSet = (config: object, key: string) => {
export const ensureIsSet = (config: object, key: string, errorMsgOptions?: ErrorMsgOptions) => {
const val = get(config, key)
if (!val) {
throwError(`Value not found`, key)
throwError(`Value not found`, key, undefined, errorMsgOptions)
}
}

export const ensureIsType = (config: object, key: string, types: string[]) => {
export const ensureIsType = (config: object, key: string, types: string[], errorMsgOptions?: ErrorMsgOptions) => {
const val = get(config, key)
const type = typeof val
if (types.indexOf(type) < 0) {
throwError(`Invalid type: ${type}, must be one of (${types.join(', ')})`, key, val)
throwError(`Invalid type: ${type}, must be one of (${types.join(', ')})`, key, val, errorMsgOptions)
}
}

export const ensureArray = (config: object, key: string, minLen = 0) => {
export const ensureArray = (config: object, key: string, minLen = 0, errorMsgOptions?: ErrorMsgOptions) => {
const val = get(config, key)
if (!Array.isArray(val)) {
throwError(`Invalid array`, key, val)
throwError(`Invalid array`, key, val, errorMsgOptions)
} else if (val.length < minLen) {
throwError(`Invalid array length (must be ${minLen})`, key, val)
throwError(`Invalid array length (must be ${minLen})`, key, val, errorMsgOptions)
}
}

export const ensureBool = (config: object, key: string) => {
export const ensureBool = (config: object, key: string, errorMsgOptions?: ErrorMsgOptions) => {
const val = get(config, key)
if (typeof val !== 'boolean') {
throwError(`Invalid boolean value`, key, val)
throwError(`Invalid boolean value`, key, val, errorMsgOptions)
}
}
27 changes: 23 additions & 4 deletions src/shared/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ const { version } = loadJson(new URL('../../../package.json', import.meta.url))

export const gemforgeVersion = version

type StringOrFunction = string | (() => string)

export interface MnemonicWalletConfig {
words: string | Function,
words: StringOrFunction,
index: number,
}

export interface PrivateKeyWalletConfig {
key: string | Function
key: StringOrFunction,
}

export type ValidWalletType = 'mnemonic' | 'private-key'
Expand All @@ -36,7 +38,16 @@ export interface WalletPrivateKeyType {
export type WalletConfig = WalletMnemonicType | WalletPrivateKeyType

export interface NetworkConfig {
rpcUrl: string | Function,
rpcUrl: StringOrFunction,
contractVerification?: {
foundry?: {
apiUrl: StringOrFunction,
apiKey: StringOrFunction,
},
hardhat?: {
networkId: string,
}
},
}

export interface TargetConfig {
Expand Down Expand Up @@ -191,6 +202,14 @@ export const sanitizeConfig = (config: GemforgeConfig) => {
}
networkNames.forEach(name => {
ensureIsType(config, `networks.${name}.rpcUrl`, ['string', 'function'])
if (config.networks[name].contractVerification) {
if (config.artifacts.format === 'foundry') {
ensureIsType(config, `networks.${name}.contractVerification.foundry.apiKey`, ['string', 'function'], { suffix: 'Foundry format requires an explorer API key' })
ensureIsType(config, `networks.${name}.contractVerification.foundry.apiUrl`, ['string', 'function'], { suffix: 'Foundry format requires an explorer API URL' })
} else {
ensureIsType(config, `networks.${name}.contractVerification.hardhat.networkId`, ['string'], { suffix: 'Hardhat format requires a network ID' })
}
}
})

// targets
Expand All @@ -215,7 +234,7 @@ export const sanitizeConfig = (config: GemforgeConfig) => {
try {
ethers.keccak256(v)
return true
} catch (e) {
} catch (_e) {
return false
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/shared/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'node:path'
import { disableLogging, enableVerboseLogging, error, info, trace } from './log.js'
import { GemforgeConfig, sanitizeConfig } from './config/index.js'
import { disableLogging, enableVerboseLogging, error, info } from './log.js'

export interface Context {
config: GemforgeConfig
Expand Down
Loading
Loading