From 8a5c881137d2753d00eebe454f62d51bf4f8b478 Mon Sep 17 00:00:00 2001 From: Noah Prince <83885631+ChewingGlass@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:59:44 -0800 Subject: [PATCH] Bugfixes for HIP-138 delegation claims (#756) --- .../src/fetchBackwardsCompatibleIdl.ts | 23 +++--- .../src/utils/formPositionClaims.ts | 78 +++++++++++++++++-- .../calculate_utility_score_v0.rs | 3 + .../delegation/claim_rewards_v1.rs | 27 +++---- .../instructions/delegation/delegate_v0.rs | 1 - .../src/instructions/issue_rewards_v0.rs | 6 +- .../src/instructions/temp_resize_account.rs | 2 + programs/helium-sub-daos/src/state.rs | 13 +++- 8 files changed, 113 insertions(+), 40 deletions(-) diff --git a/packages/spl-utils/src/fetchBackwardsCompatibleIdl.ts b/packages/spl-utils/src/fetchBackwardsCompatibleIdl.ts index aca2d3ede..88fad9a16 100644 --- a/packages/spl-utils/src/fetchBackwardsCompatibleIdl.ts +++ b/packages/spl-utils/src/fetchBackwardsCompatibleIdl.ts @@ -1340,7 +1340,7 @@ const IDLS_BY_PROGRAM: Record = { isSigner: false, }, { - name: "subDaoEpochInfo", + name: "daoEpochInfo", isMut: false, isSigner: false, pda: { @@ -1348,13 +1348,13 @@ const IDLS_BY_PROGRAM: Record = { { kind: "const", type: "string", - value: "sub_dao_epoch_info", + value: "dao_epoch_info", }, { kind: "account", type: "publicKey", - account: "SubDaoV0", - path: "sub_dao", + account: "DaoV0", + path: "dao", }, { kind: "arg", @@ -2314,6 +2314,14 @@ const IDLS_BY_PROGRAM: Record = { ], }, }, + { + name: "delegationRewardsIssued", + type: "u64", + }, + { + name: "vehntAtEpochStart", + type: "u64", + }, ], }, }, @@ -2448,13 +2456,6 @@ const IDLS_BY_PROGRAM: Record = { name: "dcOnboardingFeesPaid", type: "u64", }, - { - name: "hntDelegationRewardsIssued", - docs: [ - "The number of hnt delegation rewards issued this epoch, so that delegators can claim their share of the rewards", - ], - type: "u64", - }, { name: "hntRewardsIssued", docs: [ diff --git a/packages/voter-stake-registry-hooks/src/utils/formPositionClaims.ts b/packages/voter-stake-registry-hooks/src/utils/formPositionClaims.ts index 3ae774803..03f1e4854 100644 --- a/packages/voter-stake-registry-hooks/src/utils/formPositionClaims.ts +++ b/packages/voter-stake-registry-hooks/src/utils/formPositionClaims.ts @@ -6,6 +6,7 @@ import { import { EPOCH_LENGTH, PROGRAM_ID as HSD_PROGRAM_ID, + daoEpochInfoKey, daoKey, delegatedPositionKey, init, @@ -22,6 +23,7 @@ import { getAssociatedTokenAddressSync, } from "@solana/spl-token"; import { + AccountInfo, Connection, PublicKey, SYSVAR_CLOCK_PUBKEY, @@ -88,7 +90,7 @@ export const formPositionClaims = async ({ } return acc; }, {} as Record); - + const daoAcc = await hsdProgram.account.daoV0.fetch(Object.values(subDaos)[0].dao); for (const [idx, position] of positions.entries()) { bucketedEpochsByPosition[position.pubkey.toBase58()] = @@ -127,10 +129,58 @@ export const formPositionClaims = async ({ // Chunk size is 128 because we want each chunk to correspond to the 128 bits in bitmap for (const chunk of chunks(epochsToClaim, 128)) { + const daoEpochInfoKeys = chunk.map((epoch) => + daoEpochInfoKey(subDaoAcc.dao, epoch.mul(new BN(EPOCH_LENGTH)))[0] + ); + const daoEpochInfoAccounts = await getMultipleAccounts({ + connection: hsdProgram.provider.connection, + keys: daoEpochInfoKeys, + }); bucketedEpochsByPosition[position.pubkey.toBase58()].push( await Promise.all( - chunk.map((epoch) => - hsdProgram.methods + chunk.map((epoch, index) => { + const daoEpochInfoAccount = daoEpochInfoAccounts[index]; + const daoEpochInfoData = hsdProgram.coder.accounts.decode( + "daoEpochInfoV0", + daoEpochInfoAccount?.data + ); + + if (daoEpochInfoData.delegationRewardsIssued.gt(new BN(0))) { + return hsdProgram.methods + .claimRewardsV1({ + epoch, + }) + .accountsStrict({ + position: position.pubkey, + mint: position.mint, + positionTokenAccount: getAssociatedTokenAddressSync( + position.mint, + provider.wallet.publicKey + ), + positionAuthority: provider.wallet.publicKey, + registrar: position.registrar, + dao: DAO, + subDao: delegatedPosition.account!.subDao, + delegatedPosition: delegatedPosition.key, + hntMint: daoAcc.hntMint, + daoEpochInfo: daoEpochInfoKey(subDaoAcc.dao, epoch.mul(new BN(EPOCH_LENGTH)))[0], + delegatorPool: daoAcc.delegatorPool, + delegatorAta: getAssociatedTokenAddressSync( + daoAcc.hntMint, + provider.wallet.publicKey + ), + delegatorPoolCircuitBreaker: accountWindowedBreakerKey( + daoAcc.delegatorPool + )[0], + vsrProgram: VSR_PROGRAM_ID, + systemProgram: SystemProgram.programId, + circuitBreakerProgram: CIRCUIT_BREAKER_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .instruction(); + } else { + return hsdProgram.methods .claimRewardsV0({ epoch, }) @@ -165,8 +215,9 @@ export const formPositionClaims = async ({ associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, tokenProgram: TOKEN_PROGRAM_ID, }) - .instruction() - ) + .instruction(); + } + }) ) ); } @@ -189,3 +240,20 @@ export const formPositionClaims = async ({ return instructions; }; + +async function getMultipleAccounts({ + connection, + keys, +}): Promise[]> { + const batchSize = 100; + const batches = Math.ceil(keys.length / batchSize); + const results: AccountInfo[] = []; + + for (let i = 0; i < batches; i++) { + const batchKeys = keys.slice(i * batchSize, (i + 1) * batchSize); + const batchResults = await connection.getMultipleAccountsInfo(batchKeys); + results.push(...batchResults); + } + + return results; +} \ No newline at end of file diff --git a/programs/helium-sub-daos/src/instructions/calculate_utility_score_v0.rs b/programs/helium-sub-daos/src/instructions/calculate_utility_score_v0.rs index a4ab8eb49..27d77d89f 100644 --- a/programs/helium-sub-daos/src/instructions/calculate_utility_score_v0.rs +++ b/programs/helium-sub-daos/src/instructions/calculate_utility_score_v0.rs @@ -93,6 +93,9 @@ pub fn handler( )) .unwrap(); + ctx.accounts.dao_epoch_info.vehnt_at_epoch_start += + ctx.accounts.sub_dao_epoch_info.vehnt_at_epoch_start; + ctx.accounts.dao_epoch_info.epoch = args.epoch; ctx.accounts.dao_epoch_info.current_hnt_supply = curr_supply diff --git a/programs/helium-sub-daos/src/instructions/delegation/claim_rewards_v1.rs b/programs/helium-sub-daos/src/instructions/delegation/claim_rewards_v1.rs index c429bcdf9..b2392324b 100644 --- a/programs/helium-sub-daos/src/instructions/delegation/claim_rewards_v1.rs +++ b/programs/helium-sub-daos/src/instructions/delegation/claim_rewards_v1.rs @@ -65,11 +65,11 @@ pub struct ClaimRewardsV1<'info> { pub hnt_mint: Box>, #[account( - seeds = ["sub_dao_epoch_info".as_bytes(), sub_dao.key().as_ref(), &args.epoch.to_le_bytes()], + seeds = ["dao_epoch_info".as_bytes(), dao.key().as_ref(), &args.epoch.to_le_bytes()], bump, - constraint = sub_dao_epoch_info.rewards_issued_at.is_some() @ ErrorCode::EpochNotClosed + constraint = dao_epoch_info.done_issuing_rewards @ ErrorCode::EpochNotClosed )] - pub sub_dao_epoch_info: Box>, + pub dao_epoch_info: Box>, #[account(mut)] pub delegator_pool: Box>, #[account( @@ -137,29 +137,22 @@ pub fn handler(ctx: Context, args: ClaimRewardsArgsV0) -> Result } } - let delegated_vehnt_at_epoch = position.voting_power( - voting_mint_config, - ctx.accounts.sub_dao_epoch_info.start_ts(), - )?; + let delegated_vehnt_at_epoch = + position.voting_power(voting_mint_config, ctx.accounts.dao_epoch_info.start_ts())?; - msg!("Staked {} veHNT at start of epoch with {} total veHNT delegated to subdao and {} total rewards to subdao", + msg!("Staked {} veHNT at start of epoch with {} total veHNT delegated to dao and {} total rewards to dao", delegated_vehnt_at_epoch, - ctx.accounts.sub_dao_epoch_info.vehnt_at_epoch_start, - ctx.accounts.sub_dao_epoch_info.hnt_delegation_rewards_issued + ctx.accounts.dao_epoch_info.vehnt_at_epoch_start, + ctx.accounts.dao_epoch_info.delegation_rewards_issued ); // calculate the position's share of that epoch's rewards // rewards = staking_rewards_issued * staked_vehnt_at_epoch / total_vehnt let rewards = u64::try_from( delegated_vehnt_at_epoch - .checked_mul( - ctx - .accounts - .sub_dao_epoch_info - .hnt_delegation_rewards_issued as u128, - ) + .checked_mul(ctx.accounts.dao_epoch_info.delegation_rewards_issued as u128) .unwrap() - .checked_div(ctx.accounts.sub_dao_epoch_info.vehnt_at_epoch_start as u128) + .checked_div(ctx.accounts.dao_epoch_info.vehnt_at_epoch_start as u128) .unwrap(), ) .unwrap(); diff --git a/programs/helium-sub-daos/src/instructions/delegation/delegate_v0.rs b/programs/helium-sub-daos/src/instructions/delegation/delegate_v0.rs index 326246da6..784e98529 100644 --- a/programs/helium-sub-daos/src/instructions/delegation/delegate_v0.rs +++ b/programs/helium-sub-daos/src/instructions/delegation/delegate_v0.rs @@ -232,7 +232,6 @@ pub fn handler(ctx: Context) -> Result<()> { rewards_issued_at: None, initialized: false, dc_onboarding_fees_paid: 0, - hnt_delegation_rewards_issued: 0, hnt_rewards_issued: 0, }, }, diff --git a/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs b/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs index 5679d6a50..7fef733d9 100644 --- a/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs +++ b/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs @@ -204,10 +204,8 @@ pub fn handler(ctx: Context, args: IssueRewardsArgsV0) -> Result ctx.accounts.sub_dao_epoch_info.hnt_rewards_issued = escrow_amount; ctx.accounts.dao_epoch_info.num_rewards_issued += 1; ctx.accounts.sub_dao_epoch_info.rewards_issued_at = Some(Clock::get()?.unix_timestamp); - ctx - .accounts - .sub_dao_epoch_info - .hnt_delegation_rewards_issued = delegation_rewards_amount; + ctx.accounts.dao_epoch_info.delegation_rewards_issued = delegation_rewards_amount; + ctx.accounts.sub_dao_epoch_info.delegation_rewards_issued = 0; ctx.accounts.dao_epoch_info.done_issuing_rewards = ctx.accounts.dao.num_sub_daos == ctx.accounts.dao_epoch_info.num_rewards_issued; diff --git a/programs/helium-sub-daos/src/instructions/temp_resize_account.rs b/programs/helium-sub-daos/src/instructions/temp_resize_account.rs index 2c426f43b..f3091c4b5 100644 --- a/programs/helium-sub-daos/src/instructions/temp_resize_account.rs +++ b/programs/helium-sub-daos/src/instructions/temp_resize_account.rs @@ -27,6 +27,8 @@ pub fn handler(ctx: Context) -> Result<()> { // This is the dao account. if account.key() == Pubkey::from_str("BQ3MCuTT5zVBhNfQ4SjMh3NPVhFy73MPV8rjfq5d1zie").unwrap() { new_size += 104; // Add space for rewards_escrow, delegator_pool, delegator_rewards_percent, proposal_namespace + } else { + new_size += 8; // Delegation rewards issued } let rent = Rent::get()?; let new_minimum_balance = rent.minimum_balance(new_size); diff --git a/programs/helium-sub-daos/src/state.rs b/programs/helium-sub-daos/src/state.rs index 20af8e778..c0f0315d6 100644 --- a/programs/helium-sub-daos/src/state.rs +++ b/programs/helium-sub-daos/src/state.rs @@ -155,6 +155,9 @@ pub struct DaoEpochInfoV0 { pub done_issuing_hst_pool: bool, pub bump_seed: u8, pub recent_proposals: [RecentProposal; 4], + // The number of delegation rewards issued this epoch, so that delegators can claim their share of the rewards + pub delegation_rewards_issued: u64, + pub vehnt_at_epoch_start: u64, } #[derive(Debug, InitSpace, Clone, AnchorSerialize, AnchorDeserialize, Default)] @@ -167,6 +170,14 @@ impl DaoEpochInfoV0 { pub fn size() -> usize { 60 + 8 + std::mem::size_of::() } + + pub fn start_ts(&self) -> i64 { + i64::try_from(self.epoch).unwrap() * EPOCH_LENGTH + } + + pub fn end_ts(&self) -> i64 { + i64::try_from(self.epoch + 1).unwrap() * EPOCH_LENGTH + } } #[account] @@ -266,8 +277,6 @@ pub struct SubDaoEpochInfoV0 { pub bump_seed: u8, pub initialized: bool, pub dc_onboarding_fees_paid: u64, - /// The number of hnt delegation rewards issued this epoch, so that delegators can claim their share of the rewards - pub hnt_delegation_rewards_issued: u64, /// The number of hnt rewards issued to the reward escrow this epoch pub hnt_rewards_issued: u64, }