@kamino-finance/farms-sdk
Version:
172 lines (150 loc) • 5 kB
text/typescript
import { UserState } from "../@codegen/farms/accounts/userState";
import { FarmState } from "../@codegen/farms/accounts/farmState";
import Decimal from "decimal.js";
import { WAD } from "./utils";
import { RewardInfo, RewardType } from "../@codegen/farms/types";
import { OraclePrices } from "@kamino-finance/scope-sdk/dist/@codegen/scope/accounts";
import { DEFAULT_PUBLIC_KEY } from "./pubkey";
export function calculatePendingRewards(
farmState: FarmState,
userState: UserState,
rewardIndex: number,
ts: Decimal,
scopePrices: OraclePrices | null,
): Decimal {
let scopePrice: Decimal | null = scopePriceForFarm(farmState, scopePrices);
const newRewardPerToken = calculateRewardPerStake(
farmState,
ts,
rewardIndex,
scopePrice,
);
const rewardTally = new Decimal(
userState.rewardsTallyScaled[rewardIndex].toString(),
).div(WAD);
let activeStakeScaled = new Decimal(0);
if (farmState.delegateAuthority === DEFAULT_PUBLIC_KEY) {
activeStakeScaled = new Decimal(userState.activeStakeScaled.toString()).div(
WAD,
);
} else {
activeStakeScaled = new Decimal(userState.activeStakeScaled.toString());
}
const newRewardTally = activeStakeScaled.mul(newRewardPerToken);
const newReward = new Decimal(newRewardTally.sub(rewardTally).toFixed(0));
const prevReward = new Decimal(
userState.rewardsIssuedUnclaimed[rewardIndex].toString(),
);
const finalReward = prevReward.add(newReward);
return finalReward;
}
export function calculateCurrentRewardPerToken(
rewardInfo: RewardInfo,
currentTimeUnit: Decimal,
): number {
const rewardCurve = rewardInfo.rewardScheduleCurve;
let index = 0;
for (let i = 0; i < rewardCurve.points.length; i++) {
if (
new Decimal(rewardCurve.points[i].tsStart.toString()).lte(currentTimeUnit)
) {
index = i;
} else {
break;
}
}
return Number(rewardCurve.points[index].rewardPerTimeUnit);
}
function calculateRewardPerStake(
farmState: FarmState,
ts: Decimal,
rewardIndex: number,
scopePrice: Decimal | null,
): Decimal {
const rewardInfo = farmState.rewardInfos[rewardIndex];
const newRewards = calculateNewRewardToBeIssued(
farmState,
ts,
rewardIndex,
scopePrice,
);
let scaledRewards = newRewards.mul(WAD);
let rewardPerTokenScaled = new Decimal(
rewardInfo.rewardPerShareScaled.toString(),
).div(WAD);
let rewardPerTokenScaledAdded = new Decimal(0);
const totalActiveStakeScaled = new Decimal(
farmState.totalActiveStakeScaled.toString(),
);
if (farmState.delegateAuthority === DEFAULT_PUBLIC_KEY) {
rewardPerTokenScaledAdded = scaledRewards.div(totalActiveStakeScaled);
} else {
if (
scaledRewards.gt(new Decimal(0)) &&
totalActiveStakeScaled.gt(new Decimal(0))
) {
rewardPerTokenScaledAdded = scaledRewards
.div(totalActiveStakeScaled)
.div(WAD);
}
}
const finalRewardPerToken = rewardPerTokenScaled.add(
rewardPerTokenScaledAdded,
);
return finalRewardPerToken;
}
export function calculateNewRewardToBeIssued(
farmState: FarmState,
ts: Decimal,
rewardIndex: number,
scopePrice: Decimal | null,
): Decimal {
const rewardInfo = farmState.rewardInfos[rewardIndex];
const tsDiff = ts.sub(new Decimal(rewardInfo.lastIssuanceTs.toString()));
let rps = calculateCurrentRewardPerToken(rewardInfo, ts);
let rpsDecimal = new Decimal(10 ** rewardInfo.rewardsPerSecondDecimals);
let newRewards = tsDiff.mul(new Decimal(rps)).div(rpsDecimal);
switch (rewardInfo.rewardType) {
case RewardType.Proportional:
// `rps` means `reward per second for entire farm`
break;
case RewardType.Constant: {
// `rps` means `reward per second for each lamport staked`
const totalStaked = new Decimal(farmState.totalStakedAmount.toString());
newRewards = newRewards.mul(totalStaked);
break;
}
}
if (farmState.scopePrices !== DEFAULT_PUBLIC_KEY) {
// Oracle adjustment
if (scopePrice == null) {
throw new Error("Scope price not provided");
}
console.log("Adjusting by scope price", scopePrice.toString());
newRewards = newRewards.mul(scopePrice);
}
// We cap rewards by how much is available in the farm anyway
let cappedRewards = Decimal.min(
newRewards,
new Decimal(rewardInfo.rewardsAvailable.toString()),
);
return cappedRewards;
}
export function scopePriceForFarm(
farmState: FarmState,
scopePrices: OraclePrices | null,
): Decimal | null {
let scopePrice: Decimal | null = null;
if (farmState.scopePrices !== DEFAULT_PUBLIC_KEY) {
if (scopePrices == null) {
throw new Error("Scope prices not provided");
}
const price =
scopePrices.prices[
new Decimal(farmState.scopeOraclePriceId.toString()).toNumber()
];
const factor = new Decimal(10).pow(price.price.exp.toString());
scopePrice = new Decimal(price.price.value.toString()).div(factor);
}
return scopePrice;
}