-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Create multisig command * Prettier + example * Merged feature/relay and required changes for running the flow * Merged example with raw transaction; removed log * Change threshold working * Abstract transaction working with JSON file * Set owners, thredhold and able to run arbitrary transactions directly * Formatting * moved multisig into its library * fix rebase * multisig command wrapper * moved cli script * Working, supporting setting threshold owners and example command setBilling * Prettier * Fixed develop merge * Prettier * Addressing review comments, improving flow with single --proposal flag, adding inspection and validations * Fix run issue + fix approvals inspection * Fix version issue * Moved multisig create to package, fixed creation parameters * Able to execute setBilling from any proposal address, on access given to multisigSigner * Restructuring of multisig wrapper command + distint actions UX * Small changes * Added README * Revert BN imports * refactored multisig refactor * Fixes * Revert .map * Move multisigAddress to constructor * remove set billing default value Co-authored-by: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com>
- Loading branch information
Showing
25 changed files
with
621 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2019 Khalid Zoabi | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# Gauntlet Serum Multisig | ||
|
||
## Creating a Multisig | ||
|
||
Example with 3 owners and threshold=2 | ||
|
||
`yarn gauntlet-serum-multisig create --network=local 3W37Aopzbtzczi8XWdkFTvBeSyYgXLuUkaodkq59xBCT ETqajtkz4xcsB397qTBPetprR8jMC3JszkjJJp3cjWJS QMaHW2Fpyet4ZVf7jgrGB6iirZLjwZUjN9vPKcpQrHs --threshold=2` | ||
|
||
## Actions | ||
|
||
Rest of the commands will adhere to the following flow: | ||
|
||
### Create | ||
|
||
You run a regular command, and you just replace `gauntlet` with `gauntlet-serum-multisig`. | ||
For e.g if you have this command: `yarn gauntlet ocr2:set_billing --network=local --state=k91NrbTgTt4bo86fXN3SXqUzVvoDRiivxf2KcU1p5Gp` | ||
it becomes: | ||
`yarn gauntlet-serum-multisig ocr2:set_billing --network=local --state=k91NrbTgTt4bo86fXN3SXqUzVvoDRiivxf2KcU1p5Gp` | ||
|
||
The creator automatically signs/approves the proposal. | ||
You get a message on console with the proposal PublicKey that you need for continuing on next actions | ||
|
||
### Approve/Execute | ||
|
||
You run a previously created command and you just append `--proposal` flag, | ||
For e.g for the above: `yarn gauntlet-serum-multisig ocr2:set_billing --network=local --state=k91NrbTgTt4bo86fXN3SXqUzVvoDRiivxf2KcU1p5Gp --proposal=CyU1HR7Ebs4aQVQVabT6KeNFusHqov1nwCpCDs9CRZhw` | ||
|
||
If the threshold is met, the proposal is executed. Else, you need to repeat the action for the rest of the owners until the threshold is met. | ||
|
||
### Setting owners | ||
|
||
Example of setting one more owner to the above created multisig | ||
|
||
`yarn gauntlet-serum-multisig set:owners --network=local QMaHW2Fpyet4ZVf7jgrGB6iirZLjwZUjN9vPKcpQrHs ETqajtkz4xcsB397qTBPetprR8jMC3JszkjJJp3cjWJS 3W37Aopzbtzczi8XWdkFTvBeSyYgXLuUkaodkq59xBCT 8i1ZbY9S7VPV4AKVEL1xewyYFvtkrjAnffqfsCf3FoRB` | ||
|
||
### Setting threshold | ||
|
||
Example of setting threshold to 3 | ||
|
||
`yarn gauntlet-serum-multisig set:threshold --network=local --threshold=1` |
Empty file.
2 changes: 2 additions & 0 deletions
2
gauntlet/packages/gauntlet-serum-multisig/networks/.env.devnet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
NODE_URL=https://api.devnet.solana.com | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
NODE_URL=http://127.0.0.1:8899 |
1 change: 1 addition & 0 deletions
1
gauntlet/packages/gauntlet-serum-multisig/networks/.env.testnet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
NODE_URL=https://api.testnet.solana.com |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{ | ||
"name": "@chainlink/gauntlet-serum-multisig", | ||
"version": "0.0.1", | ||
"description": "Gauntlet Serum Multisig", | ||
"keywords": [ | ||
"typescript", | ||
"cli" | ||
], | ||
"main": "./dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"files": [ | ||
"dist/**/*", | ||
"!dist/**/*.test.js" | ||
], | ||
"scripts": { | ||
"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-solana": "*", | ||
"@chainlink/gauntlet-solana-contracts": "*", | ||
"@project-serum/anchor": "^0.20.0", | ||
"@solana/web3.js": "^1.30.2" | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
gauntlet/packages/gauntlet-serum-multisig/src/commands/create.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { Result } from '@chainlink/gauntlet-core' | ||
import { logger } from '@chainlink/gauntlet-core/dist/utils' | ||
import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' | ||
import { PublicKey, SYSVAR_RENT_PUBKEY, Keypair } from '@solana/web3.js' | ||
import BN from 'bn.js' | ||
import { CONTRACT_LIST, getContract } from '@chainlink/gauntlet-solana-contracts' | ||
|
||
type Input = { | ||
owners: string[] | ||
threshold: number | string | ||
} | ||
|
||
const DEFAULT_MAXIMUM_SIZE = 200 | ||
|
||
export default class MultisigCreate extends SolanaCommand { | ||
static id = 'create' | ||
static category = CONTRACT_LIST.MULTISIG | ||
|
||
static examples = ['yarn gauntlet-serum-multisig create --network=local'] | ||
|
||
constructor(flags, args) { | ||
super(flags, args) | ||
this.requireFlag('threshold', 'Please provide multisig threshold') | ||
} | ||
|
||
makeInput = (userInput: any): Input => { | ||
if (userInput) return userInput as Input | ||
|
||
return { | ||
//TODO: validate args. Maybe wrap them int PublicKey? | ||
owners: this.args, | ||
threshold: this.flags.threshold, | ||
} | ||
} | ||
|
||
execute = async () => { | ||
this.require(this.args.length > 0, 'Please provide at least one owner as an argument') | ||
const contract = getContract(CONTRACT_LIST.MULTISIG, '') | ||
const address = contract.programId.toString() | ||
const program = this.loadProgram(contract.idl, address) | ||
|
||
const input = this.makeInput(this.flags.input) | ||
|
||
const multisig = Keypair.generate() | ||
|
||
const [multisigSigner, nonce] = await PublicKey.findProgramAddress( | ||
[multisig.publicKey.toBuffer()], | ||
program.programId, | ||
) | ||
const maximumSize = this.flags.maximumSize || DEFAULT_MAXIMUM_SIZE | ||
const owners = input.owners.map((key) => new PublicKey(key)) | ||
|
||
const tx = await program.rpc.createMultisig(owners, new BN(input.threshold), nonce, { | ||
accounts: { | ||
multisig: multisig.publicKey, | ||
rent: SYSVAR_RENT_PUBKEY, | ||
}, | ||
signers: [multisig], | ||
instructions: [await program.account.multisig.createInstruction(multisig, maximumSize)], | ||
}) | ||
logger.info(`Multisig address: ${multisig.publicKey}`) | ||
logger.info(`Multisig Signer: ${multisigSigner.toString()}`) | ||
|
||
return { | ||
responses: [ | ||
{ | ||
tx: this.wrapResponse(tx, multisig.publicKey.toString()), | ||
contract: multisig.publicKey.toString(), | ||
}, | ||
], | ||
} as Result<TransactionResponse> | ||
} | ||
} |
4 changes: 4 additions & 0 deletions
4
gauntlet/packages/gauntlet-serum-multisig/src/commands/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import SetOwners from './setOwners' | ||
import SetThreshold from './setThreshold' | ||
|
||
export default [SetOwners, SetThreshold] |
202 changes: 202 additions & 0 deletions
202
gauntlet/packages/gauntlet-serum-multisig/src/commands/multisig.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import { Result } from '@chainlink/gauntlet-core' | ||
import { TransactionResponse, SolanaCommand, RawTransaction } from '@chainlink/gauntlet-solana' | ||
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' | ||
|
||
type ProposalContext = { | ||
rawTx: RawTransaction | ||
multisigSigner: PublicKey | ||
proposalState: any | ||
} | ||
|
||
type ProposalAction = (proposal: Keypair | PublicKey, context: ProposalContext) => Promise<string> | ||
|
||
export const wrapCommand = (command) => { | ||
return class Multisig extends SolanaCommand { | ||
command: SolanaCommand | ||
program: Program<Idl> | ||
multisigAddress: PublicKey | ||
|
||
static id = `${command.id}` | ||
|
||
constructor(flags, args) { | ||
super(flags, args) | ||
logger.info(`Running ${command.id} command using Serum Multisig`) | ||
|
||
this.command = new command(flags, args) | ||
this.command.invokeMiddlewares(this.command, this.command.middlewares) | ||
this.require(!!process.env.MULTISIG_ADDRESS, 'Please set MULTISIG_ADDRESS env var') | ||
this.multisigAddress = new PublicKey(process.env.MULTISIG_ADDRESS) | ||
} | ||
|
||
getRemainingSigners = (proposalState: any, threshold: number): number => | ||
Number(threshold) - proposalState.signers.filter(Boolean).length | ||
|
||
isReadyForExecution = (proposalState: any, threshold: number): boolean => { | ||
return this.getRemainingSigners(proposalState, threshold) <= 0 | ||
} | ||
|
||
execute = async () => { | ||
const multisig = getContract(CONTRACT_LIST.MULTISIG, '') | ||
this.program = this.loadProgram(multisig.idl, multisig.programId.toString()) | ||
const [multisigSigner] = await PublicKey.findProgramAddress( | ||
[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 | ||
|
||
logger.info(`Multisig Info: | ||
- Address: ${this.multisigAddress.toString()} | ||
- Signer: ${multisigSigner.toString()} | ||
- Threshold: ${threshold.toString()} | ||
- Owners: ${owners}`) | ||
|
||
// TODO: Should we support many txs? | ||
const rawTx = (await this.command.makeRawTransaction(multisigSigner))[0] | ||
const isCreation = !this.flags.proposal | ||
if (isCreation) { | ||
const proposal = Keypair.generate() | ||
const result = await this.wrapAction(this.createProposal)(proposal, { | ||
rawTx, | ||
multisigSigner, | ||
proposalState: {}, | ||
}) | ||
this.inspectProposalState(proposal.publicKey, threshold, owners) | ||
return result | ||
} | ||
|
||
const proposal = new PublicKey(this.flags.proposal) | ||
const proposalState = await this.program.account.transaction.fetch(proposal) | ||
const proposalContext = { | ||
rawTx, | ||
multisigSigner, | ||
proposalState, | ||
} | ||
|
||
logger.debug(`Proposal state: ${JSON.stringify(proposalState, null, 4)}`) | ||
|
||
const isAlreadyExecuted = proposalState.didExecute | ||
if (isAlreadyExecuted) { | ||
logger.info(`Proposal is already executed`) | ||
return {} as Result<TransactionResponse> | ||
} | ||
|
||
if (!this.isReadyForExecution(proposalState, threshold)) { | ||
const result = await this.wrapAction(this.approveProposal)(proposal, proposalContext) | ||
this.inspectProposalState(proposal, threshold, owners) | ||
return result | ||
} | ||
|
||
const result = await this.wrapAction(this.executeProposal)(proposal, proposalContext) | ||
this.inspectProposalState(proposal, threshold, owners) | ||
return result | ||
} | ||
|
||
wrapAction = (action: ProposalAction) => async (proposal: Keypair | PublicKey, context: ProposalContext) => { | ||
try { | ||
const tx = await action(proposal, context) | ||
return { | ||
responses: [ | ||
{ | ||
tx: this.wrapResponse(tx, proposal.toString()), | ||
contract: proposal.toString(), | ||
}, | ||
], | ||
} as Result<TransactionResponse> | ||
} catch (e) { | ||
// known errors, defined in multisig contract. see serum_multisig.json | ||
if (e.code >= 300 && e.code < 400) { | ||
logger.error(e.msg) | ||
} else { | ||
logger.error(e) | ||
} | ||
return {} as Result<TransactionResponse> | ||
} | ||
} | ||
|
||
createProposal: ProposalAction = async (proposal: Keypair, context): Promise<string> => { | ||
logger.loading(`Creating proposal`) | ||
const txSize = 1000 | ||
const tx = await this.program.rpc.createTransaction( | ||
context.rawTx.programId, | ||
context.rawTx.accounts, | ||
context.rawTx.data, | ||
{ | ||
accounts: { | ||
multisig: this.multisigAddress, | ||
transaction: proposal.publicKey, | ||
proposer: this.wallet.payer.publicKey, | ||
rent: SYSVAR_RENT_PUBKEY, | ||
}, | ||
instructions: [await this.program.account.transaction.createInstruction(proposal, txSize)], | ||
signers: [proposal, this.wallet.payer], | ||
}, | ||
) | ||
return tx | ||
} | ||
|
||
approveProposal: ProposalAction = async (proposal: PublicKey): Promise<string> => { | ||
logger.loading(`Approving proposal`) | ||
const tx = await this.program.rpc.approve({ | ||
accounts: { | ||
multisig: this.multisigAddress, | ||
transaction: proposal, | ||
owner: this.wallet.publicKey, | ||
}, | ||
}) | ||
return tx | ||
} | ||
|
||
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 | ||
} | ||
|
||
inspectProposalState = async (proposal, threshold, owners) => { | ||
const proposalState = await this.program.account.transaction.fetch(proposal) | ||
logger.debug('Proposal state after action:') | ||
logger.debug(JSON.stringify(proposalState, null, 4)) | ||
if (proposalState.didExecute == true) { | ||
logger.info(`Proposal has been executed`) | ||
return | ||
} | ||
|
||
if (this.isReadyForExecution(proposalState, threshold)) { | ||
logger.info( | ||
`Threshold has been met, an owner needs to run the command once more in order to execute it, with flag --proposal=${proposal}`, | ||
) | ||
return | ||
} | ||
// inverting the signers boolean array and filtering owners by it | ||
const remainingEligibleSigners = owners.filter((_, i) => proposalState.signers.map((s) => !s)[i]) | ||
logger.info( | ||
`${this.getRemainingSigners( | ||
proposalState, | ||
threshold, | ||
)} more owners should sign this proposal, using the same command with flag --proposal=${proposal}`, | ||
) | ||
logger.info(`Eligible owners to sign: `) | ||
logger.info(remainingEligibleSigners.toString()) | ||
} | ||
} | ||
} |
Oops, something went wrong.