diff --git a/launchpad/api_deposit.gno b/launchpad/api_deposit.gno index 9c1668282..f8afc7ff4 100644 --- a/launchpad/api_deposit.gno +++ b/launchpad/api_deposit.gno @@ -9,8 +9,6 @@ import ( ) func ApiGetClaimableDepositByAddress(address std.Address) uint64 { - calculateDepositReward() - if !address.IsValid() { return 0 } @@ -40,8 +38,6 @@ func ApiGetClaimableDepositByAddress(address std.Address) uint64 { } func ApiGetDepositByDepositId(depositId string) string { - calculateDepositReward() - deposit, exist := deposits[depositId] if !exist { return "" @@ -55,8 +51,6 @@ func ApiGetDepositByDepositId(depositId string) string { } func ApiGetDepositFullByDepositId(depositId string) string { - calculateDepositReward() - deposit, exist := deposits[depositId] if !exist { return "" diff --git a/launchpad/api_project.gno b/launchpad/api_project.gno index e5f269fdf..e579cf0c0 100644 --- a/launchpad/api_project.gno +++ b/launchpad/api_project.gno @@ -9,8 +9,6 @@ import ( ) func ApiGetProjectAndTierStatisticsByProjectId(projectId string) string { - calculateDepositReward() - project, exist := projects[projectId] if !exist { return "" @@ -24,8 +22,6 @@ func ApiGetProjectAndTierStatisticsByProjectId(projectId string) string { } func ApiGetProjectStatisticsByProjectId(projectId string) string { - calculateDepositReward() - project, exist := projects[projectId] if !exist { return "" @@ -39,8 +35,6 @@ func ApiGetProjectStatisticsByProjectId(projectId string) string { } func ApiGetProjectStatisticsByProjectTierId(tierId string) string { - calculateDepositReward() - projectId, tierStr := getProjectIdAndTierFromTierId(tierId) project, exist := projects[projectId] if !exist { diff --git a/launchpad/api_reward.gno b/launchpad/api_reward.gno index 00cdefebe..06edb0985 100644 --- a/launchpad/api_reward.gno +++ b/launchpad/api_reward.gno @@ -8,8 +8,6 @@ import ( // protocol_fee reward for project's recipient func ApiGetProjectRecipientRewardByProjectId(projectId string) string { - calculateDepositReward() - project, exist := projects[projectId] if !exist { return "0" @@ -19,8 +17,6 @@ func ApiGetProjectRecipientRewardByProjectId(projectId string) string { } func ApiGetProjectRecipientRewardByAddress(address std.Address) string { - calculateDepositReward() - if !address.IsValid() { return "0" } @@ -30,19 +26,15 @@ func ApiGetProjectRecipientRewardByAddress(address std.Address) string { // project reward for deposit func ApiGetDepositRewardByDepositId(depositId string) uint64 { - calculateDepositReward() - deposit, exist := deposits[depositId] if !exist { return 0 } - return deposit.rewardAmount + return rewardStates.Get(deposit.projectId, deposit.tier).CalculateReward(depositId) } func ApiGetDepositRewardByAddress(address std.Address) uint64 { - calculateDepositReward() - if !address.IsValid() { return 0 } @@ -58,7 +50,7 @@ func ApiGetDepositRewardByAddress(address std.Address) uint64 { if !exist { continue } - totalReward += deposit.rewardAmount + totalReward += rewardStates.Get(deposit.projectId, deposit.tier).CalculateReward(depositId) } return totalReward diff --git a/launchpad/deposit.gno b/launchpad/deposit.gno index a697e0016..3502c4dda 100644 --- a/launchpad/deposit.gno +++ b/launchpad/deposit.gno @@ -188,7 +188,6 @@ func DepositGns(targetProjectTierId string, amount uint64) string { en.MintAndDistributeGns() - calculateDepositReward() project = projects[projectId] // get updates project tier = getTier(project, tierStr) // get updates tier @@ -357,7 +356,6 @@ func CollectDepositGnsByProjectId(projectId string) uint64 { func processCollectedDeposits(dpsts []string, pid string) uint64 { common.IsHalted() en.MintAndDistributeGns() - calculateDepositReward() amount, err := processDepositCollection(dpsts, pid) if err != nil { @@ -432,7 +430,6 @@ func CollectDepositGnsByDepositId(depositId string) uint64 { en.MintAndDistributeGns() - calculateDepositReward() project = projects[deposit.projectId] // get updated project // Process deposit collection diff --git a/launchpad/json_builder.gno b/launchpad/json_builder.gno index c0a97cfd3..d7d716573 100644 --- a/launchpad/json_builder.gno +++ b/launchpad/json_builder.gno @@ -102,7 +102,6 @@ func DepositBuilder(b *json.NodeBuilder, deposit Deposit) *json.NodeBuilder { WriteString("depositCollectTime", ufmt.Sprintf("%d", deposit.depositCollectTime)). WriteString("claimableHeight", ufmt.Sprintf("%d", deposit.claimableHeight)). WriteString("claimableTime", ufmt.Sprintf("%d", deposit.claimableTime)). - WriteString("rewardAmount", ufmt.Sprintf("%d", deposit.rewardAmount)). WriteString("rewardCollected", ufmt.Sprintf("%d", deposit.rewardCollected)). WriteString("rewardCollectHeight", ufmt.Sprintf("%d", deposit.rewardCollectHeight)). WriteString("rewardCollectTime", ufmt.Sprintf("%d", deposit.rewardCollectTime)) diff --git a/launchpad/launchpad.gno b/launchpad/launchpad.gno index cdf7bc8be..7197392a3 100644 --- a/launchpad/launchpad.gno +++ b/launchpad/launchpad.gno @@ -302,7 +302,6 @@ func TransferLeftFromProjectByAdmin(projectId string, recipient std.Address) uin common.IsHalted() en.MintAndDistributeGns() - calculateDepositReward() project = projects[projectId] // get updated project leftReward := calculateLeftReward(project) diff --git a/launchpad/reward.gno b/launchpad/reward.gno index 8a9cf0f95..4fd4aa8d5 100644 --- a/launchpad/reward.gno +++ b/launchpad/reward.gno @@ -37,10 +37,6 @@ func CollectProtocolFee() { // validateRewardCollection validates if reward can be collected func validateRewardCollection(deposit Deposit, height uint64) error { - if deposit.rewardAmount == 0 { - return errors.New("no reward available") - } - if deposit.rewardCollectTime == 0 && height < deposit.claimableHeight { return errors.New("reward not yet claimable") } @@ -71,6 +67,7 @@ func calculateTierRewards(tier Tier, currentHeight uint64, lastCalcHeight uint64 return rewardX96, reward, nil } +/* // processDepositReward processes reward for a single deposit func processDepositReward(deposit Deposit, rewardX96 *u256.Uint, tierAmount uint64) (Deposit, error) { if tierAmount == 0 { @@ -86,7 +83,7 @@ func processDepositReward(deposit Deposit, rewardX96 *u256.Uint, tierAmount uint deposit.rewardAmount += depositReward return deposit, nil } - +*/ // CollectRewardByProjectId collects reward from entire deposit of certain project by caller // Returns collected reward amount // ref: https://docs.gnoswap.io/contracts/launchpad/launchpad_reward.gno#collectrewardbyprojectid @@ -99,7 +96,6 @@ func CollectRewardByProjectId(projectId string) uint64 { return 0 } - calculateDepositReward() project = projects[projectId] // get updated project caller := std.PrevRealm().Addr() @@ -114,7 +110,8 @@ func CollectRewardByProjectId(projectId string) uint64 { for _, depositId := range depositIds { deposit := deposits[depositId] - if deposit.rewardAmount == 0 { + reward := rewardStates.Get(deposit.projectId, deposit.tier).Claim(depositId, height) + if reward == 0 { continue } @@ -127,7 +124,7 @@ func CollectRewardByProjectId(projectId string) uint64 { continue } - totalReward += deposit.rewardAmount + totalReward += reward std.Emit( "CollectRewardByProjectId", @@ -135,29 +132,28 @@ func CollectRewardByProjectId(projectId string) uint64 { "prevRealm", prevRealm, "projectId", projectId, "depositId", depositId, - "amount", ufmt.Sprintf("%d", deposit.rewardAmount), + "amount", ufmt.Sprintf("%d", reward), ) // Update project and tier stats - project.stats.totalCollected += deposit.rewardAmount + project.stats.totalCollected += reward var tier Tier switch deposit.tier { case "30": tier = project.tiers[30] - tier.userCollectedAmount += deposit.rewardAmount + tier.userCollectedAmount += reward case "90": tier = project.tiers[90] - tier.userCollectedAmount += deposit.rewardAmount + tier.userCollectedAmount += reward case "180": tier = project.tiers[180] - tier.userCollectedAmount += deposit.rewardAmount + tier.userCollectedAmount += reward } project = setTier(project, deposit.tier, tier) // Update deposit - deposit.rewardCollected += deposit.rewardAmount - deposit.rewardAmount = 0 + deposit.rewardCollected += reward deposit.rewardCollectHeight = height deposit.rewardCollectTime = uint64(time.Now().Unix()) deposits[depositId] = deposit @@ -195,7 +191,6 @@ func CollectRewardByDepositId(depositId string) uint64 { return 0 } - calculateDepositReward() project = projects[deposit.projectId] deposit = deposits[depositId] @@ -204,7 +199,7 @@ func CollectRewardByDepositId(depositId string) uint64 { return 0 } - reward := deposit.rewardAmount + reward := rewardStates.Get(deposit.projectId, deposit.tier).Claim(deposit.id, height) prevAddr, prevRealm := getPrev() std.Emit( @@ -232,7 +227,6 @@ func CollectRewardByDepositId(depositId string) uint64 { // Update deposit deposit.rewardCollected += reward - deposit.rewardAmount = 0 deposit.rewardCollectHeight = height deposit.rewardCollectTime = uint64(time.Now().Unix()) deposits[depositId] = deposit @@ -245,7 +239,7 @@ func CollectRewardByDepositId(depositId string) uint64 { return reward } - +/* // calculateProjectRewards calculates rewards for a project's deposits func calculateProjectRewards(project Project, height uint64) (Project, error) { if project.started.height > height || project.stats.actualParticipant == 0 { @@ -347,7 +341,7 @@ func calculateDepositReward() { projects[projectId] = updatedProject } } - +*/ // calcDepositRatioX96 calculates the deposit ratio with Q96 precision func calcDepositRatioX96(tierAmount uint64, amount uint64) *u256.Uint { amountX96 := new(u256.Uint).Mul(u256.NewUint(amount), q96.Clone()) diff --git a/launchpad/reward_calculation.gno b/launchpad/reward_calculation.gno new file mode 100644 index 000000000..6ed5e38cd --- /dev/null +++ b/launchpad/reward_calculation.gno @@ -0,0 +1,212 @@ +// Copied from gov/staker, extract it out into acommon package? +package launchpad + +import ( + "std" + + en "gno.land/r/gnoswap/v1/emission" + ufmt "gno.land/p/demo/ufmt" + "gno.land/p/demo/avl" + u256 "gno.land/p/gnoswap/uint256" +) + +type StakerRewardInfo struct { + StartHeight uint64 // height when staker started staking + PriceDebt *u256.Uint // price debt per xGNS stake, Q96 + Amount uint64 // amount of xGNS staked + Claimed uint64 // amount of GNS reward claimed so far +} + +func (self *StakerRewardInfo) Debug() string { + return ufmt.Sprintf("{ StartHeight: %d, PriceDebt: %d, Amount: %d, Claimed: %d }", self.StartHeight, self.PriceDebtUint64(), self.Amount, self.Claimed) +} + +func (self *StakerRewardInfo) PriceDebtUint64() uint64 { + return u256.Zero().Rsh(self.PriceDebt, 96).Uint64() +} + +type RewardState struct { + // CurrentBalance is sum of all the previous balances, including the reward distribution. + // CurrentBalance uint64 // current balance of gov_staker, used to calculate RewardAccumulation + PriceAccumulation *u256.Uint // claimable GNS per xGNS stake, Q96 + // RewardAccumulation *u256.Uint // reward accumulated so far, Q96 + TotalStake uint64 // total xGNS staked + + LastHeight uint64 // last height when reward was calculated + RewardPerBlock *u256.Uint // reward per block, = Tier.tierAmountPerBlockX96 + EndHeight uint64 + + info *avl.Tree // depositId -> StakerRewardInfo +} + +func NewRewardState(rewardPerBlock *u256.Uint, startHeight uint64, endHeight uint64) *RewardState { + return &RewardState { + PriceAccumulation: u256.Zero(), + TotalStake: 0, + LastHeight: startHeight, + RewardPerBlock: rewardPerBlock, + EndHeight: endHeight, + info: avl.NewTree(), + } +} + +type RewardStates struct { + states *avl.Tree // projectId:tier string -> RewardState +} + +var rewardStates = RewardStates{ + states: avl.NewTree(), +} + +func (states RewardStates) Get(projectId string, tierStr string) *RewardState { + key := projectId + ":" + tierStr + statesI, exists := states.states.Get(key) + if !exists { + return nil + } + return statesI.(*RewardState) +} + +func (states RewardStates) Set(projectId string, tierStr string, state *RewardState) { + key := projectId + ":" + tierStr + states.states.Set(key, state) +} + +func (states RewardStates) Remove(projectId string, tierStr string) { + key := projectId + ":" + tierStr + states.states.Remove(key) +} + + +func (self *RewardState) Debug() string { + return ufmt.Sprintf("{ PriceAccumulation: %d, TotalStake: %d, LastHeight: %d, RewardPerBlock: %d, EndHeight: %d, info: len(%d) }", self.PriceAccumulationUint64(), self.TotalStake, self.LastHeight, self.RewardPerBlockUint64(), self.EndHeight, self.info.Size()) +} + +func (self *RewardState) RewardPerBlockUint64() uint64 { + return u256.Zero().Rsh(self.RewardPerBlock, 96).Uint64() +} + +func (self *RewardState) Info(depositId string) StakerRewardInfo { + infoI, exists := self.info.Get(depositId) + if !exists { + panic(ufmt.Sprintf("depositId %s not found", depositId)) + } + return infoI.(StakerRewardInfo) +} + +func (self *RewardState) CalculateReward(depositId string) uint64 { + info := self.Info(depositId) + stakerPrice := u256.Zero().Sub(self.PriceAccumulation, info.PriceDebt) + reward := stakerPrice.Mul(stakerPrice, u256.NewUint(info.Amount)) + reward = reward.Rsh(reward, 96) + return reward.Uint64() - info.Claimed +} + +func (self *RewardState) PriceAccumulationUint64() uint64 { + return u256.Zero().Rsh(self.PriceAccumulation, 96).Uint64() +} + +// amount MUST be less than or equal to the amount of xGNS staked +// This function does not check it +func (self *RewardState) deductReward(depositId string, currentHeight uint64) uint64 { + info := self.Info(depositId) + stakerPrice := u256.Zero().Sub(self.PriceAccumulation, info.PriceDebt) + reward := stakerPrice.Mul(stakerPrice, u256.NewUint(info.Amount)) + reward = reward.Rsh(reward, 96) + reward64 := reward.Uint64() - info.Claimed + + info.Claimed += reward64 + self.info.Set(depositId, info) + + self.LastHeight = currentHeight + + return reward64 +} + +// This function MUST be called as a part of AddStake or RemoveStake +// CurrentBalance / StakeChange / IsRemoveStake will be updated in those functions +func (self *RewardState) finalize(currentHeight uint64) { + if currentHeight <= self.LastHeight { + // Not started yet + return + } + if currentHeight > self.EndHeight { + currentHeight = self.EndHeight + } + + delta := u256.NewUint(currentHeight - self.LastHeight) + delta = delta.Mul(delta, self.RewardPerBlock) + + if self.TotalStake == uint64(0) { + // no staker + return + } + + price := delta.Div(delta, u256.NewUint(self.TotalStake)) + self.PriceAccumulation.Add(self.PriceAccumulation, price) + self.LastHeight = currentHeight +} + +func (self *RewardState) AddStake(currentHeight uint64, depositId string, amount uint64) { + if self.info.Has(depositId) { + panic(ufmt.Sprintf("depositId %s already exists", depositId)) + } + + self.finalize(currentHeight) + + self.TotalStake += amount + + info := StakerRewardInfo { + StartHeight: currentHeight, + PriceDebt: self.PriceAccumulation.Clone(), + Amount: amount, + Claimed: 0, + } + + self.info.Set(depositId, info) +} + +func (self *RewardState) Claim(depositId string, currentHeight uint64) uint64 { + if !self.info.Has(depositId) { + return 0 + } + + self.finalize(currentHeight) + + reward := self.deductReward(depositId, currentHeight) + + return reward +} + +func (self *RewardState) RemoveStake(depositId string, amount uint64, currentHeight uint64) uint64 { + self.finalize(currentHeight) + + reward := self.deductReward(depositId, currentHeight) + + self.info.Remove(depositId) + + self.TotalStake -= amount + + return reward +} +/* +var ( + //q96 = u256.MustFromDecimal(consts.Q96) + lastCalculatedHeight uint64 // flag to prevent same block calculation +) + +var ( + gotGnsForEmission uint64 + leftGnsEmissionFromLast uint64 + alreadyCalculatedGnsEmission uint64 + + leftProtocolFeeFromLast = avl.NewTree() // tokenPath -> tokenAmount + alreadyCalculatedProtocolFee = avl.NewTree() // tokenPath -> tokenAmount +) + +var ( + userXGnsRatio = avl.NewTree() // address -> ratioX96 + userEmissionReward = avl.NewTree() // address -> gnsAmount + userProtocolFeeReward = avl.NewTree() // address -> tokenPath -> tokenAmount +) + */ \ No newline at end of file diff --git a/launchpad/reward_test.gno b/launchpad/reward_test.gno index 3836f9bfe..7685447d4 100644 --- a/launchpad/reward_test.gno +++ b/launchpad/reward_test.gno @@ -3,6 +3,7 @@ package launchpad import ( "testing" + "gno.land/p/demo/ufmt" "gno.land/p/demo/uassert" u256 "gno.land/p/gnoswap/uint256" ) @@ -20,7 +21,7 @@ func TestValidateRewardCollection(t *testing.T) { { name: "Valid reward ready to collect", deposit: Deposit{ - rewardAmount: 1000, + //rewardAmount: 1000, claimableHeight: 50, // Less than current height rewardCollectTime: 0, }, @@ -30,7 +31,7 @@ func TestValidateRewardCollection(t *testing.T) { { name: "No reward available", deposit: Deposit{ - rewardAmount: 0, + //rewardAmount: 0, claimableHeight: 50, }, height: currentHeight, @@ -40,7 +41,7 @@ func TestValidateRewardCollection(t *testing.T) { { name: "Reward not yet claimable", deposit: Deposit{ - rewardAmount: 1000, + //rewardAmount: 1000, claimableHeight: 150, // Greater than current height rewardCollectTime: 0, }, @@ -51,7 +52,7 @@ func TestValidateRewardCollection(t *testing.T) { { name: "Already collected reward can be collected before claimable height", deposit: Deposit{ - rewardAmount: 1000, + //rewardAmount: 1000, claimableHeight: 150, rewardCollectTime: 1, // Already collected before }, @@ -132,47 +133,48 @@ func TestProcessDepositReward(t *testing.T) { name: "Normal reward calculation", deposit: Deposit{ amount: 1000, - rewardAmount: 0, + //rewardAmount: 0, }, rewardX96: u256.NewUint(1000).Mul(u256.NewUint(1000), q96), tierAmount: 2000, expectedReward: 500, // (1000/2000) * 1000 = 500 shouldError: false, }, + /* { name: "Tier amount is 0", deposit: Deposit{ amount: 1000, - rewardAmount: 0, + //rewardAmount: 0, }, rewardX96: u256.NewUint(1000), tierAmount: 0, expectedReward: 0, shouldError: true, - }, + },*/ { name: "Reward is accumulated", deposit: Deposit{ amount: 1000, - rewardAmount: 100, + //rewardAmount: 100, }, rewardX96: u256.NewUint(1000).Mul(u256.NewUint(1000), q96), tierAmount: 2000, - expectedReward: 600, // Existing 100 + New reward 500 + expectedReward: 500, // 600, // Existing 100 + New reward 500 shouldError: false, }, } - for _, tt := range tests { + for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := processDepositReward(tt.deposit, tt.rewardX96, tt.tierAmount) - - if tt.shouldError { - uassert.Error(t, err) - } else { - uassert.NoError(t, err) - uassert.Equal(t, tt.expectedReward, result.rewardAmount) - } + state := NewRewardState(tt.rewardX96, 1000, 2000) + depositId := ufmt.Sprintf("depositId-%d", i) + state.AddStake(1000, depositId, tt.deposit.amount) + state.TotalStake = tt.tierAmount // force overriding total stake + state.finalize(1001) + + reward := state.CalculateReward(depositId) + uassert.Equal(t, tt.expectedReward, reward) }) } } diff --git a/launchpad/type.gno b/launchpad/type.gno index ab5116fd6..c3c1c36b3 100644 --- a/launchpad/type.gno +++ b/launchpad/type.gno @@ -86,7 +86,7 @@ type Deposit struct { claimableHeight uint64 claimableTime uint64 - rewardAmount uint64 // calculated, not collected + // rewardAmount uint64 // calculated, not collected rewardCollected uint64 // accu, collected rewardCollectHeight uint64 // last collected height rewardCollectTime uint64 // last collected time