Skip to content

Commit

Permalink
Move derivation utils to the separate module
Browse files Browse the repository at this point in the history
  • Loading branch information
RolginRoman committed Sep 20, 2024
1 parent 90fd7e6 commit ea7d43b
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 74 deletions.
2 changes: 2 additions & 0 deletions packages/staking/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { default as SolanaStakingClient } from "./solana/client.js";
export * from "./solana/utils.js";
export * from "./solana/types.js";
export * from "./solana/lib/derive-accounts.js";
export * from "./solana/lib/rewards.js";
export * as constants from "./solana/constants.js";
2 changes: 1 addition & 1 deletion packages/staking/solana/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import {
deriveStakeEntryPDA,
deriveStakeMintPDA,
deriveStakePoolPDA,
} from "./utils.js";
} from "./lib/derive-accounts.js";

interface Programs {
stakePoolProgram: Program<StakePoolProgramType>;
Expand Down
78 changes: 78 additions & 0 deletions packages/staking/solana/lib/derive-accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { PublicKey } from "@solana/web3.js";
// eslint-disable-next-line no-restricted-imports
import BN from "bn.js";

import {
STAKE_POOL_PREFIX,
STAKE_VAULT_PREFIX,
STAKE_MINT_PREFIX,
STAKE_ENTRY_PREFIX,
REWARD_POOL_PREFIX,
REWARD_VAULT_PREFIX,
REWARD_ENTRY_PREFIX,
CONFIG_PREFIX,
FEE_VALUE_PREFIX,
} from "../constants.js";

export const deriveStakePoolPDA = (
programId: PublicKey,
mint: PublicKey,
authority: PublicKey,
nonce: number,
): PublicKey => {
return PublicKey.findProgramAddressSync(
[STAKE_POOL_PREFIX, mint.toBuffer(), authority.toBuffer(), new BN(nonce).toArrayLike(Buffer, "le", 1)],
programId,
)[0];
};

export const deriveStakeVaultPDA = (programId: PublicKey, stakePool: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync([STAKE_VAULT_PREFIX, stakePool.toBuffer()], programId)[0];
};

export const deriveStakeMintPDA = (programId: PublicKey, stakePool: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync([STAKE_MINT_PREFIX, stakePool.toBuffer()], programId)[0];
};

export const deriveStakeEntryPDA = (
programId: PublicKey,
stakePool: PublicKey,
authority: PublicKey,
nonce: number,
): PublicKey => {
return PublicKey.findProgramAddressSync(
[STAKE_ENTRY_PREFIX, stakePool.toBuffer(), authority.toBuffer(), new BN(nonce).toArrayLike(Buffer, "le", 4)],
programId,
)[0];
};

export const deriveRewardPoolPDA = (
programId: PublicKey,
stakePool: PublicKey,
mint: PublicKey,
nonce: number,
): PublicKey => {
return PublicKey.findProgramAddressSync(
[REWARD_POOL_PREFIX, stakePool.toBuffer(), mint.toBuffer(), new BN(nonce).toArrayLike(Buffer, "le", 1)],
programId,
)[0];
};

export const deriveRewardVaultPDA = (programId: PublicKey, rewardPool: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync([REWARD_VAULT_PREFIX, rewardPool.toBuffer()], programId)[0];
};

export const deriveRewardEntryPDA = (programId: PublicKey, rewardPool: PublicKey, stakeEntry: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync(
[REWARD_ENTRY_PREFIX, rewardPool.toBuffer(), stakeEntry.toBuffer()],
programId,
)[0];
};

export const deriveConfigPDA = (programId: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync([CONFIG_PREFIX], programId)[0];
};

export const deriveFeeValuePDA = (programId: PublicKey, target: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync([FEE_VALUE_PREFIX, target.toBuffer()], programId)[0];
};
209 changes: 209 additions & 0 deletions packages/staking/solana/lib/rewards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { ProgramAccount } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
// eslint-disable-next-line no-restricted-imports
import BN from "bn.js";

import { RewardEntry, StakeEntry, RewardPool } from "../types.js";
import { SCALE_PRECISION_FACTOR_BN } from "../constants.js";

export const REWARD_AMOUNT_PRECISION_FACTOR = new BN("1000000000");

export class RewardEntryAccumulator implements RewardEntry {
lastAccountedTs: BN;

claimedAmount: BN;

accountedAmount: BN;

rewardPool: PublicKey;

stakeEntry: PublicKey;

createdTs: BN;

lastRewardAmount: BN;

lastRewardPeriod: BN;

buffer: number[];

constructor(public delegate: RewardEntry) {
this.lastAccountedTs = delegate.lastAccountedTs;
this.claimedAmount = delegate.claimedAmount;
this.accountedAmount = delegate.accountedAmount;
this.rewardPool = delegate.rewardPool;
this.stakeEntry = delegate.stakeEntry;
this.createdTs = delegate.createdTs;
this.buffer = delegate.buffer;
this.lastRewardAmount = delegate.lastRewardAmount;
this.lastRewardPeriod = delegate.lastRewardPeriod;
}

// Calculate accountable amount by calculating how many seconds have passed since last claim/stake time
getAccountableAmount(
stakedTs: BN,
accountableTs: BN,
effectiveStakedAmount: BN,
rewardAmount: BN,
rewardPeriod: BN,
): BN {
const lastAccountedTs = this.lastAccountedTs.gt(new BN(0)) ? this.lastAccountedTs : stakedTs;
const secondsPassed = accountableTs.sub(lastAccountedTs);

if (secondsPassed.lt(rewardPeriod)) {
return new BN(0);
}

const periodsPassed = secondsPassed.div(rewardPeriod);

const claimablePerEffectiveStake = periodsPassed.mul(rewardAmount);

const accountableAmount = claimablePerEffectiveStake.mul(effectiveStakedAmount).div(SCALE_PRECISION_FACTOR_BN);

return accountableAmount;
}

// Calculates claimable amount from accountable amount.
getClaimableAmount(): BN {
const claimedAmount = this.claimedAmount.mul(REWARD_AMOUNT_PRECISION_FACTOR);
const nonClaimedAmount = this.accountedAmount.sub(claimedAmount);
const claimableAmount = nonClaimedAmount.div(REWARD_AMOUNT_PRECISION_FACTOR);

return claimableAmount;
}

// Get the time of the last unlock
getLastAccountedTs(stakedTs: BN, claimableTs: BN, rewardPeriod: BN): BN {
const lastAccountedTs = this.lastAccountedTs.gtn(0) ? this.lastAccountedTs : stakedTs;
const totalSecondsPassed = claimableTs.sub(lastAccountedTs);
const periodsPassed = totalSecondsPassed.div(rewardPeriod);
const periodsToSeconds = periodsPassed.mul(rewardPeriod);
const currClaimTs = lastAccountedTs.add(periodsToSeconds);

return currClaimTs;
}

// Adds accounted amount
addAccountedAmount(accountedAmount: BN): void {
this.accountedAmount = this.accountedAmount.add(accountedAmount);
}

// Adds claimed amount
addClaimedAmount(claimedAmount: BN): void {
this.claimedAmount = this.claimedAmount.add(claimedAmount);
}
}

const createDefaultRewardEntry = (
stakeEntry: ProgramAccount<StakeEntry>,
rewardPool: ProgramAccount<RewardPool>,
): RewardEntry => {
return {
stakeEntry: new PublicKey(stakeEntry.publicKey),
rewardPool: new PublicKey(rewardPool.publicKey),
createdTs: stakeEntry.account.createdTs,
lastAccountedTs: new BN(0),
lastRewardAmount: new BN(0),
lastRewardPeriod: new BN(0),
accountedAmount: new BN(0),
claimedAmount: new BN(0),
buffer: [],
};
};

export const calcRewards = (
rewardEntryAccount: ProgramAccount<RewardEntry> | undefined,
stakeEntryAccount: ProgramAccount<StakeEntry>,
rewardPoolAccount: ProgramAccount<RewardPool>,
) => {
const rewardEntry: RewardEntry =
rewardEntryAccount?.account ?? createDefaultRewardEntry(stakeEntryAccount, rewardPoolAccount);
const stakeEntry = stakeEntryAccount.account;
const rewardPool = rewardPoolAccount.account;

const rewardEntryAccumulator = new RewardEntryAccumulator(rewardEntry);
if (rewardEntryAccumulator.createdTs.lt(stakeEntry.createdTs)) {
throw new Error("InvalidRewardEntry");
}

const currTs = Math.floor(Date.now() / 1000);

const stakedTs = rewardPool.createdTs ? BN.max(stakeEntry.createdTs, rewardPool.createdTs) : stakeEntry.createdTs;
const claimableTs = stakeEntry.closedTs.gtn(0) ? stakeEntry.closedTs : new BN(currTs);

const amountUpdated =
!rewardPool.rewardAmount.eq(rewardPool.lastRewardAmount) &&
rewardPool.lastAmountUpdateTs.gt(stakeEntry.createdTs) &&
rewardPool.lastAmountUpdateTs.gt(stakeEntry.closedTs);
const periodUpdated =
!rewardPool.rewardPeriod.eq(rewardPool.lastRewardPeriod) &&
rewardPool.lastPeriodUpdateTs.gt(stakeEntry.createdTs) &&
rewardPool.lastPeriodUpdateTs.gt(stakeEntry.closedTs);

if (amountUpdated || periodUpdated) {
let firstUpdateTs: BN, secondUpdateTs: BN, rewardAmount: BN, rewardPeriod: BN;
if (amountUpdated && periodUpdated) {
if (rewardPool.lastAmountUpdateTs.lt(rewardPool.lastPeriodUpdateTs)) {
firstUpdateTs = rewardPool.lastAmountUpdateTs;
secondUpdateTs = rewardPool.lastPeriodUpdateTs;
rewardAmount = rewardPool.rewardAmount;
rewardPeriod = rewardEntryAccumulator.lastRewardPeriod;
} else {
firstUpdateTs = rewardPool.lastPeriodUpdateTs;
secondUpdateTs = rewardPool.lastAmountUpdateTs;
rewardAmount = rewardEntryAccumulator.lastRewardAmount;
rewardPeriod = rewardPool.rewardPeriod;
}
} else if (amountUpdated) {
firstUpdateTs = new BN(0);
secondUpdateTs = rewardPool.lastAmountUpdateTs;
rewardAmount = rewardEntryAccumulator.lastRewardAmount;
rewardPeriod = rewardEntryAccumulator.lastRewardPeriod;
} else {
firstUpdateTs = new BN(0);
secondUpdateTs = rewardPool.lastPeriodUpdateTs;
rewardAmount = rewardEntryAccumulator.lastRewardAmount;
rewardPeriod = rewardEntryAccumulator.lastRewardPeriod;
}

if (firstUpdateTs.gtn(0)) {
const firstAccountableAmount = rewardEntryAccumulator.getAccountableAmount(
stakedTs,
firstUpdateTs,
stakeEntry.effectiveAmount,
rewardEntryAccumulator.lastRewardAmount,
rewardEntryAccumulator.lastRewardPeriod,
);
rewardEntryAccumulator.addAccountedAmount(firstAccountableAmount);
rewardEntryAccumulator.lastAccountedTs = rewardEntryAccumulator.getLastAccountedTs(
stakedTs,
firstUpdateTs,
rewardPool.lastRewardPeriod,
);
}
const secondAccountableAmount = rewardEntryAccumulator.getAccountableAmount(
stakedTs,
secondUpdateTs,
stakeEntry.effectiveAmount,
rewardAmount,
rewardPeriod,
);
rewardEntryAccumulator.addAccountedAmount(secondAccountableAmount);
rewardEntryAccumulator.lastAccountedTs = rewardEntryAccumulator.getLastAccountedTs(
stakedTs,
secondUpdateTs,
rewardPeriod,
);
}

const accountableAmount = rewardEntryAccumulator.getAccountableAmount(
stakedTs,
claimableTs,
stakeEntry.effectiveAmount,
rewardPool.rewardAmount,
rewardPool.rewardPeriod,
);
rewardEntryAccumulator.addAccountedAmount(accountableAmount);

return rewardEntryAccumulator.getClaimableAmount();
};
74 changes: 1 addition & 73 deletions packages/staking/solana/utils.ts
Original file line number Diff line number Diff line change
@@ -1,88 +1,16 @@
import { TransferFeeConfig } from "@solana/spl-token";
import { Connection, PublicKey } from "@solana/web3.js";
import { Connection } from "@solana/web3.js";
// eslint-disable-next-line no-restricted-imports
import BN from "bn.js";

import {
CONFIG_PREFIX,
DEFAULT_FEE_BN,
FEE_PRECISION_FACTOR_BN,
FEE_VALUE_PREFIX,
REWARD_ENTRY_PREFIX,
REWARD_POOL_PREFIX,
REWARD_VAULT_PREFIX,
SCALE_PRECISION_FACTOR,
SCALE_PRECISION_FACTOR_BN,
STAKE_ENTRY_PREFIX,
STAKE_MINT_PREFIX,
STAKE_POOL_PREFIX,
STAKE_VAULT_PREFIX,
U64_MAX,
} from "./constants.js";

export const deriveStakePoolPDA = (
programId: PublicKey,
mint: PublicKey,
authority: PublicKey,
nonce: number,
): PublicKey => {
return PublicKey.findProgramAddressSync(
[STAKE_POOL_PREFIX, mint.toBuffer(), authority.toBuffer(), new BN(nonce).toArrayLike(Buffer, "le", 1)],
programId,
)[0];
};

export const deriveStakeVaultPDA = (programId: PublicKey, stakePool: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync([STAKE_VAULT_PREFIX, stakePool.toBuffer()], programId)[0];
};

export const deriveStakeMintPDA = (programId: PublicKey, stakePool: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync([STAKE_MINT_PREFIX, stakePool.toBuffer()], programId)[0];
};

export const deriveStakeEntryPDA = (
programId: PublicKey,
stakePool: PublicKey,
authority: PublicKey,
nonce: number,
): PublicKey => {
return PublicKey.findProgramAddressSync(
[STAKE_ENTRY_PREFIX, stakePool.toBuffer(), authority.toBuffer(), new BN(nonce).toArrayLike(Buffer, "le", 4)],
programId,
)[0];
};

export const deriveRewardPoolPDA = (
programId: PublicKey,
stakePool: PublicKey,
mint: PublicKey,
nonce: number,
): PublicKey => {
return PublicKey.findProgramAddressSync(
[REWARD_POOL_PREFIX, stakePool.toBuffer(), mint.toBuffer(), new BN(nonce).toArrayLike(Buffer, "le", 1)],
programId,
)[0];
};

export const deriveRewardVaultPDA = (programId: PublicKey, rewardPool: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync([REWARD_VAULT_PREFIX, rewardPool.toBuffer()], programId)[0];
};

export const deriveRewardEntryPDA = (programId: PublicKey, rewardPool: PublicKey, stakeEntry: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync(
[REWARD_ENTRY_PREFIX, rewardPool.toBuffer(), stakeEntry.toBuffer()],
programId,
)[0];
};

export const deriveConfigPDA = (programId: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync([CONFIG_PREFIX], programId)[0];
};

export const deriveFeeValuePDA = (programId: PublicKey, target: PublicKey): PublicKey => {
return PublicKey.findProgramAddressSync([FEE_VALUE_PREFIX, target.toBuffer()], programId)[0];
};

export const calculateStakeWeight = (minDuration: BN, maxDuration: BN, maxWeight: BN, duration: BN) => {
const durationSpan = maxDuration.sub(minDuration);
if (durationSpan.eq(new BN(0))) {
Expand Down

0 comments on commit ea7d43b

Please sign in to comment.