Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}