Skip to content

Commit

Permalink
Token commands + Improvements (#157)
Browse files Browse the repository at this point in the history
* added balance diff in inspection

* transfer token and send uluna commands

* added mainnet ids

* refactor before execution

* refactor and improvements

* fix rebase

* Gauntlet sec improvements (#166)

* transfer ownership checks

* inpect offchain config from event info

* provider in execution context. minor improvements

* updated test

* hex to base64

* Update guantlet e2e test to use an rdd with all non zero false values so the inspect command can check all values have changed
No longer use the digest in the test as it is not needed

Co-authored-by: Tate <[email protected]>

* added cw20 code id and more validations

* small refactor

Co-authored-by: Tate <[email protected]>
  • Loading branch information
RodrigoAD and tateexon authored Mar 4, 2022
1 parent 46ee0eb commit 0abf4ec
Show file tree
Hide file tree
Showing 29 changed files with 543 additions and 142 deletions.
8 changes: 8 additions & 0 deletions packages-ts/gauntlet-terra-contracts/codeIds/mainnet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"access_controller": 2690,
"ocr2": 3024,
"proxy_ocr2": 3202,
"cw3_flex_multisig": 3192,
"cw4_group": 3193,
"cw20_base": 3
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
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'
import { AccAddress, LCDClient } from '@terra-money/terra.js'
import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils'

export type ExecutionContext<Input, ContractInput> = {
input: Input
contractInput: ContractInput
id: string
contract: string
provider: LCDClient
flags: any
}

export type BeforeExecute<Input, ContractInput> = (
context: ExecutionContext<Input, ContractInput>,
) => (signer: AccAddress) => Promise<void>

export interface AbstractInstruction<Input, ContractInput> {
examples?: string[]
Expand All @@ -13,49 +27,69 @@ export interface AbstractInstruction<Input, ContractInput> {
makeInput: (flags: any, args: string[]) => Promise<Input>
validateInput: (input: Input) => boolean
makeContractInput: (input: Input) => Promise<ContractInput>
beforeExecute?: BeforeExecute<Input, ContractInput>
afterExecute?: (response: Result<TransactionResponse>) => any
}

export const instructionToCommand = (instruction: AbstractInstruction<any, any>) => {
const defaultBeforeExecute = <Input, ContractInput>(context: ExecutionContext<Input, ContractInput>) => async () => {
logger.loading(`Executing ${context.id} from contract ${context.contract}`)
logger.log('Input Params:', context.contractInput)
await prompt(`Continue?`)
}

export const instructionToCommand = <Input, ContractInput>(instruction: AbstractInstruction<Input, ContractInput>) => {
const id = `${instruction.instruction.contract}:${instruction.instruction.function}`
const category = `${instruction.instruction.category}`
const examples = instruction.examples || []
return class Command extends TerraCommand {
static id = id
static category = category
static examples = examples

command: AbstractCommand

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

afterExecute = instruction.afterExecute
afterExecute = instruction.afterExecute || this.afterExecute

buildCommand = async (): Promise<TerraCommand> => {
const commandInput = await instruction.makeInput(this.flags, this.args)
if (!instruction.validateInput(commandInput)) {
throw new Error(`Invalid input params: ${JSON.stringify(commandInput)}`)
buildCommand = async (flags, args): Promise<TerraCommand> => {
const input = await instruction.makeInput(flags, args)
if (!instruction.validateInput(input)) {
throw new Error(`Invalid input params: ${JSON.stringify(input)}`)
}
const contractInput = await instruction.makeContractInput(input)
const executionContext: ExecutionContext<Input, ContractInput> = {
input,
contractInput,
id,
contract: this.args[0],
provider: this.provider,
flags,
}
const input = await instruction.makeContractInput(commandInput)
const abstractCommand = await makeAbstractCommand(id, this.flags, this.args, input)
this.beforeExecute = instruction.beforeExecute
? instruction.beforeExecute(executionContext)
: defaultBeforeExecute(executionContext)

const abstractCommand = await makeAbstractCommand(id, this.flags, this.args, contractInput)
await abstractCommand.invokeMiddlewares(abstractCommand, abstractCommand.middlewares)
return abstractCommand
this.command = abstractCommand

return this
}

makeRawTransaction = async (signer: AccAddress): Promise<MsgExecuteContract> => {
const command = await this.buildCommand()
return command.makeRawTransaction(signer)
makeRawTransaction = async (signer: AccAddress) => {
return this.command.makeRawTransaction(signer)
}

execute = async (): Promise<Result<TransactionResponse>> => {
const command = await this.buildCommand()
let response = await command.execute()
if (this.afterExecute) {
const data = this.afterExecute(response)
response = { ...response, data: { ...data } }
}
return response
// TODO: Command should be built from gauntet-core
await this.buildCommand(this.flags, this.args)
await this.beforeExecute(this.wallet.key.accAddress)
let response = await this.command.execute()
const data = this.afterExecute(response)
return !!data ? { ...response, data: { ...data } } : response
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const makeAbstractCommand = async (
flags: any,
args: string[],
input?: any,
): Promise<TerraCommand> => {
): Promise<AbstractCommand> => {
const commandOpts = await parseInstruction(instruction, flags.version)
const params = parseParams(commandOpts, input || flags)
return new AbstractCommand(flags, args, commandOpts, params)
Expand Down Expand Up @@ -153,11 +153,9 @@ export default class AbstractCommand extends TerraCommand {
}

abstractExecute: AbstractExecute = async (params: any, address: string) => {
logger.loading(`Executing ${this.opts.function} from contract ${this.opts.contract.id} at ${address}`)
logger.log('Input Params:', params)
await prompt(`Continue?`)
logger.debug(`Executing ${this.opts.function} from contract ${this.opts.contract.id} at ${address}`)
const tx = await this.call(address, params)
logger.success(`Execution finished at tx ${tx.hash}`)
logger.debug(`Execution finished at tx ${tx.hash}`)
return {
responses: [
{
Expand All @@ -169,7 +167,7 @@ export default class AbstractCommand extends TerraCommand {
}

abstractQuery: AbstractExecute = async (params: any, address: string) => {
logger.loading(`Calling ${this.opts.function} from contract ${this.opts.contract.id} at ${address}`)
logger.debug(`Calling ${this.opts.function} from contract ${this.opts.contract.id} at ${address}`)
const result = await this.query(address, params)
logger.debug(`Query finished with result: ${JSON.stringify(result)}`)
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import AbstractCommand, { makeAbstractCommand } from '.'
import { makeAbstractCommand } from '.'
import { Result } from '@chainlink/gauntlet-core'
import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra'
import { logger } from '@chainlink/gauntlet-core/dist/utils'
import { CATEGORIES } from '../../lib/constants'
import { CONTRACT_LIST } from '../../lib/contracts'
import { APIParams } from '@terra-money/terra.js/dist/client/lcd/APIRequester'

export type Query = (contractAddress: string, query: any, params?: APIParams) => Promise<any>
import { LCDClient } from '@terra-money/terra.js'

/**
* Inspection commands need to match this interface
Expand All @@ -31,8 +29,10 @@ export interface InspectInstruction<CommandInput, ContractExpectedInfo> {
function: string
}[]
makeInput: (flags: any, args: string[]) => Promise<CommandInput>
makeInspectionData: (query: Query) => (input: CommandInput) => Promise<ContractExpectedInfo>
makeOnchainData: (query: Query) => (instructionsData: any[]) => ContractExpectedInfo
makeInspectionData: (provider: LCDClient) => (input: CommandInput) => Promise<ContractExpectedInfo>
makeOnchainData: (
provider: LCDClient,
) => (instructionsData: any[], input: CommandInput, contractAddress: string) => Promise<ContractExpectedInfo>
inspect: (expected: ContractExpectedInfo, data: ContractExpectedInfo) => boolean
}

Expand Down Expand Up @@ -68,9 +68,8 @@ export const instructionToInspectCommand = <CommandInput, Expected>(
}),
)

const query: Query = this.provider.wasm.contractQuery.bind(this.provider.wasm)
const onchainData = inspectInstruction.makeOnchainData(query)(data)
const inspectData = await inspectInstruction.makeInspectionData(query)(input)
const onchainData = await inspectInstruction.makeOnchainData(this.provider)(data, input, this.args[0])
const inspectData = await inspectInstruction.makeInspectionData(this.provider)(input)
const inspection = inspectInstruction.inspect(inspectData, onchainData)
return {
data: inspection,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TerraCommand, TransactionResponse } 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'
import { CATEGORIES, CW20_BASE_CODE_IDs, TOKEN_DECIMALS } from '../../../lib/constants'

export default class DeployLink extends TerraCommand {
static description = 'Deploys LINK token contract'
Expand All @@ -28,7 +28,7 @@ export default class DeployLink extends TerraCommand {
const deploy = await this.deploy(CW20_BASE_CODE_IDs[this.flags.network], {
name: 'ChainLink Token',
symbol: 'LINK',
decimals: 18,
decimals: TOKEN_DECIMALS,
initial_balances: [{ address: this.wallet.key.accAddress, amount: '1000000000000000000000000000' }],
marketing: {
project: 'Chainlink',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,58 @@
import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra'
import { Result } from '@chainlink/gauntlet-core'
import { BN, logger, prompt } from '@chainlink/gauntlet-core/dist/utils'
import { CATEGORIES } from '../../../lib/constants'
import { CATEGORIES, TOKEN_DECIMALS } from '../../../lib/constants'
import { AbstractInstruction, ExecutionContext, instructionToCommand } from '../../abstract/executionWrapper'
import { AccAddress } from '@terra-money/terra.js'

export default class TransferLink extends TerraCommand {
static description = 'Transfer LINK'
static examples = [
`yarn gauntlet token:transfer --network=bombay-testnet --to=[RECEIVER] --amount=[AMOUNT_IN_TOKEN_UNITS]`,
`yarn gauntlet token:transfer --network=bombay-testnet --to=[RECEIVER] --amount=[AMOUNT_IN_TOKEN_UNITS] --link=[TOKEN_ADDRESS] --decimals=[TOKEN_DECIMALS]`,
]
type CommandInput = {
to: string
// Units in LINK
amount: string
}

static id = 'token:transfer'
static category = CATEGORIES.LINK
type ContractInput = {
recipient: string
amount: string
}

constructor(flags, args: string[]) {
super(flags, args)
const makeCommandInput = async (flags: any): Promise<CommandInput> => {
if (flags.input) return flags.input as CommandInput
return {
to: flags.to,
amount: flags.amount,
}
}

makeRawTransaction = async () => {
throw new Error('Transfer LINK command: makeRawTransaction method not implemented')
}
const validateInput = (input: CommandInput): boolean => {
if (!AccAddress.validate(input.to)) throw new Error(`Invalid destination address`)
if (isNaN(Number(input.amount))) throw new Error(`Amount ${input.amount} is not a number`)
return true
}

execute = async () => {
const decimals = this.flags.decimals || 18
const link = this.flags.link || process.env.LINK
const amount = new BN(this.flags.amount).mul(new BN(10).pow(new BN(decimals)))

await prompt(`Sending ${this.flags.amount} LINK (${amount.toString()}) to ${this.flags.to}. Continue?`)
const tx = await this.call(link, {
transfer: {
recipient: this.flags.to,
amount: amount.toString(),
},
})
logger.success(`LINK transferred successfully to ${this.flags.to} (txhash: ${tx.hash})`)
return {
responses: [
{
tx,
contract: link,
},
],
} as Result<TransactionResponse>
const makeContractInput = async (input: CommandInput): Promise<ContractInput> => {
const amount = new BN(input.amount).mul(new BN(10).pow(new BN(TOKEN_DECIMALS)))
return {
recipient: input.to,
amount: amount.toString(),
}
}

const beforeExecute = (context: ExecutionContext<CommandInput, ContractInput>) => async (): Promise<void> => {
logger.info(
`Transferring ${context.contractInput.amount} (${context.input.amount}) Tokens to ${context.contractInput.recipient}`,
)
await prompt('Continue?')
}

const transferToken: AbstractInstruction<CommandInput, ContractInput> = {
instruction: {
category: CATEGORIES.LINK,
contract: 'cw20_base',
function: 'transfer',
},
makeInput: makeCommandInput,
validateInput: validateInput,
makeContractInput: makeContractInput,
beforeExecute,
}

export default instructionToCommand(transferToken)
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { getRDD } from '../../../lib/rdd'
import { RDD } from '@chainlink/gauntlet-terra'
import { instructionToCommand, AbstractInstruction } from '../../abstract/executionWrapper'
import { CATEGORIES } from '../../../lib/constants'
import { CONTRACT_LIST } from '../../../lib/contracts'

type CommandInput = {
billingAccessController: string
Expand All @@ -25,7 +24,7 @@ type ContractInput = {

const makeCommandInput = async (flags: any, args: string[]): Promise<CommandInput> => {
if (flags.input) return flags.input as CommandInput
const rdd = getRDD(flags.rdd)
const rdd = RDD.getRDD(flags.rdd)
const contract = args[0]
const aggregator = rdd.contracts[contract]
return {
Expand Down
Loading

0 comments on commit 0abf4ec

Please sign in to comment.