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}