Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}