Search Apps Documentation Source Content File Folder Download Copy Actions Download

distribution.gno

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