Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subgraph Beanstalk 2.3.0 - Pod market fixes + apy calculation improvements #953

Merged
merged 9 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 100 additions & 110 deletions projects/subgraph-beanstalk/src/YieldHandler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Address, BigDecimal, BigInt, log } from "@graphprotocol/graph-ts";
import { Beanstalk } from "../generated/Season-Replanted/Beanstalk";
import { BEANSTALK, BEAN_ERC20, FERTILIZER } from "../../subgraph-core/utils/Constants";
import { BI_10, ONE_BD, ONE_BI, toBigInt, toDecimal, ZERO_BD, ZERO_BI } from "../../subgraph-core/utils/Decimals";
import { ONE_BD, toDecimal, ZERO_BD, ZERO_BI } from "../../subgraph-core/utils/Decimals";
import { loadFertilizer } from "./utils/Fertilizer";
import { loadFertilizerYield } from "./utils/FertilizerYield";
import {
Expand All @@ -13,8 +13,8 @@ import {
loadWhitelistTokenSetting,
SiloAsset_findIndex_token
} from "./utils/SiloEntities";
import { BigDecimal_max, BigDecimal_sum, BigInt_max, BigInt_sum } from "../../subgraph-core/utils/ArrayMath";
import { getGerminatingBdvs, tryLoadBothGerminating } from "./utils/Germinating";
import { BigDecimal_sum, f64_sum, f64_max } from "../../subgraph-core/utils/ArrayMath";
import { getGerminatingBdvs } from "./utils/Germinating";
import { getCurrentSeason } from "./utils/Season";
import { SiloAsset, WhitelistTokenSetting } from "../generated/schema";

Expand Down Expand Up @@ -234,38 +234,41 @@ export function calculateAPYPreGauge(
seeds: BigInt
): BigDecimal[] {
// Initialize sequence
let C = toDecimal(seeds); // Init: Total Seeds
let K = toDecimal(stalk, 10); // Init: Total Stalk
let b = seedsPerBDV.div(seedsPerBeanBDV); // Init: User BDV
let k = BigDecimal.fromString("1"); // Init: User Stalk
const beansPerSeason: f64 = parseFloat(n.toString());
let C: f64 = parseFloat(toDecimal(seeds).toString()); // Init: Total Seeds
let K: f64 = parseFloat(toDecimal(stalk, 10).toString()); // Init: Total Stalk
let b: f64 = parseFloat(seedsPerBDV.div(seedsPerBeanBDV).toString()); // Init: User BDV
let k: f64 = 1; // Init: User Stalk

let _seedsPerBeanBdv: f64 = parseInt(seedsPerBeanBDV.toString());

// Farmer initial values
let b_start = b;
let k_start = k;
let b_start: f64 = b;
let k_start: f64 = k;

// Placeholders for above values during each iteration
let C_i = ZERO_BD;
let K_i = ZERO_BD;
let b_i = ZERO_BD;
let k_i = ZERO_BD;
let C_i: f64 = 0;
let K_i: f64 = 0;
let b_i: f64 = 0;
let k_i: f64 = 0;

// Stalk and Seeds per Deposited Bean.
let STALK_PER_SEED = BigDecimal.fromString("0.0001"); // 1/10,000 Stalk per Seed
let STALK_PER_BEAN = seedsPerBeanBDV.div(BigDecimal.fromString("10000")); // 3 Seeds per Bean * 1/10,000 Stalk per Seed
let STALK_PER_SEED: f64 = 0.0001; // 1/10,000 Stalk per Seed
let STALK_PER_BEAN: f64 = parseFloat(seedsPerBeanBDV.div(BigDecimal.fromString("10000")).toString()); // 3 Seeds per Bean * 1/10,000 Stalk per Seed

for (let i = 0; i < 8760; i++) {
// Each Season, Farmer's ownership = `current Stalk / total Stalk`
let ownership = k.div(K);
let newBDV = n.times(ownership);
let ownership: f64 = k / K;
let newBDV: f64 = beansPerSeason * ownership;

// Total Seeds: each seignorage Bean => 3 Seeds
C_i = C.plus(n.times(seedsPerBeanBDV));
C_i = C + beansPerSeason * _seedsPerBeanBdv;
// Total Stalk: each seignorage Bean => 1 Stalk, each outstanding Bean => 1/10_000 Stalk
K_i = K.plus(n).plus(STALK_PER_SEED.times(C));
K_i = K + beansPerSeason + STALK_PER_SEED * C;
// Farmer BDV: each seignorage Bean => 1 BDV
b_i = b.plus(newBDV);
b_i = b + newBDV;
// Farmer Stalk: each 1 BDV => 1 Stalk, each outstanding Bean => d = 1/5_000 Stalk per Bean
k_i = k.plus(newBDV).plus(STALK_PER_BEAN.times(b));
k_i = k + newBDV + STALK_PER_BEAN * b;

C = C_i;
K = K_i;
Expand All @@ -282,10 +285,10 @@ export function calculateAPYPreGauge(
// b_start = 1
// b = 1.1
// b.minus(b_start) = 0.1 = 10% APY
let beanApy = b.minus(b_start); // beanAPY
let stalkApy = k.minus(k_start); // stalkAPY
let beanApy = b - b_start; // beanAPY
let stalkApy = k - k_start; // stalkAPY

return [beanApy, stalkApy];
return [BigDecimal.fromString(beanApy.toString()), BigDecimal.fromString(stalkApy.toString())];
}

/**
Expand Down Expand Up @@ -339,126 +342,113 @@ export function calculateGaugeVAPYs(
nonGaugeGerminatingBdv: BigDecimal[],
staticSeeds: Array<BigDecimal | null>
): BigDecimal[][] {
// Fixed-point arithmetic is used here to achieve >40% speedup over using BigDecimal
// Everything is still passed to this function as BigDecimal so we can normalize the precision as set here
const PRECISION: u8 = 12;
const PRECISION_BI = toBigInt(ONE_BD, PRECISION);
// A larger precision is required for tracking user balances as they can be highly fractional
const BALANCES_PRECISION: u8 = 18;
const BALANCES_PRECISION_BI = toBigInt(ONE_BD, BALANCES_PRECISION);
const _earnedBeans = parseFloat(earnedBeans.toString());

// Current percentages allocations of each LP
let currentPercentLpBdv: BigInt[] = [];
const sumLpBdv = BigDecimal_sum(gaugeLpDepositedBdv);
let currentPercentLpBdv: f64[] = [];
const sumLpBdv: BigDecimal = BigDecimal_sum(gaugeLpDepositedBdv);
for (let i = 0; i < gaugeLpDepositedBdv.length; ++i) {
currentPercentLpBdv.push(toBigInt(gaugeLpDepositedBdv[i].div(sumLpBdv), PRECISION));
currentPercentLpBdv.push(parseFloat(gaugeLpDepositedBdv[i].div(sumLpBdv).toString()));
}

// Current LP GP allocation per BDV
let lpGpPerBdv: BigInt[] = [];
let lpGpPerBdv: f64[] = [];
// Copy these input
let gaugeLpPointsCopy: BigInt[] = [];
let gaugeLpDepositedBdvCopy: BigInt[] = [];
let gaugeLpPointsCopy: f64[] = [];
let gaugeLpDepositedBdvCopy: f64[] = [];
for (let i = 0; i < gaugeLpPoints.length; ++i) {
lpGpPerBdv.push(toBigInt(gaugeLpPoints[i].div(gaugeLpDepositedBdv[i]), PRECISION));
gaugeLpDepositedBdvCopy.push(toBigInt(gaugeLpDepositedBdv[i], PRECISION));
gaugeLpPointsCopy.push(toBigInt(gaugeLpPoints[i], PRECISION));
lpGpPerBdv.push(parseFloat(gaugeLpPoints[i].div(gaugeLpDepositedBdv[i]).toString()));
gaugeLpDepositedBdvCopy.push(parseFloat(gaugeLpDepositedBdv[i].toString()));
gaugeLpPointsCopy.push(parseFloat(gaugeLpPoints[i].toString()));
}

let r = initialR;
let catchUpSeasons = toBigInt(catchUpRate, PRECISION);
let siloReward = toBigInt(earnedBeans, PRECISION);
let beanBdv = toBigInt(siloDepositedBeanBdv, PRECISION);
let totalStalk = toBigInt(siloStalk, PRECISION);
let gaugeBdv = beanBdv.plus(BigInt_sum(gaugeLpDepositedBdvCopy));
let nonGaugeDepositedBdv_ = toBigInt(nonGaugeDepositedBdv, PRECISION);
let totalBdv = gaugeBdv.plus(nonGaugeDepositedBdv_);
let largestLpGpPerBdv = BigInt_max(lpGpPerBdv);

const startingGrownStalk = totalStalk.times(PRECISION_BI).div(totalBdv).minus(toBigInt(ONE_BD, PRECISION));
let userBeans: BigInt[] = [];
let userLp: BigInt[] = [];
let userStalk: BigInt[] = [];
let initialStalk: BigInt[] = [];
let r: f64 = parseFloat(initialR.toString());
let catchUpSeasons: f64 = parseFloat(catchUpRate.toString());
let siloReward: f64 = parseFloat(earnedBeans.toString());
let beanBdv: f64 = parseFloat(siloDepositedBeanBdv.toString());
let totalStalk: f64 = parseFloat(siloStalk.toString());
let gaugeBdv: f64 = beanBdv + f64_sum(gaugeLpDepositedBdvCopy);
let _nonGaugeDepositedBdv: f64 = parseFloat(nonGaugeDepositedBdv.toString());
let totalBdv: f64 = gaugeBdv + _nonGaugeDepositedBdv;
let largestLpGpPerBdv: f64 = f64_max(lpGpPerBdv);

const startingGrownStalk: f64 = totalStalk / totalBdv - 1;
let userBeans: f64[] = [];
let userLp: f64[] = [];
let userStalk: f64[] = [];
let initialStalk: f64[] = [];
for (let i = 0; i < tokens.length; ++i) {
userBeans.push(toBigInt(tokens[i] == -1 ? ONE_BD : ZERO_BD, BALANCES_PRECISION));
userLp.push(toBigInt(tokens[i] == -1 ? ZERO_BD : ONE_BD, BALANCES_PRECISION));
userBeans.push(tokens[i] == -1 ? 1 : 0);
userLp.push(tokens[i] == -1 ? 0 : 1);
// Initial stalk from deposit + avg grown stalk
userStalk.push(toBigInt(ONE_BD, BALANCES_PRECISION).plus(startingGrownStalk.times(BI_10.pow(BALANCES_PRECISION - PRECISION))));
userStalk.push(1 + startingGrownStalk);
initialStalk.push(userStalk[i]);
}

const SEED_PRECISION = toBigInt(BigDecimal.fromString("10000"), PRECISION);
const ONE_YEAR = 8760;
for (let i = 0; i < ONE_YEAR; ++i) {
r = updateR(r, deltaRFromState(earnedBeans));
const rScaled = toBigInt(scaleR(r), PRECISION);
r = updateR(r, deltaRFromState(_earnedBeans));
const rScaled: f64 = scaleR(r);

// Add germinating bdv to actual bdv in the first 2 simulated seasons
if (i < 2) {
const index = season.mod(BigInt.fromString("2")) == ZERO_BI ? 1 : 0;
beanBdv = beanBdv.plus(toBigInt(germinatingBeanBdv[index], PRECISION));
beanBdv = beanBdv + parseFloat(germinatingBeanBdv[index].toString());
for (let j = 0; j < gaugeLpDepositedBdvCopy.length; ++j) {
gaugeLpDepositedBdvCopy[j] = gaugeLpDepositedBdvCopy[j].plus(toBigInt(gaugeLpGerminatingBdv[j][index], PRECISION));
gaugeLpDepositedBdvCopy[j] = gaugeLpDepositedBdvCopy[j] + parseFloat(gaugeLpGerminatingBdv[j][index].toString());
}
gaugeBdv = beanBdv.plus(BigInt_sum(gaugeLpDepositedBdvCopy));
nonGaugeDepositedBdv_ = nonGaugeDepositedBdv_.plus(toBigInt(nonGaugeGerminatingBdv[index], PRECISION));
totalBdv = gaugeBdv.plus(nonGaugeDepositedBdv_);
gaugeBdv = beanBdv + f64_sum(gaugeLpDepositedBdvCopy);
_nonGaugeDepositedBdv = _nonGaugeDepositedBdv + parseFloat(nonGaugeGerminatingBdv[index].toString());
totalBdv = gaugeBdv + _nonGaugeDepositedBdv;
}

if (gaugeLpPoints.length > 1) {
for (let j = 0; j < gaugeLpDepositedBdvCopy.length; ++i) {
gaugeLpPointsCopy[j] = updateGaugePoints(gaugeLpPointsCopy[j], currentPercentLpBdv[j], gaugeLpOptimalPercentBdv[j]);
lpGpPerBdv[j] = gaugeLpPointsCopy[j].times(PRECISION_BI).div(gaugeLpDepositedBdvCopy[j]);
gaugeLpPointsCopy[j] = updateGaugePoints(
gaugeLpPointsCopy[j],
currentPercentLpBdv[j],
parseFloat(gaugeLpOptimalPercentBdv[j].toString())
);
lpGpPerBdv[j] = gaugeLpPointsCopy[j] / gaugeLpDepositedBdvCopy[j];
}
largestLpGpPerBdv = BigInt_max(lpGpPerBdv);
largestLpGpPerBdv = f64_max(lpGpPerBdv);
}

const beanGpPerBdv = largestLpGpPerBdv.times(rScaled).div(PRECISION_BI);
const gpTotal = BigInt_sum(gaugeLpPointsCopy).plus(beanGpPerBdv.times(beanBdv).div(PRECISION_BI));
const avgGsPerBdv = totalStalk.times(PRECISION_BI).div(totalBdv).minus(toBigInt(ONE_BD, PRECISION));
const gs = avgGsPerBdv.times(PRECISION_BI).div(catchUpSeasons).times(gaugeBdv).div(PRECISION_BI);
const beanSeeds = gs.times(PRECISION_BI).div(gpTotal).times(beanGpPerBdv).div(PRECISION_BI).times(SEED_PRECISION);
const beanGpPerBdv: f64 = largestLpGpPerBdv * rScaled;
const gpTotal: f64 = f64_sum(gaugeLpPointsCopy) + beanGpPerBdv * beanBdv;
const avgGsPerBdv: f64 = totalStalk / totalBdv - 1;
const gs: f64 = (avgGsPerBdv / catchUpSeasons) * gaugeBdv;
const beanSeeds: f64 = (gs / gpTotal) * beanGpPerBdv;

totalStalk = totalStalk.plus(gs).plus(siloReward);
gaugeBdv = gaugeBdv.plus(siloReward);
totalBdv = totalBdv.plus(siloReward);
beanBdv = beanBdv.plus(siloReward);
totalStalk = totalStalk + gs + siloReward;
gaugeBdv = gaugeBdv + siloReward;
totalBdv = totalBdv + siloReward;
beanBdv = beanBdv + siloReward;

for (let j = 0; j < tokens.length; ++j) {
// Set this equal to the number of seeds for whichever is the user' deposited lp asset
let lpSeeds = toBigInt(ZERO_BD, PRECISION);
let lpSeeds: f64 = 0.0;
if (tokens[j] != -1) {
if (tokens[j] < 0) {
lpSeeds = toBigInt(staticSeeds[j]!, PRECISION);
lpSeeds = parseFloat(staticSeeds[j]!.toString());
} else {
lpSeeds = gs.times(PRECISION_BI).div(gpTotal).times(lpGpPerBdv[tokens[j]]).div(PRECISION_BI).times(SEED_PRECISION);
lpSeeds = (gs / gpTotal) * lpGpPerBdv[tokens[j]];
}
}

// (disabled) - for germinating deposits not receiving seignorage for 2 seasons
// const userBeanShare = i < 2 ? toBigInt(ZERO_BD, PRECISION) : siloReward.times(userStalk[j]).div(totalStalk);
const userBeanShare = siloReward.times(userStalk[j]).div(totalStalk);
userStalk[j] = userStalk[j]
.plus(userBeanShare)
.plus(userBeans[j].times(beanSeeds).div(PRECISION_BI).plus(userLp[j].times(lpSeeds).div(PRECISION_BI)).div(SEED_PRECISION));
userBeans[j] = userBeans[j].plus(userBeanShare);
const userBeanShare: f64 = (siloReward * userStalk[j]) / totalStalk;
userStalk[j] = userStalk[j] + userBeanShare + (userBeans[j] * beanSeeds + userLp[j] * lpSeeds);
userBeans[j] = userBeans[j] + userBeanShare;
}
}

let retval: BigDecimal[][] = [];
for (let i = 0; i < tokens.length; ++i) {
const beanApy = userBeans[i]
.plus(userLp[i])
.minus(BALANCES_PRECISION_BI)
.times(toBigInt(BigDecimal.fromString("100"), PRECISION));
const stalkApy = userStalk[i]
.minus(initialStalk[i])
.times(BALANCES_PRECISION_BI)
.div(initialStalk[i])
.times(toBigInt(BigDecimal.fromString("100"), PRECISION));
// Add 2 to each precision to divide by 100 (i.e. 25% is .25 not 25)
retval.push([toDecimal(beanApy, PRECISION + BALANCES_PRECISION + 2), toDecimal(stalkApy, PRECISION + BALANCES_PRECISION + 2)]);
const beanApy = userBeans[i] + userLp[i] - 1;
const stalkApy = (userStalk[i] - initialStalk[i]) / initialStalk[i];
retval.push([BigDecimal.fromString(beanApy.toString()), BigDecimal.fromString(stalkApy.toString())]);
}

return retval;
Expand Down Expand Up @@ -493,32 +483,32 @@ function updateFertAPY(t: i32, timestamp: BigInt, window: i32): void {
fertilizerYield.save();
}

function updateR(R: BigDecimal, change: BigDecimal): BigDecimal {
const newR = R.plus(change);
if (newR > ONE_BD) {
return ONE_BD;
} else if (newR < ZERO_BD) {
return ZERO_BD;
function updateR(R: f64, change: f64): f64 {
const newR = R + change;
if (newR > 1) {
return 1;
} else if (newR < 0) {
return 0;
}
return newR;
}

function scaleR(R: BigDecimal): BigDecimal {
return BigDecimal.fromString("0.5").plus(BigDecimal.fromString("0.5").times(R));
function scaleR(R: f64): f64 {
return 0.5 + 0.5 * R;
}

// For now we return an increasing R value only when there are no beans minted over the period.
// In the future this needs to take into account beanstalk state and the frequency of how many seasons have mints
function deltaRFromState(earnedBeans: BigDecimal): BigDecimal {
if (earnedBeans == ZERO_BD) {
return BigDecimal.fromString("0.01");
function deltaRFromState(earnedBeans: f64): f64 {
if (earnedBeans == 0) {
return 0.01;
}
return BigDecimal.fromString("-0.01");
return -0.01;
}

// TODO: implement the various gauge point functions and choose which one to call based on the stored selector
// see {GaugePointFacet.defaultGaugePointFunction} for implementation.
// This will become relevant once there are multiple functions implemented in the contract.
function updateGaugePoints(gaugePoints: BigInt, currentPercent: BigInt, optimalPercent: BigDecimal): BigInt {
function updateGaugePoints(gaugePoints: f64, currentPercent: f64, optimalPercent: f64): f64 {
return gaugePoints;
}
9 changes: 6 additions & 3 deletions projects/subgraph-beanstalk/tests/YieldHandler.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BigInt, BigDecimal, log, Bytes } from "@graphprotocol/graph-ts";
import { afterEach, assert, clearStore, describe, test } from "matchstick-as/assembly/index";
import * as YieldHandler from "../src/YieldHandler";
import { BigDecimal_isClose, ZERO_BD, ZERO_BI } from "../../subgraph-core/utils/Decimals";
import { BI_10, BigDecimal_isClose, ZERO_BD, ZERO_BI } from "../../subgraph-core/utils/Decimals";
import { loadSilo, loadSiloAsset, loadSiloYield, loadTokenYield, loadWhitelistTokenSetting } from "../src/utils/SiloEntities";
import {
BEAN_3CRV,
Expand Down Expand Up @@ -53,8 +53,11 @@ describe("APY Calculations", () => {
log.info(`bean apy (4 seeds): {}`, [(apy4[0] as BigDecimal).toString()]);
log.info(`stalk apy (2 seeds): {}`, [(apy2[1] as BigDecimal).toString()]);
log.info(`stalk apy (4 seeds): {}`, [(apy4[1] as BigDecimal).toString()]);
assert.assertTrue((apy4[0] as BigDecimal).gt(apy2[0] as BigDecimal));
assert.assertTrue((apy4[1] as BigDecimal).gt(apy2[1] as BigDecimal));
const desiredPrecision = BigDecimal.fromString("0.0001");
assert.assertTrue(BigDecimal_isClose(apy2[0], BigDecimal.fromString("0.14346160171558054"), desiredPrecision));
assert.assertTrue(BigDecimal_isClose(apy4[0], BigDecimal.fromString("0.18299935285933523"), desiredPrecision));
assert.assertTrue(BigDecimal_isClose(apy2[1], BigDecimal.fromString("2.9293613175698485"), desiredPrecision));
assert.assertTrue(BigDecimal_isClose(apy4[1], BigDecimal.fromString("4.318733617611663"), desiredPrecision));
});
});

Expand Down
18 changes: 18 additions & 0 deletions projects/subgraph-core/utils/ArrayMath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,21 @@ export function BigDecimal_indexOfMin(a: BigDecimal[]): u32 {
}
return retval;
}

export function f64_sum(arr: f64[]): f64 {
let sum: f64 = 0.0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}

export function f64_max(arr: f64[]): f64 {
let max = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
2 changes: 1 addition & 1 deletion projects/ui/codegen-individual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ overwrite: true
generates:
# ./src/graph/schema-beanstalk.graphql:
# schema:
# - https://graph.node.bean.money/subgraphs/name/beanstalk-testing
# - https://graph.node.bean.money/subgraphs/name/beanstalk-dev
# plugins:
# - "schema-ast"
./src/graph/schema-bean.graphql:
Expand Down
Loading
Loading