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}