staker_reward.gno
7.77 Kb · 280 lines
1package v1
2
3import (
4 "chain"
5 "time"
6
7 prbac "gno.land/p/gnoswap/rbac"
8 ufmt "gno.land/p/nt/ufmt/v0"
9
10 "gno.land/r/gnoswap/access"
11 "gno.land/r/gnoswap/common"
12 "gno.land/r/gnoswap/gns"
13 "gno.land/r/gnoswap/gov/xgns"
14 "gno.land/r/gnoswap/halt"
15)
16
17// CollectReward collects accumulated rewards based on xGNS holdings.
18//
19// Claims all pending rewards from governance staking.
20// Distributes protocol fees and emission rewards proportionally.
21// Multi-token rewards system based on xGNS share.
22//
23// Reward Types:
24// 1. Emission rewards: GNS from protocol emission
25// 2. Protocol fees: Various tokens from swap/pool fees
26// 3. Withdrawal fees: 1% of liquidity provider rewards
27// 4. Pool creation fees: 100 GNS per pool
28//
29// Distribution Formula:
30//
31// userReward = (userXGNS / totalXGNS) * accumulatedRewards
32//
33// Process:
34// 1. Calculates share based on xGNS balance
35// 2. Claims GNS emission rewards
36// 3. Claims protocol fee rewards (all tokens)
37// 4. Transfers all rewards to caller
38// 5. Resets user's reward tracking
39//
40// No parameters required - automatically determines caller's rewards.
41// Transfers rewards directly to caller.
42func (gs *govStakerV1) CollectReward(_ int, rlm realm) {
43 if !rlm.IsCurrent() {
44 panic(errSpoofedRealm)
45 }
46
47 halt.AssertIsNotHaltedWithdraw()
48
49 prev := rlm.Previous()
50 caller := prev.Address()
51 from := rlm.Address()
52 currentTimestamp := time.Now().Unix()
53
54 emissionReward, protocolFeeRewards, err := gs.claimRewards(0, rlm, caller.String(), currentTimestamp)
55 if err != nil {
56 panic(err)
57 }
58
59 // Transfer emission rewards (GNS tokens) if any
60 if emissionReward > 0 {
61 gns.Transfer(cross(rlm), caller, emissionReward)
62
63 chain.Emit(
64 "CollectEmissionReward",
65 "prevAddr", prev.Address().String(),
66 "prevRealm", prev.PkgPath(),
67 "from", from.String(),
68 "to", caller.String(),
69 "emissionRewardAmount", formatInt(emissionReward),
70 )
71 }
72
73 // Transfer protocol fee rewards for each token type
74 for tokenPath, amount := range protocolFeeRewards {
75 if amount > 0 {
76 err := transferToken(0, rlm, tokenPath, from, caller, amount)
77 if err != nil {
78 panic(err)
79 }
80
81 chain.Emit(
82 "CollectProtocolFeeReward",
83 "prevAddr", prev.Address().String(),
84 "prevRealm", prev.PkgPath(),
85 "tokenPath", tokenPath,
86 "from", from.String(),
87 "to", caller.String(),
88 "collectedAmount", formatInt(amount),
89 )
90 }
91 }
92}
93
94// CollectRewardFromLaunchPad collects rewards for launchpad project wallets.
95//
96// Parameters:
97// - to: recipient address for rewards
98//
99// Only callable by launchpad contract.
100func (gs *govStakerV1) CollectRewardFromLaunchPad(_ int, rlm realm, to address) {
101 if !rlm.IsCurrent() {
102 panic(errSpoofedRealm)
103 }
104
105 halt.AssertIsNotHaltedWithdraw()
106
107 prev := rlm.Previous()
108 caller := prev.Address()
109 access.AssertIsLaunchpad(caller)
110
111 from := rlm.Address()
112 currentTimestamp := time.Now().Unix()
113
114 launchpadRewardID := gs.makeLaunchpadRewardID(to.String())
115 _, exists := gs.getLaunchpadProjectDeposit(launchpadRewardID)
116 if !exists {
117 panic(makeErrorWithDetails(
118 errNoDelegatedAmount,
119 ufmt.Sprintf("%s is not project wallet from launchpad", to.String()),
120 ))
121 }
122
123 emissionReward, protocolFeeRewards, err := gs.claimRewardsFromLaunchpad(0, rlm, to.String(), currentTimestamp)
124 if err != nil {
125 panic(err)
126 }
127
128 // Transfer emission rewards (GNS tokens) to project wallet if any
129 if emissionReward > 0 {
130 gns.Transfer(cross(rlm), to, emissionReward)
131
132 chain.Emit(
133 "CollectEmissionFromLaunchPad",
134 "prevAddr", prev.Address().String(),
135 "prevRealm", prev.PkgPath(),
136 "from", from.String(),
137 "to", to.String(),
138 "emissionRewardAmount", formatInt(emissionReward),
139 )
140 }
141
142 // Transfer protocol fee rewards to project wallet for each token type
143 for tokenPath, amount := range protocolFeeRewards {
144 if amount > 0 {
145 err := transferToken(0, rlm, tokenPath, from, to, amount)
146 if err != nil {
147 panic(err)
148 }
149
150 chain.Emit(
151 "CollectProtocolFeeFromLaunchPad",
152 "prevAddr", prev.Address().String(),
153 "prevRealm", prev.PkgPath(),
154 "tokenPath", tokenPath,
155 "from", from.String(),
156 "to", to.String(),
157 "collectedAmount", formatInt(amount),
158 )
159 }
160 }
161}
162
163// SetAmountByProjectWallet sets the amount of reward for the project wallet.
164// This function is exclusively callable by the launchpad contract to manage
165// xGNS balances for project wallets that participate in launchpad offerings.
166//
167// The function handles both adding and removing stakes:
168// - When adding: mints xGNS to launchpad address and starts reward accumulation
169// - When removing: burns xGNS from launchpad address and stops reward accumulation
170// Adjusts stake amount for project wallet address.
171// Panics:
172// - if caller is not the launchpad contract
173// - if system is halted for withdrawals
174// - if access control operations fail
175func (gs *govStakerV1) SetAmountByProjectWallet(_ int, rlm realm, addr address, amount int64, add bool) {
176 if !rlm.IsCurrent() {
177 panic(errSpoofedRealm)
178 }
179
180 if add {
181 halt.AssertIsNotHaltedGovStaker()
182 } else {
183 halt.AssertIsNotHaltedWithdraw()
184 }
185
186 prev := rlm.Previous()
187 caller := prev.Address()
188 currentTimestamp := time.Now().Unix()
189
190 access.AssertIsLaunchpad(caller)
191
192 launchpadAddr := access.MustGetAddress(prbac.ROLE_LAUNCHPAD.String())
193
194 if add {
195 // Add stake for the project wallet and mint xGNS to launchpad
196 err := gs.addStakeFromLaunchpad(0, rlm, addr.String(), amount, currentTimestamp)
197 if err != nil {
198 panic(err)
199 }
200
201 xgns.Mint(cross(rlm), launchpadAddr, amount)
202 } else {
203 // Remove stake for the project wallet and burn xGNS from launchpad
204 err := gs.removeStakeFromLaunchpad(0, rlm, addr.String(), amount, currentTimestamp)
205 if err != nil {
206 panic(err)
207 }
208
209 xgns.Burn(cross(rlm), launchpadAddr, amount)
210 }
211}
212
213// claimRewards claims both emission and protocol fee rewards.
214// Coordinates claiming process for both reward types.
215func (gs *govStakerV1) claimRewards(_ int, rlm realm, rewardID string, currentTimestamp int64) (int64, map[string]int64, error) {
216 emissionReward, err := gs.claimRewardsEmissionReward(0, rlm, rewardID, currentTimestamp)
217 if err != nil {
218 return 0, nil, err
219 }
220
221 protocolFeeRewards, err := gs.claimRewardsProtocolFeeReward(0, rlm, rewardID, currentTimestamp)
222 if err != nil {
223 return 0, nil, err
224 }
225
226 return emissionReward, protocolFeeRewards, nil
227}
228
229// claimRewardsFromLaunchpad claims rewards for launchpad project wallets.
230// Uses special reward ID format for launchpad integration.
231func (gs *govStakerV1) claimRewardsFromLaunchpad(_ int, rlm realm, address string, currentTimestamp int64) (int64, map[string]int64, error) {
232 launchpadRewardID := gs.makeLaunchpadRewardID(address)
233
234 return gs.claimRewards(0, rlm, launchpadRewardID, currentTimestamp)
235}
236
237// transferToken transfers tokens from the staker contract to a recipient address.
238// transferToken handles token transfers for reward distribution.
239//
240// Non-crossing helper: takes `_ int, rlm realm` so the realm token threaded
241// in by the public reward-collection entry points reaches the underlying
242// GRC-20 transfer without forcing each caller to recompute it.
243func transferToken(
244 _ int,
245 rlm realm,
246 tokenPath string,
247 from, to address,
248 amount int64,
249) error {
250 common.MustRegistered(tokenPath)
251
252 // Validate recipient address
253 if !to.IsValid() {
254 return makeErrorWithDetails(
255 errInvalidAddress,
256 ufmt.Sprintf("invalid address %s to transfer protocol fee", to.String()),
257 )
258 }
259
260 // Validate transfer amount
261 if amount < 0 {
262 return makeErrorWithDetails(
263 errInvalidAmount,
264 ufmt.Sprintf("invalid amount %d to transfer protocol fee", amount),
265 )
266 }
267
268 // Check sufficient balance
269 balance := common.BalanceOf(tokenPath, from)
270 if balance < amount {
271 return makeErrorWithDetails(
272 errNotEnoughBalance,
273 ufmt.Sprintf("not enough %s balance(%d) to collect(%d)", tokenPath, balance, amount),
274 )
275 }
276
277 common.SafeGRC20Transfer(cross(rlm), tokenPath, to, amount)
278
279 return nil
280}