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