Skip to content

Commit

Permalink
update apy monitors to capture max apy instead of aggegate apy
Browse files Browse the repository at this point in the history
  • Loading branch information
gowthamsundaresan committed Nov 15, 2024
1 parent d55b353 commit 3576ec7
Show file tree
Hide file tree
Showing 8 changed files with 399 additions and 3 deletions.
10 changes: 9 additions & 1 deletion packages/api/src/routes/stakers/stakerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,11 +670,19 @@ async function calculateStakerRewards(
}
})

const isTracked =
(await prisma.user.count({
where: {
address: staker.address.toLowerCase()
}
})) > 0

return {
aggregateApy: avsApys.reduce((sum, avs) => sum + avs.apy, 0),
tokenAmounts,
strategyApys,
avsApys
avsApys,
isTracked
}
} catch {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
*/
-- AlterTable
ALTER TABLE "Avs" DROP COLUMN "apy";
ALTER TABLE "Avs" DROP COLUMN "apy",
ADD COLUMN "maxApy" DECIMAL(8,4) NOT NULL DEFAULT 0;

-- AlterTable
ALTER TABLE "Operator" DROP COLUMN "apy";
ALTER TABLE "Operator" DROP COLUMN "apy",
ADD COLUMN "maxApy" DECIMAL(8,4) NOT NULL DEFAULT 0;

-- CreateTable
CREATE TABLE "StakerTokenRewards" (
Expand Down
2 changes: 2 additions & 0 deletions packages/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ model Avs {
totalStakers Int @default(0)
totalOperators Int @default(0)
maxApy Decimal @default(0) @db.Decimal(8, 4)
tvlEth Decimal @default(0) @db.Decimal(20, 8)
sharesHash String?
Expand Down Expand Up @@ -126,6 +127,7 @@ model Operator {
totalStakers Int @default(0)
totalAvs Int @default(0)
maxApy Decimal @default(0) @db.Decimal(8, 4)
tvlEth Decimal @default(0) @db.Decimal(20, 8)
sharesHash String?
Expand Down
39 changes: 39 additions & 0 deletions packages/seeder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { monitorOperatorMetrics } from './monitors/operatorMetrics'
import { seedAvsStrategyRewards } from './seedAvsStrategyRewards'
import { seedStakerTokenRewards } from './seedStakerTokenRewards'
import { seedLogsAVSRewardsSubmission } from './events/seedLogsRewardsSubmissions'
import { monitorAvsApy } from './monitors/avsApy'
import { monitorOperatorApy } from './monitors/operatorApy'
import { seedLogStrategyWhitelist } from './events/seedLogStrategyWhitelist'
import { seedLogsDistributionRootSubmitted } from './events/seedLogsDistributionRootSubmitted'

Expand Down Expand Up @@ -173,8 +175,45 @@ async function seedEigenDailyData(retryCount = 0) {
}
}

/**
* Seed APY data
*
* @param retryCount
* @returns
*/
async function seedApyData(retryCount = 0) {
try {
console.log('\nSeeding APY data ...')
console.time('Seeded APY data in')

if (isSeedingBlockData) {
console.log('Block data is being seeded. Retrying in 15 minutes...')
setTimeout(() => seedEigenDailyData(retryCount), RETRY_DELAY * 1000)
return
}

await monitorAvsApy()
await monitorOperatorApy()

console.timeEnd('Seeded APY data in')
} catch (error) {
console.log(`Failed to seed Avs and Operator APY data at: ${Date.now()}`)
console.log(error)

if (retryCount < MAX_RETRIES) {
console.log(`Retrying in 15 minutes... (Attempt ${retryCount + 1} of ${MAX_RETRIES})`)
setTimeout(() => seedEigenDailyData(retryCount + 1), RETRY_DELAY * 1000)
} else {
console.log('Max retries reached. Avs and Operator APY seeding failed.')
}
}
}

// Start seeding data instantly
seedEigenData()

// Schedule seedEigenDailyData to run at 5 minutes past midnight every day
cron.schedule('5 0 * * *', () => seedEigenDailyData())

// Schedule seedApyData to run at 5 minutes past 2am every day
cron.schedule('5 2 * * *', () => seedApyData())
148 changes: 148 additions & 0 deletions packages/seeder/src/monitors/avsApy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import Prisma from '@prisma/client'
import { bulkUpdateDbTransactions } from '../utils/seeder'
import { getPrismaClient } from '../utils/prismaClient'
import { sharesToTVLStrategies, getStrategiesWithShareUnderlying } from '../utils/strategyShares'
import { withOperatorShares } from '../utils/operatorShares'
import { fetchTokenPrices } from '../utils/tokenPrices'

export async function monitorAvsApy() {
const prismaClient = getPrismaClient()

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const dbTransactions: any[] = []
const data: {
address: string
maxApy: Prisma.Prisma.Decimal
}[] = []

let skip = 0
const take = 32

const tokenPrices = await fetchTokenPrices()
const strategiesWithSharesUnderlying = await getStrategiesWithShareUnderlying()

while (true) {
try {
// Fetch totalStakers, totalOperators & rewards data for all avs in this iteration
const avsMetrics = await prismaClient.avs.findMany({
include: {
operators: {
where: { isActive: true },
include: {
operator: {
include: {
shares: true
}
}
}
},
rewardSubmissions: true
},
skip,
take
})

if (avsMetrics.length === 0) break

// Setup all db transactions for this iteration
for (const avs of avsMetrics) {
const strategyRewardsMap: Map<string, number> = new Map()

const shares = withOperatorShares(avs.operators).filter(
(s) => avs.restakeableStrategies.indexOf(s.strategyAddress.toLowerCase()) !== -1
)

if (avs.rewardSubmissions.length > 0) {
// Fetch the AVS tvl for each strategy
const tvlStrategiesEth = sharesToTVLStrategies(shares, strategiesWithSharesUnderlying)

// Iterate through each strategy and calculate all its rewards
for (const strategyAddress of avs.restakeableStrategies) {
const strategyTvl = tvlStrategiesEth[strategyAddress.toLowerCase()] || 0
if (strategyTvl === 0) continue

let totalRewardsEth = new Prisma.Prisma.Decimal(0)
let totalDuration = 0

// Find all reward submissions attributable to the strategy
const relevantSubmissions = avs.rewardSubmissions.filter(
(submission) =>
submission.strategyAddress.toLowerCase() === strategyAddress.toLowerCase()
)

// Calculate each reward amount for the strategy
for (const submission of relevantSubmissions) {
let rewardIncrementEth = new Prisma.Prisma.Decimal(0)
const rewardTokenAddress = submission.token.toLowerCase()

// Normalize reward amount to its ETH price
if (rewardTokenAddress) {
const tokenPrice = tokenPrices.find(
(tp) => tp.address.toLowerCase() === rewardTokenAddress
)
rewardIncrementEth = submission.amount
.mul(new Prisma.Prisma.Decimal(tokenPrice?.ethPrice ?? 0))
.div(new Prisma.Prisma.Decimal(10).pow(tokenPrice?.decimals ?? 18)) // No decimals
}

// Multiply reward amount in ETH by the strategy weight
rewardIncrementEth = rewardIncrementEth
.mul(submission.multiplier)
.div(new Prisma.Prisma.Decimal(10).pow(18))

totalRewardsEth = totalRewardsEth.add(rewardIncrementEth) // No decimals
totalDuration += submission.duration
}

if (totalDuration === 0) continue

// Annualize the reward basis its duration to find yearly APY
const rewardRate = totalRewardsEth.toNumber() / strategyTvl
const annualizedRate = rewardRate * ((365 * 24 * 60 * 60) / totalDuration)
const apy = annualizedRate * 100

strategyRewardsMap.set(strategyAddress, apy)
}

// Calculate max achievable APY
if (strategyRewardsMap.size > 0) {
const maxApy = new Prisma.Prisma.Decimal(Math.max(...strategyRewardsMap.values()))

if (avs.maxApy !== maxApy) {
data.push({
address: avs.address,
maxApy
})
}
}
}
}

skip += take

if (data.length > 0) {
const query = `
UPDATE "Avs" AS a
SET
"maxApy" = a2."maxApy"
FROM
(
VALUES
${data.map((d) => `('${d.address}', ${d.maxApy})`).join(', ')}
) AS a2 (address, "maxApy")
WHERE
a2.address = a.address;
`
dbTransactions.push(prismaClient.$executeRaw`${Prisma.Prisma.raw(query)}`)
}
} catch (error) {}
}

// Write to db
if (dbTransactions.length > 0) {
await bulkUpdateDbTransactions(
dbTransactions,
`[Monitor] Updated AVS APYs: ${dbTransactions.length}`
)
}
}
Loading

0 comments on commit 3576ec7

Please sign in to comment.