From 6e11f4fab12740c18cf590ab578adb618b28c544 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Tue, 19 Nov 2024 09:53:38 -0800 Subject: [PATCH] WIP --- .../delegation/claim_rewards_v0.rs | 3 +- .../delegation/claim_rewards_v1.rs | 171 ++++++++++++++++++ .../instructions/delegation/delegate_v0.rs | 16 +- .../src/instructions/delegation/mod.rs | 2 + .../initialize_hnt_delegator_pool.rs | 98 ++++++++++ .../src/instructions/initialize_sub_dao_v0.rs | 3 +- .../src/instructions/issue_rewards_v0.rs | 67 ++----- .../helium-sub-daos/src/instructions/mod.rs | 2 + programs/helium-sub-daos/src/state.rs | 6 + 9 files changed, 310 insertions(+), 58 deletions(-) create mode 100644 programs/helium-sub-daos/src/instructions/delegation/claim_rewards_v1.rs create mode 100644 programs/helium-sub-daos/src/instructions/initialize_hnt_delegator_pool.rs diff --git a/programs/helium-sub-daos/src/instructions/delegation/claim_rewards_v0.rs b/programs/helium-sub-daos/src/instructions/delegation/claim_rewards_v0.rs index 3bd0ec2df..66ab8533f 100644 --- a/programs/helium-sub-daos/src/instructions/delegation/claim_rewards_v0.rs +++ b/programs/helium-sub-daos/src/instructions/delegation/claim_rewards_v0.rs @@ -1,4 +1,3 @@ -use crate::{current_epoch, error::ErrorCode, state::*, TESTING}; use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, @@ -13,6 +12,8 @@ use voter_stake_registry::{ VoterStakeRegistry, }; +use crate::{current_epoch, error::ErrorCode, state::*, TESTING}; + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct ClaimRewardsArgsV0 { pub epoch: u64, 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 new file mode 100644 index 000000000..56ef257aa --- /dev/null +++ b/programs/helium-sub-daos/src/instructions/delegation/claim_rewards_v1.rs @@ -0,0 +1,171 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount}, +}; +use circuit_breaker::{ + cpi::{accounts::TransferV0, transfer_v0}, + CircuitBreaker, TransferArgsV0, +}; +use voter_stake_registry::{ + state::{PositionV0, Registrar}, + VoterStakeRegistry, +}; + +use crate::{current_epoch, error::ErrorCode, state::*, TESTING}; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct ClaimRewardsArgsV0 { + pub epoch: u64, +} + +#[derive(Accounts)] +#[instruction(args: ClaimRewardsArgsV0)] +pub struct ClaimRewardsV0<'info> { + #[account( + seeds = [b"position".as_ref(), mint.key().as_ref()], + seeds::program = vsr_program.key(), + bump = position.bump_seed, + has_one = mint, + has_one = registrar, + )] + pub position: Box>, + pub mint: Box>, + #[account( + token::mint = mint, + token::authority = position_authority, + constraint = position_token_account.amount > 0 + )] + pub position_token_account: Box>, + #[account(mut)] + pub position_authority: Signer<'info>, + pub registrar: Box>, + #[account( + has_one = registrar, + has_one = hnt_mint + )] + pub dao: Box>, + + #[account( + mut, + has_one = hnt_delegator_pool, + has_one = dao, + )] + pub sub_dao: Account<'info, SubDaoV0>, + #[account( + mut, + has_one = sub_dao, + seeds = ["delegated_position".as_bytes(), position.key().as_ref()], + bump, + )] + pub delegated_position: Account<'info, DelegatedPositionV0>, + + pub hnt_mint: Box>, + + #[account( + seeds = ["sub_dao_epoch_info".as_bytes(), sub_dao.key().as_ref(), &args.epoch.to_le_bytes()], + bump, + constraint = sub_dao_epoch_info.rewards_issued_at.is_some() @ ErrorCode::EpochNotClosed + )] + pub sub_dao_epoch_info: Box>, + #[account(mut)] + pub hnt_delegator_pool: Box>, + #[account( + init_if_needed, + payer = position_authority, + associated_token::mint = hnt_mint, + associated_token::authority = position_authority, + )] + pub delegator_ata: Box>, + + /// CHECK: checked via cpi + #[account( + mut, + seeds = ["account_windowed_breaker".as_bytes(), hnt_delegator_pool.key().as_ref()], + seeds::program = circuit_breaker_program.key(), + bump + )] + pub delegator_pool_circuit_breaker: AccountInfo<'info>, + + pub vsr_program: Program<'info, VoterStakeRegistry>, + pub system_program: Program<'info, System>, + pub circuit_breaker_program: Program<'info, CircuitBreaker>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub token_program: Program<'info, Token>, +} + +impl<'info> ClaimRewardsV0<'info> { + fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferV0<'info>> { + let cpi_accounts = TransferV0 { + from: self.hnt_delegator_pool.to_account_info(), + to: self.delegator_ata.to_account_info(), + owner: self.sub_dao.to_account_info(), + circuit_breaker: self.delegator_pool_circuit_breaker.to_account_info(), + token_program: self.token_program.to_account_info(), + }; + + CpiContext::new(self.circuit_breaker_program.to_account_info(), cpi_accounts) + } +} + +pub fn handler(ctx: Context, args: ClaimRewardsArgsV0) -> Result<()> { + // load the vehnt information + let position = &mut ctx.accounts.position; + let registrar = &ctx.accounts.registrar; + let voting_mint_config = ®istrar.voting_mints[position.voting_mint_config_idx as usize]; + + let delegated_position = &mut ctx.accounts.delegated_position; + + // check epoch that's being claimed is over + let epoch = current_epoch(registrar.clock_unix_timestamp()); + if !TESTING { + require_gt!(epoch, args.epoch, ErrorCode::EpochNotOver); + if delegated_position.is_claimed(args.epoch)? { + return Err(error!(ErrorCode::InvalidClaimEpoch)); + } + } + + let delegated_vehnt_at_epoch = position.voting_power( + voting_mint_config, + ctx.accounts.sub_dao_epoch_info.start_ts(), + )?; + + msg!("Staked {} veHNT at start of epoch with {} total veHNT delegated to subdao and {} total rewards to subdao", + delegated_vehnt_at_epoch, + ctx.accounts.sub_dao_epoch_info.vehnt_at_epoch_start, + ctx.accounts.sub_dao_epoch_info.hnt_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, + ) + .unwrap() + .checked_div(ctx.accounts.sub_dao_epoch_info.vehnt_at_epoch_start as u128) + .unwrap(), + ) + .unwrap(); + + delegated_position.set_claimed(args.epoch)?; + + let amount_left = ctx.accounts.hnt_delegator_pool.amount; + transfer_v0( + ctx.accounts.transfer_ctx().with_signer(&[&[ + b"sub_dao", + ctx.accounts.sub_dao.dnt_mint.as_ref(), + &[ctx.accounts.sub_dao.bump_seed], + ]]), + // Due to rounding down of vehnt fall rates it's possible the vehnt on the dao does not exactly match the + // vehnt remaining. It could be off by a little bit of dust. + TransferArgsV0 { + amount: std::cmp::min(rewards, amount_left), + }, + )?; + Ok(()) +} 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 51487af06..80b754213 100644 --- a/programs/helium-sub-daos/src/instructions/delegation/delegate_v0.rs +++ b/programs/helium-sub-daos/src/instructions/delegation/delegate_v0.rs @@ -1,3 +1,10 @@ +use anchor_lang::{prelude::*, Discriminator}; +use anchor_spl::token::{Mint, TokenAccount}; +use voter_stake_registry::{ + state::{LockupKind, PositionV0, Registrar}, + VoterStakeRegistry, +}; + use self::borsh::BorshSerialize; use crate::{ create_account::{create_and_serialize_account_signed, AccountMaxSize}, @@ -7,13 +14,6 @@ use crate::{ state::*, utils::*, }; -use anchor_lang::{prelude::*, Discriminator}; -use anchor_spl::token::{Mint, TokenAccount}; - -use voter_stake_registry::{ - state::{LockupKind, PositionV0, Registrar}, - VoterStakeRegistry, -}; #[derive(Accounts)] pub struct DelegateV0<'info> { @@ -215,6 +215,8 @@ 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/delegation/mod.rs b/programs/helium-sub-daos/src/instructions/delegation/mod.rs index ff4ca1044..e93afe2b6 100644 --- a/programs/helium-sub-daos/src/instructions/delegation/mod.rs +++ b/programs/helium-sub-daos/src/instructions/delegation/mod.rs @@ -1,10 +1,12 @@ pub mod claim_rewards_v0; +pub mod claim_rewards_v1; pub mod close_delegation_v0; pub mod delegate_v0; pub mod reset_lockup_v0; pub mod transfer_v0; pub use claim_rewards_v0::*; +pub use claim_rewards_v1::*; pub use close_delegation_v0::*; pub use delegate_v0::*; pub use reset_lockup_v0::*; diff --git a/programs/helium-sub-daos/src/instructions/initialize_hnt_delegator_pool.rs b/programs/helium-sub-daos/src/instructions/initialize_hnt_delegator_pool.rs new file mode 100644 index 000000000..6e1d66e7b --- /dev/null +++ b/programs/helium-sub-daos/src/instructions/initialize_hnt_delegator_pool.rs @@ -0,0 +1,98 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + self, + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount}, +}; +use circuit_breaker::{ + cpi::{accounts::InitializeAccountWindowedBreakerV0, initialize_account_windowed_breaker_v0}, + CircuitBreaker, InitializeAccountWindowedBreakerArgsV0, ThresholdType as CBThresholdType, + WindowedCircuitBreakerConfigV0 as CBWindowedCircuitBreakerConfigV0, +}; +use shared_utils::resize_to_fit; + +use crate::{state::*, sub_dao_seeds, EPOCH_LENGTH}; + +#[derive(Accounts)] +pub struct InitializeHntDelegatorPoolV0<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + mut, + has_one = authority, + has_one = hnt_mint + )] + pub dao: Box>, + pub authority: Signer<'info>, + #[account(mut, has_one = dao)] + pub sub_dao: Box>, + pub hnt_mint: Box>, + /// CHECK: Initialized via cpi + #[account( + mut, + seeds = ["account_windowed_breaker".as_bytes(), delegator_pool.key().as_ref()], + seeds::program = circuit_breaker_program.key(), + bump + )] + pub delegator_pool_circuit_breaker: AccountInfo<'info>, + #[account( + init, + payer = payer, + seeds = ["delegator_pool".as_bytes(), hnt_mint.key().as_ref()], + bump, + token::mint = hnt_mint, + token::authority = sub_dao, + )] + pub delegator_pool: Box>, + + pub system_program: Program<'info, System>, + pub token_program: Program<'info, Token>, + pub circuit_breaker_program: Program<'info, CircuitBreaker>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +impl<'info> InitializeHntDelegatorPoolV0<'info> { + fn initialize_delegator_pool_breaker_ctx( + &self, + ) -> CpiContext<'_, '_, '_, 'info, InitializeAccountWindowedBreakerV0<'info>> { + let cpi_accounts = InitializeAccountWindowedBreakerV0 { + payer: self.payer.to_account_info(), + circuit_breaker: self.delegator_pool_circuit_breaker.to_account_info(), + token_account: self.delegator_pool.to_account_info(), + owner: self.sub_dao.to_account_info(), + token_program: self.token_program.to_account_info(), + system_program: self.system_program.to_account_info(), + }; + CpiContext::new(self.circuit_breaker_program.to_account_info(), cpi_accounts) + } +} + +pub fn handler(ctx: Context) -> Result<()> { + let signer_seeds: &[&[&[u8]]] = &[sub_dao_seeds!(ctx.accounts.sub_dao)]; + + initialize_account_windowed_breaker_v0( + ctx + .accounts + .initialize_delegator_pool_breaker_ctx() + .with_signer(signer_seeds), + InitializeAccountWindowedBreakerArgsV0 { + authority: ctx.accounts.sub_dao.authority, + config: CBWindowedCircuitBreakerConfigV0 { + window_size_seconds: u64::try_from(EPOCH_LENGTH).unwrap(), + threshold_type: CBThresholdType::Absolute, + // Roughly 25% of the daily emissions + threshold: ctx.accounts.dao.emission_schedule[0].emissions_per_epoch / 25, + }, + owner: ctx.accounts.sub_dao.key(), + }, + )?; + ctx.accounts.sub_dao.hnt_delegator_pool = ctx.accounts.delegator_pool.key(); + + resize_to_fit( + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + &ctx.accounts.sub_dao, + )?; + + Ok(()) +} diff --git a/programs/helium-sub-daos/src/instructions/initialize_sub_dao_v0.rs b/programs/helium-sub-daos/src/instructions/initialize_sub_dao_v0.rs index ce8705ca6..3d19d451f 100644 --- a/programs/helium-sub-daos/src/instructions/initialize_sub_dao_v0.rs +++ b/programs/helium-sub-daos/src/instructions/initialize_sub_dao_v0.rs @@ -280,7 +280,8 @@ pub fn handler(ctx: Context, args: InitializeSubDaoArgsV0) - vehnt_delegated: 0, vehnt_last_calculated_ts: Clock::get()?.unix_timestamp, vehnt_fall_rate: 0, - delegator_pool: ctx.accounts.delegator_pool.key(), + delegator_pool: Pubkey::default(), + hnt_delegator_pool: ctx.accounts.delegator_pool.key(), delegator_rewards_percent: args.delegator_rewards_percent, onboarding_data_only_dc_fee: args.onboarding_data_only_dc_fee, active_device_authority: args.active_device_authority, 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 71d618273..f0f6452f3 100644 --- a/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs +++ b/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs @@ -98,10 +98,10 @@ impl<'info> IssueRewardsV0<'info> { pub fn mint_delegation_rewards_ctx(&self) -> CpiContext<'_, '_, '_, 'info, MintV0<'info>> { let cpi_accounts = MintV0 { - mint: self.dnt_mint.to_account_info(), + mint: self.hnt_mint.to_account_info(), to: self.delegator_pool.to_account_info(), - mint_authority: self.sub_dao.to_account_info(), - circuit_breaker: self.dnt_circuit_breaker.to_account_info(), + mint_authority: self.dao.to_account_info(), + circuit_breaker: self.hnt_circuit_breaker.to_account_info(), token_program: self.token_program.to_account_info(), }; @@ -139,7 +139,7 @@ pub fn handler(ctx: Context, args: IssueRewardsArgsV0) -> Result .checked_div(&total_utility_score) .or_arith_error()?; let total_emissions = ctx.accounts.dao_epoch_info.total_rewards; - let percent = ctx + let hst_percent = ctx .accounts .dao .hst_emission_schedule @@ -147,62 +147,26 @@ pub fn handler(ctx: Context, args: IssueRewardsArgsV0) -> Result .unwrap(); // Subdaos get the remainder after hst let emissions = 100_u64 - .checked_sub(percent.into()) + .checked_sub(hst_percent.into()) .unwrap() .checked_mul(total_emissions) .unwrap() .checked_div(100) .unwrap(); - let hst_emissions = total_emissions.checked_sub(emissions).unwrap(); let total_rewards = PreciseNumber::new(emissions.into()).or_arith_error()?; let rewards_prec = percent_share.checked_mul(&total_rewards).or_arith_error()?; - let mut rewards_amount: u64 = rewards_prec + let rewards_amount: u64 = rewards_prec .floor() // Ensure we never overspend the defined rewards .or_arith_error()? .to_imprecise() .ok_or_else(|| error!(ErrorCode::ArithmeticError))? .try_into() .unwrap(); - if ctx.accounts.dnt_mint.key() - == Pubkey::from_str("mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6").unwrap() - { - rewards_amount += hst_emissions; - ctx.accounts.dao_epoch_info.done_issuing_hst_pool = true; - } - let total_emissions = ctx - .accounts - .sub_dao - .emission_schedule - .get_emissions_at(end_of_epoch_ts) - .unwrap(); - let delegators_present = ctx.accounts.sub_dao_epoch_info.vehnt_at_epoch_start > 0; let max_percent = 100_u64.checked_mul(10_0000000).unwrap(); - let dnt_emissions = (total_emissions as u128) - .checked_mul(u128::from( - max_percent - - ctx.accounts.sub_dao.delegator_rewards_percent - - ctx.accounts.sub_dao.voting_rewards_percent, - )) - .unwrap() - .checked_div(max_percent as u128) // 100% with 2 decimals accuracy - .unwrap() - .try_into() - .unwrap(); - msg!("Minting {} DNT eissions to treasury", dnt_emissions); - mint_v0( - ctx.accounts.mint_dnt_emissions_ctx().with_signer(&[&[ - b"sub_dao", - ctx.accounts.dnt_mint.key().as_ref(), - &[ctx.accounts.sub_dao.bump_seed], - ]]), - MintArgsV0 { - amount: dnt_emissions, // send some dnt emissions to treasury - }, - )?; let delegation_rewards_amount = if delegators_present { - (total_emissions as u128) + (rewards_amount as u128) .checked_mul(u128::from(ctx.accounts.sub_dao.delegator_rewards_percent)) .unwrap() .checked_div(max_percent as u128) // 100% with 2 decimals accuracy @@ -218,9 +182,9 @@ pub fn handler(ctx: Context, args: IssueRewardsArgsV0) -> Result if delegation_rewards_amount > 0 { mint_v0( ctx.accounts.mint_delegation_rewards_ctx().with_signer(&[&[ - b"sub_dao", - ctx.accounts.dnt_mint.key().as_ref(), - &[ctx.accounts.sub_dao.bump_seed], + b"dao", + ctx.accounts.hnt_mint.key().as_ref(), + &[ctx.accounts.dao.bump_seed], ]]), MintArgsV0 { amount: delegation_rewards_amount, // send some dnt emissions to delegation pool @@ -228,7 +192,8 @@ pub fn handler(ctx: Context, args: IssueRewardsArgsV0) -> Result )?; } - msg!("Minting {} to treasury", rewards_amount); + let escrow_amount = rewards_amount - delegation_rewards_amount; + msg!("Minting {} to treasury", escrow_amount); mint_v0( ctx.accounts.mint_treasury_emissions_ctx().with_signer(&[&[ b"dao", @@ -236,13 +201,17 @@ pub fn handler(ctx: Context, args: IssueRewardsArgsV0) -> Result &[ctx.accounts.dao.bump_seed], ]]), MintArgsV0 { - amount: rewards_amount, + amount: escrow_amount, }, )?; + 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.delegation_rewards_issued = delegation_rewards_amount; + ctx + .accounts + .sub_dao_epoch_info + .hnt_delegation_rewards_issued = delegation_rewards_amount; 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/mod.rs b/programs/helium-sub-daos/src/instructions/mod.rs index eef24b5a5..ed9b912b2 100644 --- a/programs/helium-sub-daos/src/instructions/mod.rs +++ b/programs/helium-sub-daos/src/instructions/mod.rs @@ -3,6 +3,7 @@ pub mod admin_set_dc_onboarding_fees_paid_epoch_info; pub mod calculate_utility_score_v0; pub mod delegation; pub mod initialize_dao_v0; +pub mod initialize_hnt_delegator_pool; pub mod initialize_sub_dao_v0; pub mod issue_hst_pool_v0; pub mod issue_rewards_v0; @@ -19,6 +20,7 @@ pub use admin_set_dc_onboarding_fees_paid_epoch_info::*; pub use calculate_utility_score_v0::*; pub use delegation::*; pub use initialize_dao_v0::*; +pub use initialize_hnt_delegator_pool::*; pub use initialize_sub_dao_v0::*; pub use issue_hst_pool_v0::*; pub use issue_rewards_v0::*; diff --git a/programs/helium-sub-daos/src/state.rs b/programs/helium-sub-daos/src/state.rs index 633294701..8c081ad8f 100644 --- a/programs/helium-sub-daos/src/state.rs +++ b/programs/helium-sub-daos/src/state.rs @@ -194,6 +194,10 @@ 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, } impl SubDaoEpochInfoV0 { @@ -216,6 +220,7 @@ pub struct SubDaoV0 { pub dnt_mint: Pubkey, // Mint of the subdao token pub treasury: Pubkey, // Treasury of HNT pub rewards_escrow: Pubkey, // Escrow account for DNT rewards + /// DEPRECATED: use hnt_delegator_pool instead. But some people still need to claim old DNT rewards pub delegator_pool: Pubkey, // Pool of DNT tokens which veHNT delegators can claim from pub vehnt_delegated: u128, // the total amount of vehnt delegated to this subdao, with 12 decimals of extra precision pub vehnt_last_calculated_ts: i64, @@ -233,6 +238,7 @@ pub struct SubDaoV0 { pub active_device_authority: Pubkey, // authority that can mark hotspots as active/inactive pub voting_rewards_percent: u64, // number between 0 - (100_u64 * 100_000_000). The % of DNT rewards voting rewards receive with 8 decimal places of accuracy pub vetoken_tracker: Pubkey, // the vetoken tracker for subnetwork voting rewards + pub hnt_delegator_pool: Pubkey, } #[macro_export]