From e54921a7b26caf824a0ec4bb3c975615019d9565 Mon Sep 17 00:00:00 2001 From: Chewing Glass Date: Fri, 8 Dec 2023 18:26:35 -0600 Subject: [PATCH] feat(#494): Implement HIP-96 wifi onboarding fees --- Anchor.toml | 3 + Cargo.lock | 1 + Cargo.toml | 1 + .../helium-admin-cli/src/create-subdao.ts | 13 ++- .../src/update-rewardable-entity-config.ts | 20 +++-- .../src/resolvers.ts | 13 ++- programs/helium-entity-manager/Cargo.toml | 3 +- programs/helium-entity-manager/src/error.rs | 2 + .../instructions/onboard_mobile_hotspot_v0.rs | 37 ++++++++- programs/helium-entity-manager/src/state.rs | 50 ++++++++++-- tests/helium-entity-manager.ts | 79 ++++++++++++------- 11 files changed, 173 insertions(+), 49 deletions(-) diff --git a/Anchor.toml b/Anchor.toml index b8f38edb5..41703da38 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -74,6 +74,9 @@ address = "propFYxqmVcufMhk5esNMrexq2ogHbbC2kP9PU1qxKs" # Proposal [[test.validator.clone]] address = "66t3XARU6Ja3zj91gDZ2KoNLJHEMTYPSKqJWYb6PJJBA" # Proposal IDL +[[test.validator.clone]] +address = "moraMdsjyPFz8Lp1RJGoW4bQriSF5mHE7Evxt7hytSF" # Mobile price oracle + # Pyth price oracle [[test.validator.clone]] address = "7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm" diff --git a/Cargo.lock b/Cargo.lock index 4323849d6..66b4322c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1950,6 +1950,7 @@ dependencies = [ "default-env", "helium-sub-daos", "mpl-token-metadata", + "price-oracle", "rewards-burn", "shared-utils", "solana-security-txt", diff --git a/Cargo.toml b/Cargo.toml index 322f6e586..e5d581292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,4 @@ circuit-breaker = { path = "./programs/circuit-breaker", features = ["cpi"] } helium-sub-daos = { path = "./programs/helium-sub-daos", features = ["cpi"] } helium-entity-manager = { path = "./programs/helium-entity-manager", features = ["cpi"] } rewards-burn = { path = "./programs/rewards-burn", features = ["cpi"] } +price-oracle = { path = "./programs/price-oracle", features = ["cpi"] } \ No newline at end of file diff --git a/packages/helium-admin-cli/src/create-subdao.ts b/packages/helium-admin-cli/src/create-subdao.ts index 69012927c..72edca17f 100644 --- a/packages/helium-admin-cli/src/create-subdao.ts +++ b/packages/helium-admin-cli/src/create-subdao.ts @@ -51,6 +51,7 @@ import { parseEmissionsSchedule, sendInstructionsOrSquads, } from './utils'; +import { BN } from 'bn.js'; const SECS_PER_DAY = 86400; const SECS_PER_YEAR = 365 * SECS_PER_DAY; @@ -539,22 +540,28 @@ export async function run(args: any = process.argv) { }; } else { settings = { - mobileConfigV1: { + mobileConfigV2: { feesByDevice: [ { deviceType: { cbrs: {} }, dcOnboardingFee: toBN(40, 5), locationStakingFee: toBN(10, 5), + mobileOnboardingFeeUsd: toBN(0, 6), + reserved: new Array(8).fill(new BN(0)), }, { deviceType: { wifiIndoor: {} }, - dcOnboardingFee: toBN(0, 5), + dcOnboardingFee: toBN(10, 5), locationStakingFee: toBN(0, 5), + mobileOnboardingFeeUsd: toBN(10, 6), + reserved: new Array(8).fill(new BN(0)), }, { deviceType: { wifiOutdoor: {} }, - dcOnboardingFee: toBN(0, 5), + dcOnboardingFee: toBN(10, 5), locationStakingFee: toBN(0, 5), + mobileOnboardingFeeUsd: toBN(20, 6), + reserved: new Array(8).fill(new BN(0)), }, ], }, diff --git a/packages/helium-admin-cli/src/update-rewardable-entity-config.ts b/packages/helium-admin-cli/src/update-rewardable-entity-config.ts index 8326f7669..ee7cbfa24 100644 --- a/packages/helium-admin-cli/src/update-rewardable-entity-config.ts +++ b/packages/helium-admin-cli/src/update-rewardable-entity-config.ts @@ -116,22 +116,28 @@ export async function run(args: any = process.argv) { }; } else { settings = { - mobileConfigV1: { + mobileConfigV2: { feesByDevice: [ { deviceType: { cbrs: {} }, - dcOnboardingFee: new BN(argv.cbrsDcOnboardingFee!), - locationStakingFee: new BN(argv.cbrsDcLocationStakingFee!), + dcOnboardingFee: toBN(40, 5), + locationStakingFee: toBN(10, 5), + mobileOnboardingFeeUsd: toBN(0, 6), + reserved: new Array(8).fill(new BN(0)), }, { deviceType: { wifiIndoor: {} }, - dcOnboardingFee: new BN(argv.wifiDcOnboardingFee!), - locationStakingFee: new BN(argv.wifiDcLocationStakingFee!), + dcOnboardingFee: toBN(10, 5), + locationStakingFee: toBN(0, 5), + mobileOnboardingFeeUsd: toBN(10, 6), + reserved: new Array(8).fill(new BN(0)), }, { deviceType: { wifiOutdoor: {} }, - dcOnboardingFee: new BN(argv.wifiDcOnboardingFee!), - locationStakingFee: new BN(argv.wifiDcLocationStakingFee!), + dcOnboardingFee: toBN(10, 5), + locationStakingFee: toBN(0, 5), + mobileOnboardingFeeUsd: toBN(20, 6), + reserved: new Array(8).fill(new BN(0)), }, ], }, diff --git a/packages/helium-entity-manager-sdk/src/resolvers.ts b/packages/helium-entity-manager-sdk/src/resolvers.ts index 5071920a0..e7bd8a99e 100644 --- a/packages/helium-entity-manager-sdk/src/resolvers.ts +++ b/packages/helium-entity-manager-sdk/src/resolvers.ts @@ -33,6 +33,11 @@ export const heliumEntityManagerResolvers = combineResolvers( mint: "dntMint", owner: "maker", }), + resolveIndividual(async ({ path }) => { + if (path[path.length - 1] == "dntPrice") { + return new PublicKey("moraMdsjyPFz8Lp1RJGoW4bQriSF5mHE7Evxt7hytSF"); + } + }), resolveIndividual(async ({ path, args, accounts, provider }) => { if (path[path.length - 1] == "programApproval" && accounts.dao) { let programId = args[args.length - 1] && args[args.length - 1].programId; @@ -167,7 +172,13 @@ export const heliumEntityManagerResolvers = combineResolvers( instruction: "issueBurnEntityV0", mint: "mint", account: "recipientAccount", - owner: "recipient" + owner: "recipient", + }), + ataResolver({ + instruction: "onboardMobileHotspotV0", + mint: "dntMint", + account: "dntBurner", + owner: "payer", }), subDaoEpochInfoResolver ); diff --git a/programs/helium-entity-manager/Cargo.toml b/programs/helium-entity-manager/Cargo.toml index c609bb436..52939d4f8 100644 --- a/programs/helium-entity-manager/Cargo.toml +++ b/programs/helium-entity-manager/Cargo.toml @@ -34,4 +34,5 @@ data-credits = { path = "../data-credits", features = ["cpi"] } helium-sub-daos = { workspace = true } solana-security-txt = { workspace = true } default-env = { workspace = true } -rewards-burn = { workspace = true } \ No newline at end of file +rewards-burn = { workspace = true } +price-oracle = { workspace = true } \ No newline at end of file diff --git a/programs/helium-entity-manager/src/error.rs b/programs/helium-entity-manager/src/error.rs index 048deed2a..9c57723f4 100644 --- a/programs/helium-entity-manager/src/error.rs +++ b/programs/helium-entity-manager/src/error.rs @@ -45,4 +45,6 @@ pub enum ErrorCode { InvalidSymbol, #[msg("Mobile device type not found")] InvalidDeviceType, + #[msg("No mobile oracle price")] + NoOraclePrice, } diff --git a/programs/helium-entity-manager/src/instructions/onboard_mobile_hotspot_v0.rs b/programs/helium-entity-manager/src/instructions/onboard_mobile_hotspot_v0.rs index 6693a5eac..e54057be3 100644 --- a/programs/helium-entity-manager/src/instructions/onboard_mobile_hotspot_v0.rs +++ b/programs/helium-entity-manager/src/instructions/onboard_mobile_hotspot_v0.rs @@ -1,10 +1,11 @@ use crate::error::ErrorCode; use crate::state::*; use anchor_lang::{prelude::*, solana_program::hash::hash}; +use std::str::FromStr; use anchor_spl::{ associated_token::AssociatedToken, - token::{Mint, Token}, + token::{burn, Burn, Mint, Token, TokenAccount}, }; use data_credits::{ cpi::{ @@ -22,6 +23,7 @@ use helium_sub_daos::{ use account_compression_cpi::program::SplAccountCompression; use bubblegum_cpi::get_asset_id; +use price_oracle::{calculate_current_price, PriceOracleV0}; use shared_utils::*; #[derive(AnchorSerialize, AnchorDeserialize, Clone)] @@ -61,6 +63,12 @@ pub struct OnboardMobileHotspotV0<'info> { /// CHECK: Only loaded if location is being asserted #[account(mut)] pub dc_burner: UncheckedAccount<'info>, + #[account( + mut, + associated_token::authority = payer, + associated_token::mint = dnt_mint + )] + pub dnt_burner: Account<'info, TokenAccount>, #[account( has_one = sub_dao, @@ -92,10 +100,17 @@ pub struct OnboardMobileHotspotV0<'info> { #[account( mut, has_one = dao, + has_one = dnt_mint, )] pub sub_dao: Box>, #[account(mut)] pub dc_mint: Box>, + #[account(mut)] + pub dnt_mint: Box>, + #[account( + address = Pubkey::from_str("moraMdsjyPFz8Lp1RJGoW4bQriSF5mHE7Evxt7hytSF").unwrap() + )] + pub dnt_price: Box>, #[account( seeds=[ @@ -132,6 +147,16 @@ impl<'info> OnboardMobileHotspotV0<'info> { CpiContext::new(self.data_credits_program.to_account_info(), cpi_accounts) } + + pub fn mobile_burn_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Burn<'info>> { + let cpi_accounts = Burn { + mint: self.dnt_mint.to_account_info(), + from: self.dnt_burner.to_account_info(), + authority: self.payer.to_account_info(), + }; + + CpiContext::new(self.token_program.to_account_info(), cpi_accounts) + } } pub fn handler<'info>( @@ -214,5 +239,15 @@ pub fn handler<'info>( BurnWithoutTrackingArgsV0 { amount: dc_fee }, )?; + // Burn the mobile tokens + let dnt_fee = fees.mobile_onboarding_fee_usd; + let mobile_price = calculate_current_price( + &ctx.accounts.dnt_price.oracles, + Clock::get()?.unix_timestamp, + ) + .ok_or_else(|| error!(ErrorCode::NoOraclePrice))?; + let mobile_fee = dnt_fee.checked_div(mobile_price).unwrap(); + burn(ctx.accounts.mobile_burn_ctx(), mobile_fee)?; + Ok(()) } diff --git a/programs/helium-entity-manager/src/state.rs b/programs/helium-entity-manager/src/state.rs index 6eb5194a5..25b337130 100644 --- a/programs/helium-entity-manager/src/state.rs +++ b/programs/helium-entity-manager/src/state.rs @@ -27,6 +27,28 @@ pub struct DeviceFeesV0 { pub location_staking_fee: u64, } +impl From for DeviceFeesV1 { + fn from(value: DeviceFeesV0) -> Self { + DeviceFeesV1 { + device_type: value.device_type, + dc_onboarding_fee: value.dc_onboarding_fee, + location_staking_fee: value.location_staking_fee, + mobile_onboarding_fee_usd: 0, + reserved: [0_u64; 8], + } + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy)] +pub struct DeviceFeesV1 { + pub device_type: MobileDeviceTypeV0, + pub dc_onboarding_fee: u64, + pub location_staking_fee: u64, + // mobile onboarding fee in usd with 6 decimals of precision + pub mobile_onboarding_fee_usd: u64, + pub reserved: [u64; 8], +} + #[derive(AnchorSerialize, AnchorDeserialize, Clone)] #[allow(deprecated)] pub enum ConfigSettingsV0 { @@ -36,29 +58,40 @@ pub enum ConfigSettingsV0 { full_location_staking_fee: u64, dataonly_location_staking_fee: u64, }, - // Deprecated, use MobileConfigV1 + // Deprecated, use MobileConfigV2 MobileConfig { full_location_staking_fee: u64, dataonly_location_staking_fee: u64, }, + // Deprecated, use MobileConfigV2 MobileConfigV1 { fees_by_device: Vec, }, + MobileConfigV2 { + fees_by_device: Vec, + }, } impl ConfigSettingsV0 { #[allow(deprecated)] - pub fn mobile_device_fees(&self, device: MobileDeviceTypeV0) -> Option { + pub fn mobile_device_fees(&self, device: MobileDeviceTypeV0) -> Option { match self { ConfigSettingsV0::MobileConfig { full_location_staking_fee, .. - } => Some(DeviceFeesV0 { - device_type: MobileDeviceTypeV0::Cbrs, - dc_onboarding_fee: 4000000_u64, - location_staking_fee: *full_location_staking_fee, - }), + } => Some( + DeviceFeesV0 { + device_type: MobileDeviceTypeV0::Cbrs, + dc_onboarding_fee: 4000000_u64, + location_staking_fee: *full_location_staking_fee, + } + .into(), + ), ConfigSettingsV0::MobileConfigV1 { fees_by_device, .. } => fees_by_device + .iter() + .find(|d| d.device_type == device) + .map(|i| (*i).into()), + ConfigSettingsV0::MobileConfigV2 { fees_by_device, .. } => fees_by_device .iter() .find(|d| d.device_type == device) .copied(), @@ -79,7 +112,8 @@ impl ConfigSettingsV0 { } } pub fn is_mobile(&self) -> bool { - matches!(self, ConfigSettingsV0::MobileConfigV1 { .. }) + matches!(self, ConfigSettingsV0::MobileConfigV2 { .. }) + || matches!(self, ConfigSettingsV0::MobileConfigV1 { .. }) || matches!(self, ConfigSettingsV0::MobileConfig { .. }) } diff --git a/tests/helium-entity-manager.ts b/tests/helium-entity-manager.ts index bbd614575..982c51deb 100644 --- a/tests/helium-entity-manager.ts +++ b/tests/helium-entity-manager.ts @@ -97,8 +97,8 @@ describe("helium-entity-manager", () => { burnProram = await initBurn( provider, anchor.workspace.RewardsBurn.programId, - anchor.workspace.RewardsBurn.idl, - ) + anchor.workspace.RewardsBurn.idl + ); hsdProgram = await initHeliumSubDaos( provider, @@ -131,7 +131,8 @@ describe("helium-entity-manager", () => { authority: me, dao, activeDeviceAuthority: activeDeviceAuthority.publicKey, - numTokens: MAKER_STAKING_FEE.mul(new BN(2)), + // Add some padding for onboards + numTokens: MAKER_STAKING_FEE.mul(new BN(2)).add(new BN(10000000000)), })); }); @@ -160,25 +161,35 @@ describe("helium-entity-manager", () => { .preInstructions(await createMintInstructions(provider, 0, me, me, mint)) .accounts({ dao, - mint: mint.publicKey + mint: mint.publicKey, }) .signers([mint]) .rpc({ skipPreflight: true }); - const addr = getAssociatedTokenAddressSync(mint.publicKey, burnKey()[0], true); + const addr = getAssociatedTokenAddressSync( + mint.publicKey, + burnKey()[0], + true + ); const balance = await provider.connection.getTokenAccountBalance(addr); expect(balance.value.uiAmount).to.eq(1); - const tokenMint = await createMint(provider, 2, me, me) - const burnAta = await createAtaAndMint(provider, tokenMint, new BN(1000), burnKey()[0]) - await burnProram.methods.burnV0() - .accounts({ - mint: tokenMint - }) - .rpc({ skipPreflight: true }) + const tokenMint = await createMint(provider, 2, me, me); + const burnAta = await createAtaAndMint( + provider, + tokenMint, + new BN(1000), + burnKey()[0] + ); + await burnProram.methods + .burnV0() + .accounts({ + mint: tokenMint, + }) + .rpc({ skipPreflight: true }); - const postBalance = (await getAccount(provider.connection, burnAta)).amount - expect(postBalance).to.eq(BigInt(0)) + const postBalance = (await getAccount(provider.connection, burnAta)).amount; + expect(postBalance).to.eq(BigInt(0)); }); it("initializes a rewardable entity config", async () => { @@ -548,22 +559,28 @@ describe("helium-entity-manager", () => { hemProgram, subDao, { - mobileConfigV1: { + mobileConfigV2: { feesByDevice: [ { deviceType: { cbrs: {} }, dcOnboardingFee: toBN(0, 5), locationStakingFee: toBN(10, 5), + mobileOnboardingFeeUsd: toBN(0, 6), + reserved: new Array(8).fill(new BN(0)), }, { deviceType: { wifiIndoor: {} }, - dcOnboardingFee: toBN(0, 5), + dcOnboardingFee: toBN(10, 5), locationStakingFee: toBN(0, 5), + mobileOnboardingFeeUsd: toBN(10, 6), + reserved: new Array(8).fill(new BN(0)), }, { deviceType: { wifiOutdoor: {} }, - dcOnboardingFee: toBN(0, 5), + dcOnboardingFee: toBN(10, 5), locationStakingFee: toBN(0, 5), + mobileOnboardingFeeUsd: toBN(20, 6), + reserved: new Array(8).fill(new BN(0)), }, ], }, @@ -680,6 +697,7 @@ describe("helium-entity-manager", () => { rewardableEntityConfig, getAssetFn, getAssetProofFn, + deviceType: "wifiIndoor", }) ).signers([makerKeypair, hotspotOwner]); @@ -691,9 +709,7 @@ describe("helium-entity-manager", () => { ); expect(Boolean(mobileInfoAcc)).to.be.true; const subDaoAcc = await hsdProgram.account.subDaoV0.fetch(subDao); - expect(subDaoAcc.dcOnboardingFeesPaid.toNumber()).to.be.eq( - 0 - ); + expect(subDaoAcc.dcOnboardingFeesPaid.toNumber()).to.be.eq(1000000); }); describe("with hotspot", () => { @@ -783,44 +799,51 @@ describe("helium-entity-manager", () => { await hemProgram.methods .updateRewardableEntityConfigV0({ settings: { - mobileConfigV1: { + mobileConfigV2: { feesByDevice: [ { deviceType: { cbrs: {} }, dcOnboardingFee: toBN(40, 5), locationStakingFee: toBN(10, 5), + mobileOnboardingFeeUsd: toBN(0, 6), + reserved: new Array(8).fill(new BN(0)), }, { deviceType: { wifiIndoor: {} }, - dcOnboardingFee: toBN(0, 5), + dcOnboardingFee: toBN(10, 5), locationStakingFee: toBN(0, 5), + mobileOnboardingFeeUsd: toBN(10, 6), + reserved: new Array(8).fill(new BN(0)), }, { deviceType: { wifiOutdoor: {} }, - dcOnboardingFee: toBN(0, 5), + dcOnboardingFee: toBN(10, 5), locationStakingFee: toBN(0, 5), + mobileOnboardingFeeUsd: toBN(20, 6), + reserved: new Array(8).fill(new BN(0)), }, ], }, }, newAuthority: null, - stakingRequirement: MAKER_STAKING_FEE + stakingRequirement: MAKER_STAKING_FEE, }) .accounts({ rewardableEntityConfig }) .rpc({ skipPreflight: true }); - const subDaoAcc = await hsdProgram.account.subDaoV0.fetch(subDao); + await method.rpc({ skipPreflight: true }); + const postBalance = await provider.connection.getTokenAccountBalance( ata ); expect(postBalance.value.uiAmount).to.be.eq( - preBalance.value.uiAmount! - subDaoAcc.onboardingDcFee.toNumber() + preBalance.value.uiAmount! - toBN(40, 5).toNumber() ); const infoAcc = await hemProgram.account.mobileHotspotInfoV0.fetch( infoKey! ); expect(infoAcc.dcOnboardingFeePaid.toNumber()).to.eq( - subDaoAcc.onboardingDcFee.toNumber() + toBN(40, 5).toNumber() ); }); @@ -983,7 +1006,7 @@ describe("helium-entity-manager", () => { .updateRewardableEntityConfigV0({ newAuthority: PublicKey.default, settings: null, - stakingRequirement: null + stakingRequirement: null, }) .accounts({ rewardableEntityConfig,