Skip to content

Commit

Permalink
Subgraph Beanstalk 2.3.0 - Pod market fixes + apy calculation improve…
Browse files Browse the repository at this point in the history
…ments (#953)
  • Loading branch information
soilking authored Jul 15, 2024
2 parents 253a8f1 + 85838b1 commit 31d18d8
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 167 deletions.
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

0 comments on commit 31d18d8

Please sign in to comment.