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