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