protocol_fee_swap.gno
5.62 Kb · 196 lines
1package router
2
3import (
4 "chain"
5
6 ufmt "gno.land/p/nt/ufmt/v0"
7
8 gnsmath "gno.land/p/gnoswap/gnsmath/v1"
9 prbac "gno.land/p/gnoswap/rbac/v1"
10 u256 "gno.land/p/gnoswap/uint256/v1"
11 "gno.land/p/gnoswap/utils/v1"
12
13 "gno.land/r/gnoswap/access/v1"
14 "gno.land/r/gnoswap/common"
15 "gno.land/r/gnoswap/halt/v1"
16
17 pf "gno.land/r/gnoswap/protocol_fee"
18 _ "gno.land/r/gnoswap/protocol_fee/v1"
19)
20
21// GetSwapFee returns the current router fee rate in basis points.
22//
23// Returns:
24// - fee: configured router fee in basis points
25func (r *routerV1) GetSwapFee() uint64 {
26 return r.store.GetSwapFee()
27}
28
29// SetSwapFee sets the protocol swap fee rate.
30//
31// Fee is deducted from swap output and sent to protocol fee contract.
32// Only callable by admin or governance.
33//
34// Parameters:
35// - _: leading call discriminator; callers pass 0
36// - rlm: propagated current realm context; it must be current for the authorized storage write
37// - fee: new protocol fee rate in basis points, bounded to 0 through 1000
38//
39// Access:
40// - Requires: Admin or Governance role
41// - Halt check: Router and ProtocolFee must not be halted
42//
43// Events:
44// - SetSwapFee: Emits previous and new fee values
45//
46// Reverts if:
47// - Caller is not admin/governance
48// - Fee > 1000 bps (10%)
49// - Router or ProtocolFee is halted
50func (r *routerV1) SetSwapFee(_ int, rlm realm, fee uint64) {
51 access.AssertIsRlmCurrent(0, rlm)
52
53 halt.AssertIsNotHaltedRouter()
54 halt.AssertIsNotHaltedProtocolFee()
55
56 previousRealm := rlm.Previous()
57 caller := previousRealm.Address()
58 access.AssertIsAdminOrGovernance(caller)
59
60 // max swap fee is 1000 (bps)
61 if fee > 1000 {
62 panic(ufmt.Errorf(
63 "%s: fee must be in range 0 to 1000 (10%). got %d",
64 errInvalidSwapFee, fee,
65 ))
66 }
67
68 prevSwapFee := r.store.GetSwapFee()
69 if err := r.store.SetSwapFee(0, rlm, fee); err != nil {
70 panic(err)
71 }
72
73 chain.Emit(
74 "SetSwapFee",
75 "prevAddr", previousRealm.Address().String(),
76 "prevRealm", previousRealm.PkgPath(),
77 "newFee", utils.FormatUint(fee),
78 "prevFee", utils.FormatUint(prevSwapFee),
79 )
80}
81
82// handleSwapFee deducts the protocol fee from the swap amount and transfers it to the protocol fee contract.
83func (r *routerV1) handleSwapFee(
84 _ int,
85 rlm realm,
86 outputToken string,
87 amount int64,
88) int64 {
89 currentTokenPath := outputToken
90
91 swapFee := r.store.GetSwapFee()
92 if swapFee <= 0 {
93 r.settleProtocolFee(0, rlm, currentTokenPath, 0)
94 return amount
95 }
96
97 feeAmountInt64 := calculateRouterFee(amount, swapFee)
98 r.settleProtocolFee(0, rlm, currentTokenPath, feeAmountInt64)
99
100 previousRealm := rlm.Previous()
101 chain.Emit(
102 "SwapRouteFee",
103 "prevAddr", previousRealm.Address().String(),
104 "prevRealm", previousRealm.PkgPath(),
105 "tokenPath", currentTokenPath,
106 "amount", utils.FormatInt(feeAmountInt64),
107 )
108
109 return gnsmath.SafeSubInt64(amount, feeAmountInt64)
110}
111
112// GetPendingProtocolFees returns pending protocol fee amounts keyed by token path.
113//
114// Returns:
115// - fees: token-path to pending amount map held by the router
116func (r *routerV1) GetPendingProtocolFees() map[string]int64 {
117 return r.store.GetPendingProtocolFees()
118}
119
120func (r *routerV1) addPendingProtocolFee(_ int, rlm realm, tokenPath string, amount int64) {
121 if amount <= 0 {
122 return
123 }
124
125 pendingAmount := gnsmath.SafeAddInt64(r.store.GetPendingProtocolFee(tokenPath), amount)
126 if err := r.store.SetPendingProtocolFee(0, rlm, tokenPath, pendingAmount); err != nil {
127 panic(err)
128 }
129}
130
131// settleProtocolFee forwards amount, plus anything already pending for tokenPath, to
132// the protocol fee realm. When collection is halted or the transfer fails, the amount
133// is recorded as pending instead and settled by a later call for the same token, which
134// every fee path makes even when the fee is zero.
135//
136// Only tokenPath is touched, so an unrelated token can never block settlement. The
137// pending entry is written only when the fee cannot be forwarded, so the common path
138// performs no store write at all.
139func (r *routerV1) settleProtocolFee(_ int, rlm realm, tokenPath string, amount int64) {
140 if halt.IsHaltedProtocolFee() {
141 r.addPendingProtocolFee(0, rlm, tokenPath, amount)
142 return
143 }
144
145 pendingAmount := r.store.GetPendingProtocolFee(tokenPath)
146 totalAmount := gnsmath.SafeAddInt64(pendingAmount, amount)
147
148 if totalAmount > 0 {
149 protocolFeeAddr := access.MustGetAddress(prbac.ROLE_PROTOCOL_FEE.String())
150 common.SafeGRC20Approve(0, rlm, tokenPath, protocolFeeAddr, totalAmount)
151
152 // AddToProtocolFee only reports an error for a halted realm, which is handled
153 // above. The branch keeps the amount pending should that ever change, and
154 // withdraws the approval so no unspent allowance outlives the call.
155 if err := pf.AddToProtocolFee(cross(rlm), tokenPath, totalAmount); err != nil {
156 common.SafeGRC20Approve(0, rlm, tokenPath, protocolFeeAddr, 0)
157 r.addPendingProtocolFee(0, rlm, tokenPath, amount)
158 return
159 }
160 }
161
162 if err := r.store.RemovePendingProtocolFee(0, rlm, tokenPath); err != nil {
163 panic(err)
164 }
165}
166
167func calculateRouterFee(amount int64, swapFee uint64) int64 {
168 if swapFee <= 0 {
169 return 0
170 }
171
172 feeAmount := u256.MulDiv(u256.NewUintFromInt64(amount), u256.NewUint(swapFee), u256.NewUint(10000))
173 return gnsmath.SafeConvertToInt64(feeAmount)
174}
175
176// calculate amount to fetch from pool including router fee
177// poolAmount = userAmount / (1 - feeRate)
178// = userAmount * 10000 / (10000 - swapFeeBPS)
179func calculateExactOutWithRouterFee(amount int64, swapFee uint64) int64 {
180 if amount == 0 {
181 return amount
182 }
183
184 if swapFee > 0 {
185 // Use MulDiv to prevent overflow and maintain precision
186 poolAmount := u256.MulDiv(
187 u256.NewUintFromInt64(amount),
188 u256.NewUint(10000),
189 u256.NewUint(10000-swapFee),
190 )
191
192 return gnsmath.SafeConvertToInt64(poolAmount)
193 }
194
195 return amount
196}