浏览代码

[xc-admin] add bpf loader (#1097)

* Add bpf loader

* Add close program

* Cleanup

* Add comment

* Show in xc-admin

* Remove console log

* Fix comment
guibescos 2 年之前
父节点
当前提交
f72c0d93ad

+ 36 - 0
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 <pubkey>", "program that you want to close")
+  .requiredOption("-s, --spill <pubkey>", "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"

+ 105 - 0
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");
+  }
+});

+ 89 - 0
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 }
+      );
+    }
+  }
+}

+ 8 - 0
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";

+ 11 - 4
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'}
                 </div>
               </div>
@@ -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'}
                 </div>
@@ -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 ? (
                     <div className="col-span-4 mt-2 bg-[#444157] p-4 lg:col-span-3 lg:mt-0">
                       <div className="base16 flex justify-between pt-2 pb-6 font-semibold opacity-60">
@@ -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 ? (
                 <div
                   key={`${index}_accounts`}
                   className="grid grid-cols-4 justify-between"

+ 20 - 7
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx

@@ -16,6 +16,7 @@ import {
   WormholeMultisigInstruction,
   getManyProposalsInstructions,
   SystemProgramMultisigInstruction,
+  BpfUpgradableLoaderInstruction,
 } from 'xc_admin_common'
 import { ClusterContext } from '../../contexts/ClusterContext'
 import { useMultisigContext } from '../../contexts/MultisigContext'
@@ -248,7 +249,11 @@ const Proposal = ({
   const multisigCluster = getMultisigCluster(contextCluster)
   const targetClusters: (PythCluster | 'unknown')[] = []
   instructions.map((ix) => {
-    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'}
               </div>
             </div>
             {instruction instanceof PythMultisigInstruction ||
-            instruction instanceof WormholeMultisigInstruction ? (
+            instruction instanceof WormholeMultisigInstruction ||
+            instruction instanceof BpfUpgradableLoaderInstruction ||
+            instruction instanceof SystemProgramMultisigInstruction ? (
               <div
                 key={`${index}_instructionName`}
                 className="flex justify-between"
@@ -583,7 +592,9 @@ const Proposal = ({
                 className="grid grid-cols-4 justify-between"
               >
                 <div>Arguments</div>
-                {instruction instanceof PythMultisigInstruction ? (
+                {instruction instanceof PythMultisigInstruction ||
+                instruction instanceof SystemProgramMultisigInstruction ||
+                instruction instanceof BpfUpgradableLoaderInstruction ? (
                   Object.keys(instruction.args).length > 0 ? (
                     <div className="col-span-4 mt-2 bg-darkGray2 p-4 lg:col-span-3 lg:mt-0">
                       <div className="base16 flex justify-between pt-2 pb-6 font-semibold opacity-60">
@@ -634,7 +645,9 @@ const Proposal = ({
                 )}
               </div>
             )}
-            {instruction instanceof PythMultisigInstruction ? (
+            {instruction instanceof PythMultisigInstruction ||
+            instruction instanceof SystemProgramMultisigInstruction ||
+            instruction instanceof BpfUpgradableLoaderInstruction ? (
               <div
                 key={`${index}_accounts`}
                 className="grid grid-cols-4 justify-between"