protocol_fee.gno
11.52 Kb · 372 lines
1package protocol_fee
2
3import (
4 "chain"
5 "errors"
6 "strconv"
7
8 ufmt "gno.land/p/nt/ufmt/v0"
9
10 gnsmath "gno.land/p/gnoswap/gnsmath/v1"
11 prabc "gno.land/p/gnoswap/rbac/v1"
12 "gno.land/r/gnoswap/access/v1"
13 "gno.land/r/gnoswap/common"
14 "gno.land/r/gnoswap/halt/v1"
15)
16
17// DistributeProtocolFee distributes collected protocol fees.
18//
19// Splits fees between devOps and gov/staker based on configured percentages.
20// This function processes all accumulated fees since last distribution.
21//
22// Only callable by admin or gov/staker contract.
23// Note: Default split is 0% devOps, 100% gov/staker.
24//
25// Parameters:
26// - _: cross-call discriminator; callers pass 0
27// - rlm: propagated protocol_fee realm context; it must match the current crossing frame
28func (pf *protocolFeeV1) DistributeProtocolFee(_ int, rlm realm) {
29 access.AssertIsRlmCurrent(0, rlm)
30
31 prev := rlm.Previous()
32 assertIsAdminOrGovStaker(prev.Address())
33
34 if halt.IsHaltedWithdraw() {
35 return
36 }
37
38 protocolFeeAddr := access.MustGetAddress(prabc.ROLE_PROTOCOL_FEE.String())
39 pfs := pf.getProtocolFeeState()
40
41 reservedTokens := pfs.ReservedTokens()
42
43 for _, token := range reservedTokens {
44 pf.distributeProtocolFeeForToken(
45 0,
46 rlm,
47 pfs,
48 protocolFeeAddr,
49 token,
50 )
51 }
52}
53
54// DistributeProtocolFeeByTokenPath distributes collected protocol fees for one token path.
55//
56// Parameters:
57// - _: cross-call discriminator; callers pass 0
58// - rlm: propagated protocol_fee realm context; it must match the current crossing frame
59// - tokenPath: token contract path whose reserved fee is eligible for distribution
60func (pf *protocolFeeV1) DistributeProtocolFeeByTokenPath(_ int, rlm realm, tokenPath string) {
61 access.AssertIsRlmCurrent(0, rlm)
62
63 prev := rlm.Previous()
64 assertIsAdminOrGovStaker(prev.Address())
65
66 if halt.IsHaltedWithdraw() {
67 return
68 }
69
70 protocolFeeAddr := access.MustGetAddress(prabc.ROLE_PROTOCOL_FEE.String())
71 pfs := pf.getProtocolFeeState()
72
73 if !pfs.store.HasReservedToken(tokenPath) {
74 return
75 }
76
77 pf.distributeProtocolFeeForToken(
78 0,
79 rlm,
80 pfs,
81 protocolFeeAddr,
82 tokenPath,
83 )
84}
85
86// distributeProtocolFeeForToken validates and distributes one reserved token's pending fees.
87func (pf *protocolFeeV1) distributeProtocolFeeForToken(_ int, rlm realm, pfs *protocolFeeState, protocolFeeAddr address, tokenPath string) {
88 toDevOpsAmount := gnsmath.SafeSubInt64(
89 pfs.GetAccuTransferToDevOpsByTokenPath(tokenPath),
90 pfs.GetActualDistributedToDevOpsByTokenPath(tokenPath),
91 )
92 toGovStakerAmount := gnsmath.SafeSubInt64(
93 pfs.GetAccuTransferToGovStakerByTokenPath(tokenPath),
94 pfs.GetActualDistributedToGovStakerByTokenPath(tokenPath),
95 )
96
97 amount := gnsmath.SafeAddInt64(toDevOpsAmount, toGovStakerAmount)
98 balance := common.BalanceOf(tokenPath, protocolFeeAddr)
99
100 // amount should be less than or equal to balance
101 if amount > balance {
102 panic(makeErrorWithDetail(
103 errInvalidAmount,
104 ufmt.Sprintf("amount: %d should be less than or equal to balance: %d", amount, balance),
105 ))
106 }
107
108 if err := pfs.removeReservedToken(0, rlm, tokenPath); err != nil {
109 panic(err)
110 }
111
112 if amount <= 0 {
113 return
114 }
115
116 // distributeToDevOps and distributeToGovStaker record history before transferring.
117 if err := pfs.distributeToDevOps(0, rlm, tokenPath, toDevOpsAmount); err != nil {
118 panic(err)
119 }
120 if err := pfs.distributeToGovStaker(0, rlm, tokenPath, toGovStakerAmount); err != nil {
121 panic(err)
122 }
123
124 prev := rlm.Previous()
125
126 chain.Emit(
127 "TransferProtocolFee",
128 "prevAddr", prev.Address().String(),
129 "prevRealm", prev.PkgPath(),
130 "tokenPath", tokenPath,
131 "toDevOpsAmount", strconv.FormatInt(toDevOpsAmount, 10),
132 "toGovStakerAmount", strconv.FormatInt(toGovStakerAmount, 10),
133 "amount", strconv.FormatInt(amount, 10),
134 )
135}
136
137// SetDevOpsPct sets the devOpsPct.
138//
139// Parameters:
140// - _: cross-call discriminator; callers pass 0
141// - rlm: propagated protocol_fee realm context; it must match the current crossing frame
142// - pct: percentage for devOps (0-10000, where 10000 = 100%)
143//
144// Only callable by admin or governance.
145// Note: GovStaker percentage is automatically adjusted to (10000 - devOpsPct).
146func (pf *protocolFeeV1) SetDevOpsPct(_ int, rlm realm, pct int64) {
147 access.AssertIsRlmCurrent(0, rlm)
148
149 halt.AssertIsNotHaltedProtocolFee()
150
151 prev := rlm.Previous()
152 access.AssertIsAdminOrGovernance(prev.Address())
153
154 assertIsValidPercent(pct)
155
156 prevDevOpsPct := pf.getProtocolFeeState().DevOpsPct()
157 prevGovStakerPct := pf.getProtocolFeeState().GovStakerPct()
158
159 newDevOpsPct, err := pf.getProtocolFeeState().setDevOpsPct(0, rlm, pct)
160 if err != nil {
161 panic(err)
162 }
163 newGovStakerPct := pf.getProtocolFeeState().GovStakerPct()
164
165 chain.Emit(
166 "SetDevOpsPct",
167 "prevAddr", prev.Address().String(),
168 "prevRealm", prev.PkgPath(),
169 "newDevOpsPct", strconv.FormatInt(newDevOpsPct, 10),
170 "prevDevOpsPct", strconv.FormatInt(prevDevOpsPct, 10),
171 "newGovStakerPct", strconv.FormatInt(newGovStakerPct, 10),
172 "prevGovStakerPct", strconv.FormatInt(prevGovStakerPct, 10),
173 )
174}
175
176// SetGovStakerPct sets the stakerPct.
177//
178// Parameters:
179// - _: cross-call discriminator; callers pass 0
180// - rlm: propagated protocol_fee realm context; it must match the current crossing frame
181// - pct: percentage for gov/staker (0-10000, where 10000 = 100%)
182//
183// Only callable by admin or governance.
184// Note: DevOps percentage is automatically adjusted to (10000 - govStakerPct).
185func (pf *protocolFeeV1) SetGovStakerPct(_ int, rlm realm, pct int64) {
186 access.AssertIsRlmCurrent(0, rlm)
187
188 halt.AssertIsNotHaltedProtocolFee()
189
190 prev := rlm.Previous()
191 access.AssertIsAdminOrGovernance(prev.Address())
192
193 assertIsValidPercent(pct)
194
195 prevDevOpsPct := pf.getProtocolFeeState().DevOpsPct()
196 prevGovStakerPct := pf.getProtocolFeeState().GovStakerPct()
197
198 newGovStakerPct, err := pf.getProtocolFeeState().setGovStakerPct(0, rlm, pct)
199 if err != nil {
200 panic(err)
201 }
202 newDevOpsPct := pf.getProtocolFeeState().DevOpsPct()
203
204 chain.Emit(
205 "SetGovStakerPct",
206 "prevAddr", prev.Address().String(),
207 "prevRealm", prev.PkgPath(),
208 "newDevOpsPct", strconv.FormatInt(newDevOpsPct, 10),
209 "prevDevOpsPct", strconv.FormatInt(prevDevOpsPct, 10),
210 "newGovStakerPct", strconv.FormatInt(newGovStakerPct, 10),
211 "prevGovStakerPct", strconv.FormatInt(prevGovStakerPct, 10),
212 )
213}
214
215// AddToProtocolFee pulls the approved amount into protocol fee accounting.
216//
217// Parameters:
218// - _: cross-call discriminator; callers pass 0
219// - rlm: propagated protocol_fee realm context; a non-current value returns errSpoofedRealm
220// - tokenPath: token contract path from which the approved fee is pulled
221// - amount: non-negative fee amount in token base units; zero returns nil without transferring
222//
223// Returns:
224// - err: nil on success; errSpoofedRealm for a spoofed-realm call; errProtocolFeeHalted when protocol-fee collection is halted
225//
226// Only callable by pool, position, router or staker contracts.
227// Caller must approve the protocol fee realm for at least amount before calling.
228// Note: Accumulated fees are distributed when DistributeProtocolFee is called.
229func (pf *protocolFeeV1) AddToProtocolFee(_ int, rlm realm, tokenPath string, amount int64) error {
230 if !rlm.IsCurrent() {
231 return errors.New(errSpoofedRealm)
232 }
233
234 if halt.IsHaltedProtocolFee() {
235 return errors.New(errProtocolFeeHalted)
236 }
237
238 prev := rlm.Previous()
239 caller := prev.Address()
240 assertIsPoolOrPositionOrRouterOrStaker(caller)
241
242 if amount < 0 {
243 panic(makeErrorWithDetail(
244 errInvalidAmount,
245 ufmt.Sprintf("amount(%d) should not be negative", amount),
246 ))
247 }
248
249 if amount == 0 {
250 return nil
251 }
252
253 pf.reserveCollectedProtocolFee(0, rlm, tokenPath, amount)
254 protocolFeeAddr := access.MustGetAddress(prabc.ROLE_PROTOCOL_FEE.String())
255 common.SafeGRC20TransferFrom(0, rlm, tokenPath, caller, protocolFeeAddr, amount)
256
257 chain.Emit(
258 "AddToProtocolFee",
259 "prevAddr", caller.String(),
260 "prevRealm", prev.PkgPath(),
261 "tokenPath", tokenPath,
262 "amount", strconv.FormatInt(amount, 10),
263 )
264
265 return nil
266}
267
268func (pf *protocolFeeV1) reserveCollectedProtocolFee(_ int, rlm realm, tokenPath string, amount int64) {
269 pfs := pf.getProtocolFeeState()
270
271 toDevOpsAmount := gnsmath.SafeMulDivInt64(amount, pfs.DevOpsPct(), 10000)
272 toGovStakerAmount := gnsmath.SafeSubInt64(amount, toDevOpsAmount)
273
274 if err := pfs.addAccuToDevOps(0, rlm, tokenPath, toDevOpsAmount); err != nil {
275 panic(err)
276 }
277 if err := pfs.addAccuToGovStaker(0, rlm, tokenPath, toGovStakerAmount); err != nil {
278 panic(err)
279 }
280
281 if err := pfs.store.AddReservedToken(0, rlm, tokenPath); err != nil {
282 panic(err)
283 }
284
285 // Bucket the gov/staker share under the accrual epoch in force so that gov/staker
286 // can later fold it against the stake distribution that was live when it arrived.
287 // A split that leaves gov/staker nothing moves no accumulator, so it is not bucketed.
288 if toGovStakerAmount > 0 {
289 epoch := pfs.store.GetAccrualEpoch()
290 if err := pfs.store.AddAccrualBucket(0, rlm, tokenPath, epoch, toGovStakerAmount); err != nil {
291 panic(err)
292 }
293 if err := pfs.store.AddAccrualPendingToken(0, rlm, tokenPath); err != nil {
294 panic(err)
295 }
296 }
297}
298
299// AdvanceAccrualEpoch closes the accrual epoch in force and returns the new one.
300//
301// gov/staker calls it on every stake change, before it records the new stake
302// distribution, so every fee bucketed from now on is attributed to that distribution.
303// Restricted to gov/staker: an epoch advanced by anyone else would fragment the buckets
304// without a matching stake record on the gov/staker side.
305//
306// Parameters:
307// - _: cross-call discriminator; callers pass 0
308// - rlm: propagated protocol_fee realm context; it must be current and be called by GovStaker
309//
310// Returns:
311// - epoch: newly stored non-negative accrual epoch for fees collected after the stake change
312func (pf *protocolFeeV1) AdvanceAccrualEpoch(_ int, rlm realm) int64 {
313 if !rlm.IsCurrent() {
314 panic(errors.New(errSpoofedRealm))
315 }
316
317 prev := rlm.Previous()
318 access.AssertIsGovStaker(prev.Address())
319
320 pfs := pf.getProtocolFeeState()
321 epoch := gnsmath.SafeAddInt64(pfs.store.GetAccrualEpoch(), 1)
322 if err := pfs.store.SetAccrualEpoch(0, rlm, epoch); err != nil {
323 panic(err)
324 }
325
326 return epoch
327}
328
329// ConsumeAccrualBuckets returns and clears up to limit of the oldest pending buckets of
330// tokenPath, as parallel epoch and amount slices in epoch order. A limit of zero or
331// less consumes every pending bucket. Once no bucket remains the token leaves the
332// pending set.
333//
334// Restricted to gov/staker: clearing a bucket without folding it into an accumulator
335// would strand that fee share.
336//
337// Parameters:
338// - _: cross-call discriminator; callers pass 0
339// - rlm: propagated protocol_fee realm context; it must be current and be called by GovStaker
340// - tokenPath: token contract path whose pending accrual buckets are consumed
341// - limit: maximum number of oldest buckets to consume; zero or negative consumes all pending buckets
342//
343// Returns:
344// - epochs: consumed accrual epoch numbers in ascending epoch order
345// - amounts: fee amounts corresponding positionally to epochs, in token base units
346func (pf *protocolFeeV1) ConsumeAccrualBuckets(_ int, rlm realm, tokenPath string, limit int) ([]int64, []int64) {
347 if !rlm.IsCurrent() {
348 panic(errors.New(errSpoofedRealm))
349 }
350
351 prev := rlm.Previous()
352 access.AssertIsGovStaker(prev.Address())
353
354 pfs := pf.getProtocolFeeState()
355 epochs, amounts := pfs.store.GetAccrualBuckets(tokenPath, limit)
356 if len(epochs) == 0 {
357 return epochs, amounts
358 }
359
360 if err := pfs.store.RemoveAccrualBuckets(0, rlm, tokenPath, epochs); err != nil {
361 panic(err)
362 }
363
364 remainingEpochs, _ := pfs.store.GetAccrualBuckets(tokenPath, 1)
365 if len(remainingEpochs) == 0 {
366 if err := pfs.store.RemoveAccrualPendingToken(0, rlm, tokenPath); err != nil {
367 panic(err)
368 }
369 }
370
371 return epochs, amounts
372}