Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}