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}