Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit b3f68a8

Browse files
authored
stake-pool-js: Remove divideBnToNumber, use BNs more (#6482)
* stake-pool-js: Remove `divideBnToNumber`, use BNs more * Add defensive check for negative number * Revert ceil on calc
1 parent 2f09a32 commit b3f68a8

File tree

4 files changed

+67
-52
lines changed

4 files changed

+67
-52
lines changed

stake-pool/js/src/index.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export interface StakePoolAccount {
6060
export interface WithdrawAccount {
6161
stakeAddress: PublicKey;
6262
voteAddress?: PublicKey;
63-
poolAmount: number;
63+
poolAmount: BN;
6464
}
6565

6666
/**
@@ -354,7 +354,7 @@ export async function withdrawStake(
354354
validatorComparator?: (_a: ValidatorAccount, _b: ValidatorAccount) => number,
355355
) {
356356
const stakePool = await getStakePoolAccount(connection, stakePoolAddress);
357-
const poolAmount = solToLamports(amount);
357+
const poolAmount = new BN(solToLamports(amount));
358358

359359
if (!poolTokenAccount) {
360360
poolTokenAccount = getAssociatedTokenAddressSync(stakePool.account.data.poolMint, tokenOwner);
@@ -363,7 +363,7 @@ export async function withdrawStake(
363363
const tokenAccount = await getAccount(connection, poolTokenAccount);
364364

365365
// Check withdrawFrom balance
366-
if (tokenAccount.amount < poolAmount) {
366+
if (tokenAccount.amount < poolAmount.toNumber()) {
367367
throw new Error(
368368
`Not enough token balance to withdraw ${lamportsToSol(poolAmount)} pool tokens.
369369
Maximum withdraw amount is ${lamportsToSol(tokenAccount.amount)} pool tokens.`,
@@ -420,10 +420,10 @@ export async function withdrawStake(
420420

421421
const availableForWithdrawal = calcLamportsWithdrawAmount(
422422
stakePool.account.data,
423-
stakeAccount.lamports - MINIMUM_ACTIVE_STAKE - stakeAccountRentExemption,
423+
new BN(stakeAccount.lamports - MINIMUM_ACTIVE_STAKE - stakeAccountRentExemption),
424424
);
425425

426-
if (availableForWithdrawal < poolAmount) {
426+
if (availableForWithdrawal.lt(poolAmount)) {
427427
throw new Error(
428428
`Not enough lamports available for withdrawal from ${stakeAccountAddress},
429429
${poolAmount} asked, ${availableForWithdrawal} available.`,
@@ -450,12 +450,18 @@ export async function withdrawStake(
450450
throw new Error('Invalid Stake Account');
451451
}
452452

453+
const availableLamports = new BN(
454+
stakeAccount.lamports - MINIMUM_ACTIVE_STAKE - stakeAccountRentExemption,
455+
);
456+
if (availableLamports.lt(new BN(0))) {
457+
throw new Error('Invalid Stake Account');
458+
}
453459
const availableForWithdrawal = calcLamportsWithdrawAmount(
454460
stakePool.account.data,
455-
stakeAccount.lamports - MINIMUM_ACTIVE_STAKE - stakeAccountRentExemption,
461+
availableLamports,
456462
);
457463

458-
if (availableForWithdrawal < poolAmount) {
464+
if (availableForWithdrawal.lt(poolAmount)) {
459465
// noinspection ExceptionCaughtLocallyJS
460466
throw new Error(
461467
`Not enough lamports available for withdrawal from ${stakeAccountAddress},
@@ -492,7 +498,7 @@ export async function withdrawStake(
492498
poolTokenAccount,
493499
userTransferAuthority.publicKey,
494500
tokenOwner,
495-
poolAmount,
501+
poolAmount.toNumber(),
496502
),
497503
);
498504

@@ -508,8 +514,9 @@ export async function withdrawStake(
508514
break;
509515
}
510516
// Convert pool tokens amount to lamports
511-
const solWithdrawAmount = Math.ceil(
512-
calcLamportsWithdrawAmount(stakePool.account.data, withdrawAccount.poolAmount),
517+
const solWithdrawAmount = calcLamportsWithdrawAmount(
518+
stakePool.account.data,
519+
withdrawAccount.poolAmount,
513520
);
514521

515522
let infoMsg = `Withdrawing ◎${solWithdrawAmount},
@@ -542,7 +549,7 @@ export async function withdrawStake(
542549
sourcePoolAccount: poolTokenAccount,
543550
managerFeeAccount: stakePool.account.data.managerFeeAccount,
544551
poolMint: stakePool.account.data.poolMint,
545-
poolTokens: withdrawAccount.poolAmount,
552+
poolTokens: withdrawAccount.poolAmount.toNumber(),
546553
withdrawAuthority,
547554
}),
548555
);

stake-pool/js/src/layouts.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { publicKey, struct, u32, u64, u8, option, vec } from '@coral-xyz/borsh';
2-
import { Lockup, PublicKey } from '@solana/web3.js';
2+
import { PublicKey } from '@solana/web3.js';
33
import BN from 'bn.js';
44
import {
55
Infer,
@@ -76,6 +76,11 @@ export const StakeAccount = type({
7676
type: StakeAccountType,
7777
info: optional(StakeAccountInfo),
7878
});
79+
export interface Lockup {
80+
unixTimestamp: BN;
81+
epoch: BN;
82+
custodian: PublicKey;
83+
}
7984

8085
export interface StakePool {
8186
accountType: AccountType;

stake-pool/js/src/utils/stake.ts

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ export interface ValidatorAccount {
4141
type: 'preferred' | 'active' | 'transient' | 'reserve';
4242
voteAddress?: PublicKey | undefined;
4343
stakeAddress: PublicKey;
44-
lamports: number;
44+
lamports: BN;
4545
}
4646

4747
export async function prepareWithdrawAccounts(
4848
connection: Connection,
4949
stakePool: StakePool,
5050
stakePoolAddress: PublicKey,
51-
amount: number,
51+
amount: BN,
5252
compareFn?: (a: ValidatorAccount, b: ValidatorAccount) => number,
5353
skipFee?: boolean,
5454
): Promise<WithdrawAccount[]> {
@@ -62,13 +62,13 @@ export async function prepareWithdrawAccounts(
6262
const minBalanceForRentExemption = await connection.getMinimumBalanceForRentExemption(
6363
StakeProgram.space,
6464
);
65-
const minBalance = minBalanceForRentExemption + MINIMUM_ACTIVE_STAKE;
65+
const minBalance = new BN(minBalanceForRentExemption + MINIMUM_ACTIVE_STAKE);
6666

6767
let accounts = [] as Array<{
6868
type: 'preferred' | 'active' | 'transient' | 'reserve';
6969
voteAddress?: PublicKey | undefined;
7070
stakeAddress: PublicKey;
71-
lamports: number;
71+
lamports: BN;
7272
}>;
7373

7474
// Prepare accounts
@@ -91,12 +91,12 @@ export async function prepareWithdrawAccounts(
9191
type: isPreferred ? 'preferred' : 'active',
9292
voteAddress: validator.voteAccountAddress,
9393
stakeAddress: stakeAccountAddress,
94-
lamports: validator.activeStakeLamports.toNumber(),
94+
lamports: validator.activeStakeLamports,
9595
});
9696
}
9797

98-
const transientStakeLamports = validator.transientStakeLamports.toNumber() - minBalance;
99-
if (transientStakeLamports > 0) {
98+
const transientStakeLamports = validator.transientStakeLamports.sub(minBalance);
99+
if (transientStakeLamports.gt(new BN(0))) {
100100
const transientStakeAccountAddress = await findTransientStakeProgramAddress(
101101
STAKE_POOL_PROGRAM_ID,
102102
validator.voteAccountAddress,
@@ -113,11 +113,11 @@ export async function prepareWithdrawAccounts(
113113
}
114114

115115
// Sort from highest to lowest balance
116-
accounts = accounts.sort(compareFn ? compareFn : (a, b) => b.lamports - a.lamports);
116+
accounts = accounts.sort(compareFn ? compareFn : (a, b) => b.lamports.sub(a.lamports).toNumber());
117117

118118
const reserveStake = await connection.getAccountInfo(stakePool.reserveStake);
119-
const reserveStakeBalance = (reserveStake?.lamports ?? 0) - minBalanceForRentExemption;
120-
if (reserveStakeBalance > 0) {
119+
const reserveStakeBalance = new BN((reserveStake?.lamports ?? 0) - minBalanceForRentExemption);
120+
if (reserveStakeBalance.gt(new BN(0))) {
121121
accounts.push({
122122
type: 'reserve',
123123
stakeAddress: stakePool.reserveStake,
@@ -127,7 +127,7 @@ export async function prepareWithdrawAccounts(
127127

128128
// Prepare the list of accounts to withdraw from
129129
const withdrawFrom: WithdrawAccount[] = [];
130-
let remainingAmount = amount;
130+
let remainingAmount = new BN(amount);
131131

132132
const fee = stakePool.stakeWithdrawalFee;
133133
const inverseFee: Fee = {
@@ -139,40 +139,39 @@ export async function prepareWithdrawAccounts(
139139
const filteredAccounts = accounts.filter((a) => a.type == type);
140140

141141
for (const { stakeAddress, voteAddress, lamports } of filteredAccounts) {
142-
if (lamports <= minBalance && type == 'transient') {
142+
if (lamports.lte(minBalance) && type == 'transient') {
143143
continue;
144144
}
145145

146146
let availableForWithdrawal = calcPoolTokensForDeposit(stakePool, lamports);
147147

148148
if (!skipFee && !inverseFee.numerator.isZero()) {
149-
availableForWithdrawal = divideBnToNumber(
150-
new BN(availableForWithdrawal).mul(inverseFee.denominator),
151-
inverseFee.numerator,
152-
);
149+
availableForWithdrawal = availableForWithdrawal
150+
.mul(inverseFee.denominator)
151+
.div(inverseFee.numerator);
153152
}
154153

155-
const poolAmount = Math.min(availableForWithdrawal, remainingAmount);
156-
if (poolAmount <= 0) {
154+
const poolAmount = BN.min(availableForWithdrawal, remainingAmount);
155+
if (poolAmount.lte(new BN(0))) {
157156
continue;
158157
}
159158

160159
// Those accounts will be withdrawn completely with `claim` instruction
161160
withdrawFrom.push({ stakeAddress, voteAddress, poolAmount });
162-
remainingAmount -= poolAmount;
161+
remainingAmount = remainingAmount.sub(poolAmount);
163162

164-
if (remainingAmount == 0) {
163+
if (remainingAmount.isZero()) {
165164
break;
166165
}
167166
}
168167

169-
if (remainingAmount == 0) {
168+
if (remainingAmount.isZero()) {
170169
break;
171170
}
172171
}
173172

174173
// Not enough stake to withdraw the specified amount
175-
if (remainingAmount > 0) {
174+
if (remainingAmount.gt(new BN(0))) {
176175
throw new Error(
177176
`No stake accounts found in this pool with enough balance to withdraw ${lamportsToSol(
178177
amount,
@@ -186,35 +185,24 @@ export async function prepareWithdrawAccounts(
186185
/**
187186
* Calculate the pool tokens that should be minted for a deposit of `stakeLamports`
188187
*/
189-
export function calcPoolTokensForDeposit(stakePool: StakePool, stakeLamports: number): number {
188+
export function calcPoolTokensForDeposit(stakePool: StakePool, stakeLamports: BN): BN {
190189
if (stakePool.poolTokenSupply.isZero() || stakePool.totalLamports.isZero()) {
191190
return stakeLamports;
192191
}
193-
return Math.floor(
194-
divideBnToNumber(new BN(stakeLamports).mul(stakePool.poolTokenSupply), stakePool.totalLamports),
195-
);
192+
const numerator = stakeLamports.mul(stakePool.poolTokenSupply);
193+
return numerator.div(stakePool.totalLamports);
196194
}
197195

198196
/**
199197
* Calculate lamports amount on withdrawal
200198
*/
201-
export function calcLamportsWithdrawAmount(stakePool: StakePool, poolTokens: number): number {
202-
const numerator = new BN(poolTokens).mul(stakePool.totalLamports);
199+
export function calcLamportsWithdrawAmount(stakePool: StakePool, poolTokens: BN): BN {
200+
const numerator = poolTokens.mul(stakePool.totalLamports);
203201
const denominator = stakePool.poolTokenSupply;
204202
if (numerator.lt(denominator)) {
205-
return 0;
206-
}
207-
return divideBnToNumber(numerator, denominator);
208-
}
209-
210-
export function divideBnToNumber(numerator: BN, denominator: BN): number {
211-
if (denominator.isZero()) {
212-
return 0;
203+
return new BN(0);
213204
}
214-
const quotient = numerator.div(denominator).toNumber();
215-
const rem = numerator.umod(denominator);
216-
const gcd = rem.gcd(denominator);
217-
return quotient + rem.div(gcd).toNumber() / denominator.div(gcd).toNumber();
205+
return numerator.div(denominator);
218206
}
219207

220208
export function newStakeAccount(
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { LAMPORTS_PER_SOL } from '@solana/web3.js';
2+
import { stakePoolMock } from './mocks';
3+
import { calcPoolTokensForDeposit } from '../src/utils/stake';
4+
import BN from 'bn.js';
5+
6+
describe('calculations', () => {
7+
it('should successfully calculate pool tokens for a pool with a lot of stake', () => {
8+
const lamports = new BN(LAMPORTS_PER_SOL * 100);
9+
const bigStakePoolMock = stakePoolMock;
10+
bigStakePoolMock.totalLamports = new BN('11000000000000000'); // 11 million SOL
11+
bigStakePoolMock.poolTokenSupply = new BN('10000000000000000'); // 10 million tokens
12+
const availableForWithdrawal = calcPoolTokensForDeposit(bigStakePoolMock, lamports);
13+
expect(availableForWithdrawal.toNumber()).toEqual(90909090909);
14+
});
15+
});

0 commit comments

Comments
 (0)