From b2f1f5b99e501640d806ae4947d9f9f7d05797c3 Mon Sep 17 00:00:00 2001 From: mconcat Date: Wed, 15 Jan 2025 15:43:43 +0900 Subject: [PATCH] add reward ratio calculation --- staker/calculate_pool_position_reward.gno | 7 ++ .../reward_calculation_canonical_env_test.gno | 1 + staker/reward_calculation_canonical_test.gno | 38 +++--- staker/reward_calculation_pool.gno | 113 ++++++++++++++++++ staker/reward_calculation_tick.gno | 42 ++++++- 5 files changed, 181 insertions(+), 20 deletions(-) diff --git a/staker/calculate_pool_position_reward.gno b/staker/calculate_pool_position_reward.gno index 8153ed6a4..ae534344b 100644 --- a/staker/calculate_pool_position_reward.gno +++ b/staker/calculate_pool_position_reward.gno @@ -4,6 +4,8 @@ import ( "gno.land/r/gnoswap/v1/consts" "gno.land/r/gnoswap/v1/gns" en "gno.land/r/gnoswap/v1/emission" + pn "gno.land/r/gnoswap/v1/position" + pl "gno.land/r/gnoswap/v1/pool" u256 "gno.land/p/gnoswap/uint256" ) @@ -93,13 +95,18 @@ type CalcPositionRewardParam struct { TokenId uint64 } + + func CalcPositionReward(param CalcPositionRewardParam) []Reward { + + // cache per-pool rewards in the internal incentive(tiers) param.PoolTier.cacheReward(param.CurrentHeight, param.Pools) deposit := param.Deposits.Get(param.TokenId) poolPath := deposit.targetPoolPath + pool, ok := param.Pools.Get(poolPath) if !ok { pool = NewPool(poolPath, param.CurrentHeight) diff --git a/staker/reward_calculation_canonical_env_test.gno b/staker/reward_calculation_canonical_env_test.gno index 543214c19..6c7ff1f27 100644 --- a/staker/reward_calculation_canonical_env_test.gno +++ b/staker/reward_calculation_canonical_env_test.gno @@ -265,6 +265,7 @@ func (self *canonicalRewardState) EmulateCalcPositionReward(tokenId uint64) ([]u deposit := deposits.Get(tokenId) poolPath := deposit.targetPoolPath + pool, ok := pools.Get(poolPath) if !ok { pool = NewPool(poolPath, currentHeight) diff --git a/staker/reward_calculation_canonical_test.gno b/staker/reward_calculation_canonical_test.gno index 30e95e84a..b296a1070 100644 --- a/staker/reward_calculation_canonical_test.gno +++ b/staker/reward_calculation_canonical_test.gno @@ -20,7 +20,7 @@ func Setup(t *testing.T) *canonicalRewardState { return NewCanonicalRewardState(t, pools, deposits, TickCrossHook) } -func TestSimple(t *testing.T) { +func TestCanonicalSimple(t *testing.T) { canonical := Setup(t) gnousdc := test_gnousdc @@ -50,7 +50,7 @@ func TestSimple(t *testing.T) { } // To check precision error -func TestLargeStakedLiquidity(t *testing.T) { +func TestCanonicalLargeStakedLiquidity(t *testing.T) { canonical := Setup(t) gnousdc := test_gnousdc @@ -75,7 +75,7 @@ func TestLargeStakedLiquidity(t *testing.T) { } // To check precision error -func TestLargeStakedLiquidity_2(t *testing.T) { +func TestCanonicalLargeStakedLiquidity_2(t *testing.T) { canonical := Setup(t) gnousdc := test_gnousdc @@ -119,7 +119,7 @@ func TestLargeStakedLiquidity_2(t *testing.T) { } // Tests simple case with tick crossing -func TestTickCross_0(t *testing.T) { +func TestCanonicalTickCross_0(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -197,7 +197,7 @@ func TestTickCross_0(t *testing.T) { } // Tests tick crossing with lazy evaluation of position reward -func TestTickCross_1(t *testing.T) { +func TestCanonicalTickCross_1(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -248,7 +248,7 @@ func TestTickCross_1(t *testing.T) { } // Test tick crossing with multiple positions with same tick, same liquidity -func TestTickCross_2(t *testing.T) { +func TestCanonicalTickCross_2(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -310,7 +310,7 @@ func TestTickCross_2(t *testing.T) { } // Test tick crossing with multiple positions with same tick, different liquidity -func TestTickCross_3(t *testing.T) { +func TestCanonicalTickCross_3(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -372,7 +372,7 @@ func TestTickCross_3(t *testing.T) { } // Test tick crossing with multiple positions with different tick, same liquidity -func TestTickCross_4(t *testing.T) { +func TestCanonicalTickCross_4(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -443,7 +443,7 @@ func TestTickCross_4(t *testing.T) { } // Test tick crossing at tick boundaries, forward direction -func TestTickCross_5(t *testing.T) { +func TestCanonicalTickCross_5(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -513,7 +513,7 @@ func TestTickCross_5(t *testing.T) { } // Test tick crossing at tick boundaries, backward direction -func TestTickCross_6(t *testing.T) { +func TestCanonicalTickCross_6(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -571,7 +571,7 @@ func TestTickCross_6(t *testing.T) { canonical.AssertEmulatedRewardOf(0, 0) } -func TestExternalReward_1(t *testing.T) { +func TestCanonicalExternalReward_1(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -599,7 +599,7 @@ func TestExternalReward_1(t *testing.T) { canonical.AssertEmulatedExternalRewardOf(uint64(0), incentiveId, expected) } -func TestMultiplePool_1(t *testing.T) { +func TestCanonicalMultiplePool_1(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -662,7 +662,7 @@ func TestMultiplePool_1(t *testing.T) { canonical.AssertEmulatedRewardOf(1, expected/2) } -func TestMultiplePool_2(t *testing.T) { +func TestCanonicalMultiplePool_2(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -746,7 +746,7 @@ func TestMultiplePool_2(t *testing.T) { } // Large number of blocks passed -func TestLargeBlocksPassed(t *testing.T) { +func TestCanonicalLargeBlocksPassed(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -780,7 +780,7 @@ func GetPoolPath(token0Path, token1Path string, fee uint32) string { return ufmt.Sprintf("%s:%s:%d", token0Path, token1Path, fee) } -func TestWarmup_1(t *testing.T) { +func TestCanonicalWarmup_1(t *testing.T) { modifyWarmup(0, 10) modifyWarmup(1, 10) modifyWarmup(2, 10) @@ -860,7 +860,7 @@ func TestWarmup_1(t *testing.T) { canonical.AssertEmulatedRewardOf(0, expected*10) } -func TestWarmup_2(t *testing.T) { +func TestCanonicalWarmup_2(t *testing.T) { modifyWarmup(0, 10) modifyWarmup(1, 10) modifyWarmup(2, 10) @@ -943,7 +943,7 @@ func TestWarmup_2(t *testing.T) { // randomized // Test tick crossing at tick boundaries, random direction -func TestTickCross_7(t *testing.T) { +func TestCanonicalTickCross_7(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -987,7 +987,7 @@ func TestTickCross_7(t *testing.T) { // Test tick crossing with multiple positions with different tick, different liquidity // Equivalence test -func TestTickCross_8(t *testing.T) { +func TestCanonicalTickCross_8(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) @@ -1046,7 +1046,7 @@ func TestTickCross_8(t *testing.T) { // Test tick crossing with multiple positions with different tick, different liquidity, emulated reward flushed for every 100 blocks // Equivalence test -func TestTickCross_9(t *testing.T) { +func TestCanonicalTickCross_9(t *testing.T) { canonical := Setup(t) gnousdc := GetPoolPath(wugnotPath, gnsPath, 3000) diff --git a/staker/reward_calculation_pool.gno b/staker/reward_calculation_pool.gno index a3ea20496..271271f0a 100644 --- a/staker/reward_calculation_pool.gno +++ b/staker/reward_calculation_pool.gno @@ -79,6 +79,10 @@ type Pool struct { incentives Incentives ticks *Ticks + + // BlockNumber / TotalStake accumulation, Q128. + globalRewardRatioAccumulation *u256.Uint + lastGlobalRewardRatioAccumulationHeight *uint64 } func NewPool(poolPath string, currentHeight uint64) *Pool { @@ -95,6 +99,8 @@ func NewPool(poolPath string, currentHeight uint64) *Pool { lastRewardCacheHeight: ¤tHeight, incentives: NewIncentives(currentHeight), ticks: NewTicks(), + globalRewardRatioAccumulation: u256.Zero(), + lastGlobalRewardRatioAccumulationHeight: ¤tHeight, } } @@ -137,6 +143,7 @@ func (self *Pool) cacheRewardPerLiquidityUnit(startHeight, endHeight uint64, cur if *self.lastUnclaimableHeight != 0 { self.endInternalUnclaimablePeriod(startHeight) *self.tierRewardTotal = currentTierReward + println(" ->tierRewardTotal : ", *self.tierRewardTotal) self.startInternalUnclaimablePeriod(startHeight) } @@ -214,6 +221,34 @@ func (self *Pool) cacheExternalReward(endHeight uint64) { }) } +// returns the old global reward ratio accumulation +func (self *Pool) updateGlobalRewardRatioAccumulation(currentHeight uint64, currentStakedLiquidity *u256.Uint) *u256.Uint { + println("[[updateGlobalRewardRatioAccumulation]]") + println(" currentHeight : ", currentHeight) + println(" lastGlobalRewardRatioAccumulationHeight : ", *self.lastGlobalRewardRatioAccumulationHeight) + blockDiff := currentHeight - *self.lastGlobalRewardRatioAccumulationHeight + println(" blockDiff : ", blockDiff) + oldAcc := self.globalRewardRatioAccumulation + println(" oldAcc : ", oldAcc.ToString()) + if blockDiff == 0 { + return oldAcc + } + + acc := u256.NewUint(blockDiff) + acc = acc.Mul(acc, q128) + println(" currentStakedLiquidity : ", currentStakedLiquidity.ToString()) + acc = acc.Div(acc, currentStakedLiquidity) + + println(" acc : ", acc.ToString()) + + self.globalRewardRatioAccumulation = oldAcc.Add(oldAcc, acc) + println(" self.globalRewardRatioAccumulation : ", self.globalRewardRatioAccumulation.ToString()) + *self.lastGlobalRewardRatioAccumulationHeight = currentHeight + println(" lastGlobalRewardRatioAccumulationHeight : ", *self.lastGlobalRewardRatioAccumulationHeight) + println("[[updateGlobalRewardRatioAccumulation]] End") + return oldAcc +} + type ExternalRewardState struct { pool *Pool deposit *Deposit @@ -406,6 +441,9 @@ func (self *InternalRewardState) ApplyWarmup() { reward := u256.Zero() totalReward := u256.Zero().Div(self.rewards[i], q192) + + println("totalReward : ", totalReward.ToString()) + reward = reward.Mul(self.rewards[i], u256.NewUint(warmup.WarmupRatio)) reward = reward.Div(reward, u256.NewUint(100)) reward = reward.Div(reward, q192) @@ -417,6 +455,13 @@ func (self *InternalRewardState) ApplyWarmup() { } } +func abs(x int64) uint64 { + if x < 0 { + return uint64(-x) + } + return uint64(x) +} + func (self *InternalRewardState) TickCrossesToInternalReward(startHeight, endHeight int64, currentlyInRange bool, tickUpperCrosses []int64, tickLowerCrosses []int64) { for _, warmup := range self.deposit.warmups { @@ -428,6 +473,44 @@ func (self *InternalRewardState) TickCrossesToInternalReward(startHeight, endHei } if endHeight < warmup.NextWarmupHeight { + // testing + upperTick := self.deposit.tickUpper + lowerTick := self.deposit.tickLower + upperCross := tickUpperCrosses[len(tickUpperCrosses)-1] + lowerCross := tickLowerCrosses[len(tickLowerCrosses)-1] + upperHeight := abs(upperCross) + lowerHeight := abs(lowerCross) + var currentTick int32 + if upperHeight == lowerHeight { + if upperCross != lowerCross { + currentTick = (upperTick + lowerTick) / 2 // denoting inrange + } else if upperCross < 0 { + currentTick = lowerTick - 1 + } else { + currentTick = upperTick + 1 + } + } else if upperHeight > lowerHeight { + if upperCross < 0 { + if lowerCross < 0 { + currentTick = lowerTick - 1 + } else { + currentTick = (upperTick + lowerTick) / 2 + } + } else { + currentTick = upperTick + 1 + } + } else { + if lowerCross < 0 { + currentTick = lowerTick - 1 + } else { + if upperCross < 0 { + currentTick = (upperTick + lowerTick) / 2 + } else { + currentTick = upperTick + 1 + } + } + } + // fully submerged in the current warmup currentlyInRange, tickUpperCrosses, tickLowerCrosses = ForEachEligibleInterval( startHeight, @@ -438,6 +521,36 @@ func (self *InternalRewardState) TickCrossesToInternalReward(startHeight, endHei self.AccumulateReward, ) + // checking if global reward ratio logic is correct + globalAcc := self.pool.globalRewardRatioAccumulation + lowerAcc := self.pool.ticks.Get(self.deposit.tickLower).CurrentOutsideAccumulation(uint64(endHeight)) + upperAcc := self.pool.ticks.Get(self.deposit.tickUpper).CurrentOutsideAccumulation(uint64(endHeight)) + println("globalAcc : ", globalAcc.ToString(), ", lowerAcc : ", lowerAcc.ToString(), ", upperAcc : ", upperAcc.ToString()) + var rewardAcc *u256.Uint + + if currentTick < lowerTick { + println("currentTick < lowerTick") + rewardAcc = u256.Zero().Sub(lowerAcc, upperAcc) + } else if currentTick >= upperTick { + println("currentTick >= upperTick") + rewardAcc = u256.Zero().Sub(upperAcc, lowerAcc) + } else { + println("currentTick >= lowerTick && currentTick < upperTick") + globalAcc := self.pool.globalRewardRatioAccumulation + rewardAcc = u256.Zero().Sub(globalAcc, lowerAcc) + rewardAcc = rewardAcc.Sub(rewardAcc, upperAcc) + } + + println("rewardAcc : ", rewardAcc.ToString()) + rewardAcc = rewardAcc.Mul(rewardAcc, self.deposit.liquidity) + println("rewardAcc : ", rewardAcc.ToString()) + currentReward := uint64(1000000000) // *self.pool.tierRewardTotal + println("currentReward : ", currentReward) + rewardAcc = rewardAcc.Mul(rewardAcc, u256.NewUint(currentReward)) + println("rewardAcc : ", rewardAcc.ToString()) + rewardAcc = rewardAcc.Div(rewardAcc, q128) + println("rewardAcc : ", rewardAcc.ToString()) + // done break } diff --git a/staker/reward_calculation_tick.gno b/staker/reward_calculation_tick.gno index a59aad6fb..0a44f9e87 100644 --- a/staker/reward_calculation_tick.gno +++ b/staker/reward_calculation_tick.gno @@ -63,6 +63,7 @@ func (self *Ticks) Get(tickId int32) *Tick { stakedLiquidityGross: u256.Zero(), stakedLiquidityDelta: i256.Zero(), cross: NewUintTree(), + outsideAccumulation: NewUintTree(), } self.tree.Set(EncodeInt(tickId), tick) return tick @@ -84,6 +85,14 @@ func (self *Ticks) Has(tickId int32) bool { return self.tree.Has(EncodeInt(tickId)) } +type TickCross struct { + cross bool + + // BlockNumber / TotalStake accumulation, Q128. + // Pool.globalRewardRatioAccumulation at the point of the tick crossing + accumulation *u256.Uint +} + // Tick represents the state of a specific tick in a pool. // // Fields: @@ -111,8 +120,24 @@ type Tick struct { // e.g. instead of having avl.Tree for each tick as key, use a segment of 32 ticks as bulk key. // The value can be a 64-bit integer, where each 2-bit represents 00(no-update), 01(backward-cross), 10(forward-cross), 11(no-update). - // block number -> zeroForOne + // block number -> TickCross cross *UintTree + + // currentOutsideAccumulation is the accumulation of the blockNumber / TotalStkae outside the tick. + // It is calculated by subtracting the current tick's currentOutsideAccumulation from the global reward ratio accumulation. + outsideAccumulation *UintTree // blockNumber -> *u256.Uint +} + +func (self *Tick) CurrentOutsideAccumulation(blockNumber uint64) *u256.Uint { + var acc *u256.Uint + self.outsideAccumulation.ReverseIterate(0, blockNumber, func(key uint64, value interface{}) bool { + acc = value.(*u256.Uint) + return true + }) + if acc == nil { + acc = u256.Zero() + } + return acc } // TickCrosses are encoded as int64, where negative value is backward cross(zeroForOne=true), positive value is forward cross(zeroForOne=false). @@ -324,6 +349,15 @@ func ForEachEligibleInterval(startHeight, endHeight int64, currentInRange bool, return currentInRange, tickUpperCross[tickUpperCrossI:], tickLowerCross[tickLowerCrossI:] } +func (self *Tick) updateCurrentOutsideAccumulation(blockNumber uint64, oldAcc *u256.Uint) { + currentOutsideAccumulation := self.CurrentOutsideAccumulation(blockNumber) + println("currentOutsideAccumulation : ", currentOutsideAccumulation.ToString()) + println("oldAcc : ", oldAcc.ToString()) + newOutsideAccumulation := u256.Zero().Sub(oldAcc, currentOutsideAccumulation) + println("newOutsideAccumulation : ", newOutsideAccumulation.ToString()) + self.outsideAccumulation.Set(blockNumber, newOutsideAccumulation) +} + func TickCrossHook(pools *Pools, height func() int64) func(poolPath string, tickId int32, zeroForOne bool) { return func(poolPath string, tickId int32, zeroForOne bool) { pool, ok := pools.Get(poolPath) @@ -331,9 +365,11 @@ func TickCrossHook(pools *Pools, height func() int64) func(poolPath string, tick return } + println("[[TickCrossHook]] tickId : ", tickId, ", zeroForOne : ", zeroForOne) tick := pool.ticks.Get(tickId) blockNumber := uint64(height()) + println(" blockNumber : ", blockNumber) tick.updateCross(blockNumber, zeroForOne) liquidityInRangeDelta := tick.stakedLiquidityDelta @@ -345,8 +381,12 @@ func TickCrossHook(pools *Pools, height func() int64) func(poolPath string, tick liquidityInRangeDelta = i256.Zero().Neg(liquidityInRangeDelta) } stakedLiquidity := pool.CurrentStakedLiquidity(blockNumber) + println("stakedLiquidity : ", stakedLiquidity.ToString()) deltaApplied := liquidityMathAddDelta(stakedLiquidity, liquidityInRangeDelta) + println("deltaApplied : ", deltaApplied.ToString()) + oldAcc := pool.updateGlobalRewardRatioAccumulation(blockNumber, stakedLiquidity) + tick.updateCurrentOutsideAccumulation(blockNumber, oldAcc) switch deltaApplied.Sign() { case -1: