distribution.gno
14.41 Kb · 440 lines
1package emission
2
3import (
4 "chain"
5 "time"
6
7 ufmt "gno.land/p/nt/ufmt/v0"
8
9 gnsmath "gno.land/p/gnoswap/gnsmath/v1"
10 prbac "gno.land/p/gnoswap/rbac/v1"
11 "gno.land/p/gnoswap/utils/v1"
12
13 "gno.land/r/gnoswap/access/v1"
14 "gno.land/r/gnoswap/gns"
15 "gno.land/r/gnoswap/halt/v1"
16)
17
18const (
19 _ int = iota
20 LIQUIDITY_STAKER
21 DEVOPS
22 COMMUNITY_POOL
23 GOV_STAKER
24)
25
26var (
27 // Stores the percentage (in basis points) for each distribution target
28 // 1 basis point = 0.01%
29 // These percentages can be modified by admin or governance.
30 distributionBpsPct map[int]int64
31
32 distributedToStaker int64 // can be cleared by staker contract
33 distributedToDevOps int64
34 distributedToCommunityPool int64
35 distributedToGovStaker int64 // can be cleared by governance staker
36
37 // Historical total distributions (never reset)
38 accuDistributedToStaker int64
39 accuDistributedToDevOps int64
40 accuDistributedToCommunityPool int64
41 accuDistributedToGovStaker int64
42)
43
44// Initialize default distribution percentages:
45// - Liquidity Stakers: 75%
46// - DevOps: 20%
47// - Community Pool: 5%
48// - Governance Stakers: 0%
49//
50// ref: https://docs.gnoswap.io/gnoswap-token/emission
51func init() {
52 distributionBpsPct = map[int]int64{
53 LIQUIDITY_STAKER: 7500,
54 DEVOPS: 2000,
55 COMMUNITY_POOL: 500,
56 GOV_STAKER: 0,
57 }
58}
59
60// ChangeDistributionPct changes distribution percentages for emission targets.
61//
62// This function redistributes how newly minted GNS tokens are allocated across
63// protocol components. Before applying new ratios, it distributes any accumulated
64// emissions using the current ratios, ensuring emissions are distributed according
65// to the ratios in effect when they were generated. This prevents retroactive
66// application of new ratios to past emissions.
67//
68// Parameters:
69// - cur: Current realm context; callers use cross(cur) when crossing into this realm.
70// - liquidityStakerPct: percentage for liquidity stakers in basis points (100 = 1%, 10000 = 100%).
71// - devOpsPct: percentage for DevOps in basis points (100 = 1%, 10000 = 100%).
72// - communityPoolPct: percentage for the community pool in basis points (100 = 1%, 10000 = 100%).
73// - govStakerPct: percentage for governance stakers in basis points (100 = 1%, 10000 = 100%).
74//
75// Requirements:
76// - Percentages must sum to exactly 10000 (100%).
77// - Each percentage must be between 0 and 10000 inclusive.
78//
79// Example:
80//
81// ChangeDistributionPct(
82// 7000, // 70% to liquidity stakers
83// 2000, // 20% to devops
84// 1000, // 10% to community pool
85// 0, // 0% to governance stakers
86// )
87//
88// Only callable by admin or governance.
89func ChangeDistributionPct(
90 cur realm,
91 liquidityStakerPct int64,
92 devOpsPct int64,
93 communityPoolPct int64,
94 govStakerPct int64,
95) {
96 halt.AssertIsNotHaltedEmission()
97
98 caller := cur.Previous().Address()
99 access.AssertIsAdminOrGovernance(caller)
100
101 assertValidDistributionPct(liquidityStakerPct, devOpsPct, communityPoolPct, govStakerPct)
102
103 // Distribute accumulated emissions with current ratios before changing ratios.
104 // This prevents retroactive application of new ratios to emissions that occurred
105 // under previous ratio configurations.
106 MintAndDistributeGns(cur)
107
108 currentTimestamp := time.Now().Unix()
109 stakerRewardPerSecond := GetEmissionAmountPerSecondBy(currentTimestamp, liquidityStakerPct)
110 govStakerRewardPerSecond := GetEmissionAmountPerSecondBy(currentTimestamp, govStakerPct)
111
112 if onDistributionPctChangeCallback != nil {
113 onDistributionPctChangeCallback(cross(cur), stakerRewardPerSecond)
114 }
115
116 changeDistributionPcts(liquidityStakerPct, devOpsPct, communityPoolPct, govStakerPct)
117
118 previousRealm := cur.Previous()
119 chain.Emit(
120 "ChangeDistributionPct",
121 "prevAddr", previousRealm.Address().String(),
122 "prevRealm", previousRealm.PkgPath(),
123 "liquidityStakerPct", utils.FormatInt(liquidityStakerPct),
124 "devOpsPct", utils.FormatInt(devOpsPct),
125 "communityPoolPct", utils.FormatInt(communityPoolPct),
126 "govStakerPct", utils.FormatInt(govStakerPct),
127 "stakerRewardPerSecond", utils.FormatInt(stakerRewardPerSecond),
128 "govStakerRewardPerSecond", utils.FormatInt(govStakerRewardPerSecond),
129 )
130}
131
132// changeDistributionPcts updates the distribution percentages for all targets.
133func changeDistributionPcts(liquidityStakerPct, devOpsPct, communityPoolPct, govStakerPct int64) {
134 setDistributionBpsPct(LIQUIDITY_STAKER, liquidityStakerPct)
135 setDistributionBpsPct(DEVOPS, devOpsPct)
136 setDistributionBpsPct(COMMUNITY_POOL, communityPoolPct)
137 setDistributionBpsPct(GOV_STAKER, govStakerPct)
138}
139
140func calculateDistributableAmounts(amount int64) (map[int]int64, int64) {
141 distributable := make(map[int]int64, 0)
142 totalSent := int64(0)
143
144 for target, pct := range distributionBpsPct {
145 distAmount := calculateAmount(amount, pct)
146 if distAmount == 0 {
147 continue
148 }
149
150 distributable[target] = distAmount
151 totalSent = gnsmath.SafeAddInt64(totalSent, distAmount)
152 }
153
154 leftAmount := gnsmath.SafeSubInt64(amount, totalSent)
155 return distributable, leftAmount
156}
157
158// calculateAmount converts basis points to actual token amount.
159func calculateAmount(amount, bptPct int64) int64 {
160 if amount < 0 || bptPct < 0 || bptPct > 10000 {
161 panic("invalid amount or bptPct")
162 }
163
164 // More precise overflow prevention
165 const maxInt64 = 9223372036854775807
166 if amount > maxInt64/10000 {
167 panic("amount too large, would cause overflow")
168 }
169
170 // Additional safety check for zero division
171 if bptPct == 0 {
172 return 0
173 }
174
175 return amount * bptPct / 10000
176}
177
178func applyDistribution(targets map[int]int64) (map[address]int64, error) {
179 amountByAddress := make(map[address]int64, 0)
180
181 for target, amount := range targets {
182 var addr address
183
184 switch target {
185 case LIQUIDITY_STAKER:
186 distributedToStaker = gnsmath.SafeAddInt64(distributedToStaker, amount)
187 accuDistributedToStaker = gnsmath.SafeAddInt64(accuDistributedToStaker, amount)
188 addr = access.MustGetAddress(prbac.ROLE_STAKER.String())
189
190 case DEVOPS:
191 distributedToDevOps = gnsmath.SafeAddInt64(distributedToDevOps, amount)
192 accuDistributedToDevOps = gnsmath.SafeAddInt64(accuDistributedToDevOps, amount)
193 addr = access.MustGetAddress(prbac.ROLE_DEVOPS.String())
194
195 case COMMUNITY_POOL:
196 distributedToCommunityPool = gnsmath.SafeAddInt64(distributedToCommunityPool, amount)
197 accuDistributedToCommunityPool = gnsmath.SafeAddInt64(accuDistributedToCommunityPool, amount)
198 addr = access.MustGetAddress(prbac.ROLE_COMMUNITY_POOL.String())
199
200 case GOV_STAKER:
201 distributedToGovStaker = gnsmath.SafeAddInt64(distributedToGovStaker, amount)
202 accuDistributedToGovStaker = gnsmath.SafeAddInt64(accuDistributedToGovStaker, amount)
203 addr = access.MustGetAddress(prbac.ROLE_GOV_STAKER.String())
204
205 default:
206 return nil, makeErrorWithDetails(
207 errInvalidEmissionTarget,
208 ufmt.Sprintf("invalid target(%d)", target),
209 )
210 }
211
212 amountByAddress[addr] = gnsmath.SafeAddInt64(amountByAddress[addr], amount)
213 }
214
215 return amountByAddress, nil
216}
217
218func transferToTarget(_ int, rlm realm, targets map[address]int64) error {
219 for address, amount := range targets {
220 gns.Transfer(cross(rlm), address, amount)
221 }
222
223 return nil
224}
225
226// GetDistributionBpsPct returns the configured distribution percentage in basis points for a target.
227//
228// Parameters:
229// - target: distribution target constant whose configured share is queried.
230//
231// Returns:
232// - pct: configured target share in basis points (100 = 1%, 10000 = 100%).
233// - error: nil when the target exists in the initialized map; otherwise an invalid-target error.
234func GetDistributionBpsPct(target int) (int64, error) {
235 if err := validateDistributionTarget(target); err != nil {
236 return 0, err
237 }
238
239 if distributionBpsPct == nil {
240 return 0, makeErrorWithDetails(
241 errInvalidEmissionTarget,
242 ufmt.Sprintf("distributionBpsPct is nil"),
243 )
244 }
245
246 pct, exist := distributionBpsPct[target]
247 if !exist {
248 return 0, makeErrorWithDetails(
249 errInvalidEmissionTarget,
250 ufmt.Sprintf("invalid target(%d)", target),
251 )
252 }
253
254 return pct, nil
255}
256
257// GetDistributedToStaker returns the pending GNS amount allocated to liquidity stakers.
258//
259// Returns:
260// - amount: pending liquidity-staker allocation in GNS base units since the last clear.
261func GetDistributedToStaker() int64 {
262 return distributedToStaker
263}
264
265// GetDistributedToDevOps returns the GNS amount currently accumulated for DevOps.
266//
267// Returns:
268// - amount: accumulated DevOps allocation in GNS base units.
269func GetDistributedToDevOps() int64 {
270 return distributedToDevOps
271}
272
273// GetDistributedToCommunityPool returns the GNS amount currently accumulated for the community pool.
274//
275// Returns:
276// - amount: accumulated community-pool allocation in GNS base units.
277func GetDistributedToCommunityPool() int64 {
278 return distributedToCommunityPool
279}
280
281// GetDistributedToGovStaker returns the pending GNS amount allocated to governance stakers.
282//
283// Returns:
284// - amount: pending governance-staker allocation in GNS base units since the last clear.
285func GetDistributedToGovStaker() int64 {
286 return distributedToGovStaker
287}
288
289// AccumulateDistributedInfo returns the current pending allocation for every distribution target.
290//
291// Returns:
292// - toStaker: pending GNS allocation for liquidity stakers, in token base units.
293// - toDevOps: accumulated GNS allocation for DevOps, in token base units.
294// - toCommunityPool: accumulated GNS allocation for the community pool, in token base units.
295// - toGovStaker: pending GNS allocation for governance stakers, in token base units.
296func AccumulateDistributedInfo() (toStaker, toDevOps, toCommunityPool, toGovStaker int64) {
297 toStaker = GetDistributedToStaker()
298 toDevOps = GetDistributedToDevOps()
299 toCommunityPool = GetDistributedToCommunityPool()
300 toGovStaker = GetDistributedToGovStaker()
301 return
302}
303
304// GetAccuDistributedToStaker returns the total historical GNS amount allocated to liquidity stakers.
305//
306// Returns:
307// - amount: cumulative liquidity-staker allocation in GNS base units; this value is not cleared.
308func GetAccuDistributedToStaker() int64 {
309 return accuDistributedToStaker
310}
311
312// GetAccuDistributedToDevOps returns the total historical GNS amount allocated to DevOps.
313//
314// Returns:
315// - amount: cumulative DevOps allocation in GNS base units; this value is not cleared.
316func GetAccuDistributedToDevOps() int64 {
317 return accuDistributedToDevOps
318}
319
320// GetAccuDistributedToCommunityPool returns the total historical GNS amount allocated to the community pool.
321//
322// Returns:
323// - amount: cumulative community-pool allocation in GNS base units; this value is not cleared.
324func GetAccuDistributedToCommunityPool() int64 {
325 return accuDistributedToCommunityPool
326}
327
328// GetAccuDistributedToGovStaker returns the total historical GNS amount allocated to governance stakers.
329//
330// Returns:
331// - amount: cumulative governance-staker allocation in GNS base units; this value is not cleared.
332func GetAccuDistributedToGovStaker() int64 {
333 return accuDistributedToGovStaker
334}
335
336// GetEmissionAmountPerSecondBy returns the GNS emission rate per second for a timestamp and target share.
337//
338// Parameters:
339// - timestamp: Unix timestamp at which the base emission rate is queried.
340// - distributionPct: target share in basis points, from 0 through 10000 inclusive.
341//
342// Returns:
343// - amountPerSecond: target's GNS emission rate in token base units per second, or zero outside the schedule.
344func GetEmissionAmountPerSecondBy(timestamp, distributionPct int64) int64 {
345 return calculateAmount(gns.GetEmissionAmountPerSecondByTimestamp(timestamp), distributionPct)
346}
347
348// GetStakerEmissionAmountPerSecond returns the current GNS emission rate allocated to liquidity stakers.
349//
350// Returns:
351// - amountPerSecond: current liquidity-staker emission rate in GNS base units per second.
352// - error: nil when the liquidity-staker distribution share is available; otherwise the target lookup error.
353func GetStakerEmissionAmountPerSecond() (int64, error) {
354 currentTimestamp := time.Now().Unix()
355 pct, err := GetDistributionBpsPct(LIQUIDITY_STAKER)
356 if err != nil {
357 return 0, err
358 }
359 return GetEmissionAmountPerSecondBy(currentTimestamp, pct), nil
360}
361
362// GetStakerEmissionAmountPerSecondInRange returns liquidity-staker emission-rate change points in a time range.
363//
364// Parameters:
365// - start: inclusive start Unix timestamp for the range.
366// - end: inclusive end Unix timestamp for the range.
367//
368// Returns:
369// - timestamps: ordered timestamps in the range where the base emission rate changes.
370// - amountsPerSecond: liquidity-staker GNS rates corresponding to timestamps, in token base units per second.
371// - error: nil when the liquidity-staker distribution share is available; otherwise the target lookup error.
372func GetStakerEmissionAmountPerSecondInRange(start, end int64) ([]int64, []int64, error) {
373 gnsHalvingBlocks, gnsHalvingEmissions := gns.GetEmissionAmountPerSecondInRange(start, end)
374 halvingBlocks := make([]int64, len(gnsHalvingBlocks))
375 halvingEmissions := make([]int64, len(gnsHalvingEmissions))
376
377 pct, err := GetDistributionBpsPct(LIQUIDITY_STAKER)
378 if err != nil {
379 return nil, nil, err
380 }
381
382 for i := range halvingBlocks {
383 halvingBlocks[i] = gnsHalvingBlocks[i]
384 // Applying staker ratio for past halving blocks
385 halvingEmissions[i] = calculateAmount(gnsHalvingEmissions[i], pct)
386 }
387
388 return halvingBlocks, halvingEmissions, nil
389}
390
391// ClearDistributedToStaker resets the pending distribution amount for liquidity stakers.
392//
393// Parameters:
394// - cur: Current realm context; callers use cross(cur) when crossing into this realm.
395//
396// Only callable by staker contract.
397func ClearDistributedToStaker(cur realm) {
398 caller := cur.Previous().Address()
399 access.AssertIsStaker(caller)
400
401 distributedToStaker = 0
402}
403
404// ClearDistributedToGovStaker resets the pending distribution amount for governance stakers.
405//
406// Parameters:
407// - cur: Current realm context; callers use cross(cur) when crossing into this realm.
408//
409// Only callable by governance staker contract.
410func ClearDistributedToGovStaker(cur realm) {
411 caller := cur.Previous().Address()
412 access.AssertIsGovStaker(caller)
413 distributedToGovStaker = 0
414}
415
416// setDistributionBpsPct changes percentage of each target for how much GNS it will get by emission.
417// Creates new map if nil.
418func setDistributionBpsPct(target int, pct int64) {
419 if distributionBpsPct == nil {
420 distributionBpsPct = make(map[int]int64)
421 }
422
423 distributionBpsPct[target] = pct
424}
425
426// targetToStr converts target constant to string representation.
427func targetToStr(target int) string {
428 switch target {
429 case LIQUIDITY_STAKER:
430 return "LIQUIDITY_STAKER"
431 case DEVOPS:
432 return "DEVOPS"
433 case COMMUNITY_POOL:
434 return "COMMUNITY_POOL"
435 case GOV_STAKER:
436 return "GOV_STAKER"
437 default:
438 return "UNKNOWN"
439 }
440}