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

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