diff --git a/Anchor.toml b/Anchor.toml index 349ce0771..dd3aa795f 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -55,6 +55,9 @@ url = "https://api.mainnet-beta.solana.com" [[test.validator.clone]] address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" # token-metadata +[[test.validator.clone]] +address = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" # associated-token-program + [[test.validator.clone]] address = "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY" # bubblegum diff --git a/Cargo.lock b/Cargo.lock index 381ad39ee..33b69b37e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1971,6 +1971,8 @@ dependencies = [ "circuit-breaker", "default-env", "mpl-token-metadata", + "nft-proxy", + "proposal", "rust_decimal", "shared-utils", "solana-security-txt", diff --git a/packages/crons/src/end-epoch.ts b/packages/crons/src/end-epoch.ts index 725b15aea..d69fb416f 100644 --- a/packages/crons/src/end-epoch.ts +++ b/packages/crons/src/end-epoch.ts @@ -181,25 +181,6 @@ async function getSolanaUnixTimestamp(connection: Connection): Promise { } } - if (!daoEpochInfo?.doneIssuingHstPool) { - try { - await sendInstructionsWithPriorityFee( - provider, - [ - await heliumSubDaosProgram.methods - .issueHstPoolV0({ epoch }) - .accounts({ dao }) - .instruction(), - ], - { - basePriorityFee: BASE_PRIORITY_FEE, - } - ); - } catch (err: any) { - errors.push(`Failed to issue hst pool: ${err}`); - } - } - targetTs = targetTs.add(new BN(EPOCH_LENGTH)); } @@ -247,76 +228,78 @@ async function getSolanaUnixTimestamp(connection: Connection): Promise { const hemProgram = await initHem(provider); const lazyProgram = await initLazy(provider); const rewardsOracleProgram = await initRewards(provider); - const [lazyDistributor] = lazyDistributorKey(iotMint); - const [keyToAsset] = keyToAssetKey(dao, IOT_OPERATIONS_FUND, "utf8"); - const assetId = (await hemProgram.account.keyToAssetV0.fetch(keyToAsset)) - .asset; - - const [recipient] = recipientKey(lazyDistributor, assetId); - if (!(await provider.connection.getAccountInfo(recipient))) { - const method = lazyProgram.methods.initializeRecipientV0().accounts({ - lazyDistributor, - mint: assetId, - }); + for (const token of [IOT_MINT, HNT_MINT]) { + const [lazyDistributor] = lazyDistributorKey(token); + const [keyToAsset] = keyToAssetKey(dao, IOT_OPERATIONS_FUND, "utf8"); + const assetId = (await hemProgram.account.keyToAssetV0.fetch(keyToAsset)) + .asset; + + const [recipient] = recipientKey(lazyDistributor, assetId); + if (!(await provider.connection.getAccountInfo(recipient))) { + const method = lazyProgram.methods.initializeRecipientV0().accounts({ + lazyDistributor, + mint: assetId, + }); + + await sendInstructionsWithPriorityFee( + provider, + [await method.instruction()], + { + basePriorityFee: BASE_PRIORITY_FEE, + } + ); + } - await sendInstructionsWithPriorityFee( - provider, - [await method.instruction()], - { - basePriorityFee: BASE_PRIORITY_FEE, - } + const rewards = await client.getCurrentRewards( + lazyProgram, + lazyDistributor, + assetId ); - } - - const rewards = await client.getCurrentRewards( - lazyProgram, - lazyDistributor, - assetId - ); - const pending = await client.getPendingRewards( - lazyProgram, - lazyDistributor, - daoKey(HNT_MINT)[0], - [IOT_OPERATIONS_FUND], - "utf8" - ); + const pending = await client.getPendingRewards( + lazyProgram, + lazyDistributor, + daoKey(HNT_MINT)[0], + [IOT_OPERATIONS_FUND], + "utf8" + ); - // Avoid claiming too much and tripping the breaker - if (new BN(pending[IOT_OPERATIONS_FUND]).gt(MAX_CLAIM_AMOUNT)) { - rewards[0].currentRewards = new BN(rewards[0].currentRewards) - .sub(new BN(pending[IOT_OPERATIONS_FUND])) - .add(MAX_CLAIM_AMOUNT) - .toString(); - } + // Avoid claiming too much and tripping the breaker + if (new BN(pending[IOT_OPERATIONS_FUND]).gt(MAX_CLAIM_AMOUNT)) { + rewards[0].currentRewards = new BN(rewards[0].currentRewards) + .sub(new BN(pending[IOT_OPERATIONS_FUND])) + .add(MAX_CLAIM_AMOUNT) + .toString(); + } - const tx = await client.formTransaction({ - program: lazyProgram, - rewardsOracleProgram: rewardsOracleProgram, - provider, - rewards, - asset: assetId, - lazyDistributor, - }); + const tx = await client.formTransaction({ + program: lazyProgram, + rewardsOracleProgram: rewardsOracleProgram, + provider, + rewards, + asset: assetId, + lazyDistributor, + }); - const signed = await provider.wallet.signTransaction(tx); + const signed = await provider.wallet.signTransaction(tx); - try { - await sendAndConfirmWithRetry( - provider.connection, - Buffer.from(signed.serialize()), - { skipPreflight: true }, - "confirmed" - ); - } catch (err: any) { - errors.push(`Failed to distribute iot op funds: ${err}`); + try { + await sendAndConfirmWithRetry( + provider.connection, + Buffer.from(signed.serialize()), + { skipPreflight: true }, + "confirmed" + ); + } catch (err: any) { + errors.push(`Failed to distribute iot op funds: ${err}`); + } } // Only do this if that feature has been deployed if (hemProgram.methods.issueNotEmittedEntityV0) { console.log("Issuing no_emit"); const noEmitProgram = await initBurn(provider); - const tokens = [MOBILE_MINT, IOT_MINT]; + const tokens = [MOBILE_MINT, IOT_MINT, HNT_MINT]; for (const token of tokens) { const [lazyDistributor] = lazyDistributorKey(token); const notEmittedEntityKta = keyToAssetKey(dao, NOT_EMITTED, "utf-8")[0]; diff --git a/packages/crons/yarn.deploy.lock b/packages/crons/yarn.deploy.lock index 9cbfcf379..2b1664dcb 100644 --- a/packages/crons/yarn.deploy.lock +++ b/packages/crons/yarn.deploy.lock @@ -316,6 +316,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/data-credits-sdk/yarn.deploy.lock b/packages/data-credits-sdk/yarn.deploy.lock index 89dfcd2e7..169b25c00 100644 --- a/packages/data-credits-sdk/yarn.deploy.lock +++ b/packages/data-credits-sdk/yarn.deploy.lock @@ -160,6 +160,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/distributor-oracle/src/client.ts b/packages/distributor-oracle/src/client.ts index dea80c542..54cce98c4 100644 --- a/packages/distributor-oracle/src/client.ts +++ b/packages/distributor-oracle/src/client.ts @@ -346,18 +346,22 @@ export async function formBulkTransactions({ assetEndpoint || provider.connection.rpcEndpoint, assets ); + const willPay = skipOracleSign || (await axios.get( + `${lazyDistributorAcc.oracles[0].url}/will-pay-recipient` + )).data.willPay; let ixsPerAsset = await Promise.all( recipientAccs.map(async (recipientAcc, idx) => { if (!recipientAcc) { return [ - await ( + await( await initializeCompressionRecipient({ program: lazyDistributorProgram, assetId: assets![idx], lazyDistributor, assetEndpoint, owner: wallet, - payer, + // Temporarily set oracle as the payer to subsidize new HNT wallets. + payer: willPay ? lazyDistributorAcc.oracles[0].oracle : payer, getAssetFn: () => Promise.resolve(compressionAssetAccs![idx]), // cache result so we don't hit again getAssetProofFn: assetProofsById ? () => @@ -591,6 +595,9 @@ export async function formTransaction({ const recipientAcc = await lazyDistributorProgram.account.recipientV0.fetchNullable(recipient); if (!recipientAcc) { + const willPay = ( + await axios.get(`${lazyDistributorAcc.oracles[0].url}/will-pay-recipient`) + ).data.willPay; let initRecipientIx; if (assetAcc.compression.compressed) { initRecipientIx = await ( @@ -600,7 +607,7 @@ export async function formTransaction({ lazyDistributor, assetEndpoint, owner: wallet, - payer, + payer: willPay ? lazyDistributorAcc.oracles[0].oracle : payer, getAssetFn: () => Promise.resolve(assetAcc), // cache result so we don't hit again getAssetProofFn, }) diff --git a/packages/distributor-oracle/src/server.ts b/packages/distributor-oracle/src/server.ts index 00afc1285..aba1ce9a1 100644 --- a/packages/distributor-oracle/src/server.ts +++ b/packages/distributor-oracle/src/server.ts @@ -233,6 +233,17 @@ export class OracleServer { private addRoutes() { this.app.get("/active-devices", this.getActiveDevicesHandler.bind(this)); this.app.post("/bulk-rewards", this.getAllRewardsHandler.bind(this)); + this.app.get( + "/will-pay-recipient", + ( + _req: FastifyRequest<{ Body: { entityKeys: string[] } }>, + res: FastifyReply + ) => { + res.send({ + willPay: process.env.WILL_PAY_RECIPIENT === "true", + }); + } + ); this.app.get<{ Querystring: { assetId?: string; @@ -361,6 +372,10 @@ export class OracleServer { initCompressionRecipientTx.accounts.findIndex( (x) => x.name === "lazyDistributor" )!; + const payerIdxInitCompressionRecipient = + initCompressionRecipientTx.accounts.findIndex( + (x) => x.name === "payer" + )!; const mintIdx = initRecipientTx.accounts.findIndex( (x) => x.name === "mint" )!; @@ -443,6 +458,15 @@ export class OracleServer { allAccs[ix.accountKeyIndexes[lazyDistributorIdxInitCompressionRecipient]].toBase58(); const merkleTree = allAccs[ix.accountKeyIndexes[merkleTreeIdxInitCompressionRecipient]]; + const payer = + allAccs[ix.accountKeyIndexes[payerIdxInitCompressionRecipient]].toBase58(); + + if (process.env.WILL_PAY_RECIPIENT !== "true" && payer === this.oracle.publicKey.toBase58()) { + return { + success: false, + message: "Cannot set this oracle as the payer", + }; + } const index = (decoded.data as any).args.index; recipientToLazyDistToMint[recipient][lazyDist] = await getLeafAssetId( diff --git a/packages/distributor-oracle/yarn.deploy.lock b/packages/distributor-oracle/yarn.deploy.lock index 8149950c4..58b141bef 100644 --- a/packages/distributor-oracle/yarn.deploy.lock +++ b/packages/distributor-oracle/yarn.deploy.lock @@ -259,6 +259,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/entity-invalidator/yarn.deploy.lock b/packages/entity-invalidator/yarn.deploy.lock index 999e3cb2b..7cfac6781 100644 --- a/packages/entity-invalidator/yarn.deploy.lock +++ b/packages/entity-invalidator/yarn.deploy.lock @@ -205,6 +205,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/helium-admin-cli/emissions/hst.json b/packages/helium-admin-cli/emissions/hst.json index d21b1f5b0..d754cf152 100644 --- a/packages/helium-admin-cli/emissions/hst.json +++ b/packages/helium-admin-cli/emissions/hst.json @@ -1,14 +1,10 @@ [ - { - "startTime": "2023-04-18T00:00:00Z", - "percent": 32 - }, - { - "startTime": "2023-08-01T00:00:00Z", - "percent": 31 - }, { "startTime": "2024-08-01T00:00:01Z", "percent": 30 + }, + { + "startTime": "2025-08-01T00:00:01Z", + "percent": 0 } ] diff --git a/packages/helium-admin-cli/src/add-expiration-to-delegations.ts b/packages/helium-admin-cli/src/add-expiration-to-delegations.ts new file mode 100644 index 000000000..5d8bb8669 --- /dev/null +++ b/packages/helium-admin-cli/src/add-expiration-to-delegations.ts @@ -0,0 +1,127 @@ +import * as anchor from "@coral-xyz/anchor"; +import { daoKey, init as initHsd, subDaoEpochInfoKey } from "@helium/helium-sub-daos-sdk"; +import { init as initProxy } from "@helium/nft-proxy-sdk"; +import { batchParallelInstructionsWithPriorityFee, HNT_MINT } from "@helium/spl-utils"; +import { init as initVsr } from "@helium/voter-stake-registry-sdk"; +import { AccountInfo, PublicKey, SystemProgram, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from "@solana/web3.js"; +import { min } from "bn.js"; +import os from "os"; +import yargs from "yargs/yargs"; +import { loadKeypair } from "./utils"; + +export async function run(args: any = process.argv) { + const yarg = yargs(args).options({ + wallet: { + alias: "k", + describe: "Anchor wallet keypair", + default: `${os.homedir()}/.config/solana/id.json`, + }, + url: { + alias: "u", + default: "http://127.0.0.1:8899", + describe: "The solana url", + }, + hntMint: { + type: "string", + describe: "HNT mint of the dao to be updated", + default: HNT_MINT.toBase58(), + }, + }); + const argv = await yarg.argv; + process.env.ANCHOR_WALLET = argv.wallet; + process.env.ANCHOR_PROVIDER_URL = argv.url; + anchor.setProvider(anchor.AnchorProvider.local(argv.url)); + const provider = anchor.getProvider() as anchor.AnchorProvider; + const wallet = new anchor.Wallet(loadKeypair(argv.wallet)); + const proxyProgram = await initProxy(provider); + const vsrProgram = await initVsr(provider); + const hsdProgram = await initHsd(provider); + + + const hntMint = new PublicKey(argv.hntMint); + const dao = daoKey(hntMint)[0]; + const registrarK = (await hsdProgram.account.daoV0.fetch(dao)).registrar; + const registrar = await vsrProgram.account.registrar.fetch(registrarK); + const proxyConfig = await proxyProgram.account.proxyConfigV0.fetch( + registrar.proxyConfig + ); + + const instructions: TransactionInstruction[] = []; + const delegations = await hsdProgram.account.delegatedPositionV0.all() + const needsMigration = delegations.filter(d => d.account.expirationTs.isZero()); + const positionKeys = needsMigration.map(d => d.account.position); + const coder = hsdProgram.coder.accounts + const positionAccs = (await getMultipleAccounts({ + connection: provider.connection, + keys: positionKeys, + })).map(a => coder.decode("positionV0", a.data)); + + const currTs = await getSolanaUnixTimestamp(provider); + const currTsBN = new anchor.BN(currTs.toString()); + const proxyEndTs = proxyConfig.seasons.find(s => currTsBN.gt(s.start))?.end; + for (const [delegation, position] of zip(needsMigration, positionAccs)) { + const subDao = delegation.account.subDao; + instructions.push( + await hsdProgram.methods + .addExpirationTs() + .accountsStrict({ + payer: wallet.publicKey, + position: delegation.account.position, + delegatedPosition: delegation.publicKey, + registrar: registrarK, + dao, + subDao: delegation.account.subDao, + oldClosingTimeSubDaoEpochInfo: subDaoEpochInfoKey( + subDao, + position.lockup.endTs + )[0], + closingTimeSubDaoEpochInfo: subDaoEpochInfoKey( + subDao, + min(position.lockup.endTs, proxyEndTs!) + )[0], + genesisEndSubDaoEpochInfo: subDaoEpochInfoKey( + subDao, + position.genesisEnd + )[0], + proxyConfig: registrar.proxyConfig, + systemProgram: SystemProgram.programId, + }) + .instruction() + ); + } + + await batchParallelInstructionsWithPriorityFee(provider, instructions, { + onProgress: (status) => { + console.log(status); + }, + }); +} + +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; +} + +function zip(a: T[], b: U[]): [T, U][] { + return a.map((_, i) => [a[i], b[i]]); +} + +async function getSolanaUnixTimestamp( + provider: anchor.AnchorProvider +): Promise { + const clock = await provider.connection.getAccountInfo(SYSVAR_CLOCK_PUBKEY); + const unixTime = clock!.data.readBigInt64LE(8 * 4); + return unixTime; +} \ No newline at end of file diff --git a/packages/helium-admin-cli/src/create-dao.ts b/packages/helium-admin-cli/src/create-dao.ts index dff7cb3ea..c48c2341c 100644 --- a/packages/helium-admin-cli/src/create-dao.ts +++ b/packages/helium-admin-cli/src/create-dao.ts @@ -7,11 +7,12 @@ import { init as initDc, } from "@helium/data-credits-sdk"; import { fanoutKey } from "@helium/fanout-sdk"; +import { init as initLazy } from "@helium/lazy-distributor-sdk"; import { dataOnlyConfigKey, init as initHem, } from "@helium/helium-entity-manager-sdk"; -import { daoKey, init as initDao } from "@helium/helium-sub-daos-sdk"; +import { daoKey, delegatorRewardsPercent, init as initDao } from "@helium/helium-sub-daos-sdk"; import { sendInstructions, toBN } from "@helium/spl-utils"; import { init as initVsr, @@ -30,6 +31,7 @@ import { withCreateRealm, withSetRealmAuthority, } from "@solana/spl-governance"; +import { organizationKey } from "@helium/organization-sdk"; import { createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync, @@ -57,6 +59,7 @@ import { sendInstructionsOrSquads, } from "./utils"; import { init } from "@helium/nft-proxy-sdk"; +import { oracleSignerKey } from "@helium/rewards-oracle-sdk"; const SECS_PER_DAY = 86400; const SECS_PER_YEAR = 365 * SECS_PER_DAY; @@ -93,6 +96,12 @@ export async function run(args: any = process.argv) { describe: "Keypair of the Data Credit token", default: `${__dirname}/../../keypairs/dc.json`, }, + delegatorRewardsPercent: { + type: "number", + required: true, + describe: + "Percentage of rewards allocated to delegators. Must be between 0-100 and can have 8 decimal places.", + }, numHnt: { type: "number", describe: @@ -165,6 +174,17 @@ export async function run(args: any = process.argv) { type: "string", required: true, }, + rewardsOracleUrl: { + alias: "ro", + type: "string", + describe: "The rewards oracle URL", + required: true, + }, + oracleKey: { + type: "string", + describe: "Pubkey of the oracle", + required: true, + }, numHst: { type: "number", describe: @@ -193,6 +213,7 @@ export async function run(args: any = process.argv) { const heliumSubDaosProgram = await initDao(provider); const heliumVsrProgram = await initVsr(provider); const hemProgram = await initHem(provider); + const lazyDistProgram = await initLazy(provider); const govProgramId = new PublicKey(argv.govProgramId); const councilKeypair = await loadKeypair(argv.councilKeypair); @@ -454,6 +475,33 @@ export async function run(args: any = process.argv) { fanout, true ); + const oracleKey = new PublicKey(argv.oracleKey!); + const { instruction: initLazyDist, pubkeys: { rewardsEscrow, lazyDistributor } } = await lazyDistProgram.methods + .initializeLazyDistributorV0({ + authority, + oracles: [ + { + oracle: oracleKey, + url: argv.rewardsOracleUrl, + }, + ], + // 5 x epoch rewards in a 24 hour period + windowConfig: { + windowSizeSeconds: new anchor.BN(24 * 60 * 60), + thresholdType: ThresholdType.Absolute as never, + threshold: new anchor.BN(currentHntEmission.emissionsPerEpoch).mul( + new anchor.BN(5) + ), + }, + approver: oracleSignerKey()[0], + }) + .accounts({ + payer: authority, + rewardsMint: hntKeypair.publicKey, + }) + .prepare(); + const ldExists = await exists(conn, lazyDistributor!); + await heliumSubDaosProgram.methods .initializeDaoV0({ registrar: registrar, @@ -462,8 +510,11 @@ export async function run(args: any = process.argv) { // Tx too large to do in initialize dao, so do it with update hstEmissionSchedule: [currentHstEmission], emissionSchedule: [currentHntEmission], + proposalNamespace: organizationKey("Helium")[0], + delegatorRewardsPercent: delegatorRewardsPercent(argv.delegatorRewardsPercent), }) .preInstructions([ + ...(ldExists ? [] : [initLazyDist]), createAssociatedTokenAccountIdempotentInstruction( provider.wallet.publicKey, hstPool, @@ -475,6 +526,7 @@ export async function run(args: any = process.argv) { dcMint: dcKeypair.publicKey, hntMint: hntKeypair.publicKey, hstPool, + rewardsEscrow, }) .rpc({ skipPreflight: true }); @@ -488,6 +540,8 @@ export async function run(args: any = process.argv) { hstEmissionSchedule: hstEmission, hstPool: null, netEmissionsCap: null, + proposalNamespace: organizationKey("Helium")[0], + delegatorRewardsPercent: delegatorRewardsPercent(argv.delegatorRewardsPercent), }) .accounts({ dao, diff --git a/packages/helium-admin-cli/src/create-subdao.ts b/packages/helium-admin-cli/src/create-subdao.ts index ccf312f6d..eafe49f66 100644 --- a/packages/helium-admin-cli/src/create-subdao.ts +++ b/packages/helium-admin-cli/src/create-subdao.ts @@ -10,7 +10,6 @@ import { init as initDao, subDaoKey, threadKey, - delegatorRewardsPercent, } from "@helium/helium-sub-daos-sdk"; import { init as initLazy, @@ -174,12 +173,6 @@ export async function run(args: any = process.argv) { describe: "Authority index for squads. Defaults to 1", default: 1, }, - delegatorRewardsPercent: { - type: "number", - required: true, - describe: - "Percentage of rewards allocated to delegators. Must be between 0-100 and can have 8 decimal places.", - }, emissionSchedulePath: { required: true, describe: "Path to file that contains the dnt emissions schedule", @@ -468,9 +461,6 @@ export async function run(args: any = process.argv) { name.toUpperCase() == "IOT" ? toBN(4000000, 0) : toBN(0, 0), onboardingDataOnlyDcFee: name.toUpperCase() == "IOT" ? toBN(1000000, 0) : toBN(0, 0), - delegatorRewardsPercent: delegatorRewardsPercent( - argv.delegatorRewardsPercent - ), activeDeviceAuthority: argv.activeDeviceAuthority ? new PublicKey(argv.activeDeviceAuthority) : authority, @@ -478,7 +468,6 @@ export async function run(args: any = process.argv) { .accounts({ dao, dntMint: subdaoKeypair.publicKey, - rewardsEscrow, hntMint: new PublicKey(argv.hntPubkey!), payer, dntMintAuthority: daoAcc.authority, @@ -518,7 +507,6 @@ export async function run(args: any = process.argv) { onboardingDcFee: null, onboardingDataOnlyDcFee: null, registrar: null, - delegatorRewardsPercent: null, activeDeviceAuthority: null, }) .accounts({ diff --git a/packages/helium-admin-cli/src/end-epoch.ts b/packages/helium-admin-cli/src/end-epoch.ts index 6b0b0a471..46dbd854a 100644 --- a/packages/helium-admin-cli/src/end-epoch.ts +++ b/packages/helium-admin-cli/src/end-epoch.ts @@ -3,10 +3,10 @@ import { daoEpochInfoKey, daoKey, EPOCH_LENGTH, - init as initDao + init as initDao, } from "@helium/helium-sub-daos-sdk"; import * as anchor from "@coral-xyz/anchor"; -import { ComputeBudgetProgram, PublicKey } from "@solana/web3.js"; +import { PublicKey } from "@solana/web3.js"; import { BN } from "bn.js"; import b58 from "bs58"; import os from "os"; @@ -44,17 +44,21 @@ export async function run(args: any = process.argv) { const heliumSubDaosProgram = await initDao(provider); const hntMint = new PublicKey(argv.hntMint!); const dao = await daoKey(hntMint)[0]; - const subdaos = await heliumSubDaosProgram.account.subDaoV0.all([{ - memcmp: { - offset: 8, - bytes: b58.encode(dao.toBuffer()), - } - }]); - let targetTs = argv.from ? new BN(argv.from) : subdaos[0].account.vehntLastCalculatedTs; + const subdaos = await heliumSubDaosProgram.account.subDaoV0.all([ + { + memcmp: { + offset: 8, + bytes: b58.encode(dao.toBuffer()), + }, + }, + ]); + let targetTs = argv.from + ? new BN(argv.from) + : subdaos[0].account.vehntLastCalculatedTs; while (targetTs.toNumber() < new Date().valueOf() / 1000) { const epoch = currentEpoch(targetTs); - console.log(epoch.toNumber(), targetTs.toNumber()) + console.log(epoch.toNumber(), targetTs.toNumber()); const daoEpochInfo = await heliumSubDaosProgram.account.daoEpochInfoV0.fetchNullable( daoEpochInfoKey(dao, targetTs)[0] @@ -101,21 +105,7 @@ export async function run(args: any = process.argv) { } } - - } - try { - if (!daoEpochInfo?.doneIssuingHstPool) { - await sendInstructionsWithPriorityFee(provider, [ - await heliumSubDaosProgram.methods - .issueHstPoolV0({ epoch }) - .accounts({ dao }) - .instruction(), - ]); - } - } catch (e: any) { - console.log(`Failed to issue hst pool: ${e.message}`); + targetTs = targetTs.add(new BN(EPOCH_LENGTH)); } - - targetTs = targetTs.add(new BN(EPOCH_LENGTH)); } } diff --git a/packages/helium-admin-cli/src/migrate-to-hip-138.ts b/packages/helium-admin-cli/src/migrate-to-hip-138.ts new file mode 100644 index 000000000..95ffcfa0c --- /dev/null +++ b/packages/helium-admin-cli/src/migrate-to-hip-138.ts @@ -0,0 +1,223 @@ +import * as anchor from "@coral-xyz/anchor"; +import { init as initLazy } from "@helium/lazy-distributor-sdk"; +import { + daoKey, + delegatorRewardsPercent, + init as initHsd, + subDaoKey, +} from "@helium/helium-sub-daos-sdk"; +import { + batchParallelInstructionsWithPriorityFee, + HNT_MINT, + IOT_MINT, + MOBILE_MINT, +} from "@helium/spl-utils"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import Squads from "@sqds/sdk"; +import os from "os"; +import { organizationKey } from "@helium/organization-sdk"; +import yargs from "yargs/yargs"; +import { + loadKeypair, + parseEmissionsSchedule, + sendInstructionsOrSquads, +} from "./utils"; +import { lazyDistributorKey } from "@helium/lazy-distributor-sdk"; +import { ThresholdType } from "@helium/circuit-breaker-sdk"; +import { oracleSignerKey } from "@helium/rewards-oracle-sdk"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; + +export async function run(args: any = process.argv) { + const yarg = yargs(args).options({ + wallet: { + alias: "k", + describe: "Anchor wallet keypair", + default: `${os.homedir()}/.config/solana/id.json`, + }, + url: { + alias: "u", + default: "http://127.0.0.1:8899", + describe: "The solana url", + }, + iotMint: { + type: "string", + describe: "IOT mint of the subdao to migrate", + default: IOT_MINT.toBase58(), + }, + hntMint: { + type: "string", + describe: "HNT mint of the subdao to migrate", + default: HNT_MINT.toBase58(), + }, + mobileMint: { + type: "string", + describe: "Mobile mint of the subdao to migrate", + default: MOBILE_MINT.toBase58(), + }, + rewardsOracleUrl: { + alias: "ro", + type: "string", + describe: "The rewards oracle URL", + required: true, + }, + oracleKey: { + type: "string", + describe: "Pubkey of the oracle", + required: true, + }, + emissionSchedulePath: { + required: true, + describe: "Path to file that contains the hnt emissions schedule", + type: "string", + }, + hstEmissionsSchedulePath: { + required: true, + describe: "Path to file that contains the new HST emissions schedule", + type: "string", + }, + executeTransaction: { + type: "boolean", + }, + multisig: { + type: "string", + describe: + "Address of the squads multisig to be authority. If not provided, your wallet will be the authority", + }, + authorityIndex: { + type: "number", + describe: "Authority index for squads. Defaults to 1", + default: 1, + }, + }); + const argv = await yarg.argv; + process.env.ANCHOR_WALLET = argv.wallet; + process.env.ANCHOR_PROVIDER_URL = argv.url; + anchor.setProvider(anchor.AnchorProvider.local(argv.url)); + const provider = anchor.getProvider() as anchor.AnchorProvider; + const wallet = new anchor.Wallet(loadKeypair(argv.wallet)); + const lazyDistProgram = await initLazy(provider); + const hsdProgram = await initHsd(provider); + + const instructions: TransactionInstruction[] = []; + + const iotMint = new PublicKey(argv.iotMint); + const mobileMint = new PublicKey(argv.mobileMint); + const hntMint = new PublicKey(argv.hntMint); + const dao = daoKey(hntMint)[0]; + + const resizes: TransactionInstruction[] = []; + resizes.push( + await hsdProgram.methods + .tempResizeAccount() + .accounts({ + account: dao, + payer: wallet.publicKey, + }) + .instruction() + ); + const daoEpochInfos = await hsdProgram.account.daoEpochInfoV0.all(); + for (const daoEpochInfo of daoEpochInfos) { + resizes.push( + await hsdProgram.methods + .tempResizeAccount() + .accounts({ + account: daoEpochInfo.publicKey, + payer: wallet.publicKey, + }) + .instruction() + ); + } + console.log("Resizing accounts"); + await batchParallelInstructionsWithPriorityFee(provider, resizes);`` + + const daoAcc = await hsdProgram.account.daoV0.fetch(dao); + const authority = daoAcc.authority; + const oracleKey = new PublicKey(argv.oracleKey!); + const emissionSchedule = await parseEmissionsSchedule( + argv.emissionSchedulePath + ); + + const ld = lazyDistributorKey(hntMint)[0]; + const rewardsEscrow = getAssociatedTokenAddressSync(hntMint, ld, true); + const ldAcc = await lazyDistProgram.account.lazyDistributorV0.fetchNullable( + ld + ); + if (ldAcc) { + console.warn("Lazy distributor already exists, skipping."); + } else { + instructions.push( + await lazyDistProgram.methods + .initializeLazyDistributorV0({ + authority: daoAcc.authority, + oracles: [ + { + oracle: oracleKey, + url: argv.rewardsOracleUrl, + }, + ], + // 5 x epoch rewards in a 24 hour period + windowConfig: { + windowSizeSeconds: new anchor.BN(24 * 60 * 60), + thresholdType: ThresholdType.Absolute as never, + threshold: new anchor.BN(emissionSchedule[0].emissionsPerEpoch).mul( + new anchor.BN(5) + ), + }, + approver: oracleSignerKey()[0], + }) + .accounts({ + payer: authority, + rewardsMint: hntMint, + }) + .instruction() + ); + } + + instructions.push( + await hsdProgram.methods + .updateDaoV0({ + authority: null, + emissionSchedule: null, + hstEmissionSchedule: await parseEmissionsSchedule( + argv.hstEmissionsSchedulePath! + ), + netEmissionsCap: null, + hstPool: null, + proposalNamespace: organizationKey("Helium")[0], + delegatorRewardsPercent: delegatorRewardsPercent(6), + }) + .accounts({ + dao, + authority: daoAcc.authority, + payer: daoAcc.authority, + }) + .instruction() + ); + + if (!daoAcc.rewardsEscrow) { + instructions.push( + await hsdProgram.methods + .initializeHntDelegatorPool() + .accounts({ + dao, + payer: daoAcc.authority, + delegatorPool: getAssociatedTokenAddressSync(hntMint, dao, true), + }) + .instruction() + ); + } + + const squads = Squads.endpoint(process.env.ANCHOR_PROVIDER_URL, wallet, { + commitmentOrConfig: "finalized", + }); + + await sendInstructionsOrSquads({ + provider, + instructions, + executeTransaction: argv.executeTransaction, + squads, + multisig: argv.multisig ? new PublicKey(argv.multisig) : undefined, + authorityIndex: argv.authorityIndex, + signers: [], + }); +} diff --git a/packages/helium-admin-cli/src/update-dao.ts b/packages/helium-admin-cli/src/update-dao.ts index 6d9ff9225..a5734c24d 100644 --- a/packages/helium-admin-cli/src/update-dao.ts +++ b/packages/helium-admin-cli/src/update-dao.ts @@ -1,77 +1,84 @@ -import * as anchor from '@coral-xyz/anchor'; +import * as anchor from "@coral-xyz/anchor"; import { init as initCb, mintWindowedBreakerKey, -} from '@helium/circuit-breaker-sdk'; -import { daoKey, init as initHsd } from '@helium/helium-sub-daos-sdk'; -import { PublicKey, TransactionInstruction } from '@solana/web3.js'; -import Squads from '@sqds/sdk'; -import os from 'os'; -import yargs from 'yargs/yargs'; +} from "@helium/circuit-breaker-sdk"; +import { daoKey, delegatorRewardsPercent, init as initHsd } from "@helium/helium-sub-daos-sdk"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import Squads from "@sqds/sdk"; +import os from "os"; +import yargs from "yargs/yargs"; import { loadKeypair, parseEmissionsSchedule, sendInstructionsOrSquads, -} from './utils'; +} from "./utils"; +import { organizationKey } from "@helium/organization-sdk"; export async function run(args: any = process.argv) { const yarg = yargs(args).options({ wallet: { - alias: 'k', - describe: 'Anchor wallet keypair', + alias: "k", + describe: "Anchor wallet keypair", default: `${os.homedir()}/.config/solana/id.json`, }, url: { - alias: 'u', - default: 'http://127.0.0.1:8899', - describe: 'The solana url', + alias: "u", + default: "http://127.0.0.1:8899", + describe: "The solana url", }, hntMint: { required: true, - type: 'string', - describe: 'HNT mint of the dao to be updated', + type: "string", + describe: "HNT mint of the dao to be updated", }, newAuthority: { required: false, - describe: 'New DAO authority', - type: 'string', + describe: "New DAO authority", + type: "string", default: null, }, newEmissionsSchedulePath: { required: false, - describe: 'Path to file that contains the new emissions schedule', - type: 'string', + describe: "Path to file that contains the new emissions schedule", + type: "string", default: null, }, newHstEmissionsSchedulePath: { required: false, - describe: 'Path to file that contains the new HST emissions schedule', - type: 'string', + describe: "Path to file that contains the new HST emissions schedule", + type: "string", default: null, }, newNetEmissionsCap: { required: false, - describe: 'New net emissions cap, without decimals', - type: 'string', + describe: "New net emissions cap, without decimals", + type: "string", default: null, }, + delegatorRewardsPercent: { + type: "number", + required: true, + describe: + "Percentage of rewards allocated to delegators. Must be between 0-100 and can have 8 decimal places.", + }, newHstPool: { required: false, - describe: 'New HST Pool', - type: 'string', + describe: "New HST Pool", + type: "string", default: null, }, executeTransaction: { - type: 'boolean', + type: "boolean", }, multisig: { - type: 'string', + type: "string", describe: - 'Address of the squads multisig to be authority. If not provided, your wallet will be the authority', + "Address of the squads multisig to be authority. If not provided, your wallet will be the authority", }, authorityIndex: { - type: 'number', - describe: 'Authority index for squads. Defaults to 1', + type: "number", + describe: "Authority index for squads. Defaults to 1", default: 1, }, }); @@ -121,6 +128,10 @@ export async function run(args: any = process.argv) { ? new anchor.BN(argv.newNetEmissionsCap) : null, hstPool: argv.newHstPool ? new PublicKey(argv.newHstPool) : null, + proposalNamespace: organizationKey("Helium")[0], + delegatorRewardsPercent: argv.delegatorRewardsPercent + ? delegatorRewardsPercent(argv.delegatorRewardsPercent) + : null, }) .accounts({ dao, @@ -131,7 +142,7 @@ export async function run(args: any = process.argv) { ); const squads = Squads.endpoint(process.env.ANCHOR_PROVIDER_URL, wallet, { - commitmentOrConfig: 'finalized', + commitmentOrConfig: "finalized", }); await sendInstructionsOrSquads({ provider, diff --git a/packages/helium-admin-cli/src/update-subdao.ts b/packages/helium-admin-cli/src/update-subdao.ts index 8b430edf2..3bb135039 100644 --- a/packages/helium-admin-cli/src/update-subdao.ts +++ b/packages/helium-admin-cli/src/update-subdao.ts @@ -11,7 +11,6 @@ import { import { init as initHsd, subDaoKey, - delegatorRewardsPercent, } from '@helium/helium-sub-daos-sdk'; import { PublicKey, TransactionInstruction } from '@solana/web3.js'; import Squads from '@sqds/sdk'; @@ -88,13 +87,6 @@ export async function run(args: any = process.argv) { describe: 'VSR Registrar of subdao', default: null, }, - delegatorRewardsPercent: { - type: 'number', - required: false, - describe: - 'Percentage of rewards allocated to delegators. Must be between 0-100 and can have 8 decimal places.', - default: null, - }, onboardingDcFee: { type: 'number', required: false, @@ -195,13 +187,6 @@ export async function run(args: any = process.argv) { ); } - if ( - argv.delegatorRewardsPercent && - (argv.delegatorRewardsPercent > 100 || argv.delegatorRewardsPercent < 0) - ) { - throw new Error('Delegator rewards percent must be between 0 and 100'); - } - instructions.push( await program.methods .updateSubDaoV0({ @@ -219,9 +204,6 @@ export async function run(args: any = process.argv) { ? new BN(argv.onboardingDataOnlyDcFee) : null, registrar: argv.registrar ? new PublicKey(argv.registrar) : null, - delegatorRewardsPercent: argv.delegatorRewardsPercent - ? delegatorRewardsPercent(argv.delegatorRewardsPercent) - : null, activeDeviceAuthority: argv.activeDeviceAuthority ? new PublicKey(argv.activeDeviceAuthority) : null, diff --git a/packages/helium-admin-cli/yarn.deploy.lock b/packages/helium-admin-cli/yarn.deploy.lock index 982bdb426..a0fc77c24 100644 --- a/packages/helium-admin-cli/yarn.deploy.lock +++ b/packages/helium-admin-cli/yarn.deploy.lock @@ -382,6 +382,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/helium-entity-manager-sdk/yarn.deploy.lock b/packages/helium-entity-manager-sdk/yarn.deploy.lock index 77a8e687e..6c2413f0a 100644 --- a/packages/helium-entity-manager-sdk/yarn.deploy.lock +++ b/packages/helium-entity-manager-sdk/yarn.deploy.lock @@ -174,6 +174,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/helium-sub-daos-sdk/package.json b/packages/helium-sub-daos-sdk/package.json index e41d8edd4..4f14a04ec 100644 --- a/packages/helium-sub-daos-sdk/package.json +++ b/packages/helium-sub-daos-sdk/package.json @@ -34,6 +34,7 @@ "@coral-xyz/anchor": "^0.28.0", "@helium/anchor-resolvers": "^0.9.18", "@helium/circuit-breaker-sdk": "^0.9.18", + "@helium/nft-proxy-sdk": "^0.0.15", "@helium/spl-utils": "^0.9.18", "@helium/treasury-management-sdk": "^0.9.18", "@helium/voter-stake-registry-sdk": "^0.9.18", diff --git a/packages/helium-sub-daos-sdk/src/index.ts b/packages/helium-sub-daos-sdk/src/index.ts index 4840ca55a..9a826d0fb 100644 --- a/packages/helium-sub-daos-sdk/src/index.ts +++ b/packages/helium-sub-daos-sdk/src/index.ts @@ -1,31 +1,6 @@ -import { HeliumSubDaos } from "@helium/idls/lib/types/helium_sub_daos"; -import { Idl, Program, Provider } from "@coral-xyz/anchor"; -import { PublicKey } from "@solana/web3.js"; -import { PROGRAM_ID } from "./constants"; -import { heliumSubDaosResolvers } from "./resolvers"; import { BN } from "bn.js"; -import { fetchBackwardsCompatibleIdl } from "@helium/spl-utils"; -export async function init( - provider: Provider, - programId: PublicKey = PROGRAM_ID, - idl?: Idl | null -): Promise> { - if (!idl) { - idl = await fetchBackwardsCompatibleIdl(programId, provider); - } - const program = new Program( - idl as HeliumSubDaos, - programId ?? PROGRAM_ID, - provider, - undefined, - () => { - return heliumSubDaosResolvers; - } - ) as Program; - - return program; -} +export * from "./init"; export function delegatorRewardsPercent(percent: number) { return new BN(Math.floor(percent * Math.pow(10, 8))); @@ -34,3 +9,4 @@ export function delegatorRewardsPercent(percent: number) { export * from "./constants"; export * from "./pdas"; export * from "./resolvers"; + diff --git a/packages/helium-sub-daos-sdk/src/init.ts b/packages/helium-sub-daos-sdk/src/init.ts new file mode 100644 index 000000000..31c5bdc42 --- /dev/null +++ b/packages/helium-sub-daos-sdk/src/init.ts @@ -0,0 +1,27 @@ +import { Idl, Program, Provider } from "@coral-xyz/anchor"; +import { HeliumSubDaos } from "@helium/idls/lib/types/helium_sub_daos"; +import { fetchBackwardsCompatibleIdl } from "@helium/spl-utils"; +import { PublicKey } from "@solana/web3.js"; +import { PROGRAM_ID } from "./constants"; +import { heliumSubDaosResolvers } from "./resolvers"; + +export async function init( + provider: Provider, + programId: PublicKey = PROGRAM_ID, + idl?: Idl | null +): Promise> { + if (!idl) { + idl = await fetchBackwardsCompatibleIdl(programId, provider); + } + const program = new Program( + idl as HeliumSubDaos, + programId ?? PROGRAM_ID, + provider, + undefined, + () => { + return heliumSubDaosResolvers; + } + ) as Program; + + return program; +} diff --git a/packages/helium-sub-daos-sdk/src/resolvers.ts b/packages/helium-sub-daos-sdk/src/resolvers.ts index 08f729c99..572e45c17 100644 --- a/packages/helium-sub-daos-sdk/src/resolvers.ts +++ b/packages/helium-sub-daos-sdk/src/resolvers.ts @@ -6,17 +6,56 @@ import { } from "@helium/anchor-resolvers"; import { treasuryManagementResolvers } from "@helium/treasury-management-sdk"; import { init, PROGRAM_ID as VSR_PROGRAM_ID, vsrResolvers } from "@helium/voter-stake-registry-sdk"; -import { AnchorProvider, Provider } from "@coral-xyz/anchor"; +import { AnchorProvider, BN, Provider } from "@coral-xyz/anchor"; import { PublicKey, SYSVAR_CLOCK_PUBKEY } from "@solana/web3.js"; import { EPOCH_LENGTH, PROGRAM_ID } from "./constants"; +import { init as initNftProxy } from "@helium/nft-proxy-sdk"; +import { init as initHsd } from "./init"; import { daoEpochInfoKey, subDaoEpochInfoKey } from "./pdas"; const THREAD_PID = new PublicKey( "CLoCKyJ6DXBJqqu2VWx9RLbgnwwR6BMHHuyasVmfMzBh" ); +export const daoEpochInfoResolver = resolveIndividual( + async ({ provider, path, accounts, args }) => { + if (path[path.length - 1] === "daoEpochInfo" && accounts.registrar) { + const vsr = await init(provider as AnchorProvider, VSR_PROGRAM_ID); + let registrar; + try { + registrar = await vsr.account.registrar.fetch( + accounts.registrar as PublicKey + ); + } catch (e: any) { + // ignore. It's fine, we just won't use time offset which is only used in testing cases + console.error(e); + } + const clock = await provider.connection.getAccountInfo( + SYSVAR_CLOCK_PUBKEY + ); + let unixTime; + if (args && args[0] && args[0].epoch) { + unixTime = args[0].epoch.toNumber() * EPOCH_LENGTH; + } else { + unixTime = + Number(clock!.data.readBigInt64LE(8 * 4)) + + (registrar?.timeOffset.toNumber() || 0); + } + const dao = get(accounts, [ + ...path.slice(0, path.length - 1), + "dao", + ]) as PublicKey; + if (dao) { + const [key] = await daoEpochInfoKey(dao, unixTime, PROGRAM_ID); + + return key; + } + } + } +); + export const subDaoEpochInfoResolver = resolveIndividual( - async ({ provider, path, accounts }) => { + async ({ provider, path, accounts, args }) => { if (path[path.length - 1] === "subDaoEpochInfo" && accounts.registrar) { const vsr = await init(provider as AnchorProvider, VSR_PROGRAM_ID); let registrar; @@ -29,7 +68,12 @@ export const subDaoEpochInfoResolver = resolveIndividual( const clock = await provider.connection.getAccountInfo( SYSVAR_CLOCK_PUBKEY ); - const unixTime = Number(clock!.data.readBigInt64LE(8 * 4)) + (registrar?.timeOffset.toNumber() || 0); + let unixTime; + if (args && args[0] && args[0].epoch) { + unixTime = args[0].epoch.toNumber() * EPOCH_LENGTH + } else { + unixTime = Number(clock!.data.readBigInt64LE(8 * 4)) + (registrar?.timeOffset.toNumber() || 0); + } const subDao = get(accounts, [ ...path.slice(0, path.length - 1), "subDao", @@ -37,6 +81,38 @@ export const subDaoEpochInfoResolver = resolveIndividual( if (subDao) { const [key] = await subDaoEpochInfoKey(subDao, unixTime, PROGRAM_ID); + return key; + } + } + if (path[path.length - 1] === "prevSubDaoEpochInfo" && accounts.registrar) { + const vsr = await init(provider as AnchorProvider, VSR_PROGRAM_ID); + let registrar; + try { + registrar = await vsr.account.registrar.fetch( + accounts.registrar as PublicKey + ); + } catch (e: any) { + // ignore. It's fine, we just won't use time offset which is only used in testing cases + console.error(e); + } + const clock = await provider.connection.getAccountInfo( + SYSVAR_CLOCK_PUBKEY + ); + let unixTime; + if (args && args[0] && args[0].epoch) { + unixTime = args[0].epoch.toNumber() * EPOCH_LENGTH; + } else { + unixTime = + Number(clock!.data.readBigInt64LE(8 * 4)) + + (registrar?.timeOffset.toNumber() || 0); + } + const subDao = get(accounts, [ + ...path.slice(0, path.length - 1), + "subDao", + ]) as PublicKey; + if (subDao) { + const [key] = await subDaoEpochInfoKey(subDao, unixTime - EPOCH_LENGTH, PROGRAM_ID); + return key; } } @@ -50,6 +126,8 @@ export const closingTimeEpochInfoResolver = resolveIndividual( provider as AnchorProvider, VSR_PROGRAM_ID, ); + const hsdProgram = await initHsd(provider as AnchorProvider); + const nftProxyProgram = await initNftProxy(provider as AnchorProvider); const subDao = get(accounts, [ ...path.slice(0, path.length - 1), @@ -59,13 +137,30 @@ export const closingTimeEpochInfoResolver = resolveIndividual( ...path.slice(0, path.length - 1), "position", ]) as PublicKey; + const proxyConfig = get(accounts, [ + ...path.slice(0, path.length - 1), + "proxyConfig", + ]) as PublicKey; + const delegatedPosition = get(accounts, [ + ...path.slice(0, path.length - 1), + "delegatedPosition", + ]) as PublicKey; const positionAcc = position && await program.account.positionV0.fetch( position ); - if (positionAcc) { + const delegatedPositionAcc = delegatedPosition && await hsdProgram.account.delegatedPositionV0.fetchNullable(delegatedPosition); + const proxyConfigAcc = proxyConfig && await nftProxyProgram.account.proxyConfigV0.fetch(proxyConfig); + const now = await getSolanaUnixTimestamp(provider); + if (positionAcc && (proxyConfigAcc || delegatedPositionAcc)) { + const expirationTs = + !delegatedPositionAcc || delegatedPositionAcc.expirationTs.isZero() + ? proxyConfigAcc?.seasons.find((s) => + new BN(now.toString()).gte(s.start) + )?.end || positionAcc.lockup.endTs + : delegatedPositionAcc.expirationTs; const [key] = await subDaoEpochInfoKey( subDao, - positionAcc.lockup.endTs, + bnMin(positionAcc.lockup.endTs, expirationTs) ); return key; @@ -135,6 +230,7 @@ export const heliumSubDaosResolvers = combineResolvers( genesisEndEpochInfoResolver, closingTimeEpochInfoResolver, treasuryManagementResolvers, + daoEpochInfoResolver, ataResolver({ instruction: "initializeSubDaoV0", account: "treasury", @@ -142,10 +238,10 @@ export const heliumSubDaosResolvers = combineResolvers( owner: "treasuryManagement", }), ataResolver({ - instruction: "initializeSubDaoV0", + instruction: "initializeDaoV0", account: "delegatorPool", - mint: "dntMint", - owner: "subDao", + mint: "hntMint", + owner: "dao", }), ataResolver({ instruction: "claimRewardsV0", @@ -153,6 +249,12 @@ export const heliumSubDaosResolvers = combineResolvers( mint: "dntMint", owner: "positionAuthority", }), + ataResolver({ + instruction: "claimRewardsV1", + account: "delegatorAta", + mint: "hntMint", + owner: "positionAuthority", + }), ataResolver({ instruction: "tempClaimFailedClaims", account: "delegatorAta", @@ -176,3 +278,7 @@ export const heliumSubDaosResolvers = combineResolvers( }), vsrResolvers ); +function bnMin(a: BN, b: BN): BN { + return a.lt(b) ? a : b; +} + diff --git a/packages/helium-sub-daos-sdk/yarn.deploy.lock b/packages/helium-sub-daos-sdk/yarn.deploy.lock index 69559a7ae..dc9679ce4 100644 --- a/packages/helium-sub-daos-sdk/yarn.deploy.lock +++ b/packages/helium-sub-daos-sdk/yarn.deploy.lock @@ -139,6 +139,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/hexboosting-sdk/yarn.deploy.lock b/packages/hexboosting-sdk/yarn.deploy.lock index 646efd58f..78a3e8c2d 100644 --- a/packages/hexboosting-sdk/yarn.deploy.lock +++ b/packages/hexboosting-sdk/yarn.deploy.lock @@ -139,6 +139,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/hotspot-utils/yarn.deploy.lock b/packages/hotspot-utils/yarn.deploy.lock index 9fcdfb8b8..63f928cdf 100644 --- a/packages/hotspot-utils/yarn.deploy.lock +++ b/packages/hotspot-utils/yarn.deploy.lock @@ -174,6 +174,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/metadata-service/yarn.deploy.lock b/packages/metadata-service/yarn.deploy.lock index 2a7a3c3e9..190b8a0ed 100644 --- a/packages/metadata-service/yarn.deploy.lock +++ b/packages/metadata-service/yarn.deploy.lock @@ -263,6 +263,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/migration-service/yarn.deploy.lock b/packages/migration-service/yarn.deploy.lock index 30ce7d8a9..5e6d2a65a 100644 --- a/packages/migration-service/yarn.deploy.lock +++ b/packages/migration-service/yarn.deploy.lock @@ -336,6 +336,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/mobile-entity-manager-sdk/yarn.deploy.lock b/packages/mobile-entity-manager-sdk/yarn.deploy.lock index 3df487164..34675767f 100644 --- a/packages/mobile-entity-manager-sdk/yarn.deploy.lock +++ b/packages/mobile-entity-manager-sdk/yarn.deploy.lock @@ -174,6 +174,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/monitor-service/src/index.ts b/packages/monitor-service/src/index.ts index b6807c415..e85b6a516 100644 --- a/packages/monitor-service/src/index.ts +++ b/packages/monitor-service/src/index.ts @@ -110,7 +110,6 @@ async function run() { const mobileMint = mobile.dntMint; const mobileTreasury = mobile.treasury; const mobileRewardsEscrow = mobile.rewardsEscrow; - await Recipient.sync(); await monitorVehnt(); @@ -127,8 +126,11 @@ async function run() { await monitorTokenBalance(mobileTreasury, "mobile_treasury"); await setTotalRewards(IOT_MINT); await setTotalRewards(MOBILE_MINT); + await setTotalRewards(HNT_MINT); + const resetMobileTotal = debounce(() => setTotalRewards(MOBILE_MINT)); const resetIotTotal = debounce(() => setTotalRewards(IOT_MINT)); + const resetHntTotal = debounce(() => setTotalRewards(HNT_MINT)); await monitorTokenBalance( iotRewardsEscrow, "iot_rewards_escrow", @@ -145,6 +147,14 @@ async function run() { resetMobileTotal(); } ); + await monitorTokenBalance( + getAssociatedTokenAddressSync(hntMint, lazyDistributorKey(hntMint)[0], true), + "hnt_rewards_escrow", + false, + async () => { + resetHntTotal(); + } + ); await monitorTokenBalance( getAssociatedTokenAddressSync(dao.dcMint, iot.activeDeviceAuthority), "iot_active_device_oracle_dc" diff --git a/packages/monitor-service/yarn.deploy.lock b/packages/monitor-service/yarn.deploy.lock index b45c8e9be..054809bdb 100644 --- a/packages/monitor-service/yarn.deploy.lock +++ b/packages/monitor-service/yarn.deploy.lock @@ -229,6 +229,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts index b24333561..ae02c4dfc 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts @@ -17,6 +17,9 @@ import { useAsync, useAsyncCallback } from "react-async-hook"; import { useHeliumVsrState } from "../contexts/heliumVsrContext"; import { HeliumVsrClient } from "../sdk/client"; import { SubDaoWithMeta } from "../sdk/types"; +import { init as initProxy } from "@helium/nft-proxy-sdk"; +import { init as initVsr } from "@helium/voter-stake-registry-sdk"; + import { daoKey, init as initHsd, @@ -59,6 +62,8 @@ export const useCreatePosition = () => { throw new Error("Unable to Create Position, Invalid params"); } else { const hsdProgram = await initHsd(provider); + const proxyProgram = await initProxy(provider); + const vsrProgram = await initVsr(provider); const [daoK] = daoKey(mint); const [subDaoK] = subDaoKey(mint); const myDao = await hsdProgram.account.daoV0.fetchNullable(daoK); @@ -66,6 +71,8 @@ export const useCreatePosition = () => { subDaoK ); const registrar = (mySubDao?.registrar || myDao?.registrar)!; + const registarAcc = await vsrProgram.account.registrar.fetch(registrar); + const proxyConfig = await proxyProgram.account.proxyConfigV0.fetch(registarAcc.proxyConfig); const mintKeypair = Keypair.generate(); const position = positionKey(mintKeypair.publicKey)[0]; const instructions: TransactionInstruction[] = []; @@ -131,7 +138,7 @@ export const useCreatePosition = () => { registrar ); const currTs = Number(unixTime) + registrarAcc.timeOffset.toNumber(); - const endTs = lockupPeriodsInDays * SECS_PER_DAY + currTs; + const endTs = proxyConfig.seasons.find(season => new BN(currTs).gte(season.start))?.end || (currTs + lockupPeriodsInDays * SECS_PER_DAY); const [subDaoEpochInfo] = subDaoEpochInfoKey(subDao.pubkey, currTs); const [endSubDaoEpochInfoKey] = subDaoEpochInfoKey( subDao.pubkey, diff --git a/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts b/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts index 1815ed0cc..553eb51ed 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts @@ -10,6 +10,7 @@ import { useSolanaUnixNow } from "@helium/helium-react-hooks"; import { calcPositionVotingPower } from "../utils/calcPositionVotingPower"; import BN from "bn.js"; import { proxyAssignmentKey } from "@helium/nft-proxy-sdk"; +import { init as initHsd } from "@helium/helium-sub-daos-sdk"; export const useRelinquishVote = (proposal: PublicKey) => { const { positions, provider, registrar } = useHeliumVsrState(); @@ -85,6 +86,8 @@ export const useRelinquishVote = (proposal: PublicKey) => { ); } else { const vsrProgram = await init(provider); + const hsdProgram = await initHsd(provider); + const instructions = ( await Promise.all( sortedPositions.map(async (position, index) => { @@ -93,45 +96,66 @@ export const useRelinquishVote = (proposal: PublicKey) => { choice ); const marker = markers?.[index]?.info; + const markerK = voteMarkerKey(position.mint, proposal)[0]; if (marker && canRelinquishVote) { + const instructions: TransactionInstruction[] = []; + if (position.isProxiedToMe) { if (marker.proxyIndex < (position.proxy?.index || 0)) { // Do not vote with a position that has been delegated to us, but voting overidden return; } - return await vsrProgram.methods - .proxiedRelinquishVoteV0({ + instructions.push( + await vsrProgram.methods + .proxiedRelinquishVoteV0({ + choice, + }) + .accounts({ + proposal, + voter: provider.wallet.publicKey, + position: position.pubkey, + marker: voteMarkerKey(position.mint, proposal)[0], + proxyAssignment: proxyAssignmentKey( + registrar!.proxyConfig, + position.mint, + provider.wallet.publicKey + )[0], + }) + .instruction() + ); + } + instructions.push( + await vsrProgram.methods + .relinquishVoteV1({ choice, }) .accounts({ proposal, voter: provider.wallet.publicKey, position: position.pubkey, - marker: voteMarkerKey(position.mint, proposal)[0], - proxyAssignment: proxyAssignmentKey( - registrar!.proxyConfig, - position.mint, - provider.wallet.publicKey, - )[0], }) - .instruction(); - } - return await vsrProgram.methods - .relinquishVoteV1({ - choice, - }) + .instruction() + ); + } + + instructions.push( + await hsdProgram.methods + .trackVoteV0() .accounts({ proposal, - voter: provider.wallet.publicKey, + marker: markerK, position: position.pubkey, }) - .instruction(); - } + .instruction() + ); + return instructions; }) ) - ).filter(truthy); + ) + .filter(truthy) + .flat(); if (onInstructions) { await onInstructions(instructions); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useVote.ts b/packages/voter-stake-registry-hooks/src/hooks/useVote.ts index fa3b07539..8e4ef8833 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useVote.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useVote.ts @@ -1,20 +1,23 @@ +import { useSolanaUnixNow } from "@helium/helium-react-hooks"; +import { init as hsdInit } from "@helium/helium-sub-daos-sdk"; import { useProposal } from "@helium/modular-governance-hooks"; -import { Status, batchParallelInstructions, truthy } from "@helium/spl-utils"; +import { proxyAssignmentKey } from "@helium/nft-proxy-sdk"; +import { + Status, + batchParallelInstructions, + truthy +} from "@helium/spl-utils"; import { init, voteMarkerKey } from "@helium/voter-stake-registry-sdk"; import { PublicKey, - SYSVAR_CLOCK_PUBKEY, - TransactionInstruction, + TransactionInstruction } from "@solana/web3.js"; import BN from "bn.js"; import { useCallback, useMemo } from "react"; import { useAsyncCallback } from "react-async-hook"; import { useHeliumVsrState } from "../contexts/heliumVsrContext"; -import { useVoteMarkers } from "./useVoteMarkers"; import { calcPositionVotingPower } from "../utils/calcPositionVotingPower"; -import { proxyAssignmentKey } from "@helium/nft-proxy-sdk"; -import { useSolanaUnixNow } from "@helium/helium-react-hooks"; -import { PositionWithMeta } from "../sdk/types"; +import { useVoteMarkers } from "./useVoteMarkers"; export const useVote = (proposalKey: PublicKey) => { const { info: proposal } = useProposal(proposalKey); @@ -154,13 +157,18 @@ export const useVote = (proposalKey: PublicKey) => { ); } else { const vsrProgram = await init(provider); + const hsdProgram = await hsdInit(provider); const instructions = ( await Promise.all( // vote with bigger positions first. sortedPositions.map(async (position, index) => { const marker = markers?.[index]?.info; + const markerK = voteMarkerKey(position.mint, proposalKey)[0]; + const canVote = canPositionVote(index, choice); if (canVote) { + const instructions: TransactionInstruction[] = []; + if (position.isProxiedToMe) { if ( marker && @@ -172,39 +180,55 @@ export const useVote = (proposalKey: PublicKey) => { return; } - return await vsrProgram.methods - .proxiedVoteV0({ + instructions.push( + await vsrProgram.methods + .proxiedVoteV0({ + choice, + }) + .accounts({ + proposal: proposalKey, + voter: provider.wallet.publicKey, + position: position.pubkey, + registrar: registrar?.pubkey, + marker: voteMarkerKey(position.mint, proposalKey)[0], + proxyAssignment: proxyAssignmentKey( + registrar!.proxyConfig, + position.mint, + provider.wallet.publicKey + )[0], + }) + .instruction() + ); + } + instructions.push( + await vsrProgram.methods + .voteV0({ choice, }) .accounts({ proposal: proposalKey, voter: provider.wallet.publicKey, position: position.pubkey, - registrar: registrar?.pubkey, marker: voteMarkerKey(position.mint, proposalKey)[0], - proxyAssignment: proxyAssignmentKey( - registrar!.proxyConfig, - position.mint, - provider.wallet.publicKey - )[0], }) - .instruction(); - } - return await vsrProgram.methods - .voteV0({ - choice, - }) + .instruction() + ); + } + + instructions.push( + await hsdProgram.methods + .trackVoteV0() .accounts({ proposal: proposalKey, - voter: provider.wallet.publicKey, + marker: markerK, position: position.pubkey, - marker: voteMarkerKey(position.mint, proposalKey)[0], }) - .instruction(); - } + .instruction() + ); + return instructions; }) ) - ).filter(truthy); + ).filter(truthy).flat(); if (onInstructions) { await onInstructions(instructions); diff --git a/packages/voter-stake-registry-hooks/yarn.deploy.lock b/packages/voter-stake-registry-hooks/yarn.deploy.lock index 59f45958f..36a6abdd7 100644 --- a/packages/voter-stake-registry-hooks/yarn.deploy.lock +++ b/packages/voter-stake-registry-hooks/yarn.deploy.lock @@ -233,6 +233,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18 diff --git a/programs/helium-sub-daos/Cargo.toml b/programs/helium-sub-daos/Cargo.toml index 6cf81d707..2c37d7bd6 100644 --- a/programs/helium-sub-daos/Cargo.toml +++ b/programs/helium-sub-daos/Cargo.toml @@ -28,6 +28,8 @@ voter-stake-registry = { path = "../voter-stake-registry", features = ["no-entry shared-utils = { workspace = true } circuit-breaker = { workspace = true } treasury-management = { path = "../treasury-management", features = ["cpi"] } +nft-proxy = { path = "../../utils/nft-proxy" } +proposal = { path = "../../utils/proposal" } time = "0.3.17" solana-security-txt = { workspace = true } diff --git a/programs/helium-sub-daos/src/error.rs b/programs/helium-sub-daos/src/error.rs index 7fe26c9a4..df102a4b1 100644 --- a/programs/helium-sub-daos/src/error.rs +++ b/programs/helium-sub-daos/src/error.rs @@ -52,4 +52,7 @@ pub enum ErrorCode { #[msg("Cannot delegate on a position ending this epoch")] NoDelegateEndingPosition, + + #[msg("Invalid vote marker")] + InvalidMarker, } 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 a78236eb2..a4ab8eb49 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 @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use anchor_spl::token::{Mint, Token}; use circuit_breaker::CircuitBreaker; -use shared_utils::precise_number::{PreciseNumber, FOUR_PREC, TWO_PREC}; +use shared_utils::precise_number::PreciseNumber; use voter_stake_registry::state::Registrar; use crate::{current_epoch, error::ErrorCode, state::*, update_subdao_vehnt, EPOCH_LENGTH}; @@ -39,7 +39,7 @@ pub struct CalculateUtilityScoreV0<'info> { #[account( init_if_needed, payer = payer, - space = 60 + 8 + std::mem::size_of::(), + space = DaoEpochInfoV0::size(), seeds = ["dao_epoch_info".as_bytes(), dao.key().as_ref(), &args.epoch.to_le_bytes()], bump, )] @@ -55,6 +55,14 @@ pub struct CalculateUtilityScoreV0<'info> { pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, pub circuit_breaker_program: Program<'info, CircuitBreaker>, + #[account( + init_if_needed, + payer = payer, + space = SubDaoEpochInfoV0::SIZE, + seeds = ["sub_dao_epoch_info".as_bytes(), sub_dao.key().as_ref(), &(args.epoch - 1).to_le_bytes()], + bump, + )] + pub prev_sub_dao_epoch_info: Box>, } pub fn handler( @@ -132,69 +140,35 @@ pub fn handler( ctx.accounts.dao_epoch_info.bump_seed = *ctx.bumps.get("dao_epoch_info").unwrap(); // Calculate utility score - // utility score = V * D * A - // V = max(1, veHNT_dnp). - // D = max(1, sqrt(DCs burned in USD)). 1 DC = $0.00001. - // A = max(1, fourth_root(Total active device count * device activation fee)). + // utility score = V + // V = veHNT_dnp. let epoch_info = &mut ctx.accounts.sub_dao_epoch_info; - let dc_burned = PreciseNumber::new(epoch_info.dc_burned.into()) - .unwrap() - .checked_div(&PreciseNumber::new(100000_u128).unwrap()) // DC has 0 decimals, plus 10^5 to get to dollars. - .unwrap(); - - msg!( - "Total onboarding dc: {}. Dc burned: {}.", - epoch_info.dc_onboarding_fees_paid, - epoch_info.dc_burned - ); - - let devices_with_fee = &PreciseNumber::new(u128::from(epoch_info.dc_onboarding_fees_paid)) - .unwrap() - .checked_div(&PreciseNumber::new(100000_u128).unwrap()) // Need onboarding fee in dollars - .unwrap(); - - // sqrt(x) = e^(ln(x)/2) - // x^1/4 = e^(ln(x)/4)) - let one = PreciseNumber::one(); - let d = if epoch_info.dc_burned > 0 { - std::cmp::max( - one.clone(), - dc_burned - .log() - .unwrap() - .checked_div(&TWO_PREC.clone().signed()) - .unwrap() - .exp() - .unwrap(), - ) - } else { - one.clone() - }; - let vehnt_staked = PreciseNumber::new(epoch_info.vehnt_at_epoch_start.into()) .unwrap() .checked_div(&PreciseNumber::new(100000000_u128).unwrap()) // vehnt has 8 decimals .unwrap(); - let v = std::cmp::max(one.clone(), vehnt_staked); - - let a = if epoch_info.dc_onboarding_fees_paid > 0 { - std::cmp::max( - one, - devices_with_fee - .log() + // Apply a 90 day smooth + let utility_score_prec = vehnt_staked + .checked_div(&PreciseNumber::new(90_u128).unwrap()) + .unwrap() + .checked_add( + &PreciseNumber::new(89_u128) .unwrap() - .checked_div(&FOUR_PREC.clone().signed()) + .checked_mul( + &ctx + .accounts + .prev_sub_dao_epoch_info + .utility_score + .and_then(PreciseNumber::new) + .unwrap_or(vehnt_staked), + ) .unwrap() - .exp() + .checked_div(&PreciseNumber::new(90_u128).unwrap()) .unwrap(), ) - } else { - one - }; - - let utility_score_prec = d.checked_mul(&a).unwrap().checked_mul(&v).unwrap(); + .unwrap(); // Convert to u128 with 12 decimals of precision let utility_score = utility_score_prec .checked_mul( diff --git a/programs/helium-sub-daos/src/instructions/delegation/add_expiration_ts.rs b/programs/helium-sub-daos/src/instructions/delegation/add_expiration_ts.rs new file mode 100644 index 000000000..a2087e66d --- /dev/null +++ b/programs/helium-sub-daos/src/instructions/delegation/add_expiration_ts.rs @@ -0,0 +1,212 @@ +use std::{cmp::min, str::FromStr}; + +use anchor_lang::prelude::*; +use nft_proxy::ProxyConfigV0; +use voter_stake_registry::state::{PositionV0, Registrar}; + +use crate::{ + caclulate_vhnt_info, current_epoch, id, DaoV0, DelegatedPositionV0, SubDaoEpochInfoV0, SubDaoV0, + TESTING, +}; + +#[derive(Accounts)] +pub struct AddExpirationTs<'info> { + #[account( + mut, + address = if TESTING { + payer.key() + } else { + Pubkey::from_str("hprdnjkbziK8NqhThmAn5Gu4XqrBbctX8du4PfJdgvW").unwrap() + } + )] + pub payer: Signer<'info>, + #[account(mut)] + pub position: Account<'info, PositionV0>, + #[account( + has_one = proxy_config + )] + pub registrar: Box>, + #[account( + has_one = registrar, + )] + pub dao: Box>, + #[account( + mut, + has_one = dao, + )] + pub sub_dao: Box>, + #[account( + mut, + seeds = ["delegated_position".as_bytes(), position.key().as_ref()], + has_one = position, + has_one = sub_dao, + bump = delegated_position.bump_seed, + constraint = TESTING || delegated_position.expiration_ts == 0 + )] + pub delegated_position: Account<'info, DelegatedPositionV0>, + #[account( + mut, + seeds = ["sub_dao_epoch_info".as_bytes(), sub_dao.key().as_ref(), ¤t_epoch( + position.lockup.end_ts + ).to_le_bytes()], + bump, + )] + pub old_closing_time_sub_dao_epoch_info: Box>, + #[account( + init_if_needed, + payer = payer, + space = SubDaoEpochInfoV0::SIZE, + seeds = ["sub_dao_epoch_info".as_bytes(), sub_dao.key().as_ref(), ¤t_epoch( + min(proxy_config.get_current_season(registrar.clock_unix_timestamp()).unwrap().end, position.lockup.end_ts) + ).to_le_bytes()], + bump, + )] + pub closing_time_sub_dao_epoch_info: Box>, + #[account( + mut, + seeds = [ + "sub_dao_epoch_info".as_bytes(), + sub_dao.key().as_ref(), + ¤t_epoch( + // If the genesis piece is no longer in effect (has been purged), + // no need to pass an extra account here. Just pass the closing time sdei and + // do not change it. + if position.genesis_end <= registrar.clock_unix_timestamp() { + position.lockup.end_ts + } else { + position.genesis_end + } + ).to_le_bytes() + ], + bump = genesis_end_sub_dao_epoch_info.bump_seed, + )] + pub genesis_end_sub_dao_epoch_info: Box>, + pub proxy_config: Box>, + pub system_program: Program<'info, System>, +} + +pub fn handler(ctx: Context) -> Result<()> { + 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 expiration_ts = ctx + .accounts + .proxy_config + .get_current_season(registrar.clock_unix_timestamp()) + .unwrap() + .end; + ctx.accounts.delegated_position.expiration_ts = expiration_ts; + let epoch = current_epoch(registrar.clock_unix_timestamp()); + + // Calculate vehnt info once + let vehnt_info_old = caclulate_vhnt_info( + ctx.accounts.delegated_position.start_ts, + position, + voting_mint_config, + i64::MAX, + )?; + let vehnt_info_new = caclulate_vhnt_info( + ctx.accounts.delegated_position.start_ts, + position, + voting_mint_config, + expiration_ts, + )?; + + // Store the account keys for comparison + let old_closing_time_key = ctx.accounts.old_closing_time_sub_dao_epoch_info.key(); + let closing_time_key = ctx.accounts.closing_time_sub_dao_epoch_info.key(); + + // Move correction from old_closing_time_sdei to closing_time_sdei if needed + if old_closing_time_key != closing_time_key { + let old_closing_time_sdei = &mut ctx.accounts.old_closing_time_sub_dao_epoch_info; + if old_closing_time_sdei.epoch > epoch { + msg!( + "Subtracting vehnt info from old closing time sdei {:?}", + vehnt_info_old + ); + old_closing_time_sdei.vehnt_in_closing_positions = old_closing_time_sdei + .vehnt_in_closing_positions + .checked_sub(vehnt_info_old.end_vehnt_correction) + .unwrap(); + old_closing_time_sdei.fall_rates_from_closing_positions = old_closing_time_sdei + .fall_rates_from_closing_positions + .checked_sub(vehnt_info_old.end_fall_rate_correction) + .unwrap(); + msg!( + "Post subtraction closing time sdei {} {}", + old_closing_time_sdei.vehnt_in_closing_positions, + old_closing_time_sdei.fall_rates_from_closing_positions + ); + old_closing_time_sdei.exit(&id())?; + } + + // Update closing_time_sdei + ctx.accounts.closing_time_sub_dao_epoch_info.sub_dao = ctx.accounts.sub_dao.key(); + ctx.accounts.closing_time_sub_dao_epoch_info.bump_seed = + *ctx.bumps.get("closing_time_sub_dao_epoch_info").unwrap(); + ctx.accounts.closing_time_sub_dao_epoch_info.epoch = current_epoch(expiration_ts); + let closing_time_sdei = &mut ctx.accounts.closing_time_sub_dao_epoch_info; + if closing_time_sdei.epoch > epoch { + msg!( + "Adding vehnt info to closing time sdei {:?}", + vehnt_info_new, + ); + closing_time_sdei.vehnt_in_closing_positions = closing_time_sdei + .vehnt_in_closing_positions + .checked_add(vehnt_info_new.end_vehnt_correction) + .unwrap(); + closing_time_sdei.fall_rates_from_closing_positions = closing_time_sdei + .fall_rates_from_closing_positions + .checked_add(vehnt_info_new.end_fall_rate_correction) + .unwrap(); + msg!( + "Post addition closing time sdei {} {}", + closing_time_sdei.vehnt_in_closing_positions, + closing_time_sdei.fall_rates_from_closing_positions + ); + } + + closing_time_sdei.exit(&id())?; + } + + // Always update genesis_end_sdei + ctx.accounts.genesis_end_sub_dao_epoch_info.reload()?; + let genesis_end_sdei = &mut ctx.accounts.genesis_end_sub_dao_epoch_info; + if genesis_end_sdei.epoch > epoch { + msg!( + "Subtracting vehnt info from genesis end sdei {:?}", + vehnt_info_old + ); + genesis_end_sdei.vehnt_in_closing_positions = genesis_end_sdei + .vehnt_in_closing_positions + .checked_sub(vehnt_info_old.genesis_end_vehnt_correction) + .unwrap(); + genesis_end_sdei.fall_rates_from_closing_positions = genesis_end_sdei + .fall_rates_from_closing_positions + .checked_sub(vehnt_info_old.genesis_end_fall_rate_correction) + .unwrap(); + msg!( + "Post subtraction genesis end sdei {} {}", + genesis_end_sdei.vehnt_in_closing_positions, + genesis_end_sdei.fall_rates_from_closing_positions + ); + + genesis_end_sdei.vehnt_in_closing_positions = genesis_end_sdei + .vehnt_in_closing_positions + .checked_add(vehnt_info_new.genesis_end_vehnt_correction) + .unwrap(); + genesis_end_sdei.fall_rates_from_closing_positions = genesis_end_sdei + .fall_rates_from_closing_positions + .checked_add(vehnt_info_new.genesis_end_fall_rate_correction) + .unwrap(); + msg!( + "Post addition genesis end sdei {} {}", + genesis_end_sdei.vehnt_in_closing_positions, + genesis_end_sdei.fall_rates_from_closing_positions + ); + + genesis_end_sdei.exit(&id())?; + } + + Ok(()) +} 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..9f2058a0b 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,10 +12,7 @@ use voter_stake_registry::{ VoterStakeRegistry, }; -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] -pub struct ClaimRewardsArgsV0 { - pub epoch: u64, -} +use crate::{current_epoch, error::ErrorCode, state::*, ClaimRewardsArgsV0, TESTING}; #[derive(Accounts)] #[instruction(args: ClaimRewardsArgsV0)] 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..c429bcdf9 --- /dev/null +++ b/programs/helium-sub-daos/src/instructions/delegation/claim_rewards_v1.rs @@ -0,0 +1,223 @@ +use std::collections::HashSet; + +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{burn, Burn, 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, dao_seeds, error::ErrorCode, state::*, TESTING}; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct ClaimRewardsArgsV0 { + pub epoch: u64, +} + +#[derive(Accounts)] +#[instruction(args: ClaimRewardsArgsV0)] +pub struct ClaimRewardsV1<'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, + has_one = delegator_pool, + )] + pub dao: Box>, + + #[account( + mut, + 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 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(), 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> ClaimRewardsV1<'info> { + fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, TransferV0<'info>> { + let cpi_accounts = TransferV0 { + from: self.delegator_pool.to_account_info(), + to: self.delegator_ata.to_account_info(), + owner: self.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) + } + + fn burn_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Burn<'info>> { + let cpi_accounts = Burn { + mint: self.mint.to_account_info(), + authority: self.position_authority.to_account_info(), + from: self.delegator_ata.to_account_info(), + }; + + CpiContext::new(self.token_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 first_ts = ctx.accounts.dao.recent_proposals.last().unwrap().ts; + let last_ts = ctx.accounts.dao.recent_proposals.first().unwrap().ts; + ctx + .accounts + .delegated_position + .remove_proposals_older_than(first_ts - 1); + let proposal_set = ctx + .accounts + .delegated_position + .recent_proposals + .iter() + .filter(|p| p.ts <= last_ts) + .map(|rp| rp.proposal) + .collect::>(); + // Check eligibility based on recent proposals + let eligible_count = ctx + .accounts + .dao + .recent_proposals + .iter() + .filter(|&proposal| proposal_set.contains(&proposal.proposal)) + .count(); + let not_two_proposals = ctx.accounts.dao.recent_proposals.len() < 2 + || ctx + .accounts + .dao + .recent_proposals + .iter() + .filter(|p| p.proposal == Pubkey::default()) + .count() + < 2; + + let amount_left = ctx.accounts.delegator_pool.amount; + let amount = std::cmp::min(rewards, amount_left); + transfer_v0( + ctx + .accounts + .transfer_ctx() + .with_signer(&[dao_seeds!(ctx.accounts.dao)]), + // 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 }, + )?; + + if !not_two_proposals && eligible_count < 2 { + msg!( + "Position is not eligible, burning rewards. Position proposals {:?}, recent proposals {:?}", + ctx.accounts.delegated_position.recent_proposals, + ctx.accounts.dao.recent_proposals + ); + burn(ctx.accounts.burn_ctx(), amount)?; + } + + Ok(()) +} diff --git a/programs/helium-sub-daos/src/instructions/delegation/close_delegation_v0.rs b/programs/helium-sub-daos/src/instructions/delegation/close_delegation_v0.rs index 4c680e47e..d461baa15 100644 --- a/programs/helium-sub-daos/src/instructions/delegation/close_delegation_v0.rs +++ b/programs/helium-sub-daos/src/instructions/delegation/close_delegation_v0.rs @@ -1,15 +1,17 @@ -use crate::{ - caclulate_vhnt_info, current_epoch, id, state::*, update_subdao_vehnt, PrecisePosition, - VehntInfo, TESTING, -}; +use std::cmp::min; + use anchor_lang::prelude::*; use anchor_spl::token::{Mint, TokenAccount}; - use voter_stake_registry::{ state::{LockupKind, PositionV0, Registrar}, VoterStakeRegistry, }; +use crate::{ + caclulate_vhnt_info, current_epoch, id, state::*, update_subdao_vehnt, PrecisePosition, + VehntInfo, TESTING, +}; + #[derive(Accounts)] pub struct CloseDelegationV0<'info> { #[account(mut)] @@ -48,7 +50,7 @@ pub struct CloseDelegationV0<'info> { seeds = ["delegated_position".as_bytes(), position.key().as_ref()], has_one = position, has_one = sub_dao, - bump + bump = delegated_position.bump_seed )] pub delegated_position: Account<'info, DelegatedPositionV0>, #[account( @@ -63,7 +65,16 @@ pub struct CloseDelegationV0<'info> { // They were used when delegate_v0 was called #[account( mut, - seeds = ["sub_dao_epoch_info".as_bytes(), sub_dao.key().as_ref(), ¤t_epoch(position.lockup.end_ts).to_le_bytes()], + seeds = ["sub_dao_epoch_info".as_bytes(), sub_dao.key().as_ref(), ¤t_epoch( + min( + position.lockup.end_ts, + if delegated_position.expiration_ts == 0 { + position.lockup.end_ts + } else { + delegated_position.expiration_ts + } + ) + ).to_le_bytes()], bump = closing_time_sub_dao_epoch_info.bump_seed, )] pub closing_time_sub_dao_epoch_info: Box>, @@ -98,10 +109,12 @@ pub fn handler(ctx: Context) -> Result<()> { let voting_mint_config = ®istrar.voting_mints[position.voting_mint_config_idx as usize]; let curr_ts = registrar.clock_unix_timestamp(); let vehnt_at_curr_ts = position.voting_power_precise(voting_mint_config, curr_ts)?; + let expiration_ts = ctx.accounts.delegated_position.expiration_ts; let vehnt_info = caclulate_vhnt_info( ctx.accounts.delegated_position.start_ts, position, voting_mint_config, + expiration_ts, )?; let VehntInfo { 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..326246da6 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,13 @@ +use std::cmp::min; + +use anchor_lang::{prelude::*, Discriminator}; +use anchor_spl::token::{Mint, TokenAccount}; +use nft_proxy::ProxyConfigV0; +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 +17,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> { @@ -37,6 +40,9 @@ pub struct DelegateV0<'info> { pub position_token_account: Box>, #[account(mut)] pub position_authority: Signer<'info>, + #[account( + has_one = proxy_config + )] pub registrar: Box>, #[account( has_one = registrar, @@ -60,7 +66,9 @@ pub struct DelegateV0<'info> { init_if_needed, payer = payer, space = SubDaoEpochInfoV0::SIZE, - seeds = ["sub_dao_epoch_info".as_bytes(), sub_dao.key().as_ref(), ¤t_epoch(position.lockup.end_ts).to_le_bytes()], + seeds = ["sub_dao_epoch_info".as_bytes(), sub_dao.key().as_ref(), ¤t_epoch( + min(position.lockup.end_ts, proxy_config.get_current_season(registrar.clock_unix_timestamp()).unwrap().end) + ).to_le_bytes()], bump, )] pub closing_time_sub_dao_epoch_info: Box>, @@ -95,6 +103,7 @@ pub struct DelegateV0<'info> { pub vsr_program: Program<'info, VoterStakeRegistry>, pub system_program: Program<'info, System>, + pub proxy_config: Account<'info, ProxyConfigV0>, } pub struct SubDaoEpochInfoV0WithDescriminator { @@ -121,7 +130,14 @@ pub fn handler(ctx: Context) -> Result<()> { let voting_mint_config = ®istrar.voting_mints[position.voting_mint_config_idx as usize]; let curr_ts = registrar.clock_unix_timestamp(); - let vehnt_info = caclulate_vhnt_info(curr_ts, position, voting_mint_config)?; + let expiration_ts = ctx + .accounts + .proxy_config + .get_current_season(curr_ts) + .unwrap() + .end; + + let vehnt_info = caclulate_vhnt_info(curr_ts, position, voting_mint_config, expiration_ts)?; let VehntInfo { has_genesis, vehnt_at_curr_ts, @@ -133,7 +149,11 @@ pub fn handler(ctx: Context) -> Result<()> { end_vehnt_correction, } = vehnt_info; - msg!("Vehnt calculations: {:?}", vehnt_info); + msg!( + "Vehnt calculations: {:?}, expiration ts {}", + vehnt_info, + expiration_ts + ); let curr_epoch = current_epoch(curr_ts); @@ -180,92 +200,90 @@ pub fn handler(ctx: Context) -> Result<()> { .checked_add(end_vehnt_correction) .unwrap(); ctx.accounts.closing_time_sub_dao_epoch_info.sub_dao = sub_dao.key(); - ctx.accounts.closing_time_sub_dao_epoch_info.epoch = current_epoch(position.lockup.end_ts); + ctx.accounts.closing_time_sub_dao_epoch_info.epoch = current_epoch(expiration_ts); ctx.accounts.closing_time_sub_dao_epoch_info.bump_seed = ctx.bumps["closing_time_sub_dao_epoch_info"]; let genesis_end_is_closing = ctx.accounts.genesis_end_sub_dao_epoch_info.key() == ctx.accounts.closing_time_sub_dao_epoch_info.key(); - if position.genesis_end > curr_ts - && (genesis_end_fall_rate_correction > 0 || genesis_end_vehnt_correction > 0) - { - // If the end account doesn't exist, init it. Otherwise just set the correcitons - if !genesis_end_is_closing && ctx.accounts.genesis_end_sub_dao_epoch_info.data_len() == 0 { - msg!("Genesis end doesn't exist, initting"); - let genesis_end_epoch = current_epoch(position.genesis_end); - // Anchor doesn't natively support dynamic account creation using remaining_accounts - // and we have to take it on the manual drive - create_and_serialize_account_signed( - &ctx.accounts.payer.to_account_info(), + // If the end account doesn't exist, init it. Otherwise just set the correcitons + if !genesis_end_is_closing && ctx.accounts.genesis_end_sub_dao_epoch_info.data_len() == 0 { + msg!("Genesis end doesn't exist, initting"); + let genesis_end_epoch = current_epoch(position.genesis_end); + // Anchor doesn't natively support dynamic account creation using remaining_accounts + // and we have to take it on the manual drive + create_and_serialize_account_signed( + &ctx.accounts.payer.to_account_info(), + &ctx + .accounts + .genesis_end_sub_dao_epoch_info + .to_account_info(), + &SubDaoEpochInfoV0WithDescriminator { + sub_dao_epoch_info: SubDaoEpochInfoV0 { + epoch: genesis_end_epoch, + bump_seed: ctx.bumps["genesis_end_sub_dao_epoch_info"], + sub_dao: sub_dao.key(), + dc_burned: 0, + vehnt_at_epoch_start: 0, + vehnt_in_closing_positions: genesis_end_vehnt_correction, + fall_rates_from_closing_positions: genesis_end_fall_rate_correction, + delegation_rewards_issued: 0, + utility_score: None, + rewards_issued_at: None, + initialized: false, + dc_onboarding_fees_paid: 0, + hnt_delegation_rewards_issued: 0, + hnt_rewards_issued: 0, + }, + }, + &[ + "sub_dao_epoch_info".as_bytes(), + sub_dao.key().as_ref(), + &genesis_end_epoch.to_le_bytes(), + ], + &id(), + &ctx.accounts.system_program.to_account_info(), + &Rent::get()?, + 0, + )?; + } else { + // closing can be the same account as genesis end. Make sure to use the proper account + let mut parsed: Account; + let genesis_end_sub_dao_epoch_info: &mut Account = if genesis_end_is_closing + { + &mut ctx.accounts.closing_time_sub_dao_epoch_info + } else { + parsed = Account::try_from( &ctx .accounts .genesis_end_sub_dao_epoch_info .to_account_info(), - &SubDaoEpochInfoV0WithDescriminator { - sub_dao_epoch_info: SubDaoEpochInfoV0 { - epoch: genesis_end_epoch, - bump_seed: ctx.bumps["genesis_end_sub_dao_epoch_info"], - sub_dao: sub_dao.key(), - dc_burned: 0, - vehnt_at_epoch_start: 0, - vehnt_in_closing_positions: genesis_end_vehnt_correction, - fall_rates_from_closing_positions: genesis_end_fall_rate_correction, - delegation_rewards_issued: 0, - utility_score: None, - rewards_issued_at: None, - initialized: false, - dc_onboarding_fees_paid: 0, - }, - }, - &[ - "sub_dao_epoch_info".as_bytes(), - sub_dao.key().as_ref(), - &genesis_end_epoch.to_le_bytes(), - ], - &id(), - &ctx.accounts.system_program.to_account_info(), - &Rent::get()?, - 0, )?; - } else { - // closing can be the same account as genesis end. Make sure to use the proper account - let mut parsed: Account; - let genesis_end_sub_dao_epoch_info: &mut Account = - if genesis_end_is_closing { - &mut ctx.accounts.closing_time_sub_dao_epoch_info - } else { - parsed = Account::try_from( - &ctx - .accounts - .genesis_end_sub_dao_epoch_info - .to_account_info(), - )?; - &mut parsed - }; + &mut parsed + }; - // EDGE CASE: The genesis end could be this epoch. Do not override what was done with update_subdao_vehnt - if genesis_end_sub_dao_epoch_info.key() == ctx.accounts.sub_dao_epoch_info.key() { - genesis_end_sub_dao_epoch_info.fall_rates_from_closing_positions = ctx - .accounts - .sub_dao_epoch_info - .fall_rates_from_closing_positions; - genesis_end_sub_dao_epoch_info.vehnt_in_closing_positions = - ctx.accounts.sub_dao_epoch_info.vehnt_in_closing_positions; - } else { - genesis_end_sub_dao_epoch_info.fall_rates_from_closing_positions = - genesis_end_sub_dao_epoch_info - .fall_rates_from_closing_positions - .checked_add(genesis_end_fall_rate_correction) - .unwrap(); - - genesis_end_sub_dao_epoch_info.vehnt_in_closing_positions = genesis_end_sub_dao_epoch_info - .vehnt_in_closing_positions - .checked_add(genesis_end_vehnt_correction) + // EDGE CASE: The genesis end could be this epoch. Do not override what was done with update_subdao_vehnt + if genesis_end_sub_dao_epoch_info.key() == ctx.accounts.sub_dao_epoch_info.key() { + genesis_end_sub_dao_epoch_info.fall_rates_from_closing_positions = ctx + .accounts + .sub_dao_epoch_info + .fall_rates_from_closing_positions; + genesis_end_sub_dao_epoch_info.vehnt_in_closing_positions = + ctx.accounts.sub_dao_epoch_info.vehnt_in_closing_positions; + } else { + genesis_end_sub_dao_epoch_info.fall_rates_from_closing_positions = + genesis_end_sub_dao_epoch_info + .fall_rates_from_closing_positions + .checked_add(genesis_end_fall_rate_correction) .unwrap(); - } - genesis_end_sub_dao_epoch_info.exit(&id())?; + genesis_end_sub_dao_epoch_info.vehnt_in_closing_positions = genesis_end_sub_dao_epoch_info + .vehnt_in_closing_positions + .checked_add(genesis_end_vehnt_correction) + .unwrap(); } + + genesis_end_sub_dao_epoch_info.exit(&id())?; } delegated_position.purged = false; @@ -276,6 +294,7 @@ pub fn handler(ctx: Context) -> Result<()> { delegated_position.mint = ctx.accounts.mint.key(); delegated_position.position = ctx.accounts.position.key(); delegated_position.bump_seed = ctx.bumps["delegated_position"]; + delegated_position.expiration_ts = expiration_ts; ctx.accounts.sub_dao_epoch_info.sub_dao = ctx.accounts.sub_dao.key(); ctx.accounts.sub_dao_epoch_info.bump_seed = *ctx.bumps.get("sub_dao_epoch_info").unwrap(); diff --git a/programs/helium-sub-daos/src/instructions/delegation/mod.rs b/programs/helium-sub-daos/src/instructions/delegation/mod.rs index ff4ca1044..5c4eefe2f 100644 --- a/programs/helium-sub-daos/src/instructions/delegation/mod.rs +++ b/programs/helium-sub-daos/src/instructions/delegation/mod.rs @@ -1,11 +1,17 @@ +pub mod add_expiration_ts; 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 track_vote_v0; pub mod transfer_v0; +pub use add_expiration_ts::*; pub use claim_rewards_v0::*; +pub use claim_rewards_v1::*; pub use close_delegation_v0::*; pub use delegate_v0::*; pub use reset_lockup_v0::*; +pub use track_vote_v0::*; pub use transfer_v0::*; diff --git a/programs/helium-sub-daos/src/instructions/delegation/track_vote_v0.rs b/programs/helium-sub-daos/src/instructions/delegation/track_vote_v0.rs new file mode 100644 index 000000000..44f5d708f --- /dev/null +++ b/programs/helium-sub-daos/src/instructions/delegation/track_vote_v0.rs @@ -0,0 +1,104 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::Mint; +use proposal::ProposalV0; +use shared_utils::resize_to_fit; +use voter_stake_registry::{ + state::{PositionV0, Registrar, VoteMarkerV0}, + VoterStakeRegistry, +}; + +use crate::{ + current_epoch, + error::ErrorCode, + state::{DaoEpochInfoV0, DaoV0, DelegatedPositionV0}, + SubDaoV0, +}; +#[derive(Accounts)] +pub struct TrackVoteV0<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + constraint = proposal.namespace == dao.proposal_namespace + )] + pub proposal: Account<'info, ProposalV0>, + pub registrar: Box>, + #[account( + mut, + has_one = mint, + has_one = registrar, + constraint = position.registrar == dao.registrar + )] + pub position: Box>, + pub mint: Box>, + /// CHECK: Checked by seeds + #[account( + seeds = [b"marker", mint.key().as_ref(), proposal.key().as_ref()], + bump, + seeds::program = vsr_program + )] + pub marker: UncheckedAccount<'info>, + #[account(mut)] + pub dao: Box>, + #[account(has_one = dao)] + pub sub_dao: Box>, + #[account(mut, + has_one = sub_dao, + seeds = ["delegated_position".as_bytes(), position.key().as_ref()], + bump = delegated_position.bump_seed, + )] + pub delegated_position: Box>, + #[account( + init_if_needed, + payer = payer, + space = DaoEpochInfoV0::size(), + seeds = ["dao_epoch_info".as_bytes(), dao.key().as_ref(), ¤t_epoch(registrar.clock_unix_timestamp()).to_le_bytes()], + bump, + )] + pub dao_epoch_info: Account<'info, DaoEpochInfoV0>, + pub vsr_program: Program<'info, VoterStakeRegistry>, + pub system_program: Program<'info, System>, +} +pub fn handler(ctx: Context) -> Result<()> { + ctx.accounts.dao_epoch_info.epoch = current_epoch(ctx.accounts.registrar.clock_unix_timestamp()); + ctx.accounts.dao_epoch_info.dao = ctx.accounts.dao.key(); + ctx.accounts.dao_epoch_info.bump_seed = *ctx.bumps.get("dao_epoch_info").unwrap(); + ctx.accounts.dao.add_recent_proposal( + ctx.accounts.proposal.key(), + ctx.accounts.proposal.created_at, + ); + ctx.accounts.dao_epoch_info.recent_proposals = ctx.accounts.dao.recent_proposals.clone(); + let data = ctx.accounts.marker.data.try_borrow().unwrap(); + let has_data = !data.is_empty(); + drop(data); + let mut voted = has_data; + if has_data { + let marker: Account = Account::try_from(&ctx.accounts.marker.to_account_info())?; + require_eq!( + marker.registrar, + ctx.accounts.position.registrar, + ErrorCode::InvalidMarker + ); + voted = !marker.choices.is_empty(); + } + if voted { + ctx.accounts.delegated_position.add_recent_proposal( + ctx.accounts.proposal.key(), + ctx.accounts.proposal.created_at, + ); + msg!( + "Proposals are now {:?}", + ctx.accounts.delegated_position.recent_proposals + ); + resize_to_fit( + &ctx.accounts.payer, + &ctx.accounts.system_program.to_account_info(), + &ctx.accounts.delegated_position, + )?; + } else { + ctx + .accounts + .delegated_position + .remove_recent_proposal(ctx.accounts.proposal.key()); + } + Ok(()) +} diff --git a/programs/helium-sub-daos/src/instructions/initialize_dao_v0.rs b/programs/helium-sub-daos/src/instructions/initialize_dao_v0.rs index 07fc6bea8..1e225287d 100644 --- a/programs/helium-sub-daos/src/instructions/initialize_dao_v0.rs +++ b/programs/helium-sub-daos/src/instructions/initialize_dao_v0.rs @@ -1,13 +1,23 @@ -use crate::{state::*, EPOCH_LENGTH}; +use std::array; + use anchor_lang::prelude::*; -use anchor_spl::token::spl_token::instruction::AuthorityType; -use anchor_spl::token::{set_authority, SetAuthority, TokenAccount}; -use anchor_spl::token::{Mint, Token}; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{ + set_authority, spl_token::instruction::AuthorityType, Mint, SetAuthority, Token, TokenAccount, + }, +}; use circuit_breaker::{ - cpi::{accounts::InitializeMintWindowedBreakerV0, initialize_mint_windowed_breaker_v0}, - CircuitBreaker, InitializeMintWindowedBreakerArgsV0, + cpi::{ + accounts::{InitializeAccountWindowedBreakerV0, InitializeMintWindowedBreakerV0}, + initialize_account_windowed_breaker_v0, initialize_mint_windowed_breaker_v0, + }, + CircuitBreaker, InitializeAccountWindowedBreakerArgsV0, InitializeMintWindowedBreakerArgsV0, + ThresholdType, ThresholdType as CBThresholdType, WindowedCircuitBreakerConfigV0, + WindowedCircuitBreakerConfigV0 as CBWindowedCircuitBreakerConfigV0, }; -use circuit_breaker::{ThresholdType, WindowedCircuitBreakerConfigV0}; + +use crate::{dao_seeds, state::*, EPOCH_LENGTH}; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct InitializeDaoArgsV0 { @@ -16,6 +26,8 @@ pub struct InitializeDaoArgsV0 { pub hst_emission_schedule: Vec, pub net_emissions_cap: u64, pub registrar: Pubkey, + pub proposal_namespace: Pubkey, + pub delegator_rewards_percent: u64, } #[derive(Accounts)] @@ -51,9 +63,77 @@ pub struct InitializeDaoV0<'info> { pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, pub circuit_breaker_program: Program<'info, CircuitBreaker>, + /// 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( + token::mint = hnt_mint + )] + pub rewards_escrow: Box>, + + #[account( + init, + payer = payer, + associated_token::mint = hnt_mint, + associated_token::authority = dao, + )] + pub delegator_pool: Box>, + pub associated_token_program: Program<'info, AssociatedToken>, } pub fn handler(ctx: Context, args: InitializeDaoArgsV0) -> Result<()> { + require_gte!( + 100_u64.checked_mul(10_0000000).unwrap(), + args.delegator_rewards_percent, + ); + ctx.accounts.dao.set_inner(DaoV0 { + delegator_rewards_percent: args.delegator_rewards_percent, + hst_emission_schedule: args.hst_emission_schedule, + dc_mint: ctx.accounts.dc_mint.key(), + hnt_mint: ctx.accounts.hnt_mint.key(), + authority: args.authority, + num_sub_daos: 0, + emission_schedule: args.emission_schedule.clone(), + registrar: args.registrar, + bump_seed: ctx.bumps["dao"], + net_emissions_cap: args.net_emissions_cap, + hst_pool: ctx.accounts.hst_pool.key(), + delegator_pool: ctx.accounts.delegator_pool.key(), + rewards_escrow: ctx.accounts.rewards_escrow.key(), + recent_proposals: array::from_fn(|_| RecentProposal::default()), + proposal_namespace: args.proposal_namespace, + }); + initialize_account_windowed_breaker_v0( + CpiContext::new_with_signer( + ctx.accounts.circuit_breaker_program.to_account_info(), + InitializeAccountWindowedBreakerV0 { + payer: ctx.accounts.payer.to_account_info(), + circuit_breaker: ctx + .accounts + .delegator_pool_circuit_breaker + .to_account_info(), + token_account: ctx.accounts.delegator_pool.to_account_info(), + owner: ctx.accounts.dao.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + }, + &[dao_seeds!(&ctx.accounts.dao)], + ), + InitializeAccountWindowedBreakerArgsV0 { + authority: args.authority, + config: CBWindowedCircuitBreakerConfigV0 { + window_size_seconds: u64::try_from(EPOCH_LENGTH).unwrap(), + threshold_type: CBThresholdType::Absolute, + threshold: 5 * args.emission_schedule[0].emissions_per_epoch, + }, + owner: ctx.accounts.dao.key(), + }, + )?; initialize_mint_windowed_breaker_v0( CpiContext::new( ctx.accounts.circuit_breaker_program.to_account_info(), @@ -90,18 +170,5 @@ pub fn handler(ctx: Context, args: InitializeDaoArgsV0) -> Resu Some(ctx.accounts.dao.key()), )?; - ctx.accounts.dao.set_inner(DaoV0 { - hst_emission_schedule: args.hst_emission_schedule, - dc_mint: ctx.accounts.dc_mint.key(), - hnt_mint: ctx.accounts.hnt_mint.key(), - authority: args.authority, - num_sub_daos: 0, - emission_schedule: args.emission_schedule, - registrar: args.registrar, - bump_seed: ctx.bumps["dao"], - net_emissions_cap: args.net_emissions_cap, - hst_pool: ctx.accounts.hst_pool.key(), - }); - Ok(()) } 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..62ff6b91d --- /dev/null +++ b/programs/helium-sub-daos/src/instructions/initialize_hnt_delegator_pool.rs @@ -0,0 +1,91 @@ +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 crate::{dao_seeds, state::*, EPOCH_LENGTH}; + +#[derive(Accounts)] +pub struct InitializeHntDelegatorPool<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + mut, + has_one = authority, + has_one = hnt_mint + )] + pub dao: Box>, + pub authority: Signer<'info>, + 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, + associated_token::mint = hnt_mint, + associated_token::authority = dao, + )] + pub delegator_pool: Box>, + #[account( + token::mint = hnt_mint, + )] + pub rewards_escrow: 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> InitializeHntDelegatorPool<'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.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]]] = &[dao_seeds!(ctx.accounts.dao)]; + + initialize_account_windowed_breaker_v0( + ctx + .accounts + .initialize_delegator_pool_breaker_ctx() + .with_signer(signer_seeds), + InitializeAccountWindowedBreakerArgsV0 { + authority: ctx.accounts.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.dao.key(), + }, + )?; + ctx.accounts.dao.delegator_pool = ctx.accounts.delegator_pool.key(); + + 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 ee267f204..cb6bc545b 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 @@ -1,20 +1,9 @@ -use crate::next_epoch_ts; -use crate::{state::*, EPOCH_LENGTH}; use anchor_lang::prelude::*; -use anchor_spl::associated_token::AssociatedToken; -use anchor_spl::token::spl_token::instruction::AuthorityType; -use anchor_spl::token::{set_authority, Mint, SetAuthority, Token, TokenAccount}; -use circuit_breaker::{ - cpi::{ - accounts::InitializeAccountWindowedBreakerV0, accounts::InitializeMintWindowedBreakerV0, - initialize_account_windowed_breaker_v0, initialize_mint_windowed_breaker_v0, - }, - CircuitBreaker, InitializeAccountWindowedBreakerArgsV0, InitializeMintWindowedBreakerArgsV0, -}; -use circuit_breaker::{ - ThresholdType as CBThresholdType, - WindowedCircuitBreakerConfigV0 as CBWindowedCircuitBreakerConfigV0, +use anchor_spl::{ + associated_token::AssociatedToken, + token::{set_authority, spl_token::instruction::AuthorityType, Mint, SetAuthority, Token}, }; +use circuit_breaker::CircuitBreaker; use shared_utils::resize_to_fit; use time::OffsetDateTime; use treasury_management::{ @@ -26,6 +15,8 @@ use treasury_management::{ Curve as TreasuryCurve, InitializeTreasuryManagementArgsV0, TreasuryManagement, }; +use crate::{next_epoch_ts, state::*}; + #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub enum Curve { // c^k @@ -55,7 +46,6 @@ pub struct InitializeSubDaoArgsV0 { /// Authority to burn delegated data credits pub dc_burn_authority: Pubkey, pub registrar: Pubkey, - pub delegator_rewards_percent: u64, pub onboarding_data_only_dc_fee: u64, pub active_device_authority: Pubkey, } @@ -85,14 +75,6 @@ pub struct InitializeSubDaoV0<'info> { pub dnt_mint: Box>, pub dnt_mint_authority: Signer<'info>, pub sub_dao_freeze_authority: Signer<'info>, - /// CHECK: Initialized via cpi - #[account( - mut, - seeds = ["mint_windowed_breaker".as_bytes(), dnt_mint.key().as_ref()], - seeds::program = circuit_breaker_program.key(), - bump - )] - pub circuit_breaker: AccountInfo<'info>, /// CHECK: Checked via CPI #[account(mut)] pub treasury: AccountInfo<'info>, @@ -112,28 +94,6 @@ pub struct InitializeSubDaoV0<'info> { bump, )] pub treasury_management: AccountInfo<'info>, - #[account( - token::mint = dnt_mint - )] - pub rewards_escrow: 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(), dnt_mint.key().as_ref()], - bump, - token::mint = dnt_mint, - token::authority = sub_dao, - )] - pub delegator_pool: Box>, pub system_program: Program<'info, System>, pub token_program: Program<'info, Token>, @@ -151,42 +111,7 @@ pub fn create_end_epoch_cron(curr_ts: i64, offset: u64) -> String { format!("0 {:?} {:?} * * * *", dt.minute(), dt.hour()) } -impl<'info> InitializeSubDaoV0<'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) - } - - fn initialize_dnt_mint_breaker_ctx( - &self, - ) -> CpiContext<'_, '_, '_, 'info, InitializeMintWindowedBreakerV0<'info>> { - let cpi_accounts = InitializeMintWindowedBreakerV0 { - payer: self.payer.to_account_info(), - circuit_breaker: self.circuit_breaker.to_account_info(), - mint: self.dnt_mint.to_account_info(), - mint_authority: self.dnt_mint_authority.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, args: InitializeSubDaoArgsV0) -> Result<()> { - let signer_seeds: &[&[&[u8]]] = &[&[ - "sub_dao".as_bytes(), - ctx.accounts.dnt_mint.to_account_info().key.as_ref(), - &[ctx.bumps["sub_dao"]], - ]]; initialize_treasury_management_v0( CpiContext::new( ctx.accounts.treasury_management_program.to_account_info(), @@ -216,37 +141,6 @@ pub fn handler(ctx: Context, args: InitializeSubDaoArgsV0) - }, )?; - initialize_mint_windowed_breaker_v0( - ctx.accounts.initialize_dnt_mint_breaker_ctx(), - InitializeMintWindowedBreakerArgsV0 { - authority: args.authority, - config: CBWindowedCircuitBreakerConfigV0 { - // No more than 5 epochs worth can be distributed. We should be distributing once per epoch so this - // should never get triggered. - window_size_seconds: u64::try_from(EPOCH_LENGTH).unwrap(), - threshold_type: CBThresholdType::Absolute, - threshold: 5 * args.emission_schedule[0].emissions_per_epoch, - }, - mint_authority: ctx.accounts.sub_dao.key(), - }, - )?; - - initialize_account_windowed_breaker_v0( - ctx - .accounts - .initialize_delegator_pool_breaker_ctx() - .with_signer(signer_seeds), - InitializeAccountWindowedBreakerArgsV0 { - authority: args.authority, - config: CBWindowedCircuitBreakerConfigV0 { - window_size_seconds: u64::try_from(EPOCH_LENGTH).unwrap(), - threshold_type: CBThresholdType::Absolute, - threshold: 5 * args.emission_schedule[0].emissions_per_epoch, - }, - owner: ctx.accounts.sub_dao.key(), - }, - )?; - set_authority( CpiContext::new( ctx.accounts.token_program.to_account_info(), @@ -259,10 +153,6 @@ pub fn handler(ctx: Context, args: InitializeSubDaoArgsV0) - Some(ctx.accounts.sub_dao.key()), )?; - require_gte!( - 100_u64.checked_mul(10_0000000).unwrap(), - args.delegator_rewards_percent, - ); ctx.accounts.dao.num_sub_daos += 1; ctx.accounts.sub_dao.set_inner(SubDaoV0 { _deprecated_active_device_aggregator: Pubkey::default(), @@ -271,7 +161,7 @@ pub fn handler(ctx: Context, args: InitializeSubDaoArgsV0) - dc_burn_authority: args.dc_burn_authority, treasury: ctx.accounts.treasury.key(), onboarding_dc_fee: args.onboarding_dc_fee, - rewards_escrow: ctx.accounts.rewards_escrow.key(), + rewards_escrow: Pubkey::default(), authority: args.authority, emission_schedule: args.emission_schedule, registrar: args.registrar, @@ -279,8 +169,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_rewards_percent: args.delegator_rewards_percent, + delegator_pool: Pubkey::default(), + _deprecated_delegator_rewards_percent: 0, onboarding_data_only_dc_fee: args.onboarding_data_only_dc_fee, active_device_authority: args.active_device_authority, dc_onboarding_fees_paid: 0, diff --git a/programs/helium-sub-daos/src/instructions/issue_hst_pool_v0.rs b/programs/helium-sub-daos/src/instructions/issue_hst_pool_v0.rs deleted file mode 100644 index ea255968c..000000000 --- a/programs/helium-sub-daos/src/instructions/issue_hst_pool_v0.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::{current_epoch, error::ErrorCode, state::*, EPOCH_LENGTH, TESTING}; -use anchor_lang::prelude::*; -use anchor_spl::token::{Mint, Token, TokenAccount}; -use circuit_breaker::{ - cpi::{accounts::MintV0, mint_v0}, - CircuitBreaker, MintArgsV0, MintWindowedCircuitBreakerV0, -}; - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] -pub struct IssueHstPoolArgsV0 { - pub epoch: u64, -} - -#[derive(Accounts)] -#[instruction(args: IssueHstPoolArgsV0)] -pub struct IssueHstPoolV0<'info> { - #[account( - mut, - has_one = hnt_mint, - has_one = hst_pool - )] - pub dao: Box>, - #[account( - mut, - has_one = dao, - constraint = dao_epoch_info.num_utility_scores_calculated >= dao.num_sub_daos @ ErrorCode::MissingUtilityScores, - seeds = ["dao_epoch_info".as_bytes(), dao.key().as_ref(), &args.epoch.to_le_bytes()], - bump = dao_epoch_info.bump_seed, - constraint = !dao_epoch_info.done_issuing_hst_pool - )] - pub dao_epoch_info: Box>, - #[account( - mut, - seeds = ["mint_windowed_breaker".as_bytes(), hnt_mint.key().as_ref()], - seeds::program = circuit_breaker_program.key(), - bump = hnt_circuit_breaker.bump_seed - )] - pub hnt_circuit_breaker: Box>, - #[account(mut)] - pub hnt_mint: Box>, - #[account(mut)] - pub hst_pool: Box>, - pub system_program: Program<'info, System>, - pub token_program: Program<'info, Token>, - pub circuit_breaker_program: Program<'info, CircuitBreaker>, -} - -impl<'info> IssueHstPoolV0<'info> { - pub fn mint_hst_emissions_ctx(&self) -> CpiContext<'_, '_, '_, 'info, MintV0<'info>> { - let cpi_accounts = MintV0 { - mint: self.hnt_mint.to_account_info(), - to: self.hst_pool.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(), - }; - - CpiContext::new(self.circuit_breaker_program.to_account_info(), cpi_accounts) - } -} - -pub fn handler(ctx: Context, args: IssueHstPoolArgsV0) -> Result<()> { - let curr_ts = Clock::get()?.unix_timestamp; - let epoch_curr_ts = current_epoch(curr_ts); - let end_of_epoch_ts = i64::try_from(args.epoch + 1).unwrap() * EPOCH_LENGTH; - - if !TESTING && args.epoch >= epoch_curr_ts { - return Err(error!(ErrorCode::EpochNotOver)); - } - - let total_emissions = ctx.accounts.dao_epoch_info.total_rewards; - let percent = ctx - .accounts - .dao - .hst_emission_schedule - .get_percent_at(end_of_epoch_ts) - .unwrap(); - // Subdaos get the remainder after hst - let emissions = u64::from(percent) - .checked_mul(total_emissions) - .unwrap() - .checked_div(100) - .unwrap(); - - mint_v0( - ctx.accounts.mint_hst_emissions_ctx().with_signer(&[&[ - b"dao", - ctx.accounts.hnt_mint.key().as_ref(), - &[ctx.accounts.dao.bump_seed], - ]]), - MintArgsV0 { amount: emissions }, - )?; - - ctx.accounts.dao_epoch_info.done_issuing_hst_pool = true; - - Ok(()) -} 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 6d7ece90d..5679d6a50 100644 --- a/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs +++ b/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs @@ -22,6 +22,8 @@ pub struct IssueRewardsArgsV0 { pub struct IssueRewardsV0<'info> { #[account( has_one = hnt_mint, + has_one = delegator_pool, + has_one = rewards_escrow, )] pub dao: Box>, #[account( @@ -29,8 +31,6 @@ pub struct IssueRewardsV0<'info> { has_one = dao, has_one = treasury, has_one = dnt_mint, - has_one = rewards_escrow, - has_one = delegator_pool, )] pub sub_dao: Box>, #[account( @@ -57,13 +57,6 @@ pub struct IssueRewardsV0<'info> { bump = hnt_circuit_breaker.bump_seed )] pub hnt_circuit_breaker: Box>, - #[account( - mut, - seeds = ["mint_windowed_breaker".as_bytes(), dnt_mint.key().as_ref()], - seeds::program = circuit_breaker_program.key(), - bump = dnt_circuit_breaker.bump_seed - )] - pub dnt_circuit_breaker: Box>, #[account(mut)] pub hnt_mint: Box>, #[account(mut)] @@ -86,24 +79,12 @@ fn to_prec(n: Option) -> Option { } impl<'info> IssueRewardsV0<'info> { - pub fn mint_dnt_emissions_ctx(&self) -> CpiContext<'_, '_, '_, 'info, MintV0<'info>> { - let cpi_accounts = MintV0 { - mint: self.dnt_mint.to_account_info(), - to: self.rewards_escrow.to_account_info(), - mint_authority: self.sub_dao.to_account_info(), - circuit_breaker: self.dnt_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 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(), }; @@ -141,7 +122,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 @@ -149,7 +130,7 @@ 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() @@ -164,52 +145,23 @@ pub fn handler(ctx: Context, args: IssueRewardsArgsV0) -> Result .ok_or_else(|| error!(ErrorCode::ArithmeticError))? .try_into() .unwrap(); - - 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, - )) + + let delegation_rewards_amount = (rewards_amount as u128) + .checked_mul(u128::from(ctx.accounts.dao.delegator_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.checked_sub(dnt_emissions).unwrap() - } else { - 0 - }; - - msg!("Minting {} delegation rewards", delegation_rewards_amount); if delegation_rewards_amount > 0 { + msg!("Minting {} delegation rewards", delegation_rewards_amount); 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], - ]]), + ctx + .accounts + .mint_delegation_rewards_ctx() + .with_signer(&[dao_seeds!(ctx.accounts.dao)]), MintArgsV0 { amount: delegation_rewards_amount, // send some dnt emissions to delegation pool }, @@ -237,20 +189,25 @@ 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(&[dao_seeds!(ctx.accounts.dao)]), 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 f9b5b423e..416d0077d 100644 --- a/programs/helium-sub-daos/src/instructions/mod.rs +++ b/programs/helium-sub-daos/src/instructions/mod.rs @@ -3,10 +3,11 @@ 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; pub mod switch_mobile_ops_fund; +pub mod temp_resize_account; pub mod temp_update_sub_dao_epoch_info; pub mod track_dc_burn_v0; pub mod track_dc_onboarding_fees_v0; @@ -19,10 +20,11 @@ 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::*; pub use switch_mobile_ops_fund::*; +pub use temp_resize_account::*; pub use temp_update_sub_dao_epoch_info::*; pub use track_dc_burn_v0::*; pub use track_dc_onboarding_fees_v0::*; diff --git a/programs/helium-sub-daos/src/instructions/temp_resize_account.rs b/programs/helium-sub-daos/src/instructions/temp_resize_account.rs new file mode 100644 index 000000000..791a3ae8a --- /dev/null +++ b/programs/helium-sub-daos/src/instructions/temp_resize_account.rs @@ -0,0 +1,42 @@ +use std::str::FromStr; + +use anchor_lang::{ + prelude::*, + solana_program::{program::invoke, system_instruction}, +}; +use voter_stake_registry::TESTING; + +use crate::RecentProposal; + +#[derive(Accounts)] +pub struct TempResizeAccount<'info> { + #[account( + mut, + address = if TESTING { payer.key() } else { Pubkey::from_str("hprdnjkbziK8NqhThmAn5Gu4XqrBbctX8du4PfJdgvW").unwrap() } + )] + pub payer: Signer<'info>, + /// CHECK: Resizing account + #[account(mut)] + pub account: AccountInfo<'info>, + pub system_program: Program<'info, System>, +} + +pub fn handler(ctx: Context) -> Result<()> { + let account = &mut ctx.accounts.account; + let new_size = account.data_len() + std::mem::size_of::() * 4; + let rent = Rent::get()?; + let new_minimum_balance = rent.minimum_balance(new_size); + let lamports_diff = new_minimum_balance.saturating_sub(account.to_account_info().lamports()); + + invoke( + &system_instruction::transfer(ctx.accounts.payer.key, &account.key(), lamports_diff), + &[ + ctx.accounts.payer.to_account_info().clone(), + account.to_account_info().clone(), + ctx.accounts.system_program.to_account_info().clone(), + ], + )?; + account.to_account_info().realloc(new_size, false)?; + + Ok(()) +} diff --git a/programs/helium-sub-daos/src/instructions/update_dao_v0.rs b/programs/helium-sub-daos/src/instructions/update_dao_v0.rs index 0dc53c214..a3c722987 100644 --- a/programs/helium-sub-daos/src/instructions/update_dao_v0.rs +++ b/programs/helium-sub-daos/src/instructions/update_dao_v0.rs @@ -1,7 +1,8 @@ -use crate::state::*; use anchor_lang::prelude::*; use shared_utils::resize_to_fit; +use crate::state::*; + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct UpdateDaoArgsV0 { pub authority: Option, @@ -9,6 +10,8 @@ pub struct UpdateDaoArgsV0 { pub hst_emission_schedule: Option>, pub hst_pool: Option, pub net_emissions_cap: Option, + pub proposal_namespace: Option, + pub delegator_rewards_percent: Option, } #[derive(Accounts)] @@ -51,6 +54,18 @@ pub fn handler(ctx: Context, args: UpdateDaoArgsV0) -> Result<()> { ctx.accounts.dao.hst_pool = hst_pool; } + if let Some(proposal_namespace) = args.proposal_namespace { + ctx.accounts.dao.proposal_namespace = proposal_namespace; + } + + if let Some(delegator_rewards_percent) = args.delegator_rewards_percent { + require_gte!( + 100_u64.checked_mul(10_0000000).unwrap(), + delegator_rewards_percent, + ); + ctx.accounts.dao.delegator_rewards_percent = delegator_rewards_percent; + } + if should_resize { resize_to_fit( &ctx.accounts.payer.to_account_info(), diff --git a/programs/helium-sub-daos/src/instructions/update_sub_dao_v0.rs b/programs/helium-sub-daos/src/instructions/update_sub_dao_v0.rs index c2c9f873f..f209bc782 100644 --- a/programs/helium-sub-daos/src/instructions/update_sub_dao_v0.rs +++ b/programs/helium-sub-daos/src/instructions/update_sub_dao_v0.rs @@ -1,7 +1,8 @@ -use crate::state::*; use anchor_lang::prelude::*; use shared_utils::resize_to_fit; +use crate::state::*; + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct UpdateSubDaoArgsV0 { pub authority: Option, @@ -9,7 +10,6 @@ pub struct UpdateSubDaoArgsV0 { pub onboarding_dc_fee: Option, pub dc_burn_authority: Option, pub registrar: Option, - pub delegator_rewards_percent: Option, pub onboarding_data_only_dc_fee: Option, pub active_device_authority: Option, } @@ -55,12 +55,6 @@ pub fn handler(ctx: Context, args: UpdateSubDaoArgsV0) -> Result ctx.accounts.sub_dao.onboarding_data_only_dc_fee = onboarding_data_only_dc_fee; } - let max_percent = 100_u64.checked_mul(10_0000000).unwrap(); - if let Some(delegator_rewards_percent) = args.delegator_rewards_percent { - require_gte!(max_percent, delegator_rewards_percent); - ctx.accounts.sub_dao.delegator_rewards_percent = delegator_rewards_percent; - } - if let Some(active_device_authority) = args.active_device_authority { ctx.accounts.sub_dao.active_device_authority = active_device_authority; } diff --git a/programs/helium-sub-daos/src/lib.rs b/programs/helium-sub-daos/src/lib.rs index 5cb9c8726..35f300312 100644 --- a/programs/helium-sub-daos/src/lib.rs +++ b/programs/helium-sub-daos/src/lib.rs @@ -95,12 +95,12 @@ pub mod helium_sub_daos { claim_rewards_v0::handler(ctx, args) } - pub fn transfer_v0(ctx: Context, args: TransferArgsV0) -> Result<()> { - transfer_v0::handler(ctx, args) + pub fn claim_rewards_v1(ctx: Context, args: ClaimRewardsArgsV0) -> Result<()> { + claim_rewards_v1::handler(ctx, args) } - pub fn issue_hst_pool_v0(ctx: Context, args: IssueHstPoolArgsV0) -> Result<()> { - issue_hst_pool_v0::handler(ctx, args) + pub fn transfer_v0(ctx: Context, args: TransferArgsV0) -> Result<()> { + transfer_v0::handler(ctx, args) } pub fn reset_lockup_v0(ctx: Context, args: ResetLockupArgsV0) -> Result<()> { @@ -131,4 +131,20 @@ pub mod helium_sub_daos { pub fn switch_mobile_ops_fund(ctx: Context) -> Result<()> { switch_mobile_ops_fund::handler(ctx) } + + pub fn initialize_hnt_delegator_pool(ctx: Context) -> Result<()> { + initialize_hnt_delegator_pool::handler(ctx) + } + + pub fn add_expiration_ts(ctx: Context) -> Result<()> { + add_expiration_ts::handler(ctx) + } + + pub fn temp_resize_account(ctx: Context) -> Result<()> { + temp_resize_account::handler(ctx) + } + + pub fn track_vote_v0(ctx: Context) -> Result<()> { + track_vote_v0::handler(ctx) + } } diff --git a/programs/helium-sub-daos/src/state.rs b/programs/helium-sub-daos/src/state.rs index 025a13230..20af8e778 100644 --- a/programs/helium-sub-daos/src/state.rs +++ b/programs/helium-sub-daos/src/state.rs @@ -1,7 +1,6 @@ -use crate::error::ErrorCode; use anchor_lang::prelude::*; -use crate::EPOCH_LENGTH; +use crate::{error::ErrorCode, EPOCH_LENGTH}; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct EmissionScheduleItem { @@ -101,6 +100,11 @@ pub struct DaoV0 { pub emission_schedule: Vec, pub hst_emission_schedule: Vec, pub bump_seed: u8, + pub rewards_escrow: Pubkey, + pub delegator_pool: Pubkey, + pub delegator_rewards_percent: u64, // number between 0 - (100_u64 * 100_000_000). The % of DNT rewards delegators receive with 8 decimal places of accuracy + pub proposal_namespace: Pubkey, + pub recent_proposals: [RecentProposal; 4], } #[macro_export] @@ -110,6 +114,31 @@ macro_rules! dao_seeds { }; } +impl DaoV0 { + pub fn add_recent_proposal(&mut self, proposal: Pubkey, ts: i64) { + let new_proposal = RecentProposal { proposal, ts }; + // Find the insertion point to maintain descending order by timestamp + let insert_index = self + .recent_proposals + .iter() + .position(|p| p.ts <= ts) + .unwrap_or(self.recent_proposals.len()); + let cloned_proposals = self.recent_proposals.clone(); + // Shift elements to make room for the new proposal + if insert_index < self.recent_proposals.len() { + for i in (insert_index + 1..self.recent_proposals.len()).rev() { + self.recent_proposals[i] = cloned_proposals[i - 1].clone(); + } + self.recent_proposals[insert_index] = new_proposal; + } else if ts > self.recent_proposals[self.recent_proposals.len() - 1].ts { + // If the new proposal is more recent than the oldest one, replace the oldest + self.recent_proposals[self.recent_proposals.len() - 1] = new_proposal; + } + // Re-sort the array to ensure it's in descending order by timestamp + self.recent_proposals.sort_by(|a, b| b.ts.cmp(&a.ts)); + } +} + #[account] #[derive(Default)] pub struct DaoEpochInfoV0 { @@ -125,6 +154,19 @@ pub struct DaoEpochInfoV0 { pub done_issuing_rewards: bool, pub done_issuing_hst_pool: bool, pub bump_seed: u8, + pub recent_proposals: [RecentProposal; 4], +} + +#[derive(Debug, InitSpace, Clone, AnchorSerialize, AnchorDeserialize, Default)] +pub struct RecentProposal { + pub proposal: Pubkey, + pub ts: i64, +} + +impl DaoEpochInfoV0 { + pub fn size() -> usize { + 60 + 8 + std::mem::size_of::() + } } #[account] @@ -142,6 +184,8 @@ pub struct DelegatedPositionV0 { // This bitmap gets rotated as last_claimed_epoch increases. // This allows for claiming ~128 epochs worth of rewards in parallel. pub claimed_epochs_bitmap: u128, + pub expiration_ts: i64, + pub recent_proposals: Vec, } impl DelegatedPositionV0 { @@ -175,6 +219,26 @@ impl DelegatedPositionV0 { Ok(()) } } + + // Add a proposal to the recent proposals list + pub fn add_recent_proposal(&mut self, proposal: Pubkey, ts: i64) { + let new_proposal = RecentProposal { proposal, ts }; + // Find the insertion point to maintain descending order by timestamp + let insert_index = self + .recent_proposals + .iter() + .position(|p| p.ts <= ts) + .unwrap_or(self.recent_proposals.len()); + // Insert the new proposal + self.recent_proposals.insert(insert_index, new_proposal); + } + pub fn remove_recent_proposal(&mut self, proposal: Pubkey) { + self.recent_proposals.retain(|p| p.proposal != proposal); + } + // Remove proposals older than the given timestamp + pub fn remove_proposals_older_than(&mut self, ts: i64) { + self.recent_proposals.retain(|p| p.ts >= ts); + } } #[account] @@ -202,6 +266,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 { @@ -224,6 +292,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 dao.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, @@ -234,8 +303,8 @@ pub struct SubDaoV0 { pub onboarding_dc_fee: u64, pub emission_schedule: Vec, pub bump_seed: u8, - pub registrar: Pubkey, // vsr registrar - pub delegator_rewards_percent: u64, // number between 0 - (100_u64 * 100_000_000). The % of DNT rewards delegators receive with 8 decimal places of accuracy + pub registrar: Pubkey, // vsr registrar + pub _deprecated_delegator_rewards_percent: u64, // number between 0 - (100_u64 * 100_000_000). The % of DNT rewards delegators receive with 8 decimal places of accuracy pub onboarding_data_only_dc_fee: u64, pub dc_onboarding_fees_paid: u64, // the total amount of dc onboarding fees paid to this subdao by active hotspots (inactive hotspots are excluded) pub active_device_authority: Pubkey, // authority that can mark hotspots as active/inactive diff --git a/programs/helium-sub-daos/src/utils.rs b/programs/helium-sub-daos/src/utils.rs index c7da2a75d..f9e013a61 100644 --- a/programs/helium-sub-daos/src/utils.rs +++ b/programs/helium-sub-daos/src/utils.rs @@ -1,13 +1,15 @@ -use crate::{error::ErrorCode, state::*, TESTING}; -use anchor_lang::prelude::*; -use shared_utils::{precise_number::PreciseNumber, signed_precise_number::SignedPreciseNumber}; use std::{ cmp::{min, Ordering}, convert::TryInto, }; + +use anchor_lang::prelude::*; +use shared_utils::{precise_number::PreciseNumber, signed_precise_number::SignedPreciseNumber}; use time::{Duration, OffsetDateTime}; use voter_stake_registry::state::{LockupKind, PositionV0, VotingMintConfigV0}; +use crate::{error::ErrorCode, state::*, TESTING}; + pub trait OrArithError { fn or_arith_error(self) -> Result; } @@ -279,6 +281,7 @@ pub fn caclulate_vhnt_info( curr_ts: i64, position: &PositionV0, voting_mint_config: &VotingMintConfigV0, + expiration_ts: i64, ) -> Result { let vehnt_at_curr_ts = position.voting_power_precise(voting_mint_config, curr_ts)?; @@ -308,7 +311,10 @@ pub fn caclulate_vhnt_info( ) .unwrap() } else { - position.lockup.seconds_left(curr_ts) + min( + u64::try_from(expiration_ts.checked_sub(curr_ts).unwrap()).unwrap(), + position.lockup.seconds_left(curr_ts), + ) }; // One second before genesis end, the last moment we have the multiplier let vehnt_at_genesis_end = position.voting_power_precise( @@ -317,26 +323,27 @@ pub fn caclulate_vhnt_info( .checked_add(i64::try_from(seconds_to_genesis).unwrap()) .unwrap(), )?; - let vehnt_at_genesis_end_exact = if has_genesis { + let vehnt_at_genesis_end_exact = if has_genesis && position.genesis_end < expiration_ts { position.voting_power_precise(voting_mint_config, position.genesis_end)? } else { position.voting_power_precise(voting_mint_config, curr_ts)? }; - let vehnt_at_position_end = - position.voting_power_precise(voting_mint_config, position.lockup.end_ts)?; + let delegation_end_ts = min(expiration_ts, position.lockup.end_ts); + let vehnt_at_delegation_end = + position.voting_power_precise(voting_mint_config, delegation_end_ts)?; let pre_genesis_end_fall_rate = calculate_fall_rate(vehnt_at_curr_ts, vehnt_at_genesis_end, seconds_to_genesis).unwrap(); let post_genesis_end_fall_rate = calculate_fall_rate( vehnt_at_genesis_end_exact, - vehnt_at_position_end, + vehnt_at_delegation_end, seconds_from_genesis_to_end, ) .unwrap(); let mut genesis_end_vehnt_correction = 0; let mut genesis_end_fall_rate_correction = 0; - if has_genesis { + if has_genesis && position.genesis_end < expiration_ts { let genesis_end_epoch_start_ts = i64::try_from(current_epoch(position.genesis_end)).unwrap() * EPOCH_LENGTH; @@ -353,7 +360,7 @@ pub fn caclulate_vhnt_info( // Only do this if the genesis end epoch isn't the same as the position end epoch. // If these are the same, then the full vehnt at epoch start is already being taken off. if position.lockup.kind == LockupKind::Constant - || current_epoch(position.genesis_end) != current_epoch(position.lockup.end_ts) + || current_epoch(position.genesis_end) != current_epoch(delegation_end_ts) { // edge case, if the genesis end is _exactly_ the start of the epoch, getting the voting power at the epoch start // will not include the genesis. When this happens, we'll miss a vehnt correction @@ -390,12 +397,16 @@ pub fn caclulate_vhnt_info( let mut end_vehnt_correction = 0; if position.lockup.kind == LockupKind::Cliff { let end_epoch_start_ts = - i64::try_from(current_epoch(position.lockup.end_ts)).unwrap() * EPOCH_LENGTH; + i64::try_from(current_epoch(delegation_end_ts)).unwrap() * EPOCH_LENGTH; let vehnt_at_closing_epoch_start = position.voting_power_precise(voting_mint_config, end_epoch_start_ts)?; end_vehnt_correction = vehnt_at_closing_epoch_start; - end_fall_rate_correction = post_genesis_end_fall_rate; + if position.genesis_end < expiration_ts { + end_fall_rate_correction = post_genesis_end_fall_rate; + } else { + end_fall_rate_correction = pre_genesis_end_fall_rate; + } } Ok(VehntInfo { diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 4aad4f1ba..9835a8372 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -29,22 +29,18 @@ RND=$RANDOM echo "Using $RND for dao names" -./packages/helium-admin-cli/bin/helium-admin.js create-price-oracle -u $SOLANA_URL \ - --wallet $ANCHOR_WALLET \ - --priceOracleKeypair ./packages/helium-admin-cli/keypairs/hnt-price-oracle.json \ - --oracles packages/helium-admin-cli/price-oracle-authorities.json \ - --decimals 8 - # init the dao and subdaos ./packages/helium-admin-cli/bin/helium-admin.js create-dao \ - --hntPriceOracle 4DdmDswskDxXGpwHrXUfn2CNUm9rt21ac79GHNTN3J33 \ - --numHnt 200136852 --numHst 200000000 --numDc 2000000000000 --realmName "Helium DAO" -u $CLUSTER_URL + --hntPriceOracle 4DdmDswskDxXGpwHrXUfn2CNUm9rt21ac79GHNTN3J33 --delegatorRewardsPercent 6\ + --numHnt 200136852 --numHst 200000000 --numDc 2000000000000 --realmName "Helium DAO" -u $CLUSTER_URL \ + --oracleKey $(solana address -k ./packages/helium-admin-cli/keypairs/hnt-price-oracle.json) \ + --rewardsOracleUrl https://hnt-price-oracle.oracle.test-helium.com ./packages/helium-admin-cli/bin/helium-admin.js create-subdao \ --hntPubkey $(solana address -k packages/helium-admin-cli/keypairs/hnt.json) \ -rewardsOracleUrl https://iot-oracle.oracle.test-helium.com \ -n IOT --subdaoKeypair packages/helium-admin-cli/keypairs/iot.json \ - --numTokens 100302580998 --startEpochRewards 65000000000 --realmName "Helium IOT" --dcBurnAuthority $(solana address) -u $CLUSTER_URL --decimals 6 --delegatorRewardsPercent 6 \ + --numTokens 100302580998 --startEpochRewards 65000000000 --realmName "Helium IOT" --dcBurnAuthority $(solana address) -u $CLUSTER_URL --decimals 6 \ --emissionSchedulePath ./packages/helium-admin-cli/emissions/iot.json ./packages/helium-admin-cli/bin/helium-admin.js create-subdao \ @@ -52,7 +48,7 @@ echo "Using $RND for dao names" -rewardsOracleUrl https://mobile-oracle.oracle.test-helium.com \ -n MOBILE --subdaoKeypair packages/helium-admin-cli/keypairs/mobile.json \ --numTokens 100302580998 --startEpochRewards 66000000000 --realmName "Helium MOBILE" --decimals 6 \ - --dcBurnAuthority $(solana address) -u $CLUSTER_URL --delegatorRewardsPercent 6 --emissionSchedulePath ./packages/helium-admin-cli/emissions/mobile.json + --dcBurnAuthority $(solana address) -u $CLUSTER_URL --emissionSchedulePath ./packages/helium-admin-cli/emissions/mobile.json # if test -f "./packages/helium-admin-cli/makers.json"; then # ./packages/helium-admin-cli/bin/helium-admin.js create-maker -u $CLUSTER --symbol IOT --subdaoMint $(solana address -k packages/helium-admin-cli/keypairs/iot.json) --fromFile packages/helium-admin-cli/makers.json --councilKeypair ./packages/helium-admin-cli/keypairs/council.json diff --git a/tests/data-credits.ts b/tests/data-credits.ts index 85dd30291..6ebcce6db 100644 --- a/tests/data-credits.ts +++ b/tests/data-credits.ts @@ -26,13 +26,15 @@ import { } from "../packages/data-credits-sdk/src"; import { PROGRAM_ID } from "../packages/data-credits-sdk/src/constants"; import * as hsd from "../packages/helium-sub-daos-sdk/src"; -import { daoKey } from "../packages/helium-sub-daos-sdk/src"; +import { daoKey, delegatorRewardsPercent } from "../packages/helium-sub-daos-sdk/src"; import { toBN, toNumber } from "../packages/spl-utils/src"; import * as vsr from "../packages/voter-stake-registry-sdk/src"; import { DataCredits } from "../target/types/data_credits"; +import { NftProxy } from "@helium/modular-governance-idls/lib/types/nft_proxy"; import { HeliumSubDaos } from "../target/types/helium_sub_daos"; import { initTestSubdao } from "./utils/daos"; import { ensureHSDIdl, ensureVSRIdl } from "./utils/fixtures"; +import { init as initNftProxy } from "@helium/nft-proxy-sdk"; import { initVsr } from "./utils/vsr"; const EPOCH_REWARDS = 100000000; @@ -77,6 +79,7 @@ describe("data-credits", () => { let program: Program; let hsdProgram: Program; let vsrProgram: Program; + let nftProxyProgram: Program; let pythProgram: Program; let dcKey: PublicKey; let hntMint: PublicKey; @@ -108,6 +111,7 @@ describe("data-credits", () => { vsr.PROGRAM_ID, anchor.workspace.VoterStakeRegistry.idl ); + nftProxyProgram = await initNftProxy(provider); ensureVSRIdl(vsrProgram); // fresh start hntMint = await createMint(provider, hntDecimals, me, me); @@ -163,12 +167,19 @@ describe("data-credits", () => { const registrar = ( await initVsr( vsrProgram, + nftProxyProgram, provider, provider.wallet.publicKey, hntMint, daoKey(hntMint)[0] ) ).registrar; + const rewardsEscrow = await createAtaAndMint( + provider, + hntMint, + 0, + provider.wallet.publicKey + ); const method = await hsdProgram.methods .initializeDaoV0({ authority: me, @@ -186,6 +197,8 @@ describe("data-credits", () => { emissionsPerEpoch: new BN(EPOCH_REWARDS), }, ], + proposalNamespace: PublicKey.default, + delegatorRewardsPercent: delegatorRewardsPercent(6), }) .preInstructions([ createAssociatedTokenAccountIdempotentInstruction( @@ -197,6 +210,7 @@ describe("data-credits", () => { ]) .accounts({ dcMint, + rewardsEscrow, hntMint, hstPool: await getAssociatedTokenAddress(hntMint, me), }); diff --git a/tests/distributor-oracle.ts b/tests/distributor-oracle.ts index d7c47df1e..45a0f483a 100644 --- a/tests/distributor-oracle.ts +++ b/tests/distributor-oracle.ts @@ -34,6 +34,7 @@ import { import chai, { assert, expect } from "chai"; import chaiHttp from "chai-http"; import fs from "fs"; +import { init as initNftProxy } from "@helium/nft-proxy-sdk"; import * as client from "../packages/distributor-oracle/src/client"; import { Database, @@ -301,9 +302,10 @@ describe("distributor-oracle", () => { VSR_PID, anchor.workspace.VoterStakeRegistry.idl ); - + const nftProxyProgram = await initNftProxy(provider); const { registrar } = await initVsr( vsrProgram, + nftProxyProgram, provider, me, hntMint, diff --git a/tests/helium-entity-manager.ts b/tests/helium-entity-manager.ts index 7811cb9e7..10adaba15 100644 --- a/tests/helium-entity-manager.ts +++ b/tests/helium-entity-manager.ts @@ -890,7 +890,6 @@ describe("helium-entity-manager", () => { onboardingDcFee: new BN(0), onboardingDataOnlyDcFee: null, registrar: null, - delegatorRewardsPercent: null, activeDeviceAuthority: null, }) .accounts({ diff --git a/tests/helium-sub-daos.ts b/tests/helium-sub-daos.ts index 5573a2cc8..e122ee3ad 100644 --- a/tests/helium-sub-daos.ts +++ b/tests/helium-sub-daos.ts @@ -6,6 +6,9 @@ import { daoKey, EPOCH_LENGTH } from "@helium/helium-sub-daos-sdk"; import { CircuitBreaker } from "@helium/idls/lib/types/circuit_breaker"; import { HeliumSubDaos } from "@helium/idls/lib/types/helium_sub_daos"; import { VoterStakeRegistry } from "@helium/idls/lib/types/voter_stake_registry"; +import { Proposal } from "@helium/modular-governance-idls/lib/types/proposal"; +import { init as initProposal } from "@helium/proposal-sdk"; +import { init as initProxy } from "@helium/nft-proxy-sdk"; import { createAtaAndMint, createAtaAndTransfer, @@ -41,6 +44,7 @@ import { DataCredits } from "../target/types/data_credits"; import { HeliumEntityManager } from "../target/types/helium_entity_manager"; import { burnDataCredits } from "./data-credits"; import { createMockCompression } from "./utils/compression"; +import { NftProxy } from "@helium/modular-governance-idls/lib/types/nft_proxy"; import { initTestDao, initTestSubdao } from "./utils/daos"; import { expectBnAccuracy } from "./utils/expectBnAccuracy"; import { @@ -55,6 +59,7 @@ import { getUnixTimestamp, loadKeypair } from "./utils/solana"; import { createPosition, initVsr } from "./utils/vsr"; // @ts-ignore import bs58 from "bs58"; +import { random } from "./utils/string"; chai.use(chaiAsPromised); @@ -91,6 +96,8 @@ describe("helium-sub-daos", () => { let hemProgram: Program; let cbProgram: Program; let vsrProgram: Program; + let proxyProgram: Program; + let proposalProgram: Program; let registrar: PublicKey; let position: PublicKey; @@ -119,6 +126,7 @@ describe("helium-sub-daos", () => { anchor.workspace.HeliumEntityManager.programId, anchor.workspace.HeliumEntityManager.idl ); + proxyProgram = await initProxy(provider); vsrProgram = await vsrInit( provider, @@ -126,6 +134,8 @@ describe("helium-sub-daos", () => { anchor.workspace.VoterStakeRegistry.idl ); ensureVSRIdl(vsrProgram); + + proposalProgram = await initProposal(provider); }); it("initializes a dao", async () => { @@ -177,6 +187,9 @@ describe("helium-sub-daos", () => { let dcMint: PublicKey; let rewardsEscrow: PublicKey; let genesisVotePowerMultiplierExpirationTs = 1; + let proxySeasonEnd = new BN( + new Date().valueOf() / 1000 + 24 * 60 * 60 * 5 * 365 + ); let initialSupply = toBN(223_000_000, 8); async function burnDc( @@ -227,18 +240,20 @@ describe("helium-sub-daos", () => { ({ registrar } = await initVsr( vsrProgram, + proxyProgram, provider, me, hntMint, daoKey(hntMint)[0], genesisVotePowerMultiplierExpirationTs, - 3 + 3, + proxySeasonEnd, )); ({ dataCredits: { dcMint }, - subDao: { subDao, treasury, rewardsEscrow }, - dao: { dao }, + subDao: { subDao, treasury }, + dao: { dao, rewardsEscrow }, } = await initWorld( provider, hemProgram, @@ -261,6 +276,8 @@ describe("helium-sub-daos", () => { hstEmissionSchedule: null, hstPool: null, netEmissionsCap: null, + proposalNamespace: null, + delegatorRewardsPercent: null, }) .accounts({ dao, @@ -281,7 +298,6 @@ describe("helium-sub-daos", () => { onboardingDcFee: null, onboardingDataOnlyDcFee: null, registrar: null, - delegatorRewardsPercent: null, activeDeviceAuthority: null, }) .accounts({ @@ -482,7 +498,6 @@ describe("helium-sub-daos", () => { ecc, hotspotOwner, }); - console.log("I AM ISSUING"); const issueMethod = hemProgram.methods .issueEntityV0({ entityKey: Buffer.from(bs58.decode(ecc)), @@ -584,8 +599,7 @@ describe("helium-sub-daos", () => { const supply = (await getMint(provider.connection, hntMint)).supply; const veHnt = toNumber(subDaoInfo.vehntAtEpochStart, 8); - const totalUtility = - Math.max(veHnt, 1) * Math.pow(50, 1 / 4) * Math.sqrt(16) * 1; + const totalUtility = veHnt; expect(daoInfo.totalRewards.toString()).to.eq( EPOCH_REWARDS.toString() ); @@ -873,15 +887,6 @@ describe("helium-sub-daos", () => { }) .rpc({ skipPreflight: true }); - await program.methods - .issueHstPoolV0({ - epoch, - }) - .accounts({ - dao, - }) - .rpc({ skipPreflight: true }); - const postBalance = AccountLayout.decode( (await provider.connection.getAccountInfo(treasury))?.data! ).amount; @@ -891,14 +896,13 @@ describe("helium-sub-daos", () => { const postHstBalance = AccountLayout.decode( (await provider.connection.getAccountInfo(hstPool))?.data! ).amount; - expect((postBalance - preBalance).toString()).to.eq( - ((1 - 0.32) * EPOCH_REWARDS).toString() - ); - expect((postHstBalance - preHstBalance).toString()).to.eq( - (0.32 * EPOCH_REWARDS).toString() + expect(Number(postBalance - preBalance)).to.be.closeTo( + (1 - 0.32) * EPOCH_REWARDS * (1 - 0.06), + 1 // Allow for 1 unit of difference to handle rounding ); + expect((postHstBalance - preHstBalance).toString()).to.eq("0"); expect((postMobileBalance - preMobileBalance).toString()).to.eq( - ((SUB_DAO_EPOCH_REWARDS / 100) * 94).toString() + "0" ); const acc = await program.account.subDaoEpochInfoV0.fetch( @@ -908,6 +912,73 @@ describe("helium-sub-daos", () => { }); it("claim rewards", async () => { + // Create and vote on two proposals + const { + pubkeys: { proposalConfig }, + } = await proposalProgram.methods + .initializeProposalConfigV0({ + name: random(10), + voteController: registrar, + stateController: me, + onVoteHook: PublicKey.default, + authority: me, + }) + .rpcAndKeys({ skipPreflight: true }); + for (let i = 0; i < 2; i++) { + const proposalName = `Proposal ${random(10)}`; + const { + pubkeys: { proposal }, + } = await proposalProgram.methods + .initializeProposalV0({ + seed: Buffer.from(proposalName, "utf-8"), + maxChoicesPerVoter: 1, + name: proposalName, + uri: "https://example.com", + choices: [ + { name: "Yes", uri: null }, + { name: "No", uri: null }, + ], + tags: ["test"], + }) + .accounts({ proposalConfig }) + .rpcAndKeys({ skipPreflight: true }); + await proposalProgram.methods + .updateStateV0({ + newState: { + voting: { + startTs: new anchor.BN(new Date().valueOf() / 1000), + } as any, + }, + }) + .accounts({ proposal }) + .rpc({ skipPreflight: true }); + const { + pubkeys: { marker }, + } = await vsrProgram.methods + .voteV0({ + choice: 0, + }) + .accounts({ + position, + proposal: proposal as PublicKey, + voter: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpcAndKeys({ skipPreflight: true }); + console.log( + "track", + await program.methods + .trackVoteV0() + .accounts({ + marker: marker as PublicKey, + dao, + subDao, + proposal: proposal as PublicKey, + position, + }) + .rpc({ skipPreflight: true }) + ); + } // issue rewards await sendInstructions(provider, [ await program.methods @@ -921,7 +992,7 @@ describe("helium-sub-daos", () => { ]); const method = program.methods - .claimRewardsV0({ + .claimRewardsV1({ epoch, }) .accounts({ @@ -931,14 +1002,19 @@ describe("helium-sub-daos", () => { }) .signers([positionAuthorityKp]); const { delegatorAta } = await method.pubkeys(); + const preAtaBalance = AccountLayout.decode( + (await provider.connection.getAccountInfo(delegatorAta!))?.data! + ).amount; await method.rpc({ skipPreflight: true }); const postAtaBalance = AccountLayout.decode( (await provider.connection.getAccountInfo(delegatorAta!))?.data! ).amount; - expect(Number(postAtaBalance)).to.be.within( - (SUB_DAO_EPOCH_REWARDS * 6) / 100 - 5, - (SUB_DAO_EPOCH_REWARDS * 6) / 100 + expect( + Number(postAtaBalance) - Number(preAtaBalance) + ).to.be.within( + (EPOCH_REWARDS * 0.68 * 6) / 100 - 5, + (EPOCH_REWARDS * 0.68 * 6) / 100 ); }); }); @@ -1149,6 +1225,282 @@ describe("helium-sub-daos", () => { 0.0000001 ); }); + + it("allows adding expiration ts", async () => { + const registrarAcc = await vsrProgram.account.registrar.fetch(registrar); + const proxyConfig = registrarAcc.proxyConfig; + + ({ position, vault } = await createPosition( + vsrProgram, + provider, + registrar, + hntMint, + // max lockup + { + lockupPeriods: 1460, + lockupAmount: 100, + kind: { cliff: {} }, + }, + positionAuthorityKp + )); + const { pubkeys: { closingTimeSubDaoEpochInfo, genesisEndSubDaoEpochInfo } } = await program.methods + .delegateV0() + .accounts({ + position, + subDao, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpcAndKeys({ skipPreflight: true }); + const seasonEnd = new BN( + new Date().valueOf() / 1000 + EPOCH_LENGTH * 5 + ); + await proxyProgram.methods + .updateProxyConfigV0({ + maxProxyTime: null, + seasons: [ + { + start: new BN(0), + end: seasonEnd, + }, + ], + }) + .accounts({ + proxyConfig, + authority: me, + }) + .rpc({ skipPreflight: true }); + const subDaoEpochInfo = await program.account.subDaoEpochInfoV0.fetch( + closingTimeSubDaoEpochInfo! + ); + const expectedFallRates = subDaoEpochInfo.fallRatesFromClosingPositions.toString(); + const expectedVehntInClosingPositions = subDaoEpochInfo.vehntInClosingPositions.toString(); + + console.log("dat old one is ", closingTimeSubDaoEpochInfo!.toBase58()); + const newClosingTimeSubDaoEpochInfo = subDaoEpochInfoKey( + subDao, + seasonEnd + )[0] + await program.methods + .addExpirationTs() + .accounts({ + position, + subDao, + oldClosingTimeSubDaoEpochInfo: closingTimeSubDaoEpochInfo, + closingTimeSubDaoEpochInfo: newClosingTimeSubDaoEpochInfo, + }) + .rpc({ skipPreflight: true }); + + const oldSubDaoEpochInfo = await program.account.subDaoEpochInfoV0.fetch(closingTimeSubDaoEpochInfo!); + expect(oldSubDaoEpochInfo.fallRatesFromClosingPositions.toNumber()).to.eq(0); + expect(oldSubDaoEpochInfo.vehntInClosingPositions.toNumber()).to.eq(0); + + const newSubDaoEpochInfo = + await program.account.subDaoEpochInfoV0.fetch( + newClosingTimeSubDaoEpochInfo! + ); + expect(newSubDaoEpochInfo.fallRatesFromClosingPositions.toString()).to.eq(expectedFallRates); + + const genesisEndEpoch = await program.account.subDaoEpochInfoV0.fetch( + genesisEndSubDaoEpochInfo! + ); + expect(genesisEndEpoch.fallRatesFromClosingPositions.toNumber()).to.eq(0); + expect(genesisEndEpoch.vehntInClosingPositions.toNumber()).to.eq(0); + }); + + describe("with proxy season that ends before genesis end", () => { + before(async () => { + // 15 days from now + proxySeasonEnd = new BN( + new Date().valueOf() / 1000 + (15 * EPOCH_LENGTH) + ); + }); + + it("correctly adjusts total vehnt at epoch start with changing genesis positions", async () => { + ({ position, vault } = await createPosition( + vsrProgram, + provider, + registrar, + hntMint, + // max lockup + { + lockupPeriods: 1460, + lockupAmount: 100, + kind: { constant: {} }, + }, + positionAuthorityKp + )); + await program.methods + .delegateV0() + .accounts({ + position, + subDao, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpc({ skipPreflight: true }); + + // Burn dc to cause an update to subdao epoch info + await burnDc(1); + + let offset = 0; + async function getCurrEpochInfo() { + const unixTime = Number(await getUnixTimestamp(provider)) + offset; + return await program.account.subDaoEpochInfoV0.fetch( + subDaoEpochInfoKey(subDao, unixTime)[0] + ); + } + + async function ffwd(amount: number) { + offset = amount; + await vsrProgram.methods + .setTimeOffsetV0(new BN(offset)) + .accounts({ registrar }) + .rpc({ skipPreflight: true }); + } + + // Start off the epoch with 0 vehnt since we staked at the start of the epoch + let subDaoEpochInfo = await getCurrEpochInfo(); + expect(subDaoEpochInfo.vehntAtEpochStart.toNumber()).to.eq(0); + + // Fast forward to a later epoch before genesis end and position expiration + await ffwd(EPOCH_LENGTH * 10); + // Burn dc to cause an update to subdao epoch info + await burnDc(1); + subDaoEpochInfo = await getCurrEpochInfo(); + expect(toNumber(subDaoEpochInfo.vehntAtEpochStart, 8)).to.eq( + 300 * 100 + ); + + // Switch to a cliff vest (start cooldown) + const { + pubkeys: { genesisEndSubDaoEpochInfo }, + } = await program.methods + .closeDelegationV0() + .accounts({ + position, + subDao, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpcAndKeys({ skipPreflight: true }); + await program.methods + .resetLockupV0({ + kind: { cliff: {} }, + periods: 1460, + }) + .accounts({ + dao, + position: position, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpc({ skipPreflight: true }); + let subDaoAcc = await program.account.subDaoV0.fetch(subDao); + console.log(subDaoAcc.vehntDelegated); + console.log(subDaoAcc.vehntFallRate); + expect(subDaoAcc.vehntDelegated.eq(new BN(0))).to.be.true; + expect(subDaoAcc.vehntFallRate.eq(new BN(0))).to.be.true; + const genesisEndEpoch = await program.account.subDaoEpochInfoV0.fetch( + genesisEndSubDaoEpochInfo! + ); + expect(genesisEndEpoch.vehntInClosingPositions.eq(new BN(0))).to.be + .true; + expect(genesisEndEpoch.fallRatesFromClosingPositions.eq(new BN(0))).to + .be.true; + + const { + pubkeys: { + genesisEndSubDaoEpochInfo: finalGenesisEndSubDaoEpochInfo, + }, + } = await program.methods + .delegateV0() + .accounts({ + position, + subDao, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpcAndKeys({ skipPreflight: true }); + console.log( + "Final end epoch subdao epoch info", + finalGenesisEndSubDaoEpochInfo!.toBase58() + ); + let positionAcc = await vsrProgram.account.positionV0.fetch(position); + const stakeTime = positionAcc.lockup.startTs; + + // Get to the actual expiration epoch and make sure to get an update + await ffwd(EPOCH_LENGTH * 15); + // Burn dc to cause an update to subdao epoch info + await burnDc(1); + + console.log("Checking after delegation expiration"); + await ffwd(EPOCH_LENGTH * 20); + await burnDc(1); + subDaoEpochInfo = await getCurrEpochInfo(); + let currTime = subDaoEpochInfo.epoch.toNumber() * EPOCH_LENGTH; + let timeStaked = currTime - stakeTime.toNumber(); + let expected = 0 + expect(toNumber(subDaoEpochInfo.vehntAtEpochStart, 8)).to.be.closeTo( + // Fall rates aren't a perfect measurement, we divide the total fall of the position by + // the total time staked. Imagine the total fall was 1 and the total time was 3. We would have + // a fall rate of 0.3333333333333333 and could never have enough decimals to represent it + expected, + 0.0000001 + ); + + console.log("Checking genesis end"); + await ffwd(EPOCH_LENGTH * 1460); + await burnDc(1); + subDaoEpochInfo = await getCurrEpochInfo(); + currTime = subDaoEpochInfo.epoch.toNumber() * EPOCH_LENGTH; + timeStaked = currTime - stakeTime.toNumber(); + expected = 0 + expect(toNumber(subDaoEpochInfo.vehntAtEpochStart, 8)).to.be.closeTo( + expected, + 0.0000001 + ); + + console.log("Checking after genesis end"); + await ffwd(EPOCH_LENGTH * 1461); + await burnDc(1); + subDaoEpochInfo = await getCurrEpochInfo(); + currTime = subDaoEpochInfo.epoch.toNumber() * EPOCH_LENGTH; + timeStaked = currTime - stakeTime.toNumber(); + expected = 0 + expect(toNumber(subDaoEpochInfo.vehntAtEpochStart, 8)).to.be.closeTo( + expected, + 0.0000001 + ); + + console.log("Checking at expiry"); + const unixTime = Number(await getUnixTimestamp(provider)); + const expiryOffset = + stakeTime.toNumber() + EPOCH_LENGTH * 1460 - unixTime; + await ffwd(expiryOffset); + await burnDc(1); + subDaoEpochInfo = await getCurrEpochInfo(); + currTime = subDaoEpochInfo.epoch.toNumber() * EPOCH_LENGTH; + timeStaked = currTime - stakeTime.toNumber(); + expected = 0 + expect(toNumber(subDaoEpochInfo.vehntAtEpochStart, 8)).to.be.closeTo( + expected, + 0.0000001 + ); + + console.log("Checking after expiry"); + await ffwd(expiryOffset + EPOCH_LENGTH * 2); + await burnDc(1); + subDaoEpochInfo = await getCurrEpochInfo(); + currTime = subDaoEpochInfo.epoch.toNumber() * EPOCH_LENGTH; + timeStaked = currTime - stakeTime.toNumber(); + console.log(toNumber(subDaoEpochInfo.vehntAtEpochStart, 8)); + expect(toNumber(subDaoEpochInfo.vehntAtEpochStart, 8)).to.be.closeTo( + 0, + 0.0000001 + ); + }); + }); }); }); }); diff --git a/tests/sus.ts b/tests/sus.ts index 4b2dcdd99..1ec30008b 100644 --- a/tests/sus.ts +++ b/tests/sus.ts @@ -29,6 +29,7 @@ import axios from "axios"; import { BN } from "bn.js"; import bs58 from "bs58"; import { expect } from "chai"; +import { ensureDCIdl } from "./utils/fixtures"; const SUS = new PublicKey("sustWW3deA7acADNGJnkYj2EAf65EmqUNLxKekDpu6w"); const hotspot = "9Cyj2K3Fi7xH8fZ1xrp4gtr1CU6Zk8VFM4fZN9NR9ncz"; @@ -107,6 +108,7 @@ describe("sus", () => { {} ) ); + await ensureDCIdl(dataCredits); transaction.add( await dataCredits.methods @@ -137,8 +139,8 @@ describe("sus", () => { })], cluster: "devnet" }); - console.log(susR.instructions[0].parsed); + console.log(susR.writableAccounts.map((r) => r.name)); expect(susR.writableAccounts.map((r) => r.name)).to.deep.eq([ "Native SOL Account", "DelegatedDataCreditsV0", diff --git a/tests/utils/daos.ts b/tests/utils/daos.ts index bff54e3f7..1d474f6ed 100644 --- a/tests/utils/daos.ts +++ b/tests/utils/daos.ts @@ -1,6 +1,9 @@ import * as anchor from "@coral-xyz/anchor"; import { BN } from "@coral-xyz/anchor"; -import { delegatorRewardsPercent, subDaoKey } from "@helium/helium-sub-daos-sdk"; +import { + delegatorRewardsPercent, + subDaoKey, +} from "@helium/helium-sub-daos-sdk"; import { createAtaAndMint, createMint, toBN } from "@helium/spl-utils"; import { createAssociatedTokenAccountIdempotentInstruction, @@ -18,10 +21,12 @@ export async function initTestDao( authority: PublicKey, dcMint?: PublicKey, mint?: PublicKey, - registrar?: PublicKey + registrar?: PublicKey, ): Promise<{ mint: PublicKey; dao: PublicKey; + rewardsEscrow: PublicKey; + delegatorPool: PublicKey; }> { const me = provider.wallet.publicKey; if (!mint) { @@ -32,8 +37,16 @@ export async function initTestDao( dcMint = await createMint(provider, 8, me, me); } + const rewardsEscrow = await createAtaAndMint( + provider, + mint, + 0, + provider.wallet.publicKey + ); + const method = await program.methods .initializeDaoV0({ + delegatorRewardsPercent: delegatorRewardsPercent(6), // 6% registrar: registrar || Keypair.generate().publicKey, authority: authority, netEmissionsCap: toBN(34.24, 8), @@ -49,6 +62,7 @@ export async function initTestDao( percent: 32, }, ], + proposalNamespace: me, }) .preInstructions([ createAssociatedTokenAccountIdempotentInstruction( @@ -59,17 +73,24 @@ export async function initTestDao( ), ]) .accounts({ + rewardsEscrow, hntMint: mint, dcMint, hstPool: await getAssociatedTokenAddress(mint, me), }); - const { dao } = await method.pubkeys(); + + const { dao, delegatorPool } = await method.pubkeys(); if (!(await provider.connection.getAccountInfo(dao!))) { await method.rpc({ skipPreflight: true }); } - return { mint: mint!, dao: dao! }; + return { + mint: mint!, + dao: dao!, + rewardsEscrow, + delegatorPool: delegatorPool!, + }; } export async function initTestSubdao( @@ -86,8 +107,6 @@ export async function initTestSubdao( mint: PublicKey; subDao: PublicKey; treasury: PublicKey; - rewardsEscrow: PublicKey; - delegatorPool: PublicKey; treasuryCircuitBreaker: PublicKey; }> { const daoAcc = await hsdProgram.account.daoV0.fetch(dao); @@ -95,12 +114,6 @@ export async function initTestSubdao( if (numTokens) { await createAtaAndMint(provider, dntMint, numTokens, authority); } - const rewardsEscrow = await createAtaAndMint( - provider, - dntMint, - 0, - provider.wallet.publicKey - ); const subDao = subDaoKey(dntMint)[0]; const method = hsdProgram.methods @@ -121,7 +134,6 @@ export async function initTestSubdao( }, } as any, dcBurnAuthority: authority, - delegatorRewardsPercent: delegatorRewardsPercent(6), // 6% activeDeviceAuthority: activeDeviceAuthority || authority, }) .preInstructions([ @@ -129,19 +141,15 @@ export async function initTestSubdao( ]) .accounts({ dao, - rewardsEscrow, dntMint, hntMint: daoAcc.hntMint, }); - const { treasury, treasuryCircuitBreaker, delegatorPool } = - await method.pubkeys(); - await method.rpc(); + const { treasury, treasuryCircuitBreaker } = await method.pubkeys(); + await method.rpc({ skipPreflight: true }); return { treasuryCircuitBreaker: treasuryCircuitBreaker!, mint: dntMint, subDao: subDao!, treasury: treasury!, - rewardsEscrow, - delegatorPool: delegatorPool!, }; } diff --git a/tests/utils/fixtures.ts b/tests/utils/fixtures.ts index c07892246..3db782bab 100644 --- a/tests/utils/fixtures.ts +++ b/tests/utils/fixtures.ts @@ -307,16 +307,18 @@ export const initWorld = async ( epochRewards?: number, subDaoEpochRewards?: number, registrar?: PublicKey, - hntMint?: PublicKey, - subDaoRegistrar?: PublicKey + hntMint?: PublicKey ): Promise<{ - dao: { mint: PublicKey; dao: PublicKey }; + dao: { + mint: PublicKey; + dao: PublicKey; + rewardsEscrow: PublicKey; + delegatorPool: PublicKey; + }; subDao: { mint: PublicKey; subDao: PublicKey; treasury: PublicKey; - rewardsEscrow: PublicKey; - delegatorPool: PublicKey; }; dataCredits: { dcKey: PublicKey; @@ -359,9 +361,8 @@ export const initWorld = async ( authority: provider.wallet.publicKey, dao: dao.dao, epochRewards: subDaoEpochRewards, - registrar: subDaoRegistrar, // Enough to stake 4 makers - numTokens: MAKER_STAKING_FEE.mul(new anchor.BN(4)) + numTokens: MAKER_STAKING_FEE.mul(new anchor.BN(4)), }); const rewardableEntityConfig = await initTestRewardableEntityConfig( diff --git a/tests/utils/vsr.ts b/tests/utils/vsr.ts index 5b5651c46..f8457eb62 100644 --- a/tests/utils/vsr.ts +++ b/tests/utils/vsr.ts @@ -1,4 +1,5 @@ import { VoterStakeRegistry } from "@helium/idls/lib/types/voter_stake_registry"; +import { NftProxy } from "@helium/modular-governance-idls/lib/types/nft_proxy"; import { createMintInstructions, sendInstructions, @@ -19,18 +20,22 @@ import { ComputeBudgetProgram, } from "@solana/web3.js"; import { positionKey } from "../../packages/voter-stake-registry-sdk/src"; +import { random } from "./string"; export const SPL_GOVERNANCE_PID = new PublicKey( "hgovkRU6Ghe1Qoyb54HdSLdqN7VtxaifBzRmh9jtd3S" ); export async function initVsr( program: Program, + proxyProgram: Program, provider: AnchorProvider, me: PublicKey, hntMint: PublicKey, positionUpdateAuthority: PublicKey, genesisVotePowerMultiplierExpirationTs = 1, genesisVotePowerMultiplier = 0, + // Default is to set proxy season to end so far ahead it isn't relevant + proxySeasonEnd = new BN(new Date().valueOf() / 1000 + (24 * 60 * 60 * 5 * 365)), ) { const programVersion = await getGovernanceProgramVersion( program.provider.connection, @@ -54,6 +59,24 @@ export async function initVsr( new BN(1) ); + const { + pubkeys: { proxyConfig }, + } = await proxyProgram.methods + .initializeProxyConfigV0({ + maxProxyTime: new BN(1000000000000), + name: random(10), + seasons: [ + { + start: new BN(0), + end: proxySeasonEnd, + }, + ], + }) + .accounts({ + authority: me, + }) + .rpcAndKeys(); + const createRegistrar = program.methods .initializeRegistrarV0({ positionUpdateAuthority, @@ -61,7 +84,7 @@ export async function initVsr( .accounts({ realm: realmPk, realmGoverningTokenMint: hntMint, - proxyConfig: null + proxyConfig }); instructions.push(await createRegistrar.instruction()); const registrar = (await createRegistrar.pubkeys()).registrar as PublicKey; diff --git a/utils/nft-proxy/src/lib.rs b/utils/nft-proxy/src/lib.rs index d34c427db..558a106dd 100644 --- a/utils/nft-proxy/src/lib.rs +++ b/utils/nft-proxy/src/lib.rs @@ -3,3 +3,36 @@ use anchor_lang::prelude::*; anchor_gen::generate_cpi_crate!("./idl.json"); declare_id!("nprx42sXf5rpVnwBWEdRg1d8tuCWsTuVLys1pRWwE6p"); + +impl ProxyConfigV0 { + // Binary search for the timestamp closest to but after `unix_time` + pub fn get_current_season(&self, unix_time: i64) -> Option { + if self.seasons.is_empty() { + return None; + } + + let mut ans: Option = None; + let mut low: usize = 0; + let mut high: usize = self.seasons.len() - 1; + + while low <= high { + let middle = (high + low) / 2; + if let Some(current) = self.seasons.get(middle) { + // Move to the right side if target time is greater + if current.start <= unix_time { + ans = Some(*current); + low = middle + 1; + } else { + if middle == 0 { + break; + } + high = middle - 1; + } + } else { + break; + } + } + + ans + } +} diff --git a/utils/shared-utils/src/precise_number.rs b/utils/shared-utils/src/precise_number.rs index ee7e6eb23..d8e3fde51 100644 --- a/utils/shared-utils/src/precise_number.rs +++ b/utils/shared-utils/src/precise_number.rs @@ -1,13 +1,11 @@ //! Defines PreciseNumber, a U192 wrapper with float-like operations // Stolen from SPL math, but changing inner unit -use std::cmp::Ordering; -use std::convert::*; +use std::{cmp::Ordering, convert::*}; use anchor_lang::prelude::msg; -use crate::signed_precise_number::SignedPreciseNumber; -use crate::uint::U192; +use crate::{signed_precise_number::SignedPreciseNumber, uint::U192}; // Allows for easy swapping between different internal representations pub type InnerUint = U192; diff --git a/utils/vehnt/Cargo.lock b/utils/vehnt/Cargo.lock index 35ed1d6be..a37c2ad16 100644 --- a/utils/vehnt/Cargo.lock +++ b/utils/vehnt/Cargo.lock @@ -1028,7 +1028,7 @@ dependencies = [ [[package]] name = "circuit-breaker" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anchor-lang", "anchor-spl", @@ -1973,13 +1973,14 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "helium-sub-daos" -version = "0.1.6" +version = "0.1.12" dependencies = [ "anchor-lang", "anchor-spl", "circuit-breaker", "default-env", "mpl-token-metadata", + "nft-proxy", "shared-utils", "solana-security-txt", "time 0.3.36", @@ -5013,7 +5014,7 @@ dependencies = [ [[package]] name = "treasury-management" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anchor-lang", "anchor-spl", @@ -5223,7 +5224,7 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "voter-stake-registry" -version = "0.3.2" +version = "0.3.4" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/utils/vehnt/src/cli/delegated.rs b/utils/vehnt/src/cli/delegated.rs index 89e7128fb..b9afcb901 100644 --- a/utils/vehnt/src/cli/delegated.rs +++ b/utils/vehnt/src/cli/delegated.rs @@ -185,6 +185,7 @@ impl Delegated { position.delegated_position.start_ts, &position.position, &voting_mint_config, + position.delegated_position.expiration_ts )?; let vehnt = position .position diff --git a/yarn.lock b/yarn.lock index e21f55de3..06f08c1b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1011,6 +1011,7 @@ __metadata: "@coral-xyz/anchor": ^0.28.0 "@helium/anchor-resolvers": ^0.9.18 "@helium/circuit-breaker-sdk": ^0.9.18 + "@helium/nft-proxy-sdk": ^0.0.15 "@helium/spl-utils": ^0.9.18 "@helium/treasury-management-sdk": ^0.9.18 "@helium/voter-stake-registry-sdk": ^0.9.18