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

emission.gno

9.02 Kb · 269 lines
  1package emission
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"math"
  7	"time"
  8
  9	gnsmath "gno.land/p/gnoswap/gnsmath"
 10	"gno.land/p/gnoswap/utils"
 11
 12	"gno.land/r/gnoswap/access"
 13	"gno.land/r/gnoswap/gns"
 14	"gno.land/r/gnoswap/halt"
 15)
 16
 17const (
 18	totalDistributionDuration = 12 * 365 * 24 * 60 * 60 // 12 years
 19
 20	// DefaultInitialPoolTierPath is the canonical default initial pool that must
 21	// exist before emission distribution can start. The pool contract registers a
 22	// checker (SetDefaultInitialPoolChecker) that verifies this pool exists.
 23	DefaultInitialPoolTierPath = "gno.land/r/gnoland/wugnot.wugnot:gno.land/r/gnoswap/gns.GNS:3000"
 24)
 25
 26var (
 27	// leftGNSAmount tracks undistributed GNS tokens from previous distributions
 28	leftGNSAmount int64
 29
 30	// lastExecutedTimestamp stores the last timestamp when distribution was executed
 31	lastExecutedTimestamp int64
 32
 33	// emissionAddr is the address of the emission realm
 34	emissionAddr address
 35
 36	// distributionStartTimestamp is the timestamp from which emission distribution starts
 37	// Default is 0, meaning distribution is not started until explicitly set
 38	distributionStartTimestamp int64
 39
 40	// onDistributionPctChangeCallback is called when distribution percentages change
 41	// This allows external contracts (like staker) to update their caches
 42	onDistributionPctChangeCallback func(cur realm, emissionAmountPerSecond int64)
 43
 44	// defaultInitialPoolChecker verifies the canonical default initial pool exists.
 45	// It is registered by pool/v1 at initialization time so that emission does not
 46	// need a compile-time dependency on the pool realm.
 47	defaultInitialPoolChecker func(poolPath string) bool
 48)
 49
 50func init(cur realm) {
 51	emissionAddr = cur.Address()
 52}
 53
 54// setLeftGNSAmount updates the undistributed GNS token amount
 55func setLeftGNSAmount(amount int64) {
 56	if amount < 0 {
 57		panic("left GNS amount cannot be negative")
 58	}
 59
 60	leftGNSAmount = amount
 61}
 62
 63// setLastExecutedTimestamp updates the timestamp of the last emission distribution execution.
 64func setLastExecutedTimestamp(timestamp int64) {
 65	if timestamp < 0 {
 66		panic("last executed timestamp cannot be negative")
 67	}
 68
 69	lastExecutedTimestamp = timestamp
 70}
 71
 72// MintAndDistributeGns mints and distributes GNS tokens according to the emission schedule.
 73//
 74// This function is called automatically by protocol contracts during user interactions
 75// to trigger periodic GNS emission. It mints new tokens based on elapsed time since
 76// last distribution and distributes them to predefined targets (staker, devops, etc.).
 77//
 78// Returns:
 79//   - int64: Total amount of GNS distributed in this call
 80//
 81// Note: Distribution only occurs if start timestamp is set and reached.
 82// Any undistributed tokens from previous calls are carried forward.
 83func MintAndDistributeGns(cur realm) (int64, bool) {
 84	if halt.IsHaltedEmission() {
 85		return 0, false
 86	}
 87
 88	currentHeight := runtime.ChainHeight()
 89	currentTimestamp := time.Now().Unix()
 90
 91	// Check if distribution start timestamp is set and if current timestamp has reached it
 92	// If distributionStartTimestamp is 0 (default), skip distribution to prevent immediate start
 93	// If current timestamp is below start timestamp, skip distribution
 94	if distributionStartTimestamp == 0 || currentTimestamp < distributionStartTimestamp {
 95		return 0, true
 96	}
 97
 98	// Skip if we've already minted tokens at this timestamp
 99	lastMintedTimestamp := gns.LastMintedTimestamp()
100	if currentTimestamp <= lastMintedTimestamp {
101		return 0, true
102	}
103
104	// Additional check to prevent re-entrancy
105	if lastExecutedTimestamp >= currentTimestamp {
106		// Skip if we've already processed this height in emission
107		return 0, true
108	}
109
110	// Mint new tokens and add any leftover amounts from previous distribution
111	mintedEmissionRewardAmount := gns.MintGns(cross(cur), emissionAddr)
112
113	// Validate minted amount
114	if mintedEmissionRewardAmount < 0 {
115		panic("minted emission reward amount cannot be negative")
116	}
117
118	distributableAmount := mintedEmissionRewardAmount
119	prevLeftAmount := GetLeftGNSAmount()
120
121	if leftGNSAmount > 0 {
122		// Check for overflow before addition
123		if distributableAmount > math.MaxInt64-prevLeftAmount {
124			panic("distributable amount would overflow")
125		}
126
127		distributableAmount += prevLeftAmount
128		setLeftGNSAmount(0)
129	}
130
131	distributable, leftAmount := calculateDistributableAmounts(distributableAmount)
132	totalDistAmount := gnsmath.SafeSubInt64(distributableAmount, leftAmount)
133	if leftAmount > 0 {
134		setLeftGNSAmount(leftAmount)
135	}
136	setLastExecutedTimestamp(currentTimestamp)
137
138	amountByAddress, err := applyDistribution(distributable)
139	if err != nil {
140		panic(err)
141	}
142
143	if err := transferToTarget(0, cur, amountByAddress); err != nil {
144		panic(err)
145	}
146
147	stakerRewardPerSecond := GetEmissionAmountPerSecondBy(currentTimestamp, GetDistributionBpsPct(LIQUIDITY_STAKER))
148	govStakerRewardPerSecond := GetEmissionAmountPerSecondBy(currentTimestamp, GetDistributionBpsPct(GOV_STAKER))
149
150	previousRealm := cur.Previous()
151	chain.Emit(
152		"MintAndDistributeGns",
153		"prevAddr", previousRealm.Address().String(),
154		"prevRealm", previousRealm.PkgPath(),
155		"lastTimestamp", utils.FormatInt(lastExecutedTimestamp),
156		"currentTimestamp", utils.FormatInt(currentTimestamp),
157		"currentHeight", utils.FormatInt(currentHeight),
158		"mintedAmount", utils.FormatInt(mintedEmissionRewardAmount),
159		"prevLeftAmount", utils.FormatInt(prevLeftAmount),
160		"distributedAmount", utils.FormatInt(totalDistAmount),
161		"currentLeftAmount", utils.FormatInt(GetLeftGNSAmount()),
162		"gnsTotalSupply", utils.FormatInt(gns.TotalSupply()),
163		"stakerRewardPerSecond", utils.FormatInt(stakerRewardPerSecond),
164		"govStakerRewardPerSecond", utils.FormatInt(govStakerRewardPerSecond),
165	)
166
167	return totalDistAmount, true
168}
169
170// SetDistributionStartTime sets the timestamp when emission distribution starts.
171//
172// This function controls when GNS emission begins. Once set and reached, the protocol
173// starts minting GNS tokens according to the emission schedule. The timestamp can only
174// be set before distribution starts - it becomes immutable once active.
175//
176// Parameters:
177//   - startTimestamp: Unix timestamp when emission should begin
178//
179// Requirements:
180//   - Must be called before distribution starts (one-time setup)
181//   - Timestamp must be in the future
182//   - Cannot be negative
183//
184// Effects:
185//   - Sets global distribution start time
186//   - Initializes GNS emission state if not already started
187//   - Emission begins automatically when timestamp is reached
188//
189// Only callable by admin or governance.
190func SetDistributionStartTime(cur realm, startTimestamp int64) {
191	halt.AssertIsNotHaltedEmission()
192
193	caller := cur.Previous().Address()
194	access.AssertIsAdminOrGovernance(caller)
195	assertDefaultInitialPoolExists()
196
197	if startTimestamp <= 0 {
198		panic("distribution start timestamp must be positive")
199	}
200
201	if startTimestamp > math.MaxInt64-totalDistributionDuration {
202		panic("distribution end timestamp must be before max int64 timestamp")
203	}
204
205	currentTimestamp := time.Now().Unix()
206
207	// Must be in the future.
208	if startTimestamp <= currentTimestamp {
209		panic("distribution start timestamp must be greater than current timestamp")
210	}
211
212	// Cannot change after distribution started.
213	if distributionStartTimestamp != 0 && distributionStartTimestamp <= currentTimestamp {
214		panic("distribution has already started, cannot change start timestamp")
215	}
216
217	prevStartTimestamp := distributionStartTimestamp
218
219	if gns.MintedEmissionAmount() == 0 {
220		currentHeight := runtime.ChainHeight()
221		gns.InitEmissionState(cross(cur), currentHeight, startTimestamp)
222	}
223
224	distributionStartTimestamp = startTimestamp
225
226	chain.Emit(
227		"SetDistributionStartTime",
228		"caller", caller.String(),
229		"prevStartTimestamp", utils.FormatInt(prevStartTimestamp),
230		"newStartTimestamp", utils.FormatInt(startTimestamp),
231		"height", utils.FormatInt(runtime.ChainHeight()),
232		"timestamp", utils.FormatInt(time.Now().Unix()),
233	)
234}
235
236// SetOnDistributionPctChangeCallback sets a callback function to be called when distribution percentages change.
237// This allows external contracts (like staker) to update their internal caches when governance changes emission rates.
238//
239// Only callable by the staker contract.
240func SetOnDistributionPctChangeCallback(cur realm, callback func(cur realm, emissionAmountPerSecond int64)) {
241	caller := cur.Previous().Address()
242	access.AssertIsStaker(caller)
243
244	onDistributionPctChangeCallback = callback
245
246	if onDistributionPctChangeCallback != nil {
247		emissionAmountPerSecond := GetStakerEmissionAmountPerSecond()
248		onDistributionPctChangeCallback(cross(cur), emissionAmountPerSecond)
249	}
250}
251
252// SetDefaultInitialPoolChecker registers the callback that verifies the
253// canonical default initial pool exists before emission starts.
254//
255// The checker receives the pool path as a parameter so emission owns the
256// canonical path policy (DefaultInitialPoolTierPath) while pool supplies only
257// the generic "does this pool exist" capability.
258//
259// Only callable by the pool contract.
260func SetDefaultInitialPoolChecker(cur realm, checker func(poolPath string) bool) {
261	caller := cur.Previous().Address()
262	access.AssertIsPool(caller)
263
264	if checker == nil {
265		panic(makeErrorWithDetails(errInvalidEmissionStart, "default initial pool checker cannot be nil"))
266	}
267
268	defaultInitialPoolChecker = checker
269}