staker_reward.gno
13.53 Kb · 450 lines
1package staker
2
3import (
4 "chain"
5 "time"
6
7 prbac "gno.land/p/gnoswap/rbac/v1"
8 "gno.land/p/gnoswap/utils/v1"
9 ufmt "gno.land/p/nt/ufmt/v0"
10
11 "gno.land/r/gnoswap/access/v1"
12 "gno.land/r/gnoswap/common"
13 "gno.land/r/gnoswap/gns"
14 "gno.land/r/gnoswap/gov/xgns"
15 "gno.land/r/gnoswap/halt/v1"
16)
17
18// CollectReward claims all rewards for the caller.
19//
20// It settles both reward streams:
21// 1. GNS emission rewards from the emission accumulator.
22// 2. Protocol-fee rewards for every known token path.
23//
24// Protocol-fee rewards are divided by the total stake in force for each
25// accrual epoch and settled across the caller's stake-event segments. They
26// are not calculated from one current xGNS/total-xGNS ratio. Q128 fractional
27// remainders remain in state for later collection.
28//
29// The all-token path intentionally grows with the number of known fee tokens.
30// Use the token-specific entry point when bounded work is required.
31//
32// No business parameters are required; the caller's reward ID is used.
33// Rewards are transferred directly to the caller.
34//
35// Parameters:
36// - _: Noncrossing implementation-call discriminator; pass 0.
37// - rlm: Current realm context forwarded by the governance-staker proxy; validated before settlement and transfers.
38func (gs *govStakerV1) CollectReward(_ int, rlm realm) {
39 access.AssertIsRlmCurrent(0, rlm)
40
41 halt.AssertIsNotHaltedWithdraw()
42
43 prev := rlm.Previous()
44 caller := prev.Address()
45 from := rlm.Address()
46 currentTimestamp := time.Now().Unix()
47
48 emissionReward, protocolFeeRewards, err := gs.claimRewards(0, rlm, caller.String(), currentTimestamp)
49 if err != nil {
50 panic(err)
51 }
52
53 // Transfer emission rewards (GNS tokens) if any
54 if emissionReward > 0 {
55 gns.Transfer(cross(rlm), caller, emissionReward)
56
57 chain.Emit(
58 "CollectEmissionReward",
59 "prevAddr", prev.Address().String(),
60 "prevRealm", prev.PkgPath(),
61 "from", from.String(),
62 "to", caller.String(),
63 "emissionRewardAmount", utils.FormatInt(emissionReward),
64 )
65 }
66
67 // Transfer protocol fee rewards for each token type
68 for tokenPath, amount := range protocolFeeRewards {
69 if amount > 0 {
70 err := transferToken(0, rlm, tokenPath, from, caller, amount)
71 if err != nil {
72 panic(err)
73 }
74
75 chain.Emit(
76 "CollectProtocolFeeReward",
77 "prevAddr", prev.Address().String(),
78 "prevRealm", prev.PkgPath(),
79 "tokenPath", tokenPath,
80 "from", from.String(),
81 "to", caller.String(),
82 "collectedAmount", utils.FormatInt(amount),
83 )
84 }
85 }
86}
87
88// CollectEmissionReward collects accumulated GNS emission rewards only.
89//
90// Parameters:
91// - _: Noncrossing implementation-call discriminator; pass 0.
92// - rlm: Current realm context forwarded by the governance-staker proxy; validated before settlement and transfer.
93func (gs *govStakerV1) CollectEmissionReward(_ int, rlm realm) {
94 access.AssertIsRlmCurrent(0, rlm)
95
96 halt.AssertIsNotHaltedWithdraw()
97
98 prev := rlm.Previous()
99 caller := prev.Address()
100 from := rlm.Address()
101 currentTimestamp := time.Now().Unix()
102
103 emissionReward, err := gs.claimRewardsEmissionReward(0, rlm, caller.String(), currentTimestamp)
104 if err != nil {
105 panic(err)
106 }
107
108 if emissionReward > 0 {
109 gns.Transfer(cross(rlm), caller, emissionReward)
110
111 chain.Emit(
112 "CollectEmissionReward",
113 "prevAddr", prev.Address().String(),
114 "prevRealm", prev.PkgPath(),
115 "from", from.String(),
116 "to", caller.String(),
117 "emissionRewardAmount", utils.FormatInt(emissionReward),
118 )
119 }
120}
121
122// CollectProtocolFeeReward collects accumulated protocol fee rewards for the provided token path.
123//
124// Parameters:
125// - _: Noncrossing implementation-call discriminator; pass 0.
126// - rlm: Current realm context forwarded by the governance-staker proxy; validated before settlement and transfer.
127// - tokenPath: registered fee-token path whose reward should be collected
128func (gs *govStakerV1) CollectProtocolFeeReward(_ int, rlm realm, tokenPath string) {
129 access.AssertIsRlmCurrent(0, rlm)
130
131 halt.AssertIsNotHaltedWithdraw()
132
133 prev := rlm.Previous()
134 caller := prev.Address()
135 from := rlm.Address()
136 currentTimestamp := time.Now().Unix()
137
138 amount, err := gs.claimRewardProtocolFeeRewardByTokenPath(0, rlm, caller.String(), tokenPath, currentTimestamp)
139 if err != nil {
140 panic(err)
141 }
142
143 if amount > 0 {
144 err := transferToken(0, rlm, tokenPath, from, caller, amount)
145 if err != nil {
146 panic(err)
147 }
148
149 chain.Emit(
150 "CollectProtocolFeeReward",
151 "prevAddr", prev.Address().String(),
152 "prevRealm", prev.PkgPath(),
153 "tokenPath", tokenPath,
154 "from", from.String(),
155 "to", caller.String(),
156 "collectedAmount", utils.FormatInt(amount),
157 )
158 }
159}
160
161// CollectRewardFromLaunchPad collects rewards for launchpad project wallets.
162//
163// Parameters:
164// - _: Noncrossing implementation-call discriminator; pass 0.
165// - rlm: Current realm context forwarded by the governance-staker proxy; validated before launchpad reward settlement.
166// - to: project wallet address receiving its emission and protocol-fee rewards
167//
168// Only callable by launchpad contract.
169func (gs *govStakerV1) CollectRewardFromLaunchPad(_ int, rlm realm, to address) {
170 access.AssertIsRlmCurrent(0, rlm)
171
172 halt.AssertIsNotHaltedWithdraw()
173
174 prev := rlm.Previous()
175 caller := prev.Address()
176 access.AssertIsLaunchpad(caller)
177
178 from := rlm.Address()
179 currentTimestamp := time.Now().Unix()
180
181 launchpadRewardID := gs.makeLaunchpadRewardID(to.String())
182 _, exists := gs.getLaunchpadProjectDeposit(launchpadRewardID)
183 if !exists {
184 panic(makeErrorWithDetails(
185 errNoDelegatedAmount,
186 ufmt.Sprintf("%s is not project wallet from launchpad", to.String()),
187 ))
188 }
189
190 emissionReward, protocolFeeRewards, err := gs.claimRewardsFromLaunchpad(0, rlm, to.String(), currentTimestamp)
191 if err != nil {
192 panic(err)
193 }
194
195 // Transfer emission rewards (GNS tokens) to project wallet if any
196 if emissionReward > 0 {
197 gns.Transfer(cross(rlm), to, emissionReward)
198
199 chain.Emit(
200 "CollectEmissionFromLaunchPad",
201 "prevAddr", prev.Address().String(),
202 "prevRealm", prev.PkgPath(),
203 "from", from.String(),
204 "to", to.String(),
205 "emissionRewardAmount", utils.FormatInt(emissionReward),
206 )
207 }
208
209 // Transfer protocol fee rewards to project wallet for each token type
210 for tokenPath, amount := range protocolFeeRewards {
211 if amount > 0 {
212 err := transferToken(0, rlm, tokenPath, from, to, amount)
213 if err != nil {
214 panic(err)
215 }
216
217 chain.Emit(
218 "CollectProtocolFeeFromLaunchPad",
219 "prevAddr", prev.Address().String(),
220 "prevRealm", prev.PkgPath(),
221 "tokenPath", tokenPath,
222 "from", from.String(),
223 "to", to.String(),
224 "collectedAmount", utils.FormatInt(amount),
225 )
226 }
227 }
228}
229
230// CollectEmissionRewardFromLaunchPad collects emission rewards only for launchpad project wallets.
231//
232// Parameters:
233// - _: Noncrossing implementation-call discriminator; pass 0.
234// - rlm: Current realm context forwarded by the governance-staker proxy; validated before launchpad reward settlement.
235// - to: project wallet address receiving its emission reward
236func (gs *govStakerV1) CollectEmissionRewardFromLaunchPad(_ int, rlm realm, to address) {
237 access.AssertIsRlmCurrent(0, rlm)
238
239 halt.AssertIsNotHaltedWithdraw()
240
241 prev := rlm.Previous()
242 caller := prev.Address()
243 access.AssertIsLaunchpad(caller)
244
245 from := rlm.Address()
246 currentTimestamp := time.Now().Unix()
247
248 launchpadRewardID := gs.makeLaunchpadRewardID(to.String())
249 _, exists := gs.getLaunchpadProjectDeposit(launchpadRewardID)
250 if !exists {
251 panic(makeErrorWithDetails(
252 errNoDelegatedAmount,
253 ufmt.Sprintf("%s is not project wallet from launchpad", to.String()),
254 ))
255 }
256
257 emissionReward, err := gs.claimRewardsEmissionReward(0, rlm, launchpadRewardID, currentTimestamp)
258 if err != nil {
259 panic(err)
260 }
261
262 if emissionReward > 0 {
263 gns.Transfer(cross(rlm), to, emissionReward)
264
265 chain.Emit(
266 "CollectEmissionFromLaunchPad",
267 "prevAddr", prev.Address().String(),
268 "prevRealm", prev.PkgPath(),
269 "from", from.String(),
270 "to", to.String(),
271 "emissionRewardAmount", utils.FormatInt(emissionReward),
272 )
273 }
274}
275
276// CollectProtocolFeeRewardFromLaunchPad collects one token path of protocol fee rewards for launchpad project wallets.
277//
278// Parameters:
279// - _: Noncrossing implementation-call discriminator; pass 0.
280// - rlm: Current realm context forwarded by the governance-staker proxy; validated before launchpad reward settlement.
281// - to: project wallet address receiving the protocol-fee reward
282// - tokenPath: registered fee-token path whose reward should be collected
283func (gs *govStakerV1) CollectProtocolFeeRewardFromLaunchPad(_ int, rlm realm, to address, tokenPath string) {
284 access.AssertIsRlmCurrent(0, rlm)
285
286 halt.AssertIsNotHaltedWithdraw()
287
288 prev := rlm.Previous()
289 caller := prev.Address()
290 access.AssertIsLaunchpad(caller)
291
292 from := rlm.Address()
293 currentTimestamp := time.Now().Unix()
294
295 launchpadRewardID := gs.makeLaunchpadRewardID(to.String())
296 _, exists := gs.getLaunchpadProjectDeposit(launchpadRewardID)
297 if !exists {
298 panic(makeErrorWithDetails(
299 errNoDelegatedAmount,
300 ufmt.Sprintf("%s is not project wallet from launchpad", to.String()),
301 ))
302 }
303
304 amount, err := gs.claimRewardProtocolFeeRewardByTokenPath(0, rlm, launchpadRewardID, tokenPath, currentTimestamp)
305 if err != nil {
306 panic(err)
307 }
308
309 if amount > 0 {
310 err := transferToken(0, rlm, tokenPath, from, to, amount)
311 if err != nil {
312 panic(err)
313 }
314
315 chain.Emit(
316 "CollectProtocolFeeFromLaunchPad",
317 "prevAddr", prev.Address().String(),
318 "prevRealm", prev.PkgPath(),
319 "tokenPath", tokenPath,
320 "from", from.String(),
321 "to", to.String(),
322 "collectedAmount", utils.FormatInt(amount),
323 )
324 }
325}
326
327// SetAmountByProjectWallet sets the amount of reward stake for the project wallet.
328// This function is exclusively callable by the launchpad contract to manage
329// xGNS balances for project wallets that participate in launchpad offerings.
330//
331// The function handles both adding and removing stakes:
332// - When adding: mints xGNS to launchpad address and starts reward accumulation
333// - When removing: burns xGNS from launchpad address and stops reward accumulation
334// Adjusts stake amount for project wallet address.
335//
336// Parameters:
337// - _: Noncrossing implementation-call discriminator; pass 0.
338// - rlm: Current realm context forwarded by the governance-staker proxy; validated before launchpad authorization and staking changes.
339// - addr: project wallet address whose launchpad-backed stake should be adjusted
340// - amount: amount of stake to add or remove
341// - add: true to add stake and mint xGNS to launchpad, false to remove stake and burn xGNS from launchpad
342//
343// Panics:
344// - if caller is not the launchpad contract
345// - if system is halted for withdrawals
346// - if access control operations fail
347func (gs *govStakerV1) SetAmountByProjectWallet(_ int, rlm realm, addr address, amount int64, add bool) {
348 access.AssertIsRlmCurrent(0, rlm)
349
350 if add {
351 halt.AssertIsNotHaltedGovStaker()
352 } else {
353 halt.AssertIsNotHaltedWithdraw()
354 }
355
356 prev := rlm.Previous()
357 caller := prev.Address()
358 currentTimestamp := time.Now().Unix()
359
360 access.AssertIsLaunchpad(caller)
361
362 launchpadAddr := access.MustGetAddress(prbac.ROLE_LAUNCHPAD.String())
363
364 if add {
365 // Add stake for the project wallet and mint xGNS to launchpad
366 err := gs.addStakeFromLaunchpad(0, rlm, addr.String(), amount, currentTimestamp)
367 if err != nil {
368 panic(err)
369 }
370
371 xgns.Mint(cross(rlm), launchpadAddr, amount)
372 } else {
373 // Remove stake for the project wallet and burn xGNS from launchpad
374 err := gs.removeStakeFromLaunchpad(0, rlm, addr.String(), amount, currentTimestamp)
375 if err != nil {
376 panic(err)
377 }
378
379 xgns.Burn(cross(rlm), launchpadAddr, amount)
380 }
381}
382
383// claimRewards claims both emission and protocol fee rewards.
384// Coordinates claiming process for both reward types.
385func (gs *govStakerV1) claimRewards(_ int, rlm realm, rewardID string, currentTimestamp int64) (int64, map[string]int64, error) {
386 emissionReward, err := gs.claimRewardsEmissionReward(0, rlm, rewardID, currentTimestamp)
387 if err != nil {
388 return 0, nil, err
389 }
390
391 protocolFeeRewards, err := gs.claimRewardsProtocolFeeReward(0, rlm, rewardID, currentTimestamp)
392 if err != nil {
393 return 0, nil, err
394 }
395
396 return emissionReward, protocolFeeRewards, nil
397}
398
399// claimRewardsFromLaunchpad claims rewards for launchpad project wallets.
400// Uses special reward ID format for launchpad integration.
401func (gs *govStakerV1) claimRewardsFromLaunchpad(_ int, rlm realm, address string, currentTimestamp int64) (int64, map[string]int64, error) {
402 launchpadRewardID := gs.makeLaunchpadRewardID(address)
403
404 return gs.claimRewards(0, rlm, launchpadRewardID, currentTimestamp)
405}
406
407// transferToken transfers tokens from the staker contract to a recipient address.
408// transferToken handles token transfers for reward distribution.
409//
410// Non-crossing helper: takes `_ int, rlm realm` so the realm token threaded
411// in by the public reward-collection entry points reaches the underlying
412// GRC-20 transfer without forcing each caller to recompute it.
413func transferToken(
414 _ int,
415 rlm realm,
416 tokenPath string,
417 from, to address,
418 amount int64,
419) error {
420 common.MustRegistered(tokenPath)
421
422 // Validate recipient address
423 if !to.IsValid() {
424 return makeErrorWithDetails(
425 errInvalidAddress,
426 ufmt.Sprintf("invalid address %s to transfer protocol fee", to.String()),
427 )
428 }
429
430 // Validate transfer amount
431 if amount < 0 {
432 return makeErrorWithDetails(
433 errInvalidAmount,
434 ufmt.Sprintf("invalid amount %d to transfer protocol fee", amount),
435 )
436 }
437
438 // Check sufficient balance
439 balance := common.BalanceOf(tokenPath, from)
440 if balance < amount {
441 return makeErrorWithDetails(
442 errNotEnoughBalance,
443 ufmt.Sprintf("not enough %s balance(%d) to collect(%d)", tokenPath, balance, amount),
444 )
445 }
446
447 common.SafeGRC20Transfer(0, rlm, tokenPath, to, amount)
448
449 return nil
450}