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

Multisig wrapper #116

Closed
wants to merge 5 commits into from
Closed
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Result } from '@chainlink/gauntlet-core'
import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils'
import { TransactionResponse, TerraCommand } from '@chainlink/gauntlet-terra'
import { TransactionResponse, TerraCommand, RawTransaction } from '@chainlink/gauntlet-terra'
import { Contract, CONTRACT_LIST, getContract, TerraABI, TERRA_OPERATIONS } from '../../lib/contracts'
import { DEFAULT_RELEASE_VERSION } from '../../lib/constants'
import schema from '../../lib/schema'
import findPolymorphic from './polymorphic'

export { findPolymorphic }

export interface AbstractOpts {
contract: Contract
Expand Down Expand Up @@ -181,6 +184,32 @@ export default class AbstractCommand extends TerraCommand {
}
}

// create and sign transaction, without executing
makeRawTransaction = async (): Promise<RawTransaction> => {
const operations = {
[TERRA_OPERATIONS.DEPLOY]: this.abstractPrepareDeploy,
[TERRA_OPERATIONS.EXECUTE]: this.abstractPrepareCall,
[TERRA_OPERATIONS.QUERY]: () => {
throw Error('makeRawTransaction: cannot make a tx from a query commmand')
},
// TODO: [TERRA_OPERATIONS.UPLOAD]: this.abstractPrepareUpload,
}

return await operations[this.opts.action](this.params, this.args[0])
}

abstractPrepareDeploy = async (params: any) => {
const codeId = this.codeIds[this.opts.contract.id]
this.require(!!codeId, `Code Id for contract ${this.opts.contract.id} not found`)
return await this.prepareDeploy(codeId, params)
}

abstractPrepareCall = async (params: any, address: string) => {
return await this.prepareCall(address, {
[this.opts.function]: params,
})
}

execute = async () => {
const operations = {
[TERRA_OPERATIONS.DEPLOY]: this.abstractDeploy,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra'
import { MultisigTerraCommand } from '../contracts/multisig'
import { Result, Command } from '@chainlink/gauntlet-core'
import { logger } from '@chainlink/gauntlet-core/dist/utils'
import DeployLink from '../../commands/contracts/link/deploy'
import AbstractCommand from '.'
import { makeAbstractCommand } from '.'

class EmptyCommand extends Command {
constructor() {
super({ help: false }, [])
}
}

type ICommandConstructor = (flags: any, args: string[]) => void
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this "polymorphic" command? Why do we need it? We are already wrapping the commands into a multisig command


export default (commands: any, slug: string): Command => {
const slugs: string[] = slug.split(':')
if (slugs.length < 3) {
throw Error(`Command ${slug} not found`)
}
const op: string = slugs.pop()!
const instruction = slugs.join(':')

const commandType = commands[instruction] ? commands[instruction] : AbstractCommand

switch (op) {
case 'multisig':
case 'propose':
case 'vote':
case 'execute':
case 'approve': // vote yes, then execute if threshold is reached
class WrappedCommand extends MultisigTerraCommand {
static id = instruction
static commandType = commandType

constructor(flags, args) {
super(flags, args)
if (commandType === AbstractCommand) {
throw Error(`Command ${instruction} not found`)
// TODO: get this working for abstract commands. Something like:
// this.command = await makeAbstractCommand(instruction, flags, args)
} else {
this.command = new commandType(flags, args)
}
}

multisigOp = () => {
return op
}
}

// This is a temporary workaround for a bug in the type specification for findPolymorphic in @gauntlet-core.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polymorphic is a legacy command that gauntlet-core had to support for old Gauntlet EVM, but we won't be using it in the future

// At runtime, it's used as a constructor. It must be callable and able to construct an actual command object
// when passed flags & args, (ie, it should be a class). But it's declared as if it were an instance of a type
// satisfying ICommand, which will cause typescript to reject it during compilation if it's a class satisfying
// the ICommand interface. The only way to satisfy both constraints is for it to look like an instance at compile
// time, and a class at runtime. The workaround is to return something that is both at once: add all properties
// of an instance of EmtpyCommand to the class WrappedCommand and return the resulting hybrid.
return Object.assign(WrappedCommand, new EmptyCommand())
default:
throw Error(`Command ${slugs.join(':')} not found`)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra'
import { TerraCommand, TransactionResponse, RawTransaction } from '@chainlink/gauntlet-terra'
import { Result } from '@chainlink/gauntlet-core'
import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils'
import { CATEGORIES, CW20_BASE_CODE_IDs } from '../../../lib/constants'
Expand All @@ -19,9 +19,8 @@ export default class DeployLink extends TerraCommand {
super(flags, args)
}

execute = async () => {
await prompt(`Begin deploying LINK Token?`)
const deploy = await this.deploy(CW20_BASE_CODE_IDs[this.flags.network], {
genParams = () => {
return {
name: 'ChainLink Token',
symbol: 'LINK',
decimals: 18,
Expand All @@ -36,7 +35,18 @@ export default class DeployLink extends TerraCommand {
mint: {
minter: this.wallet.key.accAddress,
},
})
}
}

makeRawTransaction = async (): Promise<RawTransaction> => {
const codeId = CW20_BASE_CODE_IDs[this.flags.network]
this.require(!!codeId, `Code Id for link token contract not found`)
return await this.prepareDeploy(CW20_BASE_CODE_IDs[this.flags.network], this.genParams())
}

execute = async () => {
await prompt(`Begin deploying LINK Token?`)
const deploy = await this.deploy(CW20_BASE_CODE_IDs[this.flags.network], this.genParams())
const result = await this.provider.wasm.contractQuery(deploy.address!, { token_info: {} })
logger.success(`LINK token successfully deployed at ${deploy.address} (txhash: ${deploy.hash})`)
logger.debug(result)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { CreateGroup } from './group'
import { CreateWallet } from './wallet'
import { MultisigTerraCommand } from './multisig'

export default [CreateGroup, CreateWallet]
export { MultisigTerraCommand }
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// multisig.ts
//
// For now, propose, vote, and execute functionality are all combined into one CONTRACT:COMMAND::multisig meta-command
// This is parallel to how things are implemented in Solana. The execute happens automatically as soon as the last
// vote required to exceeed the threshold is cast. And the difference between propose and vote is distinguished by
// whether the --proposal=PROPOSAL_HASH flag is passed. Later, we may want to split this into CONTRACT::COMMAND::propose,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to stick to the same process as EVM and Solana, where the multisig command tells us which step we are based on the proposal state

// CONTRACT::COMMAND::vote, and CONTRACT::COMMAND::execute. We may also want to add CONTRACT::COMMAND::close, to
// abort a proposal early (before it expires), disallowing any further voting on it.

import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils'
import { Result, ICommand } from '@chainlink/gauntlet-core'
import { TerraCommand, RawTransaction, TransactionResponse } from '@chainlink/gauntlet-terra'
import { CATEGORIES } from '../../../lib/constants'
import { CONTRACT_LIST, Contract, getContract, TERRA_OPERATIONS } from '../../../lib/contracts'
import AbstractCommand from '../../abstract'

type ProposalContext = {
rawTx: RawTransaction
multisigSigner: string
proposalState: any
}

type StringGetter = () => string
export type ICommandConstructor = (flags: any, args: string[]) => void

abstract class MultisigTerraCommand extends TerraCommand {
static category = CATEGORIES.MULTISIG

commandType: typeof TerraCommand
multisigOp: StringGetter

command: TerraCommand
multisigAddress: string
multisigContract: Promise<Contract>

constructor(flags, args) {
super(flags, args)
}

postConstructor(flags, args) {
// Called after child constructor
logger.debug(`Running ${this.commandType} in multisig mode`)

this.command.invokeMiddlewares(this.command, this.command.middlewares)
this.require(!!process.env.MULTISIG_WALLET, 'Please set MULTISIG_WALLET env var')
this.multisigContract = getContract(CONTRACT_LIST.MULTISIG, flags.version)
this.multisigAddress = process.env.MULTISIG_WALLET!
}

execute = async (): Promise<Result<TransactionResponse>> => {
const tx = this.command.makeRawTransaction()
console.debug(tx)

return {
responses: [],
} as Result<TransactionResponse>
}
}

export const wrapCommand = (command) => {
return class CustomCommand extends MultisigTerraCommand {
static id = `${command.id}:multisig`
static category = CATEGORIES.MULTISIG
}
}

export { MultisigTerraCommand }
4 changes: 2 additions & 2 deletions packages-ts/gauntlet-terra-contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { executeCLI } from '@chainlink/gauntlet-core'
import { existsSync } from 'fs'
import path from 'path'
import Terra from './commands'
import { makeAbstractCommand } from './commands/abstract'
import { makeAbstractCommand, findPolymorphic } from './commands/abstract'
import { defaultFlags } from './lib/args'

const commands = {
custom: [...Terra],
loadDefaultFlags: () => defaultFlags,
abstract: {
findPolymorphic: () => undefined,
findPolymorphic: findPolymorphic,
makeCommand: makeAbstractCommand,
},
}
Expand Down
12 changes: 0 additions & 12 deletions packages-ts/gauntlet-terra-contracts/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,3 @@ export const CW20_BASE_CODE_IDs = {
local: 32,
'bombay-testnet': 148,
}

export const CW4_GROUP_CODE_IDs = {
mainnet: -1,
local: -1,
'bombay-testnet': 36895,
}

export const CW3_FLEX_MULTISIG_CODE_IDs = {
mainnet: -1,
local: -1,
'bombay-testnet': 36059,
}
6 changes: 5 additions & 1 deletion packages-ts/gauntlet-terra-contracts/src/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import JTD from 'ajv/dist/jtd'

const ajv = new Ajv().addFormat('uint8', (value: any) => !isNaN(value))

ajv.addFormat('uint32', {
type: 'number',
validate: (x) => !isNaN(x),
})

ajv.addFormat('uint64', {
type: 'number',
validate: (x) => !isNaN(x),
Expand All @@ -14,6 +19,5 @@ ajv.addFormat('uint32', {
})

export default ajv

const jtd = new JTD()
export { jtd }
29 changes: 22 additions & 7 deletions packages-ts/gauntlet-terra/src/commands/internal/terra.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Result, WriteCommand } from '@chainlink/gauntlet-core'
import { RawTransaction } from '../types'
import { logger } from '@chainlink/gauntlet-core/dist/utils'
import { EventsByType, MsgStoreCode, TxLog } from '@terra-money/terra.js'
import { SignMode } from '@terra-money/terra.proto/cosmos/tx/signing/v1beta1/signing'
Expand Down Expand Up @@ -29,6 +30,10 @@ export default abstract class TerraCommand extends WriteCommand<TransactionRespo
this.use(withNetwork, withProvider, withWallet, withCodeIds)
}

makeRawTransaction = async (): Promise<RawTransaction> => {
throw Error('makeRawTransaction: not implemented!')
}

parseResponseValue(receipt: any, eventType: string, attributeType: string) {
try {
const parsed = JSON.parse(receipt?.raw_log)
Expand Down Expand Up @@ -63,53 +68,63 @@ export default abstract class TerraCommand extends WriteCommand<TransactionRespo
return await this.provider.wasm.contractQuery(address, input, params)
}

async call(address, input) {
async prepareCall(address, input) {
const msg = new MsgExecuteContract(this.wallet.key.accAddress, address, input)

const tx = await this.wallet.createAndSignTx({
return await this.wallet.createAndSignTx({
msgs: [msg],
...(this.wallet.key instanceof LedgerKey && {
signMode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON,
}),
})
}

async call(address, input) {
const tx = await this.prepareCall(address, input)
const res = await this.provider.tx.broadcast(tx)

logger.debug(res)

return this.wrapResponse(res)
}

async deploy(codeId, instantiateMsg, migrationContract = undefined) {
async prepareDeploy(codeId, instantiateMsg, migrationContract = undefined) {
const instantiate = new MsgInstantiateContract(
this.wallet.key.accAddress,
migrationContract,
codeId,
instantiateMsg,
)
const instantiateTx = await this.wallet.createAndSignTx({
return await this.wallet.createAndSignTx({
msgs: [instantiate],
memo: 'Instantiating',
...(this.wallet.key instanceof LedgerKey && {
signMode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON,
}),
})
}

async deploy(codeId, instantiateMsg, migrationContract = undefined) {
const instantiateTx = await this.prepareDeploy(codeId, instantiateMsg, (migrationContract = undefined))
logger.loading(`Deploying contract...`)
const res = await this.provider.tx.broadcast(instantiateTx)

return this.wrapResponse(res)
}

async upload(wasm, contractName) {
async prepareUpload(wasm, contractName) {
const code = new MsgStoreCode(this.wallet.key.accAddress, wasm)

const tx = await this.wallet.createAndSignTx({
return await this.wallet.createAndSignTx({
msgs: [code],
memo: `Storing ${contractName}`,
...(this.wallet.key instanceof LedgerKey && {
signMode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON,
}),
})
}

async upload(wasm, contractName) {
const tx = await this.prepareUpload(wasm, contractName)
logger.loading(`Uploading ${contractName} contract code...`)
const res = await this.provider.tx.broadcast(tx)

Expand Down
4 changes: 3 additions & 1 deletion packages-ts/gauntlet-terra/src/commands/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BlockTxBroadcastResult, EventsByType } from '@terra-money/terra.js'
import { BlockTxBroadcastResult, EventsByType, Tx as RawTransaction } from '@terra-money/terra.js'

export type TransactionResponse = {
hash: string
Expand All @@ -7,3 +7,5 @@ export type TransactionResponse = {
tx?: BlockTxBroadcastResult
events?: EventsByType[]
}

export { RawTransaction }
4 changes: 2 additions & 2 deletions packages-ts/gauntlet-terra/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import TerraCommand from './commands/internal/terra'
import { waitExecute } from './lib/execute'
import { TransactionResponse } from './commands/types'
import { RawTransaction, TransactionResponse } from './commands/types'
import * as constants from './lib/constants'

export { TerraCommand, waitExecute, TransactionResponse, constants }
export { RawTransaction, TerraCommand, waitExecute, TransactionResponse, constants }