Skip to content

Commit

Permalink
Multisig support for more commands (#115)
Browse files Browse the repository at this point in the history
* Draft multisig support for setConfig, setPayees, setWriter

* Camel case fix + begin_offchain_config command

* write offchain config

* Offchain config commands supported(need to add multiple txs on write)

* SetValidatorConfig supported

* SetConfig should work

* SetPayees should work

* Prettier

* Revert chunks on write_offchain_config

* Remove commented code

* SetWriter fixed

* Error translation wrapping, removing tx creation repetion, address comment, reverting write offchain config

* Prettier

* Support transfer/accept ownership commands

* Prettier

* Misc

* Prettier

* Addressing comments, README instructions, moved errorwrapping to SolanaCommand

* Error translation for multisig

* Review comments

* Review comments

* Review comments
  • Loading branch information
ebarakos authored Jan 24, 2022
1 parent 5b4d7cd commit 2b2a993
Show file tree
Hide file tree
Showing 16 changed files with 405 additions and 157 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ bin
# Gauntlet artifacts
gauntlet/packages/gauntlet-solana-contracts/artifacts/programId
gauntlet/packages/gauntlet-solana-contracts/artifacts/bin
# Multisig local env
gauntlet/packages/gauntlet-serum-multisig/networks/.env.local
6 changes: 6 additions & 0 deletions gauntlet/packages/gauntlet-serum-multisig/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# Gauntlet Serum Multisig

Current version 0.7.0

Artifacts: https://github.com/project-serum/multisig/releases/tag/v0.7.0

## Creating a Multisig

Example with 3 owners and threshold=2

`yarn gauntlet-serum-multisig create --network=local 3W37Aopzbtzczi8XWdkFTvBeSyYgXLuUkaodkq59xBCT ETqajtkz4xcsB397qTBPetprR8jMC3JszkjJJp3cjWJS QMaHW2Fpyet4ZVf7jgrGB6iirZLjwZUjN9vPKcpQrHs --threshold=2`

You will get 2 addresses, Multisig address and Multisig Signer. Please keep them both as they will be needed. Signer will be used when granting access/ownership to multisig.

## Actions

Rest of the commands will adhere to the following flow:
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export default class MultisigCreate extends SolanaCommand {
static id = 'create'
static category = CONTRACT_LIST.MULTISIG

static examples = ['yarn gauntlet-serum-multisig create --network=local']
static examples = [
'yarn gauntlet-serum-multisig create --network=local 3W37Aopzbtzczi8XWdkFTvBeSyYgXLuUkaodkq59xBCT ETqajtkz4xcsB397qTBPetprR8jMC3JszkjJJp3cjWJS QMaHW2Fpyet4ZVf7jgrGB6iirZLjwZUjN9vPKcpQrHs --threshold=2',
]

constructor(flags, args) {
super(flags, args)
Expand Down
52 changes: 31 additions & 21 deletions gauntlet/packages/gauntlet-serum-multisig/src/commands/multisig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TransactionResponse, SolanaCommand, RawTransaction } from '@chainlink/g
import { logger, BN } from '@chainlink/gauntlet-core/dist/utils'
import { PublicKey, SYSVAR_RENT_PUBKEY, Keypair } from '@solana/web3.js'
import { CONTRACT_LIST, getContract } from '@chainlink/gauntlet-solana-contracts'
import { Idl, Program } from '@project-serum/anchor'
import { ProgramError, parseIdlErrors, Idl, Program } from '@project-serum/anchor'

type ProposalContext = {
rawTx: RawTransaction
Expand Down Expand Up @@ -45,6 +45,7 @@ export const wrapCommand = (command) => {
[this.multisigAddress.toBuffer()],
this.program.programId,
)

const multisigState = await this.program.account.multisig.fetch(process.env.MULTISIG_ADDRESS)
const threshold = multisigState.threshold
const owners = multisigState.owners
Expand Down Expand Up @@ -90,9 +91,7 @@ export const wrapCommand = (command) => {
this.inspectProposalState(proposal, threshold, owners)
return result
}

const result = await this.wrapAction(this.executeProposal)(proposal, proposalContext)
this.inspectProposalState(proposal, threshold, owners)
return result
}

Expand All @@ -114,7 +113,7 @@ export const wrapCommand = (command) => {
} else {
logger.error(e)
}
return {} as Result<TransactionResponse>
throw e
}
}

Expand Down Expand Up @@ -153,23 +152,34 @@ export const wrapCommand = (command) => {

executeProposal: ProposalAction = async (proposal: PublicKey, context): Promise<string> => {
logger.loading(`Executing proposal`)

const tx = await this.program.rpc.executeTransaction({
accounts: {
multisig: this.multisigAddress,
multisigSigner: context.multisigSigner,
transaction: proposal,
},
remainingAccounts: context.proposalState.accounts
.map((t) => (t.pubkey.equals(context.multisigSigner) ? { ...t, isSigner: false } : t))
.concat({
pubkey: context.proposalState.programId,
isWritable: false,
isSigner: false,
}),
})
logger.info(`Execution TX hash: ${tx.toString()}`)
return tx
// get the command's starting word to map it to the respective IDL(ocr2, store etc)
const { idl } = getContract(command.id.split(':')[0], '')
try {
const tx = await this.program.rpc.executeTransaction({
accounts: {
multisig: this.multisigAddress,
multisigSigner: context.multisigSigner,
transaction: proposal,
},
remainingAccounts: context.proposalState.accounts
.map((t) => (t.pubkey.equals(context.multisigSigner) ? { ...t, isSigner: false } : t))
.concat({
pubkey: context.proposalState.programId,
isWritable: false,
isSigner: false,
}),
})
logger.info(`Execution TX hash: ${tx.toString()}`)
return tx
} catch (err) {
// Translate IDL error
const idlErrors = parseIdlErrors(idl)
let translatedErr = ProgramError.parse(err, idlErrors)
if (translatedErr === null) {
throw err
}
throw translatedErr
}
}

inspectProposalState = async (proposal, threshold, owners) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Result } from '@chainlink/gauntlet-core'
import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils'
import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana'
import { PublicKey } from '@solana/web3.js'
import { SolanaCommand, TransactionResponse, RawTransaction } from '@chainlink/gauntlet-solana'
import { AccountMeta, PublicKey } from '@solana/web3.js'
import BN from 'bn.js'
import { CONTRACT_LIST, getContract } from '../../../../lib/contracts'
import { makeTx } from '../../../../lib/utils'

export default class BeginOffchainConfig extends SolanaCommand {
static id = 'ocr2:begin_offchain_config'
Expand All @@ -19,31 +20,56 @@ export default class BeginOffchainConfig extends SolanaCommand {
this.require(!!this.flags.state, 'Please provide flags with "state"')
}

execute = async () => {
makeRawTransaction = async (signer: PublicKey) => {
const ocr2 = getContract(CONTRACT_LIST.OCR_2, '')
const address = ocr2.programId.toString()
const program = this.loadProgram(ocr2.idl, address)

const state = new PublicKey(this.flags.state)
const owner = this.wallet.payer
const version = new BN(2)

await prompt(`Begin setting Offchain config version ${version.toString()}?`)
const data = program.coder.instruction.encode('begin_offchain_config', {
offchainConfigVersion: version,
})

const tx = await program.rpc.beginOffchainConfig(version, {
accounts: {
state: state,
authority: owner.publicKey,
const accounts: AccountMeta[] = [
{
pubkey: state,
isSigner: false,
isWritable: true,
},
})
{
pubkey: signer,
isSigner: true,
isWritable: false,
},
]

const rawTx: RawTransaction = {
data,
accounts,
programId: ocr2.programId,
}

logger.success(`Begin set offchain config on tx ${tx}`)
return [rawTx]
}

execute = async () => {
const contract = getContract(CONTRACT_LIST.OCR_2, '')
const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey)
const tx = makeTx(rawTx)
logger.debug(tx)
const version = new BN(2)
await prompt(`Begin setting Offchain config version ${version.toString()}?`)
logger.loading('Sending tx...')
const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl)
logger.success(`Begin set offchain config on tx hash: ${txhash}`)

return {
responses: [
{
tx: this.wrapResponse(tx, state.toString(), { state: state.toString() }),
contract: state.toString(),
tx: this.wrapResponse(txhash, this.flags.state),
contract: this.flags.state,
},
],
} as Result<TransactionResponse>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Result } from '@chainlink/gauntlet-core'
import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils'
import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana'
import { PublicKey } from '@solana/web3.js'
import { SolanaCommand, TransactionResponse, RawTransaction } from '@chainlink/gauntlet-solana'
import { AccountMeta, PublicKey } from '@solana/web3.js'
import { CONTRACT_LIST, getContract } from '../../../../lib/contracts'
import { makeTx } from '../../../../lib/utils'

export default class CommitOffchainConfig extends SolanaCommand {
static id = 'ocr2:commit_offchain_config'
Expand All @@ -18,30 +19,53 @@ export default class CommitOffchainConfig extends SolanaCommand {
this.require(!!this.flags.state, 'Please provide flags with "state"')
}

execute = async () => {
makeRawTransaction = async (signer: PublicKey) => {
const ocr2 = getContract(CONTRACT_LIST.OCR_2, '')
const address = ocr2.programId.toString()
const program = this.loadProgram(ocr2.idl, address)

const state = new PublicKey(this.flags.state)
const owner = this.wallet.payer

await prompt(`Commit Offchain config?`)
const data = program.coder.instruction.encode('commit_offchain_config', {})

const tx = await program.rpc.commitOffchainConfig({
accounts: {
state: state,
authority: owner.publicKey,
const accounts: AccountMeta[] = [
{
pubkey: state,
isSigner: false,
isWritable: true,
},
})
{
pubkey: signer,
isSigner: true,
isWritable: false,
},
]

const rawTx: RawTransaction = {
data,
accounts,
programId: ocr2.programId,
}

return [rawTx]
}

execute = async () => {
const contract = getContract(CONTRACT_LIST.OCR_2, '')
const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey)
const tx = makeTx(rawTx)
logger.debug(tx)
await prompt(`Commit Offchain config?`)
logger.loading('Sending tx...')
const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl)
logger.success(`Committing offchain config on tx ${tx}`)

return {
responses: [
{
tx: this.wrapResponse(tx, state.toString(), { state: state.toString() }),
contract: state.toString(),
tx: this.wrapResponse(txhash, this.flags.state),
contract: this.flags.state,
},
],
} as Result<TransactionResponse>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Result } from '@chainlink/gauntlet-core'
import { SolanaCommand, TransactionResponse, RawTransaction } from '@chainlink/gauntlet-solana'
import { AccountMeta, PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js'
import { AccountMeta, PublicKey } from '@solana/web3.js'
import { logger, BN, prompt } from '@chainlink/gauntlet-core/dist/utils'
import { CONTRACT_LIST, getContract } from '../../../lib/contracts'
import { getRDD } from '../../../lib/rdd'
import { makeTx } from '../../../lib/utils'

type Input = {
observationPaymentGjuels: number | string
Expand Down Expand Up @@ -47,7 +48,6 @@ export default class SetBilling extends SolanaCommand {
const billingAC = new PublicKey(info.config.billingAccessController)
logger.loading('Generating billing tx information...')
logger.log('Billing information:', input)
await prompt('Continue setting billing?')
const data = program.coder.instruction.encode('set_billing', {
observationPaymentGjuels: new BN(input.observationPaymentGjuels),
transmissionPaymentGjuels: new BN(input.transmissionPaymentGjuels),
Expand Down Expand Up @@ -81,21 +81,13 @@ export default class SetBilling extends SolanaCommand {
}

execute = async () => {
const contract = getContract(CONTRACT_LIST.OCR_2, '')
const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey)
const tx = rawTx.reduce(
(tx, meta) =>
tx.add(
new TransactionInstruction({
programId: meta.programId,
keys: meta.accounts,
data: meta.data,
}),
),
new Transaction(),
)

const tx = makeTx(rawTx)
logger.debug(tx)
await prompt('Continue setting billing?')
logger.loading('Sending tx...')
const txhash = await this.provider.send(tx, [this.wallet.payer])
const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl)
logger.success(`Billing set on tx hash: ${txhash}`)

return {
Expand Down
Loading

0 comments on commit 2b2a993

Please sign in to comment.