distribution.gno
11.18 Kb · 360 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"
10 prbac "gno.land/p/gnoswap/rbac"
11 "gno.land/p/gnoswap/utils"
12
13 "gno.land/r/gnoswap/access"
14 "gno.land/r/gnoswap/gns"
15 "gno.land/r/gnoswap/halt"
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 through 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// - liquidityStakerPct: Percentage for liquidity stakers in basis points (100 = 1%, 10000 = 100%)
70// - devOpsPct: Percentage for devops in basis points
71// - communityPoolPct: Percentage for community pool in basis points
72// - govStakerPct: Percentage for governance stakers in basis points
73//
74// Requirements:
75// - Percentages must sum to exactly 10000 (100%)
76// - Each percentage must be 0-10000
77//
78// Example:
79//
80// ChangeDistributionPct(
81// 7000, // 70% to liquidity stakers
82// 2000, // 20% to devops
83// 1000, // 10% to community pool
84// 0, // 0% to governance stakers
85// )
86//
87// Only callable by admin or governance.
88func ChangeDistributionPct(
89 cur realm,
90 liquidityStakerPct int64,
91 devOpsPct int64,
92 communityPoolPct int64,
93 govStakerPct int64,
94) {
95 halt.AssertIsNotHaltedEmission()
96
97 caller := cur.Previous().Address()
98 access.AssertIsAdminOrGovernance(caller)
99
100 assertValidDistributionPct(liquidityStakerPct, devOpsPct, communityPoolPct, govStakerPct)
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(cur)
106
107 currentTimestamp := time.Now().Unix()
108 stakerRewardPerSecond := GetEmissionAmountPerSecondBy(currentTimestamp, liquidityStakerPct)
109 govStakerRewardPerSecond := GetEmissionAmountPerSecondBy(currentTimestamp, govStakerPct)
110
111 if onDistributionPctChangeCallback != nil {
112 onDistributionPctChangeCallback(cross(cur), stakerRewardPerSecond)
113 }
114
115 changeDistributionPcts(liquidityStakerPct, devOpsPct, communityPoolPct, govStakerPct)
116
117 previousRealm := cur.Previous()
118 chain.Emit(
119 "ChangeDistributionPct",
120 "prevAddr", previousRealm.Address().String(),
121 "prevRealm", previousRealm.PkgPath(),
122 "liquidityStakerPct", utils.FormatInt(liquidityStakerPct),
123 "devOpsPct", utils.FormatInt(devOpsPct),
124 "communityPoolPct", utils.FormatInt(communityPoolPct),
125 "govStakerPct", utils.FormatInt(govStakerPct),
126 "stakerRewardPerSecond", utils.FormatInt(stakerRewardPerSecond),
127 "govStakerRewardPerSecond", utils.FormatInt(govStakerRewardPerSecond),
128 )
129}
130
131// changeDistributionPcts updates the distribution percentages for all targets.
132func changeDistributionPcts(liquidityStakerPct, devOpsPct, communityPoolPct, govStakerPct int64) {
133 setDistributionBpsPct(LIQUIDITY_STAKER, liquidityStakerPct)
134 setDistributionBpsPct(DEVOPS, devOpsPct)
135 setDistributionBpsPct(COMMUNITY_POOL, communityPoolPct)
136 setDistributionBpsPct(GOV_STAKER, govStakerPct)
137}
138
139func calculateDistributableAmounts(amount int64) (map[int]int64, int64) {
140 distributable := make(map[int]int64, 0)
141 totalSent := int64(0)
142
143 for target, pct := range distributionBpsPct {
144 distAmount := calculateAmount(amount, pct)
145 if distAmount == 0 {
146 continue
147 }
148
149 distributable[target] = distAmount
150 totalSent = gnsmath.SafeAddInt64(totalSent, distAmount)
151 }
152
153 leftAmount := gnsmath.SafeSubInt64(amount, totalSent)
154 return distributable, leftAmount
155}
156
157// calculateAmount converts basis points to actual token amount.
158func calculateAmount(amount, bptPct int64) int64 {
159 if amount < 0 || bptPct < 0 || bptPct > 10000 {
160 panic("invalid amount or bptPct")
161 }
162
163 // More precise overflow prevention
164 const maxInt64 = 9223372036854775807
165 if amount > maxInt64/10000 {
166 panic("amount too large, would cause overflow")
167 }
168
169 // Additional safety check for zero division
170 if bptPct == 0 {
171 return 0
172 }
173
174 return amount * bptPct / 10000
175}
176
177func applyDistribution(targets map[int]int64) (map[address]int64, error) {
178 amountByAddress := make(map[address]int64, 0)
179
180 for target, amount := range targets {
181 var addr address
182
183 switch target {
184 case LIQUIDITY_STAKER:
185 distributedToStaker = gnsmath.SafeAddInt64(distributedToStaker, amount)
186 accuDistributedToStaker = gnsmath.SafeAddInt64(accuDistributedToStaker, amount)
187 addr = access.MustGetAddress(prbac.ROLE_STAKER.String())
188
189 case DEVOPS:
190 distributedToDevOps = gnsmath.SafeAddInt64(distributedToDevOps, amount)
191 accuDistributedToDevOps = gnsmath.SafeAddInt64(accuDistributedToDevOps, amount)
192 addr = access.MustGetAddress(prbac.ROLE_DEVOPS.String())
193
194 case COMMUNITY_POOL:
195 distributedToCommunityPool = gnsmath.SafeAddInt64(distributedToCommunityPool, amount)
196 accuDistributedToCommunityPool = gnsmath.SafeAddInt64(accuDistributedToCommunityPool, amount)
197 addr = access.MustGetAddress(prbac.ROLE_COMMUNITY_POOL.String())
198
199 case GOV_STAKER:
200 distributedToGovStaker = gnsmath.SafeAddInt64(distributedToGovStaker, amount)
201 accuDistributedToGovStaker = gnsmath.SafeAddInt64(accuDistributedToGovStaker, amount)
202 addr = access.MustGetAddress(prbac.ROLE_GOV_STAKER.String())
203
204 default:
205 return nil, makeErrorWithDetails(
206 errInvalidEmissionTarget,
207 ufmt.Sprintf("invalid target(%d)", target),
208 )
209 }
210
211 amountByAddress[addr] = gnsmath.SafeAddInt64(amountByAddress[addr], amount)
212 }
213
214 return amountByAddress, nil
215}
216
217func transferToTarget(_ int, rlm realm, targets map[address]int64) error {
218 for address, amount := range targets {
219 gns.Transfer(cross(rlm), address, amount)
220 }
221
222 return nil
223}
224
225// GetDistributionBpsPct returns the distribution percentage in basis points for a specific target.
226func GetDistributionBpsPct(target int) int64 {
227 assertValidDistributionTarget(target)
228 if distributionBpsPct == nil {
229 panic("distributionBpsPct is nil")
230 }
231
232 pct, exist := distributionBpsPct[target]
233 if !exist {
234 panic(makeErrorWithDetails(
235 errInvalidEmissionTarget,
236 ufmt.Sprintf("invalid target(%d)", target),
237 ))
238 }
239
240 return pct
241}
242
243// GetDistributedToStaker returns pending GNS for liquidity stakers.
244func GetDistributedToStaker() int64 {
245 return distributedToStaker
246}
247
248// GetDistributedToDevOps returns accumulated GNS for DevOps.
249func GetDistributedToDevOps() int64 {
250 return distributedToDevOps
251}
252
253// GetDistributedToCommunityPool returns the amount of GNS distributed to Community Pool.
254func GetDistributedToCommunityPool() int64 {
255 return distributedToCommunityPool
256}
257
258// GetDistributedToGovStaker returns the amount of GNS distributed to governance stakers since last clear.
259func GetDistributedToGovStaker() int64 {
260 return distributedToGovStaker
261}
262
263func AccumulateDistributedInfo() (toStaker, toDevOps, toCommunityPool, toGovStaker int64) {
264 toStaker = GetDistributedToStaker()
265 toDevOps = GetDistributedToDevOps()
266 toCommunityPool = GetDistributedToCommunityPool()
267 toGovStaker = GetDistributedToGovStaker()
268 return
269}
270
271// GetAccuDistributedToStaker returns the total historical GNS distributed to liquidity stakers.
272func GetAccuDistributedToStaker() int64 {
273 return accuDistributedToStaker
274}
275
276// GetAccuDistributedToDevOps returns the total historical GNS distributed to DevOps.
277func GetAccuDistributedToDevOps() int64 {
278 return accuDistributedToDevOps
279}
280
281// GetAccuDistributedToCommunityPool returns the total historical GNS distributed to Community Pool.
282func GetAccuDistributedToCommunityPool() int64 {
283 return accuDistributedToCommunityPool
284}
285
286// GetAccuDistributedToGovStaker returns the total historical GNS distributed to governance stakers.
287func GetAccuDistributedToGovStaker() int64 {
288 return accuDistributedToGovStaker
289}
290
291// GetEmissionAmountPerSecondBy returns the emission amount per second for a given timestamp and distribution percentage.
292func GetEmissionAmountPerSecondBy(timestamp, distributionPct int64) int64 {
293 return calculateAmount(gns.GetEmissionAmountPerSecondByTimestamp(timestamp), distributionPct)
294}
295
296// GetStakerEmissionAmountPerSecond returns the current per-second emission amount allocated to liquidity stakers.
297func GetStakerEmissionAmountPerSecond() int64 {
298 currentTimestamp := time.Now().Unix()
299 return GetEmissionAmountPerSecondBy(currentTimestamp, GetDistributionBpsPct(LIQUIDITY_STAKER))
300}
301
302// GetStakerEmissionAmountPerSecondInRange returns emission amounts allocated to liquidity stakers for a time range.
303func GetStakerEmissionAmountPerSecondInRange(start, end int64) ([]int64, []int64) {
304 gnsHalvingBlocks, gnsHalvingEmissions := gns.GetEmissionAmountPerSecondInRange(start, end)
305 halvingBlocks := make([]int64, len(gnsHalvingBlocks))
306 halvingEmissions := make([]int64, len(gnsHalvingEmissions))
307
308 for i := range halvingBlocks {
309 halvingBlocks[i] = gnsHalvingBlocks[i]
310 // Applying staker ratio for past halving blocks
311 halvingEmissions[i] = calculateAmount(gnsHalvingEmissions[i], GetDistributionBpsPct(LIQUIDITY_STAKER))
312 }
313
314 return halvingBlocks, halvingEmissions
315}
316
317// ClearDistributedToStaker resets the pending distribution amount for liquidity stakers.
318//
319// Only callable by staker contract.
320func ClearDistributedToStaker(cur realm) {
321 caller := cur.Previous().Address()
322 access.AssertIsStaker(caller)
323
324 distributedToStaker = 0
325}
326
327// ClearDistributedToGovStaker resets the pending distribution amount for governance stakers.
328//
329// Only callable by governance staker contract.
330func ClearDistributedToGovStaker(cur realm) {
331 caller := cur.Previous().Address()
332 access.AssertIsGovStaker(caller)
333 distributedToGovStaker = 0
334}
335
336// setDistributionBpsPct changes percentage of each target for how much GNS it will get by emission.
337// Creates new map if nil.
338func setDistributionBpsPct(target int, pct int64) {
339 if distributionBpsPct == nil {
340 distributionBpsPct = make(map[int]int64)
341 }
342
343 distributionBpsPct[target] = pct
344}
345
346// targetToStr converts target constant to string representation.
347func targetToStr(target int) string {
348 switch target {
349 case LIQUIDITY_STAKER:
350 return "LIQUIDITY_STAKER"
351 case DEVOPS:
352 return "DEVOPS"
353 case COMMUNITY_POOL:
354 return "COMMUNITY_POOL"
355 case GOV_STAKER:
356 return "GOV_STAKER"
357 default:
358 return "UNKNOWN"
359 }
360}