11const sdk = require ( '@defillama/sdk' ) ;
22const superagent = require ( 'superagent' ) ;
3+ const { ethers } = require ( 'ethers' ) ;
4+ const { length } = require ( '../agave/abiIncentivesController' ) ;
35
46const BOLD_TOKEN = '0x6440f144b7e50d6a8439336510312d2f54beb01d' ;
7+ const DAY_IN_SECONDS = 24 * 60 * 60 ;
58
69const 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
3541const 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+
3775const 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