Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}