diff --git a/governance/xc_admin/packages/xc_admin_cli/src/index.ts b/governance/xc_admin/packages/xc_admin_cli/src/index.ts index 710d9cc2..f29de40c 100644 --- a/governance/xc_admin/packages/xc_admin_cli/src/index.ts +++ b/governance/xc_admin/packages/xc_admin_cli/src/index.ts @@ -190,6 +190,42 @@ multisigCommand("upgrade-program", "Upgrade a program from a buffer") await vault.proposeInstructions([proposalInstruction], cluster); }); +multisigCommand( + "close-program", + "Close a program, retrieve the funds. WARNING : THIS WILL BRICK THE PROGRAM AND THE ACCOUNTS IT OWNS FOREVER" +) + .requiredOption("-p, --program-id ", "program that you want to close") + .requiredOption("-s, --spill ", "address to receive the funds") + .action(async (options: any) => { + const vault = await loadVaultFromOptions(options); + const spill = new PublicKey(options.spill); + const cluster: PythCluster = options.cluster; + const programId: PublicKey = new PublicKey(options.programId); + + const programDataAccount = PublicKey.findProgramAddressSync( + [programId.toBuffer()], + BPF_UPGRADABLE_LOADER + )[0]; + + const proposalInstruction: TransactionInstruction = { + programId: BPF_UPGRADABLE_LOADER, + // 4-bytes instruction discriminator, got it from https://docs.rs/solana-program/latest/src/solana_program/loader_upgradeable_instruction.rs.html + data: Buffer.from([5, 0, 0, 0]), + keys: [ + { pubkey: programDataAccount, isSigner: false, isWritable: true }, + { pubkey: spill, isSigner: false, isWritable: true }, + { + pubkey: await vault.getVaultAuthorityPDA(cluster), + isSigner: true, + isWritable: false, + }, + { pubkey: programId, isSigner: false, isWritable: true }, + ], + }; + + await vault.proposeInstructions([proposalInstruction], cluster); + }); + multisigCommand( "init-price", "Init price (useful for changing the exponent), only to be used on unused price feeds" diff --git a/governance/xc_admin/packages/xc_admin_common/src/__tests__/BpfUpgradableLoaderInstruction.test.ts b/governance/xc_admin/packages/xc_admin_common/src/__tests__/BpfUpgradableLoaderInstruction.test.ts new file mode 100644 index 00000000..d543854b --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_common/src/__tests__/BpfUpgradableLoaderInstruction.test.ts @@ -0,0 +1,105 @@ +import { PythCluster } from "@pythnetwork/client"; +import { + BpfUpgradableLoaderInstruction, + MultisigInstructionProgram, + MultisigParser, + UNRECOGNIZED_INSTRUCTION, +} from "../multisig_transaction"; +import { + PublicKey, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { BPF_UPGRADABLE_LOADER } from "../bpf_upgradable_loader"; + +test("Bpf Upgradable Loader multisig instruction parse", (done) => { + jest.setTimeout(60000); + + const cluster: PythCluster = "devnet"; + + const parser = MultisigParser.fromCluster(cluster); + + const upgradeInstruction = new TransactionInstruction({ + programId: BPF_UPGRADABLE_LOADER, + data: Buffer.from([3, 0, 0, 0]), + keys: [ + { pubkey: new PublicKey(0), isSigner: false, isWritable: true }, + { pubkey: new PublicKey(1), isSigner: false, isWritable: true }, + { pubkey: new PublicKey(2), isSigner: false, isWritable: true }, + { pubkey: new PublicKey(3), isSigner: false, isWritable: true }, + { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, + { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false }, + { + pubkey: new PublicKey(4), + isSigner: true, + isWritable: false, + }, + { + pubkey: new PublicKey(5), + isSigner: true, + isWritable: false, + }, + ], + }); + + const parsedInstruction = parser.parseInstruction(upgradeInstruction); + if (parsedInstruction instanceof BpfUpgradableLoaderInstruction) { + expect(parsedInstruction.program).toBe( + MultisigInstructionProgram.BpfUpgradableLoader + ); + expect(parsedInstruction.name).toBe("Upgrade"); + expect( + parsedInstruction.accounts.named.programData.pubkey.equals( + new PublicKey(0) + ) + ).toBeTruthy(); + expect( + parsedInstruction.accounts.named.program.pubkey.equals(new PublicKey(1)) + ).toBeTruthy(); + expect( + parsedInstruction.accounts.named.buffer.pubkey.equals(new PublicKey(2)) + ).toBeTruthy(); + expect( + parsedInstruction.accounts.named.spill.pubkey.equals(new PublicKey(3)) + ).toBeTruthy(); + expect( + parsedInstruction.accounts.named.rent.pubkey.equals(SYSVAR_RENT_PUBKEY) + ).toBeTruthy(); + expect( + parsedInstruction.accounts.named.clock.pubkey.equals(SYSVAR_CLOCK_PUBKEY) + ).toBeTruthy(); + expect( + parsedInstruction.accounts.named.upgradeAuthority.pubkey.equals( + new PublicKey(4) + ) + ).toBeTruthy(); + expect(parsedInstruction.accounts.remaining.length).toBe(1); + expect( + parsedInstruction.accounts.remaining[0].pubkey.equals(new PublicKey(5)) + ).toBeTruthy(); + expect(parsedInstruction.args).toEqual({}); + } else { + done("Not instance of BpfUpgradableLoaderInstruction"); + } + + const badInstruction = new TransactionInstruction({ + keys: [], + programId: new PublicKey(BPF_UPGRADABLE_LOADER), + data: Buffer.from([9]), + }); + + const parsedBadInstruction = parser.parseInstruction(badInstruction); + if (parsedBadInstruction instanceof BpfUpgradableLoaderInstruction) { + expect(parsedBadInstruction.program).toBe( + MultisigInstructionProgram.BpfUpgradableLoader + ); + expect(parsedBadInstruction.name).toBe(UNRECOGNIZED_INSTRUCTION); + expect( + parsedBadInstruction.args.data.equals(Buffer.from([9])) + ).toBeTruthy(); + done(); + } else { + done("Not instance of BpfUpgradableLoaderInstruction"); + } +}); diff --git a/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/BpfUpgradableLoaderMultisigInstruction.ts b/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/BpfUpgradableLoaderMultisigInstruction.ts new file mode 100644 index 00000000..35627bd8 --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/BpfUpgradableLoaderMultisigInstruction.ts @@ -0,0 +1,89 @@ +import { TransactionInstruction } from "@solana/web3.js"; +import { + MultisigInstruction, + MultisigInstructionProgram, + UNRECOGNIZED_INSTRUCTION, +} from "."; +import { AnchorAccounts } from "./anchor"; +import * as BufferLayout from "@solana/buffer-layout"; + +// Source: https://docs.rs/solana-program/latest/src/solana_program/loader_upgradeable_instruction.rs.html +export class BpfUpgradableLoaderInstruction implements MultisigInstruction { + readonly program = MultisigInstructionProgram.BpfUpgradableLoader; + readonly name: string; + readonly args: { [key: string]: any }; + readonly accounts: AnchorAccounts; + + constructor( + name: string, + args: { [key: string]: any }, + accounts: AnchorAccounts + ) { + this.name = name; + this.args = args; + this.accounts = accounts; + } + + static fromTransactionInstruction( + instruction: TransactionInstruction + ): BpfUpgradableLoaderInstruction { + try { + const instructionTypeLayout = BufferLayout.u32("instruction"); + const typeIndex = instructionTypeLayout.decode(instruction.data); + switch (typeIndex) { + case 3: + return new BpfUpgradableLoaderInstruction( + "Upgrade", + {}, + { + named: { + programData: instruction.keys[0], + program: instruction.keys[1], + buffer: instruction.keys[2], + spill: instruction.keys[3], + rent: instruction.keys[4], + clock: instruction.keys[5], + upgradeAuthority: instruction.keys[6], + }, + remaining: instruction.keys.slice(7), + } + ); + case 4: + return new BpfUpgradableLoaderInstruction( + "SetAuthority", + {}, + { + named: { + programData: instruction.keys[0], + currentAuthority: instruction.keys[1], + newAuthority: instruction.keys[2], + }, + remaining: instruction.keys.slice(3), + } + ); + case 5: + return new BpfUpgradableLoaderInstruction( + "Close", + {}, + { + named: { + programData: instruction.keys[0], + spill: instruction.keys[1], + upgradeAuthority: instruction.keys[2], + program: instruction.keys[3], + }, + remaining: instruction.keys.slice(4), + } + ); + default: // Many more cases are not supported + throw Error("Not implemented"); + } + } catch { + return new BpfUpgradableLoaderInstruction( + UNRECOGNIZED_INSTRUCTION, + { data: instruction.data }, + { named: {}, remaining: instruction.keys } + ); + } + } +} diff --git a/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/index.ts b/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/index.ts index 48e869d8..bdc1bc1f 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/index.ts +++ b/governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/index.ts @@ -13,6 +13,8 @@ import { MessageBufferMultisigInstruction } from "./MessageBufferMultisigInstruc import { PythMultisigInstruction } from "./PythMultisigInstruction"; import { WormholeMultisigInstruction } from "./WormholeMultisigInstruction"; import { SystemProgramMultisigInstruction } from "./SystemProgramInstruction"; +import { BpfUpgradableLoaderInstruction } from "./BpfUpgradableLoaderMultisigInstruction"; +import { BPF_UPGRADABLE_LOADER } from "../bpf_upgradable_loader"; export const UNRECOGNIZED_INSTRUCTION = "unrecognizedInstruction"; export enum MultisigInstructionProgram { @@ -20,6 +22,7 @@ export enum MultisigInstructionProgram { WormholeBridge, MessageBuffer, SystemProgram, + BpfUpgradableLoader, UnrecognizedProgram, } @@ -77,6 +80,10 @@ export class MultisigParser { return SystemProgramMultisigInstruction.fromTransactionInstruction( instruction ); + } else if (instruction.programId.equals(BPF_UPGRADABLE_LOADER)) { + return BpfUpgradableLoaderInstruction.fromTransactionInstruction( + instruction + ); } else { return UnrecognizedProgram.fromTransactionInstruction(instruction); } @@ -87,3 +94,4 @@ export { WormholeMultisigInstruction } from "./WormholeMultisigInstruction"; export { PythMultisigInstruction } from "./PythMultisigInstruction"; export { MessageBufferMultisigInstruction } from "./MessageBufferMultisigInstruction"; export { SystemProgramMultisigInstruction } from "./SystemProgramInstruction"; +export { BpfUpgradableLoaderInstruction } from "./BpfUpgradableLoaderMultisigInstruction"; diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/WormholeInstructionView.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/WormholeInstructionView.tsx index 5a1679d5..a826b4f1 100644 --- a/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/WormholeInstructionView.tsx +++ b/governance/xc_admin/packages/xc_admin_frontend/components/InstructionViews/WormholeInstructionView.tsx @@ -1,6 +1,7 @@ import { AptosAuthorizeUpgradeContract, AuthorizeGovernanceDataSourceTransfer, + BpfUpgradableLoaderInstruction, CosmosUpgradeContract, EvmSetWormholeAddress, EvmUpgradeContract, @@ -95,6 +96,9 @@ export const WormholeInstructionView = ({ : parsedInstruction instanceof SystemProgramMultisigInstruction ? 'System Program' + : parsedInstruction instanceof + BpfUpgradableLoaderInstruction + ? 'BPF Upgradable Loader' : 'Unknown'} @@ -108,7 +112,9 @@ export const WormholeInstructionView = ({ parsedInstruction instanceof WormholeMultisigInstruction || parsedInstruction instanceof MessageBufferMultisigInstruction || - parsedInstruction instanceof SystemProgramMultisigInstruction + parsedInstruction instanceof + SystemProgramMultisigInstruction || + parsedInstruction instanceof BpfUpgradableLoaderInstruction ? parsedInstruction.name : 'Unknown'} @@ -121,8 +127,8 @@ export const WormholeInstructionView = ({ {parsedInstruction instanceof PythMultisigInstruction || parsedInstruction instanceof WormholeMultisigInstruction || parsedInstruction instanceof MessageBufferMultisigInstruction || - parsedInstruction instanceof - SystemProgramMultisigInstruction ? ( + parsedInstruction instanceof SystemProgramMultisigInstruction || + parsedInstruction instanceof BpfUpgradableLoaderInstruction ? ( Object.keys(parsedInstruction.args).length > 0 ? (
@@ -204,7 +210,8 @@ export const WormholeInstructionView = ({ {parsedInstruction instanceof PythMultisigInstruction || parsedInstruction instanceof WormholeMultisigInstruction || parsedInstruction instanceof MessageBufferMultisigInstruction || - parsedInstruction instanceof SystemProgramMultisigInstruction ? ( + parsedInstruction instanceof SystemProgramMultisigInstruction || + parsedInstruction instanceof BpfUpgradableLoaderInstruction ? (
{ - if (ix instanceof PythMultisigInstruction) { + if ( + ix instanceof PythMultisigInstruction || + ix instanceof SystemProgramMultisigInstruction || + ix instanceof BpfUpgradableLoaderInstruction + ) { targetClusters.push(multisigCluster) } else if ( ix instanceof WormholeMultisigInstruction && @@ -319,9 +324,7 @@ const Proposal = ({ return ( parsedRemoteInstruction instanceof PythMultisigInstruction || parsedRemoteInstruction instanceof - MessageBufferMultisigInstruction || - parsedRemoteInstruction instanceof - SystemProgramMultisigInstruction + MessageBufferMultisigInstruction ) }) && ix.governanceAction.targetChainId === 'pythnet') @@ -551,11 +554,17 @@ const Proposal = ({ ? 'Pyth Oracle' : instruction instanceof WormholeMultisigInstruction ? 'Wormhole' + : instruction instanceof SystemProgramMultisigInstruction + ? 'System Program' + : instruction instanceof BpfUpgradableLoaderInstruction + ? 'BPF Upgradable Loader' : 'Unknown'}
{instruction instanceof PythMultisigInstruction || - instruction instanceof WormholeMultisigInstruction ? ( + instruction instanceof WormholeMultisigInstruction || + instruction instanceof BpfUpgradableLoaderInstruction || + instruction instanceof SystemProgramMultisigInstruction ? (
Arguments
- {instruction instanceof PythMultisigInstruction ? ( + {instruction instanceof PythMultisigInstruction || + instruction instanceof SystemProgramMultisigInstruction || + instruction instanceof BpfUpgradableLoaderInstruction ? ( Object.keys(instruction.args).length > 0 ? (
@@ -634,7 +645,9 @@ const Proposal = ({ )}
)} - {instruction instanceof PythMultisigInstruction ? ( + {instruction instanceof PythMultisigInstruction || + instruction instanceof SystemProgramMultisigInstruction || + instruction instanceof BpfUpgradableLoaderInstruction ? (