diff --git a/mev-programs/programs/sdk/src/instruction.rs b/mev-programs/programs/sdk/src/instruction.rs index d64ced7..47c70f7 100644 --- a/mev-programs/programs/sdk/src/instruction.rs +++ b/mev-programs/programs/sdk/src/instruction.rs @@ -249,6 +249,7 @@ pub struct ClaimArgs { pub struct ClaimAccounts { pub config: Pubkey, pub tip_distribution_account: Pubkey, + pub merkle_root_upload_authority: Pubkey, pub claim_status: Pubkey, pub claimant: Pubkey, pub payer: Pubkey, @@ -264,6 +265,7 @@ pub fn claim_ix(program_id: Pubkey, args: ClaimArgs, accounts: ClaimAccounts) -> let ClaimAccounts { config, tip_distribution_account, + merkle_root_upload_authority, claim_status, claimant, payer, @@ -281,6 +283,7 @@ pub fn claim_ix(program_id: Pubkey, args: ClaimArgs, accounts: ClaimAccounts) -> accounts: jito_tip_distribution::accounts::Claim { config, tip_distribution_account, + merkle_root_upload_authority, claimant, claim_status, payer, diff --git a/mev-programs/programs/tip-distribution/idl/jito_tip_distribution.json b/mev-programs/programs/tip-distribution/idl/jito_tip_distribution.json index 403bccd..7508fa7 100644 --- a/mev-programs/programs/tip-distribution/idl/jito_tip_distribution.json +++ b/mev-programs/programs/tip-distribution/idl/jito_tip_distribution.json @@ -30,6 +30,10 @@ "name": "tip_distribution_account", "writable": true }, + { + "name": "merkle_root_upload_authority", + "signer": true + }, { "name": "claim_status", "docs": [ @@ -213,6 +217,51 @@ } ] }, + { + "name": "initialize_merkle_root_upload_config", + "discriminator": [ + 232, + 87, + 72, + 14, + 89, + 40, + 40, + 27 + ], + "accounts": [ + { + "name": "config", + "writable": true + }, + { + "name": "merkle_root_upload_config", + "writable": true + }, + { + "name": "authority", + "signer": true + }, + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "system_program" + } + ], + "args": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "original_authority", + "type": "pubkey" + } + ] + }, { "name": "initialize_tip_distribution_account", "docs": [ @@ -270,6 +319,29 @@ } ] }, + { + "name": "migrate_tda_merkle_root_upload_authority", + "discriminator": [ + 13, + 226, + 163, + 144, + 56, + 202, + 214, + 23 + ], + "accounts": [ + { + "name": "tip_distribution_account", + "writable": true + }, + { + "name": "merkle_root_upload_config" + } + ], + "args": [] + }, { "name": "update_config", "docs": [ @@ -307,6 +379,45 @@ } ] }, + { + "name": "update_merkle_root_upload_config", + "discriminator": [ + 128, + 227, + 159, + 139, + 176, + 128, + 118, + 2 + ], + "accounts": [ + { + "name": "config" + }, + { + "name": "merkle_root_upload_config", + "writable": true + }, + { + "name": "authority", + "signer": true + }, + { + "name": "system_program" + } + ], + "args": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "original_authority", + "type": "pubkey" + } + ] + }, { "name": "upload_merkle_root", "docs": [ @@ -387,6 +498,19 @@ 130 ] }, + { + "name": "MerkleRootUploadConfig", + "discriminator": [ + 213, + 125, + 30, + 192, + 25, + 121, + 87, + 33 + ] + }, { "name": "TipDistributionAccount", "discriminator": [ @@ -582,6 +706,11 @@ "code": 6014, "name": "Unauthorized", "msg": "Unauthorized signer." + }, + { + "code": 6015, + "name": "InvalidTdaForMigration", + "msg": "TDA not valid for migration." } ], "types": [ @@ -827,6 +956,39 @@ ] } }, + { + "name": "MerkleRootUploadConfig", + "docs": [ + "Singleton account that allows overriding TDA's merkle upload authority" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "override_authority", + "docs": [ + "The authority that overrides the TipDistributionAccount merkle_root_upload_authority" + ], + "type": "pubkey" + }, + { + "name": "original_upload_authority", + "docs": [ + "The original merkle root upload authority that can be changed to the new overrided", + "authority. E.g. Jito Labs authority GZctHpWXmsZC1YHACTGGcHhYxjdRqQvTpYkb9LMvxDib" + ], + "type": "pubkey" + }, + { + "name": "bump", + "docs": [ + "The bump used to generate this account" + ], + "type": "u8" + } + ] + } + }, { "name": "MerkleRootUploadedEvent", "type": { diff --git a/mev-programs/programs/tip-distribution/src/lib.rs b/mev-programs/programs/tip-distribution/src/lib.rs index 76d5ce1..cf0204d 100644 --- a/mev-programs/programs/tip-distribution/src/lib.rs +++ b/mev-programs/programs/tip-distribution/src/lib.rs @@ -3,7 +3,7 @@ use anchor_lang::{prelude::*, solana_program::clock::Clock}; use solana_security_txt::security_txt; use crate::{ - state::{ClaimStatus, Config, MerkleRoot, TipDistributionAccount}, + state::{ClaimStatus, Config, MerkleRoot, MerkleRootUploadConfig, TipDistributionAccount}, ErrorCode::Unauthorized, }; @@ -204,6 +204,8 @@ pub mod jito_tip_distribution { /// Claims tokens from the [TipDistributionAccount]. pub fn claim(ctx: Context, bump: u8, amount: u64, proof: Vec<[u8; 32]>) -> Result<()> { + Claim::auth(&ctx)?; + let claim_status = &mut ctx.accounts.claim_status; claim_status.bump = bump; @@ -282,6 +284,58 @@ pub mod jito_tip_distribution { Ok(()) } + + pub fn initialize_merkle_root_upload_config( + ctx: Context, + authority: Pubkey, + original_authority: Pubkey, + ) -> Result<()> { + // Call the authorize function + InitializeMerkleRootUploadConfig::auth(&ctx)?; + + // Set the bump and override authority + let merkle_root_upload_config = &mut ctx.accounts.merkle_root_upload_config; + merkle_root_upload_config.override_authority = authority; + merkle_root_upload_config.original_upload_authority = original_authority; + merkle_root_upload_config.bump = ctx.bumps.merkle_root_upload_config; + Ok(()) + } + + pub fn update_merkle_root_upload_config( + ctx: Context, + authority: Pubkey, + original_authority: Pubkey, + ) -> Result<()> { + // Call the authorize function + UpdateMerkleRootUploadConfig::auth(&ctx)?; + + // Update override authority + let merkle_root_upload_config = &mut ctx.accounts.merkle_root_upload_config; + merkle_root_upload_config.override_authority = authority; + merkle_root_upload_config.original_upload_authority = original_authority; + + Ok(()) + } + + pub fn migrate_tda_merkle_root_upload_authority( + ctx: Context, + ) -> Result<()> { + let distribution_account = &mut ctx.accounts.tip_distribution_account; + // Validate TDA has no MerkleRoot uploaded to it + if distribution_account.merkle_root.is_some() { + return Err(InvalidTdaForMigration.into()); + } + // Validate the TDA key is the acceptable original authority (i.e. the original Jito Lab's authority) + if distribution_account.merkle_root_upload_authority != ctx.accounts.merkle_root_upload_config.original_upload_authority { + return Err(InvalidTdaForMigration.into()); + } + + // Change the TDA's root upload authority + distribution_account.merkle_root_upload_authority = + ctx.accounts.merkle_root_upload_config.override_authority; + + Ok(()) + } } #[error_code] @@ -330,6 +384,9 @@ pub enum ErrorCode { #[msg("Unauthorized signer.")] Unauthorized, + + #[msg("TDA not valid for migration.")] + InvalidTdaForMigration, } #[derive(Accounts)] @@ -471,6 +528,8 @@ pub struct Claim<'info> { #[account(mut, rent_exempt = enforce)] pub tip_distribution_account: Account<'info, TipDistributionAccount>, + pub merkle_root_upload_authority: Signer<'info>, + /// Status of the claim. Used to prevent the same party from claiming multiple times. #[account( init, @@ -497,6 +556,20 @@ pub struct Claim<'info> { pub system_program: Program<'info, System>, } +impl Claim<'_> { + fn auth(ctx: &Context) -> Result<()> { + if ctx.accounts.merkle_root_upload_authority.key() + != ctx + .accounts + .tip_distribution_account + .merkle_root_upload_authority + { + Err(Unauthorized.into()) + } else { + Ok(()) + } + } +} #[derive(Accounts)] pub struct UploadMerkleRoot<'info> { @@ -524,6 +597,82 @@ impl UploadMerkleRoot<'_> { } } +#[derive(Accounts)] +pub struct InitializeMerkleRootUploadConfig<'info> { + #[account(mut, rent_exempt = enforce)] + pub config: Account<'info, Config>, + + #[account( + init, + rent_exempt = enforce, + seeds = [ + MerkleRootUploadConfig::SEED, + ], + bump, + space = MerkleRootUploadConfig::SIZE, + payer = payer + )] + pub merkle_root_upload_config: Account<'info, MerkleRootUploadConfig>, + + pub authority: Signer<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +impl InitializeMerkleRootUploadConfig<'_> { + fn auth(ctx: &Context) -> Result<()> { + if ctx.accounts.config.authority != ctx.accounts.authority.key() { + Err(Unauthorized.into()) + } else { + Ok(()) + } + } +} + +#[derive(Accounts)] +pub struct UpdateMerkleRootUploadConfig<'info> { + #[account(rent_exempt = enforce)] + pub config: Account<'info, Config>, + + #[account( + mut, + seeds = [MerkleRootUploadConfig::SEED], + bump, + rent_exempt = enforce, + )] + pub merkle_root_upload_config: Account<'info, MerkleRootUploadConfig>, + + pub authority: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +impl UpdateMerkleRootUploadConfig<'_> { + fn auth(ctx: &Context) -> Result<()> { + if ctx.accounts.config.authority != ctx.accounts.authority.key() { + Err(Unauthorized.into()) + } else { + Ok(()) + } + } +} + +#[derive(Accounts)] +pub struct MigrateTdaMerkleRootUploadAuthority<'info> { + #[account(mut, rent_exempt = enforce)] + pub tip_distribution_account: Account<'info, TipDistributionAccount>, + + #[account( + seeds = [MerkleRootUploadConfig::SEED], + bump, + rent_exempt = enforce, + )] + pub merkle_root_upload_config: Account<'info, MerkleRootUploadConfig>, +} + // Events #[event] diff --git a/mev-programs/programs/tip-distribution/src/state.rs b/mev-programs/programs/tip-distribution/src/state.rs index 9ff08ef..5979235 100644 --- a/mev-programs/programs/tip-distribution/src/state.rs +++ b/mev-programs/programs/tip-distribution/src/state.rs @@ -175,3 +175,24 @@ impl ClaimStatus { pub const SIZE: usize = HEADER_SIZE + size_of::(); } + +/// Singleton account that allows overriding TDA's merkle upload authority +#[account] +#[derive(Default)] +pub struct MerkleRootUploadConfig { + /// The authority that overrides the TipDistributionAccount merkle_root_upload_authority + pub override_authority: Pubkey, + + /// The original merkle root upload authority that can be changed to the new overrided + /// authority. E.g. Jito Labs authority GZctHpWXmsZC1YHACTGGcHhYxjdRqQvTpYkb9LMvxDib + pub original_upload_authority: Pubkey, + + /// The bump used to generate this account + pub bump: u8, +} + +impl MerkleRootUploadConfig { + pub const SEED: &'static [u8] = b"ROOT_UPLOAD_CONFIG"; + + pub const SIZE: usize = HEADER_SIZE + size_of::(); +} diff --git a/mev-programs/tests/tip-distribution.ts b/mev-programs/tests/tip-distribution.ts index bbcaada..e74db0d 100644 --- a/mev-programs/tests/tip-distribution.ts +++ b/mev-programs/tests/tip-distribution.ts @@ -3,18 +3,23 @@ import * as anchor from "@coral-xyz/anchor"; import { AnchorError, Program } from "@coral-xyz/anchor"; import { JitoTipDistribution } from "../target/types/jito_tip_distribution"; import { assert, expect } from "chai"; -import {PublicKey, TransactionInstruction, VoteInit, VoteProgram} from "@solana/web3.js"; +import { + PublicKey, + VoteInit, + VoteProgram, +} from "@solana/web3.js"; import { convertBufProofToNumber, MerkleTree } from "./merkle-tree"; -const { - SystemProgram, - sendAndConfirmTransaction, - LAMPORTS_PER_SOL, -} = anchor.web3; +const { SystemProgram, sendAndConfirmTransaction, LAMPORTS_PER_SOL } = + anchor.web3; const CONFIG_ACCOUNT_SEED = "CONFIG_ACCOUNT"; const TIP_DISTRIBUTION_ACCOUNT_LEN = 168; const CLAIM_STATUS_SEED = "CLAIM_STATUS"; const CLAIM_STATUS_LEN = 104; +const ROOT_UPLOAD_CONFIG_SEED = "ROOT_UPLOAD_CONFIG"; +const JITO_MERKLE_UPLOAD_AUTHORITY = new anchor.web3.PublicKey( + "GZctHpWXmsZC1YHACTGGcHhYxjdRqQvTpYkb9LMvxDib", +); const provider = anchor.AnchorProvider.local("http://127.0.0.1:8899", { commitment: "confirmed", @@ -22,25 +27,31 @@ const provider = anchor.AnchorProvider.local("http://127.0.0.1:8899", { }); anchor.setProvider(provider); -const tipDistribution = anchor.workspace.JitoTipDistribution as Program; +const tipDistribution = anchor.workspace + .JitoTipDistribution as Program; // globals -let configAccount, configBump; +let configAccount, configBump, merkleRootUploadConfigKey; +let authority: anchor.web3.Keypair; describe("tests tip_distribution", () => { before(async () => { - const [acc, bump] = await anchor.web3.PublicKey.findProgramAddress( + const [acc, bump] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from(CONFIG_ACCOUNT_SEED, "utf8")], - tipDistribution.programId + tipDistribution.programId, ); configAccount = acc; configBump = bump; + authority = await generateAccount(100000000000000); + [merkleRootUploadConfigKey] = anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from(ROOT_UPLOAD_CONFIG_SEED, "utf8")], + tipDistribution.programId, + ); }); it("#initialize happy path", async () => { // given const initializer = await generateAccount(100000000000000); - const authority = await generateAccount(100000000000000); const expiredFundsAccount = await generateAccount(100000000000000); const numEpochsValid = new anchor.BN(3); const maxValidatorCommissionBps = 1000; @@ -60,7 +71,7 @@ describe("tests tip_distribution", () => { initializer: initializer.publicKey, }, signers: [initializer], - } + }, ); } catch (e) { assert.fail("unexpected error: " + e); @@ -68,7 +79,7 @@ describe("tests tip_distribution", () => { // expect const actualConfig = await tipDistribution.account.config.fetch( - configAccount + configAccount, ); const expected = { authority: authority.publicKey, @@ -108,7 +119,7 @@ describe("tests tip_distribution", () => { // expect const actual = await tipDistribution.account.tipDistributionAccount.fetch( - tipDistributionAccount + tipDistributionAccount, ); const expected = { validatorVoteAccount: validatorVoteAccount.publicKey, @@ -147,8 +158,8 @@ describe("tests tip_distribution", () => { // expect assert( e.errorLogs[0].includes( - "Validator's commission basis points must be less than or equal to the Config account's max_validator_commission_bps." - ) + "Validator's commission basis points must be less than or equal to the Config account's max_validator_commission_bps.", + ), ); } }); @@ -175,14 +186,14 @@ describe("tests tip_distribution", () => { }); const actualConfig = await tipDistribution.account.config.fetch( - configAccount + configAccount, ); const tda = await tipDistribution.account.tipDistributionAccount.fetch( - tipDistributionAccount + tipDistributionAccount, ); const balStart = await provider.connection.getBalance( - validatorVoteAccount.publicKey + validatorVoteAccount.publicKey, ); await sleepForEpochs(4); @@ -198,19 +209,19 @@ describe("tests tip_distribution", () => { .rpc(); const balEnd = await provider.connection.getBalance( - validatorVoteAccount.publicKey + validatorVoteAccount.publicKey, ); const minRentExempt = await provider.connection.getMinimumBalanceForRentExemption( - TIP_DISTRIBUTION_ACCOUNT_LEN + TIP_DISTRIBUTION_ACCOUNT_LEN, ); assert(balEnd - balStart === minRentExempt); try { // cannot fetch a closed account await tipDistribution.account.tipDistributionAccount.fetch( - tipDistributionAccount + tipDistributionAccount, ); assert.fail("fetch should fail"); } catch (_err) { @@ -267,14 +278,14 @@ describe("tests tip_distribution", () => { config: configAccount, }, signers: [validatorVoteAccount], - } + }, ); } catch (e) { assert.fail("Unexpected error: " + e); } const actual = await tipDistribution.account.tipDistributionAccount.fetch( - tipDistributionAccount + tipDistributionAccount, ); const expected = { validatorVoteAccount: validatorVoteAccount.publicKey, @@ -316,9 +327,9 @@ describe("tests tip_distribution", () => { await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( tipDistributionAccount, - amount0 + amount1 + amount0 + amount1, ), - "confirmed" + "confirmed", ); const preBalance0 = 10000000000; const user0 = await generateAccount(preBalance0); @@ -348,13 +359,13 @@ describe("tests tip_distribution", () => { const amount = new anchor.BN(amount0); const proof = tree.getProof(index); const claimant = user0; - const [claimStatus, _bump] = await anchor.web3.PublicKey.findProgramAddress( + const [claimStatus, _bump] = anchor.web3.PublicKey.findProgramAddressSync( [ Buffer.from(CLAIM_STATUS_SEED, "utf8"), claimant.publicKey.toBuffer(), tipDistributionAccount.toBuffer(), ], - tipDistribution.programId + tipDistribution.programId, ); await tipDistribution.methods @@ -362,12 +373,13 @@ describe("tests tip_distribution", () => { .accounts({ config: configAccount, tipDistributionAccount, + merkleRootUploadAuthority: validatorVoteAccount.publicKey, claimStatus, claimant: claimant.publicKey, payer: user1.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) - .signers([user1]) + .signers([user1, validatorVoteAccount]) .rpc(); await sleepForEpochs(4); // wait for TDA to expire @@ -413,9 +425,9 @@ describe("tests tip_distribution", () => { await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( tipDistributionAccount, - amount0 + amount1 + amount0 + amount1, ), - "confirmed" + "confirmed", ); const preBalance0 = 10000000000; const user0 = await generateAccount(preBalance0); @@ -446,13 +458,13 @@ describe("tests tip_distribution", () => { const amount = new anchor.BN(amount0); const proof = tree.getProof(index); const claimant = user0; - const [claimStatus, _bump] = await anchor.web3.PublicKey.findProgramAddress( + const [claimStatus, _bump] = anchor.web3.PublicKey.findProgramAddressSync( [ Buffer.from(CLAIM_STATUS_SEED, "utf8"), claimant.publicKey.toBuffer(), tipDistributionAccount.toBuffer(), ], - tipDistribution.programId + tipDistribution.programId, ); await tipDistribution.methods @@ -460,12 +472,13 @@ describe("tests tip_distribution", () => { .accounts({ config: configAccount, tipDistributionAccount, + merkleRootUploadAuthority: validatorVoteAccount.publicKey, claimStatus, claimant: claimant.publicKey, payer: user1.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) - .signers([user1]) + .signers([user1, validatorVoteAccount]) .rpc(); // should usually wait a few epochs after claiming to close the ClaimAccount @@ -513,9 +526,9 @@ describe("tests tip_distribution", () => { await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( tipDistributionAccount, - amount0 + amount1 + amount0 + amount1, ), - "confirmed" + "confirmed", ); const preBalance0 = 10000000000; const user0 = await generateAccount(preBalance0); @@ -546,13 +559,13 @@ describe("tests tip_distribution", () => { const amount = new anchor.BN(amount0); const proof = tree.getProof(index); const claimant = user0; - const [claimStatus, _bump] = await anchor.web3.PublicKey.findProgramAddress( + const [claimStatus, _bump] = anchor.web3.PublicKey.findProgramAddressSync( [ Buffer.from(CLAIM_STATUS_SEED, "utf8"), claimant.publicKey.toBuffer(), tipDistributionAccount.toBuffer(), ], - tipDistribution.programId + tipDistribution.programId, ); await tipDistribution.methods @@ -560,12 +573,13 @@ describe("tests tip_distribution", () => { .accounts({ config: configAccount, tipDistributionAccount, + merkleRootUploadAuthority: validatorVoteAccount.publicKey, claimStatus, claimant: claimant.publicKey, payer: user1.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) - .signers([user1]) + .signers([user1, validatorVoteAccount]) .rpc(); await sleepForEpochs(3); // wait for TDA to expire @@ -586,17 +600,18 @@ describe("tests tip_distribution", () => { .accounts({ config: configAccount, tipDistributionAccount, + merkleRootUploadAuthority: validatorVoteAccount.publicKey, claimStatus, claimant: claimant.publicKey, payer: user1.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) - .signers([user1]) + .signers([user1, validatorVoteAccount]) .rpc(); assert.fail("expected exception to be thrown"); } catch (e) { const err: AnchorError = e; - assert(err.error.errorCode.code === "ExpiredTipDistributionAccount"); + assert.equal(err.error.errorCode.code, "ExpiredTipDistributionAccount"); } }); @@ -625,9 +640,9 @@ describe("tests tip_distribution", () => { await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( tipDistributionAccount, - amount0 + amount1 + amount0 + amount1, ), - "confirmed" + "confirmed", ); const preBalance0 = 10000000000; const user0 = await generateAccount(preBalance0); @@ -658,13 +673,13 @@ describe("tests tip_distribution", () => { const amount = new anchor.BN(amount0); const proof = tree.getProof(index); const claimant = user0; - const [claimStatus, _bump] = await anchor.web3.PublicKey.findProgramAddress( + const [claimStatus, _bump] = anchor.web3.PublicKey.findProgramAddressSync( [ Buffer.from(CLAIM_STATUS_SEED, "utf8"), claimant.publicKey.toBuffer(), tipDistributionAccount.toBuffer(), ], - tipDistribution.programId + tipDistribution.programId, ); await tipDistribution.methods @@ -672,12 +687,13 @@ describe("tests tip_distribution", () => { .accounts({ config: configAccount, tipDistributionAccount, + merkleRootUploadAuthority: validatorVoteAccount.publicKey, claimStatus, claimant: claimant.publicKey, payer: user1.publicKey, //payer receives rent from closing ClaimAccount systemProgram: anchor.web3.SystemProgram.programId, }) - .signers([user1]) + .signers([user1, validatorVoteAccount]) .rpc(); await sleepForEpochs(4); // wait for TDA to expire @@ -695,7 +711,7 @@ describe("tests tip_distribution", () => { const balEnd = await provider.connection.getBalance(user1.publicKey); const minRentExempt = await provider.connection.getMinimumBalanceForRentExemption( - CLAIM_STATUS_LEN + CLAIM_STATUS_LEN, ); assert(balEnd - balStart === minRentExempt); }); @@ -724,9 +740,9 @@ describe("tests tip_distribution", () => { await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( tipDistributionAccount, - amount0 + amount1 + amount0 + amount1, ), - "confirmed" + "confirmed", ); const preBalance0 = 10000000000; const user0 = await generateAccount(preBalance0); @@ -757,35 +773,37 @@ describe("tests tip_distribution", () => { const amount = new anchor.BN(amount0); const proof = tree.getProof(index); const claimant = user0; - const [claimStatus, _bump] = await anchor.web3.PublicKey.findProgramAddress( - [ - Buffer.from(CLAIM_STATUS_SEED, "utf8"), - claimant.publicKey.toBuffer(), - tipDistributionAccount.toBuffer(), - ], - tipDistribution.programId - ); + const [claimStatus, _bump] = + await anchor.web3.PublicKey.findProgramAddressSync( + [ + Buffer.from(CLAIM_STATUS_SEED, "utf8"), + claimant.publicKey.toBuffer(), + tipDistributionAccount.toBuffer(), + ], + tipDistribution.programId, + ); await tipDistribution.methods .claim(_bump, amount, convertBufProofToNumber(proof)) .accounts({ config: configAccount, tipDistributionAccount, + merkleRootUploadAuthority: validatorVoteAccount.publicKey, claimStatus, claimant: claimant.publicKey, payer: user1.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) - .signers([user1]) + .signers([user1, validatorVoteAccount]) .rpc(); await sleepForEpochs(3); const actualConfig = await tipDistribution.account.config.fetch( - configAccount + configAccount, ); const tda = await tipDistribution.account.tipDistributionAccount.fetch( - tipDistributionAccount + tipDistributionAccount, ); //close the account @@ -811,18 +829,260 @@ describe("tests tip_distribution", () => { const balEnd = await provider.connection.getBalance(user1.publicKey); const minRentExempt = await provider.connection.getMinimumBalanceForRentExemption( - CLAIM_STATUS_LEN + CLAIM_STATUS_LEN, ); assert(balEnd - balStart === minRentExempt); }); // move to end due to PrivilegeEscalation warning it("#claim happy path", async () => { + const { + amount0, + preBalance0, + root, + tipDistributionAccount, + tree, + user0, + user1, + validatorVoteAccount, + } = await setupWithUploadedMerkleRoot(); + + const index = 0; + const amount = new anchor.BN(amount0); + const proof = tree.getProof(index); + assert(tree.verifyProof(0, proof, root)); + + const claimant = user0; + const [claimStatus, _bump] = anchor.web3.PublicKey.findProgramAddressSync( + [ + Buffer.from(CLAIM_STATUS_SEED, "utf8"), + claimant.publicKey.toBuffer(), + tipDistributionAccount.toBuffer(), + ], + tipDistribution.programId, + ); + + await tipDistribution.methods + .claim(_bump, amount, convertBufProofToNumber(proof)) + .accounts({ + config: configAccount, + tipDistributionAccount, + merkleRootUploadAuthority: validatorVoteAccount.publicKey, + claimStatus, + claimant: claimant.publicKey, + payer: user1.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .signers([user1, validatorVoteAccount]) + .rpc(); + + const user0Info = await tipDistribution.provider.connection.getAccountInfo( + user0.publicKey, + ); + assert.equal(user0Info.lamports, preBalance0 + amount0); + }); + + it("#claim fails if TDA merkle root upload authority not signer ", async () => { + const { amount0, root, tipDistributionAccount, tree, user0, user1 } = + await setupWithUploadedMerkleRoot(); + + const index = 0; + const amount = new anchor.BN(amount0); + const proof = tree.getProof(index); + assert(tree.verifyProof(0, proof, root)); + + const claimant = user0; + const [claimStatus, _bump] = anchor.web3.PublicKey.findProgramAddressSync( + [ + Buffer.from(CLAIM_STATUS_SEED, "utf8"), + claimant.publicKey.toBuffer(), + tipDistributionAccount.toBuffer(), + ], + tipDistribution.programId, + ); + + const badAuthority = anchor.web3.Keypair.generate(); + + try { + await tipDistribution.methods + .claim(_bump, amount, convertBufProofToNumber(proof)) + .accounts({ + config: configAccount, + tipDistributionAccount, + merkleRootUploadAuthority: badAuthority.publicKey, + claimStatus, + claimant: claimant.publicKey, + payer: user1.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .signers([user1, badAuthority]) + .rpc(); + assert.fail("expected exception to be thrown"); + } catch (e) { + const err: AnchorError = e; + assert(err.error.errorCode.code === "Unauthorized"); + } + }); + + it("#initialize_merkle_root_upload_conifg happy path", async () => { + await setup_initTipDistributionAccount(); + + const [_merkleRootUploadConfigKey, merkleRootUploadConfigBump] = + anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from(ROOT_UPLOAD_CONFIG_SEED, "utf8")], + tipDistribution.programId, + ); + const overrideAuthority = anchor.web3.Keypair.generate(); + + const originalAuthority = anchor.web3.Keypair.generate(); + + // call the init instruction + await tipDistribution.methods + .initializeMerkleRootUploadConfig( + overrideAuthority.publicKey, + originalAuthority.publicKey, + ) + .accounts({ + payer: tipDistribution.provider.publicKey, + config: configAccount, + authority: authority.publicKey, + merkleRootUploadConfig: merkleRootUploadConfigKey, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .signers([authority]) + .rpc({ skipPreflight: true }); + + // Valdiate that the MerkleRootUploadConfig account was created + const merkleRootUploadConfig = + await tipDistribution.account.merkleRootUploadConfig.fetch( + merkleRootUploadConfigKey, + ); + // Validate the MerkleRootUploadConfig authority is the Config authority + assert.equal(merkleRootUploadConfig.bump, merkleRootUploadConfigBump); + assert.equal( + merkleRootUploadConfig.overrideAuthority.toString(), + overrideAuthority.publicKey.toString(), + ); + assert.equal( + merkleRootUploadConfig.originalUploadAuthority.toString(), + originalAuthority.publicKey.toString(), + ); + }); + + it("#update_merkle_root_upload_conifg happy path", async () => { + await setup_initTipDistributionAccount(); + + const newOverrideAuthority = anchor.web3.Keypair.generate(); + + await tipDistribution.methods + .updateMerkleRootUploadConfig( + newOverrideAuthority.publicKey, + JITO_MERKLE_UPLOAD_AUTHORITY, + ) + .accounts({ + config: configAccount, + authority: authority.publicKey, + merkleRootUploadConfig: merkleRootUploadConfigKey, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .signers([authority]) + .rpc({ skipPreflight: true }); + + const updatedMerkleRootUploadConfig = + await tipDistribution.account.merkleRootUploadConfig.fetch( + merkleRootUploadConfigKey, + ); + // Validate the MerkleRootUploadConfig authority is the new authority + assert.equal( + updatedMerkleRootUploadConfig.overrideAuthority.toString(), + newOverrideAuthority.publicKey.toString(), + ); + assert.equal( + updatedMerkleRootUploadConfig.originalUploadAuthority.toString(), + JITO_MERKLE_UPLOAD_AUTHORITY.toString(), + ); + }); + + it("#migrate_tda_merkle_root_upload_authority happy path", async () => { const { validatorVoteAccount, + validatorIdentityKeypair, maxValidatorCommissionBps, tipDistributionAccount, + bump, + } = await setup_initTipDistributionAccount(); + await call_initTipDistributionAccount({ + validatorCommissionBps: maxValidatorCommissionBps, + config: configAccount, + validatorIdentityKeypair, + systemProgram: SystemProgram.programId, + merkleRootUploadAuthority: JITO_MERKLE_UPLOAD_AUTHORITY, + validatorVoteAccount, + tipDistributionAccount, + bump, + }); + + const merkleRootUploadConfig = + await tipDistribution.account.merkleRootUploadConfig.fetch( + merkleRootUploadConfigKey, + ); + + await tipDistribution.methods + .migrateTdaMerkleRootUploadAuthority() + .accounts({ + tipDistributionAccount: tipDistributionAccount, + merkleRootUploadConfig: merkleRootUploadConfigKey, + }) + .rpc({ skipPreflight: true }); + + const tda = await tipDistribution.account.tipDistributionAccount.fetch( + tipDistributionAccount, + ); + assert.equal( + tda.merkleRootUploadAuthority.toString(), + merkleRootUploadConfig.overrideAuthority.toString(), + ); + }); + + it("#migrate_tda_merkle_root_upload_authority should error if TDA not Jito authority", async () => { + const { + validatorVoteAccount, validatorIdentityKeypair, + maxValidatorCommissionBps, + tipDistributionAccount, + bump, + } = await setup_initTipDistributionAccount(); + await call_initTipDistributionAccount({ + validatorCommissionBps: maxValidatorCommissionBps, + config: configAccount, + validatorIdentityKeypair, + systemProgram: SystemProgram.programId, + merkleRootUploadAuthority: validatorVoteAccount.publicKey, + validatorVoteAccount, + tipDistributionAccount, + bump, + }); + try { + await tipDistribution.methods + .migrateTdaMerkleRootUploadAuthority() + .accounts({ + tipDistributionAccount: tipDistributionAccount, + merkleRootUploadConfig: merkleRootUploadConfigKey, + }) + .rpc({ skipPreflight: true }); + assert.fail("expected exception to be thrown"); + } catch (e) { + const err: AnchorError = e; + assert(err.error.errorCode.code === "InvalidTdaForMigration"); + } + }); + + it("#migrate_tda_merkle_root_upload_authority should error if merkle root is already uploaded", async () => { + const { + validatorVoteAccount, + validatorIdentityKeypair, + maxValidatorCommissionBps, + tipDistributionAccount, bump, } = await setup_initTipDistributionAccount(); await call_initTipDistributionAccount({ @@ -841,9 +1101,9 @@ describe("tests tip_distribution", () => { await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( tipDistributionAccount, - amount0 + amount1 + amount0 + amount1, ), - "confirmed" + "confirmed", ); const preBalance0 = 10000000000; const user0 = await generateAccount(preBalance0); @@ -857,7 +1117,6 @@ describe("tests tip_distribution", () => { const root = tree.getRoot(); const maxTotalClaim = new anchor.BN(amount0 + amount1); const maxNumNodes = new anchor.BN(2); - await sleepForEpochs(1); await tipDistribution.methods @@ -869,39 +1128,19 @@ describe("tests tip_distribution", () => { }) .signers([validatorVoteAccount]) .rpc(); - - const index = 0; - const amount = new anchor.BN(amount0); - const proof = tree.getProof(index); - assert(tree.verifyProof(0, proof, root)); - - const claimant = user0; - const [claimStatus, _bump] = await anchor.web3.PublicKey.findProgramAddress( - [ - Buffer.from(CLAIM_STATUS_SEED, "utf8"), - claimant.publicKey.toBuffer(), - tipDistributionAccount.toBuffer(), - ], - tipDistribution.programId - ); - - await tipDistribution.methods - .claim(_bump, amount, convertBufProofToNumber(proof)) - .accounts({ - config: configAccount, - tipDistributionAccount, - claimStatus, - claimant: claimant.publicKey, - payer: user1.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .signers([user1]) - .rpc(); - - const user0Info = await tipDistribution.provider.connection.getAccountInfo( - user0.publicKey - ); - assert.equal(user0Info.lamports, preBalance0 + amount0); + try { + await tipDistribution.methods + .migrateTdaMerkleRootUploadAuthority() + .accounts({ + tipDistributionAccount: tipDistributionAccount, + merkleRootUploadConfig: merkleRootUploadConfigKey, + }) + .rpc({ skipPreflight: true }); + assert.fail("expected exception to be thrown"); + } catch (e) { + const err: AnchorError = e; + assert(err.error.errorCode.code === "InvalidTdaForMigration"); + } }); }); @@ -931,26 +1170,26 @@ const assertConfigState = (actual, expected) => { assert.equal(actual.authority.toString(), expected.authority.toString()); assert.equal( actual.expiredFundsAccount.toString(), - expected.expiredFundsAccount.toString() + expected.expiredFundsAccount.toString(), ); assert.equal( actual.maxValidatorCommissionBps, - expected.maxValidatorCommissionBps + expected.maxValidatorCommissionBps, ); assert.equal( actual.numEpochsValid.toString(), - expected.numEpochsValid.toString() + expected.numEpochsValid.toString(), ); }; const assertDistributionAccount = (actual, expected) => { assert.equal( actual.validatorVoteAccount.toString(), - expected.validatorVoteAccount.toString() + expected.validatorVoteAccount.toString(), ); assert.equal( actual.merkleRootUploadAuthority.toString(), - expected.merkleRootUploadAuthority.toString() + expected.merkleRootUploadAuthority.toString(), ); assert.equal(actual.epochCreatedAt, expected.epochCreatedAt); assert.equal(actual.validatorCommissionBps, expected.validatorCommissionBps); @@ -958,23 +1197,23 @@ const assertDistributionAccount = (actual, expected) => { if (actual.merkleRoot && expected.merkleRoot) { assert.equal( actual.merkleRoot.root.toString(), - expected.merkleRoot.root.toString() + expected.merkleRoot.root.toString(), ); assert.equal( actual.merkleRoot.maxTotalClaim.toString(), - expected.merkleRoot.maxTotalClaim.toString() + expected.merkleRoot.maxTotalClaim.toString(), ); assert.equal( actual.merkleRoot.maxNumNodes.toString(), - expected.merkleRoot.maxNumNodes.toString() + expected.merkleRoot.maxNumNodes.toString(), ); assert.equal( actual.merkleRoot.totalFundsClaimed.toString(), - expected.merkleRoot.totalFundsClaimed.toString() + expected.merkleRoot.totalFundsClaimed.toString(), ); assert.equal( actual.merkleRoot.numNodesClaimed.toString(), - expected.merkleRoot.numNodesClaimed.toString() + expected.merkleRoot.numNodesClaimed.toString(), ); } else if (actual.merkleRoot || expected.merkleRoot) { assert.fail(); @@ -987,9 +1226,9 @@ const generateAccount = async (airdropAmount: number) => { await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( account.publicKey, - airdropAmount + airdropAmount, ), - "confirmed" + "confirmed", ); } @@ -1009,10 +1248,10 @@ const setup_initTipDistributionAccount = async () => { validatorIdentityKeypair.publicKey, validatorIdentityKeypair.publicKey, validatorIdentityKeypair.publicKey, - 0 + 0, ); const lamports = await provider.connection.getMinimumBalanceForRentExemption( - VoteProgram.space + VoteProgram.space, ); const tx = VoteProgram.createAccount({ fromPubkey: validatorIdentityKeypair.publicKey, @@ -1044,13 +1283,13 @@ const setup_initTipDistributionAccount = async () => { const epochInfo = await provider.connection.getEpochInfo("confirmed"); const epoch = new anchor.BN(epochInfo.epoch).toArrayLike(Buffer, "le", 8); const [tipDistributionAccount, bump] = - await anchor.web3.PublicKey.findProgramAddress( + anchor.web3.PublicKey.findProgramAddressSync( [ Buffer.from("TIP_DISTRIBUTION_ACCOUNT", "utf8"), validatorVoteAccount.publicKey.toBuffer(), epoch, ], - tipDistribution.programId + tipDistribution.programId, ); return { @@ -1088,10 +1327,75 @@ const call_initTipDistributionAccount = async ({ tipDistributionAccount, }, signers: [validatorIdentityKeypair], - } + }, ); }; +const setupWithUploadedMerkleRoot = async () => { + const { + validatorVoteAccount, + maxValidatorCommissionBps, + tipDistributionAccount, + validatorIdentityKeypair, + bump, + } = await setup_initTipDistributionAccount(); + await call_initTipDistributionAccount({ + validatorCommissionBps: maxValidatorCommissionBps, + config: configAccount, + validatorIdentityKeypair, + systemProgram: SystemProgram.programId, + merkleRootUploadAuthority: validatorVoteAccount.publicKey, + validatorVoteAccount, + tipDistributionAccount, + bump, + }); + + const amount0 = 1_000_000; + const amount1 = 2_000_000; + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop( + tipDistributionAccount, + amount0 + amount1, + ), + "confirmed", + ); + const preBalance0 = 10000000000; + const user0 = await generateAccount(preBalance0); + const user1 = await generateAccount(preBalance0); + const demoData = [ + { account: user0.publicKey, amount: new u64(amount0) }, + { account: user1.publicKey, amount: new u64(amount1) }, + ].map(({ account, amount }) => balanceToBuffer(account, amount)); + + const tree = new MerkleTree(demoData); + const root = tree.getRoot(); + const maxTotalClaim = new anchor.BN(amount0 + amount1); + const maxNumNodes = new anchor.BN(2); + + await sleepForEpochs(1); + + await tipDistribution.methods + .uploadMerkleRoot(root.toJSON().data, maxTotalClaim, maxNumNodes) + .accounts({ + tipDistributionAccount, + merkleRootUploadAuthority: validatorVoteAccount.publicKey, + config: configAccount, + }) + .signers([validatorVoteAccount]) + .rpc(); + return { + amount0, + amount1, + preBalance0, + root, + tipDistributionAccount, + tree, + user0, + user1, + validatorVoteAccount, + }; +}; + const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); };