diff --git a/packages-ts/gauntlet-terra-contracts/networks/.env.bombay-testnet b/packages-ts/gauntlet-terra-contracts/networks/.env.bombay-testnet
index 15fc2f61..f659c2de 100644
--- a/packages-ts/gauntlet-terra-contracts/networks/.env.bombay-testnet
+++ b/packages-ts/gauntlet-terra-contracts/networks/.env.bombay-testnet
@@ -4,5 +4,6 @@ DEFAULT_GAS_PRICE=0.5
LINK=terra1fcksmfjncl6m7apvpalvhwv5jxd9djv5lwyu82
BILLING_ACCESS_CONTROLLER=terra1trcufj64y53hxk7g8cra33xw3jkyvlr9lu99eu
REQUESTER_ACCESS_CONTROLLER=terra1s38kfu4qp0ttwxkka9zupaysefl5qruhv5rc0z
-MULTISIG_GROUP=terra168lv95kfm49y9zu0409jmplj756ukxdrew7uta
-MULTISIG_WALLET=terra1u89pduw4enewduy9qydj925738cyn9juszgj54
+
+CW4_GROUP=terra1edk45cc6rckjszfmr87qfx50pfn2mnhg5mn3vd
+CW3_FLEX_MULTISIG=terra1cql0r4csmce0ntf68dmkvu3negs7m662uyg90g
diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/executionWrapper.ts b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/executionWrapper.ts
index ca24ed2c..00766832 100644
--- a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/executionWrapper.ts
+++ b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/executionWrapper.ts
@@ -1,6 +1,7 @@
import AbstractCommand, { makeAbstractCommand } from '.'
import { Result } from '@chainlink/gauntlet-core'
import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra'
+import { AccAddress, MsgExecuteContract } from '@terra-money/terra.js'
export interface AbstractInstruction {
instruction: {
@@ -26,7 +27,7 @@ export const instructionToCommand = (instruction: AbstractInstruction)
super(flags, args)
}
- execute = async (): Promise> => {
+ buildCommand = async (): Promise => {
const commandInput = await instruction.makeInput(this.flags, this.args)
if (!instruction.validateInput(commandInput)) {
throw new Error(`Invalid input params: ${JSON.stringify(commandInput)}`)
@@ -34,7 +35,17 @@ export const instructionToCommand = (instruction: AbstractInstruction)
const input = await instruction.makeContractInput(commandInput)
const abstractCommand = await makeAbstractCommand(id, this.flags, this.args, input)
await abstractCommand.invokeMiddlewares(abstractCommand, abstractCommand.middlewares)
- let response = await abstractCommand.execute()
+ return abstractCommand
+ }
+
+ makeRawTransaction = async (signer: AccAddress): Promise => {
+ const command = await this.buildCommand()
+ return command.makeRawTransaction(signer)
+ }
+
+ execute = async (): Promise> => {
+ const command = await this.buildCommand()
+ let response = await command.execute()
if (instruction.afterExecute) {
const data = instruction.afterExecute(response)
response = { ...response, data: { ...data } }
diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts
index 67e5370a..74b60a21 100644
--- a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts
+++ b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts
@@ -1,4 +1,5 @@
import { Result } from '@chainlink/gauntlet-core'
+import { AccAddress, MsgExecuteContract } from '@terra-money/terra.js'
import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils'
import { TransactionResponse, TerraCommand } from '@chainlink/gauntlet-terra'
import { Contract, CONTRACT_LIST, getContract, TerraABI, TERRA_OPERATIONS } from '../../lib/contracts'
@@ -130,6 +131,11 @@ export default class AbstractCommand extends TerraCommand {
this.contracts = [this.opts.contract.id]
}
+ makeRawTransaction = async (signer: AccAddress): Promise => {
+ const address = this.args[0]
+ return new MsgExecuteContract(signer, address, this.params)
+ }
+
abstractDeploy: AbstractExecute = async (params: any) => {
logger.loading(`Deploying contract ${this.opts.contract.id}`)
const codeId = this.codeIds[this.opts.contract.id]
diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/inspectionWrapper.ts b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/inspectionWrapper.ts
index af08a195..c58578da 100644
--- a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/inspectionWrapper.ts
+++ b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/inspectionWrapper.ts
@@ -47,6 +47,10 @@ export const instructionToInspectCommand = (
super(flags, args)
}
+ makeRawTransaction = () => {
+ throw new Error('Inspection command does not involve any transaction')
+ }
+
execute = async (): Promise> => {
const input = await inspectInstruction.makeInput(this.flags, this.args)
const commands = await Promise.all(
diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/deploy.ts b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/deploy.ts
index c644a4e9..33fd1b45 100644
--- a/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/deploy.ts
+++ b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/deploy.ts
@@ -19,6 +19,10 @@ export default class DeployLink extends TerraCommand {
super(flags, args)
}
+ makeRawTransaction = async () => {
+ throw new Error('Deploy LINK command: makeRawTransaction method not implemented')
+ }
+
execute = async () => {
await prompt(`Begin deploying LINK Token?`)
const deploy = await this.deploy(CW20_BASE_CODE_IDs[this.flags.network], {
diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/transfer.ts b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/transfer.ts
index 367a1ea2..fd2732f6 100644
--- a/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/transfer.ts
+++ b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/transfer.ts
@@ -17,6 +17,10 @@ export default class TransferLink extends TerraCommand {
super(flags, args)
}
+ makeRawTransaction = async () => {
+ throw new Error('Transfer LINK command: makeRawTransaction method not implemented')
+ }
+
execute = async () => {
const decimals = this.flags.decimals || 18
const link = this.flags.link || process.env.LINK
diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/tooling/upload.ts b/packages-ts/gauntlet-terra-contracts/src/commands/tooling/upload.ts
index 3862513a..0840c116 100644
--- a/packages-ts/gauntlet-terra-contracts/src/commands/tooling/upload.ts
+++ b/packages-ts/gauntlet-terra-contracts/src/commands/tooling/upload.ts
@@ -23,6 +23,10 @@ export default class UploadContractCode extends TerraCommand {
super(flags, args)
}
+ makeRawTransaction = async () => {
+ throw new Error('Upload command: makeRawTransaction method not implemented')
+ }
+
getCodeId(response): number | undefined {
return Number(this.parseResponseValue(response, 'store_code', 'code_id'))
}
diff --git a/packages-ts/gauntlet-terra-contracts/src/index.ts b/packages-ts/gauntlet-terra-contracts/src/index.ts
index dcf0b1ba..ef5a87fa 100644
--- a/packages-ts/gauntlet-terra-contracts/src/index.ts
+++ b/packages-ts/gauntlet-terra-contracts/src/index.ts
@@ -1,4 +1,5 @@
import { executeCLI } from '@chainlink/gauntlet-core'
+import { multisigWrapCommand } from '@chainlink/gauntlet-terra-cw-plus'
import { existsSync } from 'fs'
import path from 'path'
import { io } from '@chainlink/gauntlet-core/dist/utils'
@@ -7,7 +8,7 @@ import { makeAbstractCommand } from './commands/abstract'
import { defaultFlags } from './lib/args'
const commands = {
- custom: [...Terra],
+ custom: [...Terra, ...Terra.map(multisigWrapCommand)],
loadDefaultFlags: () => defaultFlags,
abstract: {
findPolymorphic: () => undefined,
diff --git a/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts b/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts
index 20cb82fe..228a97ec 100644
--- a/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts
+++ b/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts
@@ -11,7 +11,7 @@ export const enum CATEGORIES {
DEVIATION_FLAGGING_VALIDATOR = 'Devaiation Flagging Validator',
}
-export const DEFAULT_RELEASE_VERSION = 'v0.0.4'
+export const DEFAULT_RELEASE_VERSION = 'local'
export const DEFAULT_CWPLUS_VERSION = 'v0.9.1'
export const ORACLES_MAX_LENGTH = 31
diff --git a/packages-ts/gauntlet-terra-cw-plus/README.md b/packages-ts/gauntlet-terra-cw-plus/README.md
new file mode 100644
index 00000000..3c95c93f
--- /dev/null
+++ b/packages-ts/gauntlet-terra-cw-plus/README.md
@@ -0,0 +1 @@
+# Gauntlet Terra CW Plus
\ No newline at end of file
diff --git a/packages-ts/gauntlet-terra-cw-plus/package.json b/packages-ts/gauntlet-terra-cw-plus/package.json
new file mode 100644
index 00000000..c7f36436
--- /dev/null
+++ b/packages-ts/gauntlet-terra-cw-plus/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@chainlink/gauntlet-terra-cw-plus",
+ "version": "0.0.1",
+ "description": "Gauntlet Terra CW Plus contracts",
+ "keywords": [
+ "typescript",
+ "cli"
+ ],
+ "main": "./dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist/**/*",
+ "!dist/**/*.test.js"
+ ],
+ "scripts": {
+ "preinstall": "node ../../scripts/require-yarn.js",
+ "gauntlet": "ts-node ./src/index.ts",
+ "lint": "tsc",
+ "test": "SKIP_PROMPTS=true jest --runInBand",
+ "test:coverage": "yarn test --collectCoverage",
+ "test:ci": "yarn test --ci",
+ "lint:format": "yarn prettier --check ./src",
+ "format": "yarn prettier --write ./src",
+ "clean": "rm -rf ./dist/ ./bin/",
+ "build": "yarn clean && tsc",
+ "bundle": "yarn build && pkg ."
+ },
+ "dependencies": {
+ "@chainlink/gauntlet-core": "0.0.7",
+ "@chainlink/gauntlet-terra": "*"
+ }
+}
diff --git a/packages-ts/gauntlet-terra-cw-plus/src/commands/multisig.ts b/packages-ts/gauntlet-terra-cw-plus/src/commands/multisig.ts
new file mode 100644
index 00000000..c15d5ce0
--- /dev/null
+++ b/packages-ts/gauntlet-terra-cw-plus/src/commands/multisig.ts
@@ -0,0 +1,275 @@
+import { Result } from '@chainlink/gauntlet-core'
+import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils'
+import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra'
+import { AccAddress, MsgExecuteContract } from '@terra-money/terra.js'
+import { isDeepEqual } from '../lib/utils'
+
+type ProposalAction = (
+ signer: AccAddress,
+ proposalId: number,
+ message: MsgExecuteContract,
+) => Promise
+
+enum Vote {
+ YES = 'yes',
+ NO = 'no',
+ ABS = 'abstain',
+ VETO = 'veto',
+}
+
+type WasmMsg = {
+ wasm: {
+ execute: {
+ contract_addr: string
+ funds: {
+ denom: string
+ amount: string
+ }[]
+ msg: string
+ }
+ }
+}
+
+enum Action {
+ CREATE = 'create',
+ APPROVE = 'approve',
+ EXECUTE = 'execute',
+ NONE = 'none',
+}
+
+type State = {
+ threshold: number
+ nextAction: Action
+ owners: AccAddress[]
+ approvers: string[]
+ // https://github.com/CosmWasm/cw-plus/blob/82138f9484e538913f7faf78bc292fb14407aae8/packages/cw3/src/query.rs#L75
+ currentStatus?: 'pending' | 'open' | 'rejected' | 'passed' | 'executed'
+ data?: WasmMsg[]
+}
+
+export const wrapCommand = (command) => {
+ return class Multisig extends TerraCommand {
+ command: TerraCommand
+ multisig: AccAddress
+ multisigGroup: AccAddress
+
+ static id = `${command.id}:multisig`
+
+ constructor(flags, args) {
+ super(flags, args)
+
+ this.command = new command(flags, args)
+
+ if (!AccAddress.validate(process.env.CW3_FLEX_MULTISIG)) throw new Error(`Invalid Multisig wallet address`)
+ if (!AccAddress.validate(process.env.CW4_GROUP)) throw new Error(`Invalid Multisig group address`)
+ this.multisig = process.env.CW3_FLEX_MULTISIG as AccAddress
+ this.multisigGroup = process.env.CW4_GROUP as AccAddress
+ }
+
+ makeRawTransaction = async (signer: AccAddress, state?: State) => {
+ const message = await this.command.makeRawTransaction(this.multisig)
+
+ const operations = {
+ [Action.CREATE]: this.makeProposeTransaction,
+ [Action.APPROVE]: this.makeAcceptTransaction,
+ [Action.EXECUTE]: this.makeExecuteTransaction,
+ [Action.NONE]: () => {
+ throw new Error('No action needed')
+ },
+ }
+
+ if (state.nextAction !== Action.CREATE) {
+ this.require(
+ await this.isSameProposal(state.data, [this.toWasmMsg(message)]),
+ 'The transaction generated is different from the proposal provided',
+ )
+ }
+
+ return operations[state.nextAction](signer, Number(this.flags.proposal), message)
+ }
+
+ isSameProposal = (proposalMsgs: WasmMsg[], generatedMsgs: WasmMsg[]) => {
+ return isDeepEqual(proposalMsgs, generatedMsgs)
+ }
+
+ toWasmMsg = (message: MsgExecuteContract): WasmMsg => {
+ return {
+ wasm: {
+ execute: {
+ contract_addr: message.contract,
+ funds: message.coins.toArray().map((c) => c.toData()),
+ msg: Buffer.from(JSON.stringify(message.execute_msg)).toString('base64'),
+ },
+ },
+ }
+ }
+
+ makeProposeTransaction: ProposalAction = async (signer, _, message) => {
+ logger.info('Generating data for creating new proposal')
+ const proposeInput = {
+ propose: {
+ description: command.id,
+ msgs: [this.toWasmMsg(message)],
+ title: command.id,
+ // TODO: Set expiration time
+ // latest: { never: {} }
+ },
+ }
+ return new MsgExecuteContract(signer, this.multisig, proposeInput)
+ }
+
+ makeAcceptTransaction: ProposalAction = async (signer, proposalId) => {
+ logger.info(`Generating data for approving proposal ${proposalId}`)
+ const approvalInput = {
+ vote: {
+ vote: Vote.YES,
+ proposal_id: proposalId,
+ },
+ }
+ return new MsgExecuteContract(signer, this.multisig, approvalInput)
+ }
+
+ makeExecuteTransaction: ProposalAction = async (signer, proposalId) => {
+ logger.info(`Generating data for executing proposal ${proposalId}`)
+ const executeInput = {
+ execute: {
+ proposal_id: proposalId,
+ },
+ }
+ return new MsgExecuteContract(signer, this.multisig, executeInput)
+ }
+
+ fetchState = async (proposalId?: number): Promise => {
+ const groupState = await this.query(this.multisigGroup, {
+ list_members: {},
+ })
+ const owners = groupState.members.map((m) => m.addr)
+ const thresholdState = await this.query(this.multisig, {
+ threshold: {},
+ })
+ const threshold = thresholdState.absolute_count.total_weight
+ if (!proposalId) {
+ return {
+ threshold,
+ nextAction: Action.CREATE,
+ owners,
+ approvers: [],
+ }
+ }
+ const proposalState = await this.query(this.multisig, {
+ proposal: {
+ proposal_id: proposalId,
+ },
+ })
+ const votes = await this.query(this.multisig, {
+ list_votes: {
+ proposal_id: proposalId,
+ },
+ })
+ const status = proposalState.status
+ const toNextAction = {
+ passed: Action.EXECUTE,
+ open: Action.APPROVE,
+ pending: Action.APPROVE,
+ rejected: Action.NONE,
+ executed: Action.NONE,
+ }
+ return {
+ threshold,
+ nextAction: toNextAction[status],
+ owners,
+ currentStatus: status,
+ data: proposalState.msgs,
+ approvers: votes.votes.filter((v) => v.vote === Vote.YES).map((v) => v.voter),
+ }
+ }
+
+ printPostInstructions = async (proposalId: number) => {
+ const state = await this.fetchState(proposalId)
+ const approvalsLeft = state.threshold - state.approvers.length
+ const messages = {
+ [Action.APPROVE]: `The proposal needs ${approvalsLeft} more approvals. Run the same command with the flag --proposal=${proposalId}`,
+ [Action.EXECUTE]: `The proposal reached the threshold and can be executed. Run the same command with the flag --proposal=${proposalId}`,
+ [Action.NONE]: `The proposal has been executed. No more actions needed`,
+ }
+ logger.line()
+ logger.info(`${messages[state.nextAction]}`)
+ logger.line()
+ }
+
+ execute = async () => {
+ let proposalId = !!this.flags.proposal && Number(this.flags.proposal)
+ const state = await this.fetchState(proposalId)
+
+ if (state.nextAction === Action.NONE) {
+ await this.printPostInstructions(proposalId)
+ return
+ }
+ const rawTx = await this.makeRawTransaction(this.wallet.key.accAddress, state)
+
+ logger.info(`Proposal State:
+ - Total Owners: ${state.owners.length}
+ - Owners List: ${state.owners}
+
+ - Threshold: ${state.threshold}
+ - Total Approvers: ${state.approvers.length}
+ - Approvers List: ${state.approvers}
+
+ - Next Action: ${state.nextAction.toUpperCase()}
+ `)
+
+ const actionMessage = {
+ [Action.CREATE]: 'CREATING',
+ [Action.APPROVE]: 'APPROVING',
+ [Action.EXECUTE]: 'EXECUTING',
+ }
+
+ if (this.flags.execute) {
+ await prompt(`Continue ${actionMessage[state.nextAction]} proposal?`)
+ const tx = await this.signAndSend([rawTx])
+
+ if (state.nextAction === Action.CREATE) {
+ const proposalFromEvent = tx.events[0].wasm.proposal_id[0]
+ logger.success(`New proposal created with ID: ${proposalFromEvent}`)
+ proposalId = Number(proposalFromEvent)
+ }
+
+ await this.printPostInstructions(proposalId)
+
+ return {
+ responses: [
+ {
+ tx,
+ contract: this.multisig,
+ },
+ ],
+ data: {
+ proposalId,
+ },
+ } as Result
+ }
+
+ // TODO: Test raw message
+ const msgData = Buffer.from(JSON.stringify(rawTx.execute_msg)).toString('base64')
+ logger.line()
+ logger.success(`Message generated succesfully for ${actionMessage[state.nextAction]} proposal`)
+ logger.log()
+ logger.log(msgData)
+ logger.log()
+ logger.line()
+
+ return {
+ responses: [
+ {
+ tx: {},
+ contract: this.multisig,
+ },
+ ],
+ data: {
+ proposalId,
+ message: msgData,
+ },
+ } as Result
+ }
+ }
+}
diff --git a/packages-ts/gauntlet-terra-cw-plus/src/index.ts b/packages-ts/gauntlet-terra-cw-plus/src/index.ts
new file mode 100644
index 00000000..08a5280c
--- /dev/null
+++ b/packages-ts/gauntlet-terra-cw-plus/src/index.ts
@@ -0,0 +1,3 @@
+import { wrapCommand as multisigWrapCommand } from './commands/multisig'
+
+export { multisigWrapCommand }
diff --git a/packages-ts/gauntlet-terra-cw-plus/src/lib/utils.ts b/packages-ts/gauntlet-terra-cw-plus/src/lib/utils.ts
new file mode 100644
index 00000000..07401085
--- /dev/null
+++ b/packages-ts/gauntlet-terra-cw-plus/src/lib/utils.ts
@@ -0,0 +1,13 @@
+import assert from 'assert'
+
+export const isDeepEqual = (a: any, b: any) => {
+ try {
+ assert.deepStrictEqual(a, b)
+ } catch (error) {
+ if (error.name === 'AssertionError') {
+ return false
+ }
+ throw error
+ }
+ return true
+}
diff --git a/packages-ts/gauntlet-terra-cw-plus/tsconfig.json b/packages-ts/gauntlet-terra-cw-plus/tsconfig.json
new file mode 100644
index 00000000..2c84c1fc
--- /dev/null
+++ b/packages-ts/gauntlet-terra-cw-plus/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["dist", "**/*.spec.ts", "**/*.test.ts"]
+}
diff --git a/packages-ts/gauntlet-terra/src/commands/internal/terra.ts b/packages-ts/gauntlet-terra/src/commands/internal/terra.ts
index 72a140bf..ee6298ee 100644
--- a/packages-ts/gauntlet-terra/src/commands/internal/terra.ts
+++ b/packages-ts/gauntlet-terra/src/commands/internal/terra.ts
@@ -1,6 +1,6 @@
import { Result, WriteCommand } from '@chainlink/gauntlet-core'
import { logger } from '@chainlink/gauntlet-core/dist/utils'
-import { EventsByType, MsgStoreCode, TxLog } from '@terra-money/terra.js'
+import { EventsByType, MsgStoreCode, AccAddress, TxLog } from '@terra-money/terra.js'
import { SignMode } from '@terra-money/terra.proto/cosmos/tx/signing/v1beta1/signing'
import { withProvider, withWallet, withCodeIds, withNetwork } from '../middlewares'
@@ -23,6 +23,7 @@ export default abstract class TerraCommand extends WriteCommand Promise>
+ abstract makeRawTransaction: (signer: AccAddress) => Promise
constructor(flags, args) {
super(flags, args)
@@ -63,6 +64,25 @@ export default abstract class TerraCommand extends WriteCommand => {
+ try {
+ logger.loading('Signing transaction...')
+ const tx = await this.wallet.createAndSignTx({
+ msgs: messages,
+ ...(this.wallet.key instanceof LedgerKey && {
+ signMode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON,
+ }),
+ })
+
+ logger.loading('Sending transaction...')
+ const res = await this.provider.tx.broadcast(tx)
+ return this.wrapResponse(res)
+ } catch (e) {
+ const message = e?.response?.data?.message || e.message
+ throw new Error(message)
+ }
+ }
+
async call(address, input) {
const msg = new MsgExecuteContract(this.wallet.key.accAddress, address, input)
@@ -74,8 +94,6 @@ export default abstract class TerraCommand extends WriteCommand