@@ -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