Search Apps Documentation Source Content File Folder Download Copy Actions Download

emission.gno

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