Skip to content

Commit dbc9a81

Browse files
committed
Incorporate liquidation gains into apy calcs
1 parent 37bec75 commit dbc9a81

File tree

1 file changed

+186
-4
lines changed

1 file changed

+186
-4
lines changed

src/adaptors/liquity-v2/index.js

Lines changed: 186 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
const sdk = require('@defillama/sdk');
22
const superagent = require('superagent');
3+
const { ethers } = require('ethers');
4+
const { length } = require('../agave/abiIncentivesController');
35

46
const BOLD_TOKEN = '0x6440f144b7e50d6a8439336510312d2f54beb01d';
7+
const DAY_IN_SECONDS = 24 * 60 * 60;
58

69
const WETH_BRANCH = {
710
activePool: '0xeb5a8c825582965f1d84606e078620a84ab16afe',
811
defaultPool: '0xd4558240d50c2e219a21c9d25afd513bb6e5b1a0',
912
stabilityPool: '0x5721cbbd64fc7ae3ef44a0a3f9a790a9264cf9bf',
1013
borrowerOperations: '0x0b995602b5a797823f92027e8b40c0f2d97aff1c',
14+
troveManager: '0x7bcb64b2c9206a5b699ed43363f6f98d4776cf5a',
1115
collToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'.toLowerCase(),
1216
symbol: 'ETH'
1317
}
@@ -17,6 +21,7 @@ const WSTETH_BRANCH = {
1721
defaultPool: '0xd796e1648526400386cc4d12fa05e5f11e6a22a1',
1822
stabilityPool: '0x9502b7c397e9aa22fe9db7ef7daf21cd2aebe56b',
1923
borrowerOperations: '0x94c1610a7373919bd9cfb09ded19894601f4a1be',
24+
troveManager: '0xa2895d6a3bf110561dfe4b71ca539d84e1928b22',
2025
collToken: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0'.toLowerCase(),
2126
symbol: 'WSTETH'
2227
}
@@ -26,6 +31,7 @@ const RETH_BRANCH = {
2631
defaultPool: '0x5cc5cefd034fdc4728d487a72ca58a410cddcd6b',
2732
stabilityPool: '0xd442e41019b7f5c4dd78f50dc03726c446148695',
2833
borrowerOperations: '0xa351d5b9cda9eb518727c3ceff02208915fda60d',
34+
troveManager: '0xb2b2abeb5c357a234363ff5d180912d319e3e19e',
2935
collToken: '0xae78736cd615f374d3085123a210448e74fc6393'.toLowerCase(),
3036
symbol: 'RETH'
3137
}
@@ -34,6 +40,38 @@ const branches = [WETH_BRANCH, WSTETH_BRANCH, RETH_BRANCH];
3440

3541
const SP_YIELD_SPLIT = 0.75;
3642

43+
const toNumber = (value) => Number(ethers.utils.formatUnits(value, 18));
44+
45+
const STABILITY_POOL_BALANCE_TOPIC = ethers.utils.id(
46+
'StabilityPoolBoldBalanceUpdated(uint256)'
47+
);
48+
const LIQUIDATION_TOPIC = ethers.utils.id(
49+
'Liquidation(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)'
50+
);
51+
const LIQUIDATION_EVENT_TYPES = [
52+
'uint256',
53+
'uint256',
54+
'uint256',
55+
'uint256',
56+
'uint256',
57+
'uint256',
58+
'uint256',
59+
'uint256',
60+
'uint256',
61+
'uint256',
62+
];
63+
64+
const getBlockWindow = async () => {
65+
const latestBlock = await sdk.api.util.getLatestBlock('ethereum');
66+
const startTimestamp = latestBlock.timestamp - DAY_IN_SECONDS;
67+
const startBlock = await sdk.api.util.lookupBlock(startTimestamp, { chain: 'ethereum' });
68+
69+
return {
70+
endBlock: latestBlock.number,
71+
startBlock: startBlock.number,
72+
};
73+
};
74+
3775
const ABIS = {
3876
getTotalBoldDeposits: {
3977
inputs: [],
@@ -164,13 +202,150 @@ const ABIS = {
164202
}
165203
};
166204

205+
const getStabilityPoolDepositsAtBlock = async (stabilityPoolAddr, blockNumber) => {
206+
const res = await sdk.api.abi.call({
207+
target: stabilityPoolAddr,
208+
abi: ABIS.getTotalBoldDeposits,
209+
block: blockNumber,
210+
chain: 'ethereum',
211+
});
212+
213+
return toNumber(res.output);
214+
};
215+
216+
const getStabilityPoolBalanceUpdates = async (stabilityPoolAddr, startBlock, endBlock) => {
217+
const logs = (
218+
await sdk.api.util.getLogs({
219+
target: stabilityPoolAddr,
220+
fromBlock: startBlock,
221+
toBlock: endBlock,
222+
topics: [STABILITY_POOL_BALANCE_TOPIC],
223+
keys: [],
224+
chain: 'ethereum',
225+
})
226+
).output;
227+
228+
return logs
229+
.map((log) => ({
230+
blockNumber: Number(log.blockNumber),
231+
logIndex: Number(log.logIndex),
232+
balance: toNumber(log.data),
233+
transactionHash: log.transactionHash,
234+
}))
235+
.sort(
236+
(a, b) =>
237+
a.blockNumber - b.blockNumber || a.logIndex - b.logIndex
238+
);
239+
};
240+
241+
const getLiquidationEvents = async (troveManagerAddr, startBlock, endBlock) => {
242+
const logs = (
243+
await sdk.api.util.getLogs({
244+
target: troveManagerAddr,
245+
fromBlock: startBlock,
246+
toBlock: endBlock,
247+
topics: [LIQUIDATION_TOPIC],
248+
keys: [],
249+
chain: 'ethereum',
250+
})
251+
).output;
252+
253+
return logs
254+
.map((log) => {
255+
const decoded = ethers.utils.defaultAbiCoder.decode(LIQUIDATION_EVENT_TYPES, log.data);
256+
257+
return {
258+
blockNumber: Number(log.blockNumber),
259+
logIndex: Number(log.logIndex),
260+
transactionHash: log.transactionHash,
261+
debtOffsetBySP: toNumber(decoded[0]),
262+
collSentToSP: toNumber(decoded[4]),
263+
price: toNumber(decoded[9]),
264+
};
265+
})
266+
.sort(
267+
(a, b) =>
268+
a.blockNumber - b.blockNumber || a.logIndex - b.logIndex
269+
);
270+
};
271+
272+
const isBeforeLog = (a, b) =>
273+
a.blockNumber < b.blockNumber ||
274+
(a.blockNumber === b.blockNumber && a.logIndex < b.logIndex);
275+
276+
const calculateLiquidationApyForBranch = async (
277+
branch,
278+
blockWindow,
279+
boldPrice
280+
) => {
281+
// Skip until TroveManager addresses are populated.
282+
if (!branch.troveManager || branch.troveManager === '0x0000000000000000000000000000000000000000') {
283+
return 0;
284+
}
285+
286+
// Fetch all liquidations for the branch in the past 24h.
287+
const liquidations = await getLiquidationEvents(
288+
branch.troveManager,
289+
blockWindow.startBlock,
290+
blockWindow.endBlock
291+
);
292+
293+
if (!liquidations.length) return 0;
294+
295+
// Seed the stability pool balance at the start block and gather later balance updates.
296+
const [startBalance, balanceUpdates] = await Promise.all([
297+
getStabilityPoolDepositsAtBlock(branch.stabilityPool, blockWindow.startBlock),
298+
getStabilityPoolBalanceUpdates(branch.stabilityPool, blockWindow.startBlock, blockWindow.endBlock),
299+
]);
300+
301+
let currentBalance = startBalance;
302+
let balanceIdx = 0;
303+
let dailyReturn = 0;
304+
305+
const advanceBalance = (target) => {
306+
while (
307+
balanceIdx < balanceUpdates.length &&
308+
isBeforeLog(balanceUpdates[balanceIdx], target)
309+
) {
310+
currentBalance = balanceUpdates[balanceIdx].balance;
311+
balanceIdx++;
312+
}
313+
};
314+
315+
for (const liquidation of liquidations) {
316+
advanceBalance(liquidation);
317+
318+
// Only process liquidations if there was SP liquidity at that instant
319+
if (currentBalance === 0) continue;
320+
321+
// Net liquidation gain is collateral sent minus BOLD burned, expressed in USD
322+
const collateralValueUsd = liquidation.collSentToSP * liquidation.price;
323+
const rewardUsd =
324+
collateralValueUsd - liquidation.debtOffsetBySP * boldPrice;
325+
326+
if (rewardUsd <= 0) continue;
327+
328+
// Denominator is the SP deposits present just before the liquidation
329+
const depositsUsd = currentBalance * boldPrice;
330+
if (depositsUsd <= 0) continue;
331+
332+
dailyReturn += rewardUsd / depositsUsd;
333+
}
334+
335+
// Convert daily return to percentage yearly return
336+
return dailyReturn * 100 * 365;
337+
};
338+
167339
const getSPSupplyAndApy = async (spAddr, avgBranchInterestRate, branchBoldSupply) => {
168-
const spSupply = (await sdk.api.abi.call({
340+
let spSupply = (await sdk.api.abi.call({
169341
target: spAddr,
170342
abi: ABIS.getTotalBoldDeposits,
171343
chain: 'ethereum',
172344
})).output / 1e18;
173345

346+
347+
if (spSupply === 0) return [0, 0]
348+
174349
// Yield is the branch interest rate amplifyed by ratio of branch supply to the BOLD in the SP
175350
const spApy = avgBranchInterestRate * SP_YIELD_SPLIT * branchBoldSupply / spSupply;
176351

@@ -226,7 +401,6 @@ const ABIS = {
226401

227402
return 1 / (res.output / 1e18);
228403
}
229-
230404
const getNewApproxAvgInterestRateFromTroveChange = async(activePoolAddr) => {
231405
const res = await sdk.api.abi.call({
232406
target: activePoolAddr,
@@ -266,6 +440,8 @@ const ABIS = {
266440
RETH_BRANCH.price = prices[RETH_BRANCH.collToken];
267441

268442
const pools = [];
443+
const blockWindow = await getBlockWindow();
444+
const boldPrice = prices[BOLD_TOKEN] ?? 1;
269445

270446
for (const branch of branches) {
271447
const collPools = [branch.activePool, branch.defaultPool];
@@ -281,14 +457,20 @@ const ABIS = {
281457

282458
const [spSupply, spApy] = await getSPSupplyAndApy(branch.stabilityPool, borrowApy, totalDebt);
283459
const spSupplyUsd = spSupply * prices[BOLD_TOKEN];
460+
const liquidationApy = await calculateLiquidationApyForBranch(
461+
branch,
462+
blockWindow,
463+
boldPrice
464+
);
465+
const totalSpApy = spApy + liquidationApy;
284466

285467
const spPool =
286468
{
287469
pool: branch.stabilityPool,
288470
project: 'liquity-v2',
289471
symbol: 'BOLD',
290472
chain: 'ethereum',
291-
apy: spApy,
473+
apy: totalSpApy,
292474
tvlUsd: spSupplyUsd,
293475
underlyingTokens: [BOLD_TOKEN],
294476
rewardTokens: [BOLD_TOKEN, branch.collToken],
@@ -321,4 +503,4 @@ module.exports = {
321503
timetravel: false,
322504
apy: main,
323505
url: 'https://www.liquity.org/',
324-
};
506+
};

0 commit comments

Comments
 (0)