Skip to content

Commit 09e58ab

Browse files
feat: custom ante NewDeductFeeDecorator allowing 0 fee for zero gas actors (#2415)
* feat: custom ante NewDeductFeeDecorator allowing 0 fee for zero gas actors * chore: changelog * fix: linter * refactor: moved private code to fee.go * impl last PR comment + cleanup --------- Co-authored-by: Unique Divine <[email protected]>
1 parent 63257f0 commit 09e58ab

File tree

5 files changed

+492
-4
lines changed

5 files changed

+492
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ address when Wasm contract addresses are queried because 32-byte address space
6767
- [#2412](https://github.com/NibiruChain/nibiru/pull/2412) - fix(evm-rpc): remove
6868
unsafe debug API methods.
6969
- [#2413](https://github.com/NibiruChain/nibiru/pull/2413) - refactor(upgrades): simplify upgrade handler code to use less abstractions and combine micro-packages
70+
- [#2415](https://github.com/NibiruChain/nibiru/pull/2415) - feat: custom ante NewDeductFeeDecorator allowing 0 fee for zero gas actors
7071

7172
### Dependencies
7273
- Bump `cipher-base` from 1.0.4 to 1.0.6 ([#2390](https://github.com/NibiruChain/nibiru/pull/2390))

app/ante.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func NewAnteHandlerNonEVM(
6363
authante.NewConsumeGasForTxSizeDecorator(opts.AccountKeeper),
6464
// TODO: spike(security): Does minimum gas price of 0 pose a risk?
6565
// ticket: https://github.com/NibiruChain/nibiru/issues/1916
66-
authante.NewDeductFeeDecorator(opts.AccountKeeper, opts.BankKeeper, opts.FeegrantKeeper, opts.TxFeeChecker),
66+
ante.NewDeductFeeDecorator(opts.AccountKeeper, opts.BankKeeper, opts.FeegrantKeeper, opts.TxFeeChecker),
6767
// ----------- Ante Handlers: devgas
6868
devgasante.NewDevGasPayoutDecorator(opts.DevGasBankKeeper, opts.DevGasKeeper),
6969
// ----------- Ante Handlers: Keys and signatures

app/ante/deduct_fee.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package ante
2+
3+
import (
4+
"fmt"
5+
"math"
6+
7+
sdkmath "cosmossdk.io/math"
8+
9+
sdkioerrors "cosmossdk.io/errors"
10+
sdk "github.com/cosmos/cosmos-sdk/types"
11+
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
12+
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
13+
"github.com/cosmos/cosmos-sdk/x/auth/types"
14+
)
15+
16+
// DeductFeeDecorator deducts fees from the fee payer. The fee payer is the fee
17+
// granter (if specified) or first signer of the tx. If the fee payer does not
18+
// have the funds to pay for the fees, return an InsufficientFunds error.
19+
// Call next AnteHandler if fees successfully deducted.
20+
// CONTRACT: Tx must implement FeeTx interface to use DeductFeeDecorator
21+
type DeductFeeDecorator struct {
22+
accountKeeper authante.AccountKeeper
23+
bankKeeper types.BankKeeper
24+
feegrantKeeper authante.FeegrantKeeper
25+
txFeeChecker authante.TxFeeChecker
26+
}
27+
28+
func NewDeductFeeDecorator(ak authante.AccountKeeper, bk types.BankKeeper, fk authante.FeegrantKeeper, tfc authante.TxFeeChecker) DeductFeeDecorator {
29+
if tfc == nil {
30+
tfc = checkTxFeeWithValidatorMinGasPrices
31+
}
32+
33+
return DeductFeeDecorator{
34+
accountKeeper: ak,
35+
bankKeeper: bk,
36+
feegrantKeeper: fk,
37+
txFeeChecker: tfc,
38+
}
39+
}
40+
41+
func (anteDec DeductFeeDecorator) AnteHandle(
42+
ctx sdk.Context,
43+
tx sdk.Tx,
44+
simulate bool,
45+
next sdk.AnteHandler,
46+
) (sdk.Context, error) {
47+
var (
48+
priority int64
49+
err error
50+
feeTx, ok = tx.(sdk.FeeTx)
51+
)
52+
53+
// Early return in the case of zero gas meter, skipping fee deduction.
54+
if isZeroGasMeter(ctx) {
55+
newCtx := ctx.WithPriority(priority)
56+
return next(newCtx, tx, simulate)
57+
}
58+
59+
if !ok {
60+
return ctx, sdkioerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx")
61+
}
62+
63+
if !simulate && ctx.BlockHeight() > 0 && feeTx.GetGas() == 0 {
64+
return ctx, sdkioerrors.Wrap(sdkerrors.ErrInvalidGasLimit, "must provide positive gas")
65+
}
66+
67+
// Deduct fees
68+
fee := feeTx.GetFee()
69+
if !simulate {
70+
fee, priority, err = anteDec.txFeeChecker(ctx, tx)
71+
if err != nil {
72+
return ctx, err
73+
}
74+
}
75+
if err := anteDec.checkDeductFee(ctx, tx, fee); err != nil {
76+
return ctx, err
77+
}
78+
79+
newCtx := ctx.WithPriority(priority)
80+
return next(newCtx, tx, simulate)
81+
}
82+
83+
func (anteDec DeductFeeDecorator) checkDeductFee(
84+
ctx sdk.Context,
85+
sdkTx sdk.Tx,
86+
fee sdk.Coins,
87+
) error {
88+
feeTx, ok := sdkTx.(sdk.FeeTx)
89+
if !ok {
90+
return sdkioerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx")
91+
}
92+
93+
if addr := anteDec.accountKeeper.GetModuleAddress(types.FeeCollectorName); addr == nil {
94+
return fmt.Errorf("fee collector module account (%s) has not been set", types.FeeCollectorName)
95+
}
96+
97+
feePayer := feeTx.FeePayer()
98+
feeGranter := feeTx.FeeGranter()
99+
deductFeesFrom := feePayer
100+
101+
// if feegranter set deduct fee from feegranter account.
102+
// this works with only when feegrant enabled.
103+
if feeGranter != nil {
104+
if anteDec.feegrantKeeper == nil {
105+
return sdkerrors.ErrInvalidRequest.Wrap("fee grants are not enabled")
106+
} else if !feeGranter.Equals(feePayer) {
107+
err := anteDec.feegrantKeeper.UseGrantedFees(ctx, feeGranter, feePayer, fee, sdkTx.GetMsgs())
108+
if err != nil {
109+
return sdkioerrors.Wrapf(err, "%s does not allow to pay fees for %s", feeGranter, feePayer)
110+
}
111+
}
112+
113+
deductFeesFrom = feeGranter
114+
}
115+
116+
deductFeesFromAcc := anteDec.accountKeeper.GetAccount(ctx, deductFeesFrom)
117+
if deductFeesFromAcc == nil {
118+
return sdkerrors.ErrUnknownAddress.Wrapf("fee payer address: %s does not exist", deductFeesFrom)
119+
}
120+
121+
// deduct the fees
122+
if !fee.IsZero() {
123+
err := deductFees(anteDec.bankKeeper, ctx, deductFeesFromAcc, fee)
124+
if err != nil {
125+
return err
126+
}
127+
}
128+
129+
events := sdk.Events{
130+
sdk.NewEvent(
131+
sdk.EventTypeTx,
132+
sdk.NewAttribute(sdk.AttributeKeyFee, fee.String()),
133+
sdk.NewAttribute(sdk.AttributeKeyFeePayer, deductFeesFrom.String()),
134+
),
135+
}
136+
ctx.EventManager().EmitEvents(events)
137+
138+
return nil
139+
}
140+
141+
// deductFees deducts fees from the given account.
142+
func deductFees(
143+
bankKeeper types.BankKeeper,
144+
ctx sdk.Context,
145+
acc types.AccountI,
146+
fees sdk.Coins,
147+
) error {
148+
if !fees.IsValid() {
149+
return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees)
150+
}
151+
152+
err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), types.FeeCollectorName, fees)
153+
if err != nil {
154+
return sdkioerrors.Wrap(sdkerrors.ErrInsufficientFunds, err.Error())
155+
}
156+
157+
return nil
158+
}
159+
160+
// isZeroGasMeter checks if the context has a zero gas meter set.
161+
// ZeroGas is detected in fixed_gas.go and sets NewFixedGasMeter(ZeroTxGas) to ctx
162+
func isZeroGasMeter(ctx sdk.Context) bool {
163+
gasMeter := ctx.GasMeter()
164+
if _, ok := gasMeter.(*fixedGasMeter); ok {
165+
if gasMeter.GasConsumed() == ZeroTxGas && gasMeter.Limit() == math.MaxUint64 {
166+
return true
167+
}
168+
}
169+
return false
170+
}
171+
172+
// checkTxFeeWithValidatorMinGasPrices implements the default fee logic, where
173+
// the minimum price per unit of gas is fixed and set by each validator, can the
174+
// tx priority is computed from the gas price.
175+
func checkTxFeeWithValidatorMinGasPrices(
176+
ctx sdk.Context,
177+
tx sdk.Tx,
178+
) (sdk.Coins, int64, error) {
179+
feeTx, ok := tx.(sdk.FeeTx)
180+
if !ok {
181+
return nil, 0, sdkioerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx")
182+
}
183+
184+
feeCoins := feeTx.GetFee()
185+
gas := feeTx.GetGas()
186+
187+
// Ensure that the provided fees meet a minimum threshold for the validator,
188+
// if this is a CheckTx. This is only for local mempool purposes, and thus
189+
// is only ran on check tx.
190+
if ctx.IsCheckTx() {
191+
minGasPrices := ctx.MinGasPrices()
192+
if !minGasPrices.IsZero() {
193+
requiredFees := make(sdk.Coins, len(minGasPrices))
194+
195+
// Determine the required fees by multiplying each required minimum gas
196+
// price by the gas limit, where fee = ceil(minGasPrice * gasLimit).
197+
glDec := sdkmath.LegacyNewDec(int64(gas))
198+
for i, gp := range minGasPrices {
199+
fee := gp.Amount.Mul(glDec)
200+
requiredFees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt())
201+
}
202+
203+
if !feeCoins.IsAnyGTE(requiredFees) {
204+
return nil, 0, sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees)
205+
}
206+
}
207+
}
208+
209+
priority := getTxPriority(feeCoins, int64(gas))
210+
return feeCoins, priority, nil
211+
}
212+
213+
// getTxPriority returns a naive tx priority based on the amount of the smallest
214+
// denomination of the gas price provided in a transaction.
215+
// NOTE: This implementation should be used with a great consideration as it
216+
// opens potential attack vectors where txs with multiple coins could not be
217+
// prioritize as expected.
218+
func getTxPriority(fee sdk.Coins, gas int64) int64 {
219+
var priority int64
220+
for _, c := range fee {
221+
p := int64(math.MaxInt64)
222+
gasPrice := c.Amount.QuoRaw(gas)
223+
if gasPrice.IsInt64() {
224+
p = gasPrice.Int64()
225+
}
226+
if priority == 0 || p < priority {
227+
priority = p
228+
}
229+
}
230+
231+
return priority
232+
}

0 commit comments

Comments
 (0)