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

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}