From 6fb7c6a7991361375658ad0eb44933ef4736cdb0 Mon Sep 17 00:00:00 2001
From: guibescos <59208140+guibescos@users.noreply.github.com>
Date: Mon, 6 Nov 2023 20:32:20 +0100
Subject: [PATCH] Transfer pda authority (#256)

* Do it

* Let's go
---
 staking/programs/staking/src/context.rs | 10 ++++
 staking/programs/staking/src/lib.rs     |  9 +++
 staking/target/idl/staking.json         | 30 ++++++++++
 staking/target/types/staking.ts         | 60 +++++++++++++++++++
 staking/tests/config.ts                 | 76 ++++++++++++++++++++++++-
 5 files changed, 184 insertions(+), 1 deletion(-)

diff --git a/staking/programs/staking/src/context.rs b/staking/programs/staking/src/context.rs
index c31d1c16..33f9599f 100644
--- a/staking/programs/staking/src/context.rs
+++ b/staking/programs/staking/src/context.rs
@@ -67,6 +67,16 @@ pub struct UpdateGovernanceAuthority<'info> {
     pub config:            Account<'info, global_config::GlobalConfig>,
 }
 
+#[derive(Accounts)]
+#[instruction(new_authority : Pubkey)]
+pub struct UpdatePdaAuthority<'info> {
+    #[account(address = config.pda_authority)]
+    pub governance_signer: Signer<'info>,
+    #[account(mut, seeds = [CONFIG_SEED.as_bytes()], bump = config.bump)]
+    pub config:            Account<'info, global_config::GlobalConfig>,
+}
+
+
 #[derive(Accounts)]
 #[instruction(freeze : bool)]
 pub struct UpdateFreeze<'info> {
diff --git a/staking/programs/staking/src/lib.rs b/staking/programs/staking/src/lib.rs
index 7a407888..44b55b0a 100644
--- a/staking/programs/staking/src/lib.rs
+++ b/staking/programs/staking/src/lib.rs
@@ -87,6 +87,15 @@ pub mod staking {
         Ok(())
     }
 
+    pub fn update_pda_authority(
+        ctx: Context<UpdatePdaAuthority>,
+        new_authority: Pubkey,
+    ) -> Result<()> {
+        let config = &mut ctx.accounts.config;
+        config.pda_authority = new_authority;
+        Ok(())
+    }
+
     pub fn update_freeze(ctx: Context<UpdateFreeze>, freeze: bool) -> Result<()> {
         let config = &mut ctx.accounts.config;
         config.freeze = freeze;
diff --git a/staking/target/idl/staking.json b/staking/target/idl/staking.json
index 3d83c968..716d63a8 100644
--- a/staking/target/idl/staking.json
+++ b/staking/target/idl/staking.json
@@ -74,6 +74,36 @@
         }
       ]
     },
+    {
+      "name": "updatePdaAuthority",
+      "accounts": [
+        {
+          "name": "governanceSigner",
+          "isMut": false,
+          "isSigner": true
+        },
+        {
+          "name": "config",
+          "isMut": true,
+          "isSigner": false,
+          "pda": {
+            "seeds": [
+              {
+                "kind": "const",
+                "type": "string",
+                "value": "config"
+              }
+            ]
+          }
+        }
+      ],
+      "args": [
+        {
+          "name": "newAuthority",
+          "type": "publicKey"
+        }
+      ]
+    },
     {
       "name": "updateFreeze",
       "accounts": [
diff --git a/staking/target/types/staking.ts b/staking/target/types/staking.ts
index be475a00..8afd5133 100644
--- a/staking/target/types/staking.ts
+++ b/staking/target/types/staking.ts
@@ -74,6 +74,36 @@ export type Staking = {
         }
       ]
     },
+    {
+      "name": "updatePdaAuthority",
+      "accounts": [
+        {
+          "name": "governanceSigner",
+          "isMut": false,
+          "isSigner": true
+        },
+        {
+          "name": "config",
+          "isMut": true,
+          "isSigner": false,
+          "pda": {
+            "seeds": [
+              {
+                "kind": "const",
+                "type": "string",
+                "value": "config"
+              }
+            ]
+          }
+        }
+      ],
+      "args": [
+        {
+          "name": "newAuthority",
+          "type": "publicKey"
+        }
+      ]
+    },
     {
       "name": "updateFreeze",
       "accounts": [
@@ -1981,6 +2011,36 @@ export const IDL: Staking = {
         }
       ]
     },
+    {
+      "name": "updatePdaAuthority",
+      "accounts": [
+        {
+          "name": "governanceSigner",
+          "isMut": false,
+          "isSigner": true
+        },
+        {
+          "name": "config",
+          "isMut": true,
+          "isSigner": false,
+          "pda": {
+            "seeds": [
+              {
+                "kind": "const",
+                "type": "string",
+                "value": "config"
+              }
+            ]
+          }
+        }
+      ],
+      "args": [
+        {
+          "name": "newAuthority",
+          "type": "publicKey"
+        }
+      ]
+    },
     {
       "name": "updateFreeze",
       "accounts": [
diff --git a/staking/tests/config.ts b/staking/tests/config.ts
index 3448f42a..6b5a9ab8 100644
--- a/staking/tests/config.ts
+++ b/staking/tests/config.ts
@@ -31,8 +31,9 @@ describe("config", async () => {
   const pythMintAuthority = new Keypair();
   const zeroPubkey = new PublicKey(0);
 
+  const pdaAuthorityKeypair = new Keypair();
   const config = readAnchorConfig(ANCHOR_CONFIG_PATH);
-  const pdaAuthority = PublicKey.unique();
+  const pdaAuthority = pdaAuthorityKeypair.publicKey;
   const governanceProgram = new PublicKey(config.programs.localnet.governance);
 
   let errMap: Map<number, string>;
@@ -416,4 +417,77 @@ describe("config", async () => {
       errMap
     );
   });
+
+  it("updates pda authority", async () => {
+    // governance authority can't update pda authority
+    await expectFail(
+      program.methods.updatePdaAuthority(program.provider.wallet.publicKey),
+      "An address constraint was violated",
+      errMap
+    );
+
+    const pdaConnection = await StakeConnection.createStakeConnection(
+      program.provider.connection,
+      new Wallet(pdaAuthorityKeypair),
+      program.programId
+    );
+
+    await pdaConnection.program.provider.connection.requestAirdrop(
+      pdaAuthorityKeypair.publicKey,
+      1_000_000_000_000
+    );
+
+    // Airdrops are not instant unfortunately, wait
+    await new Promise((resolve) => setTimeout(resolve, 2000));
+
+    // pda_authority updates pda_authority to the holder of governance_authority
+    await pdaConnection.program.methods
+      .updatePdaAuthority(program.provider.wallet.publicKey)
+      .rpc();
+
+    let configAccountData = await program.account.globalConfig.fetch(
+      configAccount
+    );
+
+    assert.equal(
+      JSON.stringify(configAccountData),
+      JSON.stringify({
+        bump,
+        governanceAuthority: program.provider.wallet.publicKey,
+        pythTokenMint: pythMintAccount.publicKey,
+        pythGovernanceRealm: zeroPubkey,
+        unlockingDuration: 2,
+        epochDuration: new BN(3600),
+        freeze: true,
+        pdaAuthority: program.provider.wallet.publicKey,
+        governanceProgram,
+        pythTokenListTime: null,
+        agreementHash: getDummyAgreementHash(),
+        mockClockTime: new BN(30),
+      })
+    );
+
+    // the authority gets returned to the original pda_authority
+    await program.methods.updatePdaAuthority(pdaAuthority).rpc();
+
+    configAccountData = await program.account.globalConfig.fetch(configAccount);
+
+    assert.equal(
+      JSON.stringify(configAccountData),
+      JSON.stringify({
+        bump,
+        governanceAuthority: program.provider.wallet.publicKey,
+        pythTokenMint: pythMintAccount.publicKey,
+        pythGovernanceRealm: zeroPubkey,
+        unlockingDuration: 2,
+        epochDuration: new BN(3600),
+        freeze: true,
+        pdaAuthority: pdaAuthority,
+        governanceProgram,
+        pythTokenListTime: null,
+        agreementHash: getDummyAgreementHash(),
+        mockClockTime: new BN(30),
+      })
+    );
+  });
 });