Skip to content

Commit 606001d

Browse files
committed
Accrue ARM lending yield per-segment (settle at each allocation + daily snapshot); removes day-boundary noise and the marketNetFlow field
1 parent 80c50ce commit 606001d

5 files changed

Lines changed: 54 additions & 61 deletions

File tree

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

schema.graphql

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -793,16 +793,11 @@ type ArmDailyStat @entity {
793793
# Per-asset protocol redemptions in-flight, aligned to Arm.assets. [0] = 0 (liquidity asset);
794794
# [i>0] = liquidity-denominated pendingRedeemAssets for base asset i. == sum is outstandingAssets1.
795795
outstandingAssets: [String!]!
796-
# Lending-market position factors (carried day-over-day for ArmDailyAssetYield.lendingYield).
797-
# See ArmState for semantics.
796+
# Lending-market position snapshot (informational; ArmDailyAssetYield.lendingYield is accrued
797+
# segment-by-segment, not derived from these). See ArmState for semantics.
798798
marketShares: BigInt!
799799
marketPricePerShare: BigInt!
800800
activeMarket: String!
801-
# Net signed flow into the lending market for the day (deposits − withdrawals), liquidity-asset
802-
# terms. Derived from the actual market-share change x price-per-share at each allocation (not the
803-
# Allocated event amount, which on old ARMs is a target, not the filled amount).
804-
# lendingYield = Δ marketAssets − marketNetFlow.
805-
marketNetFlow: BigInt!
806801
}
807802

808803
type ArmAddressYield @entity {

src/model/generated/armDailyStat.model.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,4 @@ export class ArmDailyStat {
115115

116116
@StringColumn_({nullable: false})
117117
activeMarket!: string
118-
119-
@BigIntColumn_({nullable: false})
120-
marketNetFlow!: bigint
121118
}

src/templates/origin-arm/origin-arm.graphql

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,11 @@ type ArmDailyStat @entity {
113113
# Per-asset protocol redemptions in-flight, aligned to Arm.assets. [0] = 0 (liquidity asset);
114114
# [i>0] = liquidity-denominated pendingRedeemAssets for base asset i. == sum is outstandingAssets1.
115115
outstandingAssets: [String!]!
116-
# Lending-market position factors (carried day-over-day for ArmDailyAssetYield.lendingYield).
117-
# See ArmState for semantics.
116+
# Lending-market position snapshot (informational; ArmDailyAssetYield.lendingYield is accrued
117+
# segment-by-segment, not derived from these). See ArmState for semantics.
118118
marketShares: BigInt!
119119
marketPricePerShare: BigInt!
120120
activeMarket: String!
121-
# Net signed flow into the lending market for the day (deposits − withdrawals), liquidity-asset
122-
# terms. Derived from the actual market-share change x price-per-share at each allocation (not the
123-
# Allocated event amount, which on old ARMs is a target, not the filled amount).
124-
# lendingYield = Δ marketAssets − marketNetFlow.
125-
marketNetFlow: BigInt!
126121
}
127122

128123
type ArmAddressYield @entity {

src/templates/origin-arm/origin-arm.ts

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -200,15 +200,17 @@ export const createOriginARMProcessors = ({
200200
// Persists across batches; seeded once at processor start from today's persisted rows so a
201201
// mid-day restart keeps summing. Keyed `${date}:${assetLower}`.
202202
const currentDayAssetYield = new Map<string, { tradingYield: bigint; swapVolume: bigint }>()
203-
// Per-date running net flow (signed, liquidity-asset terms) into the lending market. Subtracted
204-
// from the day's marketAssets change to isolate true lending yield. Flow is derived from the
205-
// actual share change x price-per-share at each allocation (NOT the Allocated event's amount,
206-
// which on old single-arg ARMs is the target delta, not the filled amount). Keyed by date;
207-
// seeded at start from today's persisted ArmDailyStat.marketNetFlow.
208-
const currentDayMarketFlow = new Map<string, bigint>()
209-
// Market share count + address carried across allocations to compute each flow's share delta.
210-
// Seeded from the latest persisted ArmState so a resumed run doesn't book a spurious first flow.
203+
// Per-date accrued lending yield (liquidity-asset terms), built by segmented accrual: each segment
204+
// between consecutive checkpoints (an allocation or a daily snapshot) earns sharesHeld x Δpps, and
205+
// is credited to the segment-end block's day. Closing the appreciation window *before* a flow lands
206+
// means a deposit/withdrawal can never bleed across a day boundary — the per-day value is exact.
207+
// Keyed by date; seeded at start from today's persisted ArmDailyAssetYield lending row.
208+
const currentDayLending = new Map<string, bigint>()
209+
// Carried checkpoint of the market position. lastMarketShares/Pps are the position held since the
210+
// last checkpoint (constant until the next flow), used to settle the segment's appreciation.
211+
// Seeded from the latest persisted ArmState so a resumed run continues from the right baseline.
211212
let lastMarketShares = 0n
213+
let lastMarketPps = 10n ** 18n
212214
let lastMarketAddress = ADDRESS_ZERO
213215
let yieldSourceInitialized = false
214216
let initialize = async (ctx: Context) => {
@@ -312,18 +314,19 @@ export const createOriginARMProcessors = ({
312314
tradingYield: row.tradingYield,
313315
swapVolume: row.swapVolume,
314316
})
317+
// Seed today's accrued lending from the liquidity asset's persisted row so a mid-day
318+
// restart continues accruing rather than restarting today's total from zero.
319+
if (row.lendingYield !== 0n) currentDayLending.set(row.date, row.lendingYield)
315320
}
316-
// Seed today's running market flow so a mid-day restart keeps the net-flow total intact.
317-
const todayStat = await ctx.store.get(ArmDailyStat, `${ctx.chain.id}:${today}:${armAddress}`)
318-
if (todayStat) currentDayMarketFlow.set(today, todayStat.marketNetFlow)
319-
// Seed the market-share baseline from the latest state so the first allocation in a resumed
320-
// run computes a correct delta rather than treating the whole position as a fresh inflow.
321+
// Seed the carried checkpoint (shares + pps + market) from the latest state so the first
322+
// segment after a resume settles from the correct baseline.
321323
const lastState = await ctx.store.findOne(ArmState, {
322324
where: { chainId: ctx.chain.id, address: armAddress },
323325
order: { blockNumber: 'DESC' },
324326
})
325327
if (lastState) {
326328
lastMarketShares = lastState.marketShares
329+
lastMarketPps = lastState.marketPricePerShare
327330
lastMarketAddress = lastState.activeMarket
328331
}
329332
yieldSourceInitialized = true
@@ -518,6 +521,23 @@ export const createOriginARMProcessors = ({
518521
acc.swapVolume += token0Volume
519522
currentDayAssetYield.set(key, acc)
520523
}
524+
// Settle the lending segment from the last checkpoint up to this block: the position held
525+
// since then (lastMarketShares) earned lastShares x (ppsNow - lastPps), credited to this
526+
// block's day. Then advance the carried checkpoint to the post-flow position. Call at every
527+
// allocation (settle before the flow's share change takes effect) and at each daily snapshot
528+
// (the end-of-day boundary). A market switch resets the baseline so pps is never diffed
529+
// across two different markets. `state` must be the post-allocation snapshot for this block.
530+
const settleLending = (block: Block, state: ArmState) => {
531+
const switched = lastMarketAddress !== ADDRESS_ZERO && state.activeMarket !== lastMarketAddress
532+
if (!switched && lastMarketShares > 0n) {
533+
const appreciation = (lastMarketShares * (state.marketPricePerShare - lastMarketPps)) / 10n ** 18n
534+
const dateStr = new Date(block.header.timestamp).toISOString().slice(0, 10)
535+
currentDayLending.set(dateStr, (currentDayLending.get(dateStr) ?? 0n) + appreciation)
536+
}
537+
lastMarketShares = state.marketShares
538+
lastMarketPps = state.marketPricePerShare
539+
lastMarketAddress = state.activeMarket
540+
}
521541
// A base asset's rate in asset0 (liquidity) terms, 1e18-scaled (token0 per base).
522542
// Mirrors the assetRates logic: pegged base assets and the liquidity asset are 1:1;
523543
// appreciating assets use their adapter (post-upgrade) or getRate1 (pre-upgrade single base).
@@ -705,23 +725,13 @@ export const createOriginARMProcessors = ({
705725
await ctx.store.save(armEntity)
706726
}
707727
if (allocatedFilter.matches(log)) {
708-
// An allocation moved liquidity in/out of the market. Snapshot the post-allocation
709-
// state and derive the actual flow from the share change x price-per-share — the real
710-
// asset value moved, independent of the Allocated event's (target, not actual) amount.
711-
// On a market switch the new market starts fresh (the old position was redeemed and its
712-
// outflow already counted), so reset the share baseline to 0 — otherwise the new
713-
// market's opening deposit is mistaken for the switch and dropped, leaving the later
714-
// withdrawals unbalanced and surfacing as phantom lending. The switch day itself is
715-
// zeroed by the marketUnchanged guard.
728+
// An allocation is about to change the market position. Snapshot the post-allocation
729+
// state and settle the lending segment first: the pre-flow position earned up to this
730+
// block (the deposit/withdraw mints/burns at the current pps, so pps is unchanged across
731+
// the flow), then the carried baseline advances to the new position. This closes the
732+
// appreciation window before the flow, so the flow never bleeds into another day.
716733
const state = await getCurrentState(block)
717-
if (lastMarketAddress !== ADDRESS_ZERO && state.activeMarket !== lastMarketAddress) {
718-
lastMarketShares = 0n
719-
}
720-
const flow = ((state.marketShares - lastMarketShares) * state.marketPricePerShare) / 10n ** 18n
721-
const dateStr = new Date(block.header.timestamp).toISOString().slice(0, 10)
722-
currentDayMarketFlow.set(dateStr, (currentDayMarketFlow.get(dateStr) ?? 0n) + flow)
723-
lastMarketShares = state.marketShares
724-
lastMarketAddress = state.activeMarket
734+
settleLending(block, state)
725735
}
726736
}
727737

@@ -878,9 +888,10 @@ export const createOriginARMProcessors = ({
878888
const previousDailyStat =
879889
dailyStatsMap.get(previousDayId) ?? (await ctx.store.get(ArmDailyStat, previousDayId))
880890
const armDayApy = calculateArmDailyApy({ block, state, previousDailyStat })
881-
// Net lending-market flow accumulated for this date (used below for lendingYield and
882-
// persisted on the daily stat for restart-safe seeding).
883-
const marketNetFlow = currentDayMarketFlow.get(dateStr) ?? 0n
891+
// Close the day's final lending segment at this end-of-day snapshot (the position held
892+
// since the last checkpoint earned up to now). After this, currentDayLending[dateStr]
893+
// holds the full, exact lending yield for the day.
894+
settleLending(block, state)
884895

885896
// asset->liquidity rate, 1e18-scaled, aligned to armEntity.assets. [0] (liquidity) = 1e18.
886897
const upgraded = armEntity.upgradeBlock != null && block.header.height >= armEntity.upgradeBlock
@@ -947,7 +958,6 @@ export const createOriginARMProcessors = ({
947958
marketShares: state.marketShares,
948959
marketPricePerShare: state.marketPricePerShare,
949960
activeMarket: state.activeMarket,
950-
marketNetFlow,
951961
})
952962
dailyStatsMap.set(currentDayId, armDailyStatEntity)
953963

@@ -957,14 +967,10 @@ export const createOriginARMProcessors = ({
957967
const ONE_BIG = 10n ** 18n
958968
const prevBalances = previousDailyStat?.assetBalances ?? []
959969
const prevRates = previousDailyStat?.assetRates ?? []
960-
const marketUnchanged =
961-
previousDailyStat != null && previousDailyStat.activeMarket === state.activeMarket
962-
// Exact lending yield: the day's market-position value change minus net deposits/
963-
// withdrawals (which carry their own asset value and are not yield). Zeroed across a
964-
// market switch, where the day-over-day marketAssets delta spans incomparable positions.
965-
const lendingYield = marketUnchanged
966-
? state.marketAssets - previousDailyStat!.marketAssets - marketNetFlow
967-
: 0n
970+
// Exact lending yield: the appreciation accrued segment-by-segment over the day (settled
971+
// at each allocation + this end-of-day snapshot). No flow subtraction, so it can't carry
972+
// day-boundary noise; goes on the liquidity-asset row only.
973+
const lendingYield = currentDayLending.get(dateStr) ?? 0n
968974
for (let i = 0; i < armEntity.assets.length; i++) {
969975
const asset = armEntity.assets[i].toLowerCase()
970976
const acc = currentDayAssetYield.get(`${dateStr}:${asset}`)

0 commit comments

Comments
 (0)