Search Apps Documentation Source Content File Folder Download Copy Actions Download

emission.gno

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