Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ handling for NIBI via WNIBI.
- [#2379](https://github.com/NibiruChain/nibiru/pull/2379) - fix(evm): disallow permissionless creation of FunToken mappings when tokens do not already have metadata.
- [#2381](https://github.com/NibiruChain/nibiru/pull/2381) - feat(evm): Overwrite
ERC20 metadata for stNIBI on Nibiru Testnet 2, and make the contract upgradeable.
- [#2384](https://github.com/NibiruChain/nibiru/pull/2384) - feat: Multi VM Gas Token Flexibility

### Dependencies
- Bump `base-x` from 3.0.10 to 3.0.11 ([#2355](https://github.com/NibiruChain/nibiru/pull/2355))
Expand Down
2 changes: 1 addition & 1 deletion app/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func NewAnteHandlerNonEVM(
authante.NewConsumeGasForTxSizeDecorator(opts.AccountKeeper),
// TODO: spike(security): Does minimum gas price of 0 pose a risk?
// ticket: https://github.com/NibiruChain/nibiru/issues/1916
authante.NewDeductFeeDecorator(opts.AccountKeeper, opts.BankKeeper, opts.FeegrantKeeper, opts.TxFeeChecker),
ante.NewDeductFeeDecorator(opts.AccountKeeper, opts.EvmKeeper, opts.BankKeeper, opts.FeegrantKeeper, opts.TxFeeChecker),
// ----------- Ante Handlers: devgas
devgasante.NewDevGasPayoutDecorator(opts.DevGasBankKeeper, opts.DevGasKeeper),
// ----------- Ante Handlers: Keys and signatures
Expand Down
290 changes: 290 additions & 0 deletions app/ante/deductfee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
package ante

import (
"fmt"
"math"

sdkioerrors "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
"github.com/cosmos/cosmos-sdk/x/auth/types"
gethcommon "github.com/ethereum/go-ethereum/common"

"github.com/NibiruChain/nibiru/v2/app/appconst"
"github.com/NibiruChain/nibiru/v2/eth"
"github.com/NibiruChain/nibiru/v2/x/evm"
evmkeeper "github.com/NibiruChain/nibiru/v2/x/evm/keeper"
)
Comment on lines +3 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix imports: use authtypes and banktypes (current types.BankKeeper won’t compile)

You're importing x/auth/types as "types" and later using types.BankKeeper, which doesn't exist in that package. Use explicit aliases and import bank types.

Apply:

@@
-import (
+import (
 	"fmt"
 	"math"
 
 	sdkioerrors "cosmossdk.io/errors"
 	sdkmath "cosmossdk.io/math"
 	sdk "github.com/cosmos/cosmos-sdk/types"
 	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
 	authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
 	authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
-	"github.com/cosmos/cosmos-sdk/x/auth/types"
+	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
+	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
 	gethcommon "github.com/ethereum/go-ethereum/common"
 
 	"github.com/NibiruChain/nibiru/v2/app/appconst"
 	"github.com/NibiruChain/nibiru/v2/eth"
 	"github.com/NibiruChain/nibiru/v2/x/evm"
 	evmkeeper "github.com/NibiruChain/nibiru/v2/x/evm/keeper"
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import (
"fmt"
"math"
sdkioerrors "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
"github.com/cosmos/cosmos-sdk/x/auth/types"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/NibiruChain/nibiru/v2/app/appconst"
"github.com/NibiruChain/nibiru/v2/eth"
"github.com/NibiruChain/nibiru/v2/x/evm"
evmkeeper "github.com/NibiruChain/nibiru/v2/x/evm/keeper"
)
import (
"fmt"
"math"
sdkioerrors "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/NibiruChain/nibiru/v2/app/appconst"
"github.com/NibiruChain/nibiru/v2/eth"
"github.com/NibiruChain/nibiru/v2/x/evm"
evmkeeper "github.com/NibiruChain/nibiru/v2/x/evm/keeper"
)
🤖 Prompt for AI Agents
In app/ante/deductfee.go around lines 3 to 20, the import alias "types" for
x/auth is incorrect because types.BankKeeper does not exist there; replace the
imports to explicitly alias x/auth/types as authtypes and import x/bank/types as
banktypes (or similar names used elsewhere), then update any usages from
types.BankKeeper to banktypes.BankKeeper and any auth-specific types to
authtypes to match the correct packages so the file compiles.


var (
_ sdk.AnteDecorator = DeductFeeDecorator{}
)

// DeductFeeDecorator deducts fees from the fee payer. The fee payer is the fee granter (if specified) or first signer of the tx.
// If the fee payer does not have the funds to pay for the fees, return an InsufficientFunds error.
// Call next AnteHandler if fees successfully deducted.
// CONTRACT: Tx must implement FeeTx interface to use DeductFeeDecorator
type DeductFeeDecorator struct {
accountKeeper authkeeper.AccountKeeper
evmkeeper *evmkeeper.Keeper
bankKeeper types.BankKeeper
feegrantKeeper FeegrantKeeper
txFeeChecker authante.TxFeeChecker
}
Comment on lines +31 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use the correct BankKeeper interface type

Update the field to the bank module’s interface; current type reference is invalid.

 type DeductFeeDecorator struct {
 	accountKeeper  authkeeper.AccountKeeper
 	evmkeeper      *evmkeeper.Keeper
-	bankKeeper     types.BankKeeper
+	bankKeeper     banktypes.BankKeeper
 	feegrantKeeper FeegrantKeeper
 	txFeeChecker   authante.TxFeeChecker
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
accountKeeper authkeeper.AccountKeeper
evmkeeper *evmkeeper.Keeper
bankKeeper types.BankKeeper
feegrantKeeper FeegrantKeeper
txFeeChecker authante.TxFeeChecker
}
type DeductFeeDecorator struct {
accountKeeper authkeeper.AccountKeeper
evmkeeper *evmkeeper.Keeper
bankKeeper banktypes.BankKeeper
feegrantKeeper FeegrantKeeper
txFeeChecker authante.TxFeeChecker
}
🤖 Prompt for AI Agents
In app/ante/deductfee.go around lines 31 to 36, the bankKeeper field is
currently typed as types.BankKeeper which is not the bank module's keeper
interface; change the field type to the bank module keeper (e.g.
bankkeeper.Keeper or the bank keeper interface exported by x/bank) and update
imports accordingly so the field uses the correct bank keeper interface from the
bank module package.


func NewDeductFeeDecorator(ak authkeeper.AccountKeeper, ek *evmkeeper.Keeper, bk types.BankKeeper, fk FeegrantKeeper, tfc authante.TxFeeChecker) DeductFeeDecorator {
if tfc == nil {
tfc = checkTxFeeWithValidatorMinGasPrices
}

return DeductFeeDecorator{
accountKeeper: ak,
evmkeeper: ek,
bankKeeper: bk,
feegrantKeeper: fk,
txFeeChecker: tfc,
}
}

func (dfd DeductFeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
feeTx, ok := tx.(sdk.FeeTx)
if !ok {
return ctx, sdkioerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx")
}

if !simulate && ctx.BlockHeight() > 0 && feeTx.GetGas() == 0 {
return ctx, sdkioerrors.Wrap(sdkerrors.ErrInvalidGasLimit, "must provide positive gas")
}

if ctx.BlockHeight() == 0 {
return next(ctx, tx, simulate)
}

pausedGasMeter := ctx.GasMeter()
ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter())

var (
priority int64
err error
)

fee := feeTx.GetFee()
if !simulate {
fee, priority, err = dfd.txFeeChecker(ctx, tx)
if err != nil {
return ctx, err
}
}
if err := dfd.checkDeductFee(ctx, tx, fee); err != nil {
return ctx, err
}

// TODO: print gas consumption values to verify this works as expected

newCtx := ctx.WithPriority(priority).WithGasMeter(pausedGasMeter)
fmt.Printf("newCtx.GasMeter().GasConsumed(): %v\n", newCtx.GasMeter().GasConsumed())

return next(newCtx, tx, simulate)
}

func (dfd DeductFeeDecorator) checkDeductFee(ctx sdk.Context, sdkTx sdk.Tx, fee sdk.Coins) error {
feeTx, ok := sdkTx.(sdk.FeeTx)
if !ok {
return sdkioerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx")
}

if addr := dfd.accountKeeper.GetModuleAddress(types.FeeCollectorName); addr == nil {
return fmt.Errorf("fee collector module account (%s) has not been set", types.FeeCollectorName)
}
Comment on lines +99 to +101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use authtypes.FeeCollectorName after alias change

Replace references to types.FeeCollectorName accordingly.

-	if addr := dfd.accountKeeper.GetModuleAddress(types.FeeCollectorName); addr == nil {
+	if addr := dfd.accountKeeper.GetModuleAddress(authtypes.FeeCollectorName); addr == nil {
 		return fmt.Errorf("fee collector module account (%s) has not been set", types.FeeCollectorName)
 	}
@@
-		err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), types.FeeCollectorName, fees)
+		err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), authtypes.FeeCollectorName, fees)
@@
-	feeCollector := eth.NibiruAddrToEthAddr(accountKeeper.GetModuleAddress(types.FeeCollectorName))
+	feeCollector := eth.NibiruAddrToEthAddr(accountKeeper.GetModuleAddress(authtypes.FeeCollectorName))
@@
-			ctx, stateDB, wnibi, sender, accountKeeper.GetModuleAddress(types.FeeCollectorName),
+			ctx, stateDB, wnibi, sender, accountKeeper.GetModuleAddress(authtypes.FeeCollectorName),

Also applies to: 158-160, 203-205, 213-216

🤖 Prompt for AI Agents
In app/ante/deductfee.go around lines 93-95 (and similarly at 158-160, 203-205,
213-216), replace usages of types.FeeCollectorName with
authtypes.FeeCollectorName and update imports to include the auth types package
aliased as authtypes (e.g. import authtypes
"github.com/cosmos/cosmos-sdk/x/auth/types"); ensure any existing types import
isn’t clashing and adjust references accordingly so all FeeCollectorName usages
point to authtypes.FeeCollectorName.


feePayer := feeTx.FeePayer()
feeGranter := feeTx.FeeGranter()
deductFeesFrom := feePayer

// if feegranter set deduct fee from feegranter account.
// this works with only when feegrant enabled.
if feeGranter != nil {
if dfd.feegrantKeeper == nil {
return sdkerrors.ErrInvalidRequest.Wrap("fee grants are not enabled")
} else if !feeGranter.Equals(feePayer) {
err := dfd.feegrantKeeper.UseGrantedFees(ctx, feeGranter, feePayer, fee, sdkTx.GetMsgs())
if err != nil {
return sdkioerrors.Wrapf(err, "%s does not allow to pay fees for %s", feeGranter, feePayer)
}
}

deductFeesFrom = feeGranter
}

deductFeesFromAcc := dfd.accountKeeper.GetAccount(ctx, deductFeesFrom)
if deductFeesFromAcc == nil {
return sdkerrors.ErrUnknownAddress.Wrapf("fee payer address: %s does not exist", deductFeesFrom)
}

// deduct the fees
if !fee.IsZero() {
err := DeductFees(dfd.accountKeeper, dfd.evmkeeper, dfd.bankKeeper, ctx, deductFeesFromAcc, fee)
if err != nil {
return err
}
}

events := sdk.Events{
sdk.NewEvent(
sdk.EventTypeTx,
sdk.NewAttribute(sdk.AttributeKeyFee, fee.String()),
sdk.NewAttribute(sdk.AttributeKeyFeePayer, deductFeesFrom.String()),
),
}
ctx.EventManager().EmitEvents(events)

return nil
}

// DeductFees deducts fees from the given account.
func DeductFees(accountKeeper authante.AccountKeeper, evmKeeper *evmkeeper.Keeper, bankKeeper types.BankKeeper, ctx sdk.Context, acc types.AccountI, fees sdk.Coins) error {
if !fees.IsValid() {
return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees)
}

Comment on lines +147 to +152
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Correct signatures and validate single‑denom fees

Make types explicit (banktypes.BankKeeper, authtypes.AccountI) and enforce exactly one fee denom (BondDenom) to avoid indexing fees[0] on multi‑denom inputs.

-// DeductFees deducts fees from the given account.
-func DeductFees(accountKeeper authante.AccountKeeper, evmKeeper *evmkeeper.Keeper, bankKeeper types.BankKeeper, ctx sdk.Context, acc types.AccountI, fees sdk.Coins) error {
+// DeductFees deducts fees from the given account.
+func DeductFees(accountKeeper authante.AccountKeeper, evmKeeper *evmkeeper.Keeper, bankKeeper banktypes.BankKeeper, ctx sdk.Context, acc authtypes.AccountI, fees sdk.Coins) error {
 	if !fees.IsValid() {
 		return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees)
 	}
+	if fees.Len() != 1 {
+		return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "exactly 1 fee denom (%s) supported; got %s", appconst.BondDenom, fees.String())
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// DeductFees deducts fees from the given account.
func DeductFees(accountKeeper authante.AccountKeeper, evmKeeper *evmkeeper.Keeper, bankKeeper types.BankKeeper, ctx sdk.Context, acc types.AccountI, fees sdk.Coins) error {
if !fees.IsValid() {
return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees)
}
// DeductFees deducts fees from the given account.
func DeductFees(
accountKeeper authante.AccountKeeper,
evmKeeper *evmkeeper.Keeper,
bankKeeper banktypes.BankKeeper,
ctx sdk.Context,
acc authtypes.AccountI,
fees sdk.Coins,
) error {
if !fees.IsValid() {
return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees)
}
if fees.Len() != 1 {
return sdkioerrors.Wrapf(
sdkerrors.ErrInsufficientFee,
"exactly 1 fee denom (%s) supported; got %s",
appconst.BondDenom,
fees.String(),
)
}
// ... rest of existing logic ...
}
🤖 Prompt for AI Agents
In app/ante/deductfee.go around lines 141-146, update the DeductFees signature
to use concrete types banktypes.BankKeeper and authtypes.AccountI (replace
types.BankKeeper and types.AccountI), and add validation to ensure fees contains
exactly one coin and that coin.Denom equals the chain BondDenom before indexing
fees[0]; if not, return an appropriate wrapped sdkerror (e.g.
ErrInvalidCoins/ErrInsufficientFee). After validating single-denom and denom
equality, proceed to extract the coin and continue existing fee deduction logic.
Ensure necessary imports (banktypes, authtypes, and the BondDenom constant
source) are added.

gasMeterBefore := ctx.GasMeter()
gasConsumedBefore := gasMeterBefore.GasConsumed()
baseOpGasConsumed := uint64(0)

defer func() {
// NOTE: we have to refund the entire gasMeterBefore because it's modified by AfterOp
// stateDB.getStateObject() reads from state using the local root ctx which affects the gas meter
gasMeterBefore.RefundGas(gasMeterBefore.GasConsumed(), "")
gasMeterBefore.ConsumeGas(gasConsumedBefore+baseOpGasConsumed, "DeductFeeDecorator invariant")
}()
Comment on lines +153 to +162
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Gas refund math double‑counts; overcharges gas

You snapshot gas twice (gasConsumedBefore + baseOpGasConsumed where baseOp already includes the former). Preserve the pre‑fallback consumption only and ignore EVM fallback gas as intended.

-	gasMeterBefore := ctx.GasMeter()
-	gasConsumedBefore := gasMeterBefore.GasConsumed()
-	baseOpGasConsumed := uint64(0)
+	gasMeterBefore := ctx.GasMeter()
+	preConsumed := gasMeterBefore.GasConsumed()
 
 	defer func() {
-		// NOTE: we have to refund the entire gasMeterBefore because it's modified by AfterOp
-		// stateDB.getStateObject() reads from state using the local root ctx which affects the gas meter
-		gasMeterBefore.RefundGas(gasMeterBefore.GasConsumed(), "")
-		gasMeterBefore.ConsumeGas(gasConsumedBefore+baseOpGasConsumed, "DeductFeeDecorator invariant")
+		// Reset to the pre‑fallback consumption; EVM reads should not affect gas here.
+		gasMeterBefore.RefundGas(gasMeterBefore.GasConsumed(), "")
+		gasMeterBefore.ConsumeGas(preConsumed, "DeductFeeDecorator invariant")
 	}()
@@
-		baseOpGasConsumed = ctx.GasMeter().GasConsumed()
-		ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
+		ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter())

Also applies to: 164-166

🤖 Prompt for AI Agents
In app/ante/deductfee.go around lines 147-156, the defer currently refunds then
reconsumes gas using gasConsumedBefore+baseOpGasConsumed which double-counts gas
(baseOpGasConsumed already includes the pre-fallback consumption); change the
logic to refund the entire current meter and then only consume the original
pre-fallback consumption (gasConsumedBefore) so EVM fallback gas is ignored as
intended; make the same correction at the other occurrence around lines 164-166.


if fees[0].Denom == appconst.BondDenom {
err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), types.FeeCollectorName, fees)
if err == nil {
return nil
}

baseOpGasConsumed = ctx.GasMeter().GasConsumed()
ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter())

// fallback to WNIBI
err = DeductFeesWithWNIBI(ctx, accountKeeper, evmKeeper, acc, fees)
if err == nil {
return nil
}

return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "insufficient balance across supported gas tokens to cover %s", fees[0].Amount)
} else {
return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "fee denom must be %s, got %s", appconst.BondDenom, fees[0].Denom)
}
}

// DeductFeesWithWNIBI tries to deduct fees from WNIBI balance if native deduction fails.
Comment on lines +147 to +185
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate single-denom fees before indexing

Both deduction paths assume fees[0] exists, so multi-denom or empty inputs panic or misclassify valid fees. Enforce exactly one coin before touching fees[0].

 	if !fees.IsValid() {
 		return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees)
 	}
+	if fees.Len() != 1 {
+		return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "exactly one fee denom (%s) is supported; got %s", appconst.BondDenom, fees)
+	}
@@
 func DeductFeesWithWNIBI(
 	ctx sdk.Context,
 	accountKeeper authante.AccountKeeper,
 	evmKeeper *evmkeeper.Keeper,
-	acc types.AccountI,
+	acc authtypes.AccountI,
 	fees sdk.Coins,
 ) error {
+	if fees.Len() != 1 {
+		return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "exactly one fee denom (%s) is supported; got %s", appconst.BondDenom, fees)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// DeductFees deducts fees from the given account.
func DeductFees(accountKeeper authante.AccountKeeper, evmKeeper *evmkeeper.Keeper, bankKeeper types.BankKeeper, ctx sdk.Context, acc types.AccountI, fees sdk.Coins) error {
if !fees.IsValid() {
return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees)
}
gasMeterBefore := ctx.GasMeter()
gasConsumedBefore := gasMeterBefore.GasConsumed()
baseOpGasConsumed := uint64(0)
defer func() {
// NOTE: we have to refund the entire gasMeterBefore because it's modified by AfterOp
// stateDB.getStateObject() reads from state using the local root ctx which affects the gas meter
gasMeterBefore.RefundGas(gasMeterBefore.GasConsumed(), "")
gasMeterBefore.ConsumeGas(gasConsumedBefore+baseOpGasConsumed, "DeductFeeDecorator invariant")
}()
if fees[0].Denom == appconst.BondDenom {
err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), types.FeeCollectorName, fees)
if err == nil {
return nil
}
baseOpGasConsumed = ctx.GasMeter().GasConsumed()
ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
// fallback to WNIBI
err = DeductFeesWithWNIBI(ctx, accountKeeper, evmKeeper, acc, fees)
if err == nil {
return nil
}
return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "insufficient balance across supported gas tokens to cover %s", fees[0].Amount)
} else {
return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "fee denom must be %s, got %s", appconst.BondDenom, fees[0].Denom)
}
}
// DeductFeesWithWNIBI tries to deduct fees from WNIBI balance if native deduction fails.
// DeductFees deducts fees from the given account.
func DeductFees(
accountKeeper authante.AccountKeeper,
evmKeeper *evmkeeper.Keeper,
bankKeeper types.BankKeeper,
ctx sdk.Context,
acc types.AccountI,
fees sdk.Coins,
) error {
if !fees.IsValid() {
return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees)
}
if fees.Len() != 1 {
return sdkioerrors.Wrapf(
sdkerrors.ErrInsufficientFee,
"exactly one fee denom (%s) is supported; got %s",
appconst.BondDenom,
fees,
)
}
gasMeterBefore := ctx.GasMeter()
gasConsumedBefore := gasMeterBefore.GasConsumed()
baseOpGasConsumed := uint64(0)
defer func() {
// NOTE: we have to refund the entire gasMeterBefore because it's modified by AfterOp
// stateDB.getStateObject() reads from state using the local root ctx which affects the gas meter
gasMeterBefore.RefundGas(gasMeterBefore.GasConsumed(), "")
gasMeterBefore.ConsumeGas(gasConsumedBefore+baseOpGasConsumed, "DeductFeeDecorator invariant")
}()
if fees[0].Denom == appconst.BondDenom {
err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), types.FeeCollectorName, fees)
if err == nil {
return nil
}
baseOpGasConsumed = ctx.GasMeter().GasConsumed()
ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
// fallback to WNIBI
err = DeductFeesWithWNIBI(ctx, accountKeeper, evmKeeper, acc, fees)
if err == nil {
return nil
}
return sdkioerrors.Wrapf(
sdkerrors.ErrInsufficientFunds,
"insufficient balance across supported gas tokens to cover %s",
fees[0].Amount,
)
}
return sdkioerrors.Wrapf(
sdkerrors.ErrInsufficientFee,
"fee denom must be %s, got %s",
appconst.BondDenom,
fees[0].Denom,
)
}
// DeductFeesWithWNIBI tries to deduct fees from WNIBI balance if native deduction fails.
func DeductFeesWithWNIBI(
ctx sdk.Context,
accountKeeper authante.AccountKeeper,
evmKeeper *evmkeeper.Keeper,
acc authtypes.AccountI,
fees sdk.Coins,
) error {
if fees.Len() != 1 {
return sdkioerrors.Wrapf(
sdkerrors.ErrInsufficientFee,
"exactly one fee denom (%s) is supported; got %s",
appconst.BondDenom,
fees,
)
}
// ... rest of implementation unchanged
}
🤖 Prompt for AI Agents
In app/ante/deductfee.go around lines 147 to 185, the code indexes fees[0]
without ensuring the coins slice has exactly one entry, which can panic or
misclassify valid fees; add a guard after validating fees to require len(fees)
== 1 (and non-empty) and return a clear sdkerrors.ErrInsufficientFee-wrapped
error if not, so both branches safely use fees[0] only when a single-denom fee
is provided. Ensure the new check runs before any access to fees[0] and keep
existing error types/messages consistent with surrounding logic.

func DeductFeesWithWNIBI(
ctx sdk.Context,
accountKeeper authante.AccountKeeper,
evmKeeper *evmkeeper.Keeper,
acc types.AccountI,
fees sdk.Coins,
) error {
Comment on lines +185 to +192
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Correct DeductFeesWithWNIBI signature (AccountI type)

Use authtypes.AccountI to match the auth module interface after alias changes.

-func DeductFeesWithWNIBI(
+func DeductFeesWithWNIBI(
 	ctx sdk.Context,
-	accountKeeper authante.AccountKeeper,
-	evmKeeper *evmkeeper.Keeper,
-	acc types.AccountI,
+	accountKeeper authante.AccountKeeper,
+	evmKeeper *evmkeeper.Keeper,
+	acc authtypes.AccountI,
 	fees sdk.Coins,
 ) error {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// DeductFeesWithWNIBI tries to deduct fees from WNIBI balance if native deduction fails.
func DeductFeesWithWNIBI(
ctx sdk.Context,
accountKeeper authante.AccountKeeper,
evmKeeper *evmkeeper.Keeper,
acc types.AccountI,
fees sdk.Coins,
) error {
// DeductFeesWithWNIBI tries to deduct fees from WNIBI balance if native deduction fails.
func DeductFeesWithWNIBI(
ctx sdk.Context,
accountKeeper authante.AccountKeeper,
evmKeeper *evmkeeper.Keeper,
acc authtypes.AccountI,
fees sdk.Coins,
) error {
// ...existing implementation...
}
🤖 Prompt for AI Agents
In app/ante/deductfee.go around lines 179 to 186, the DeductFeesWithWNIBI
function parameter uses types.AccountI but the auth module now exposes
authtypes.AccountI; update the function signature to accept authtypes.AccountI,
change any local references accordingly, and add/adjust the import for authtypes
(or update alias) so the file compiles with the new auth alias. Ensure any
callers are updated if necessary to pass authtypes.AccountI (or keep existing
variables but cast to authtypes.AccountI where appropriate).

wnibi := evmKeeper.GetParams(ctx).CanonicalWnibi

stateDB := evmKeeper.Bank.StateDB
if stateDB == nil {
stateDB = evmKeeper.NewStateDB(ctx, evmKeeper.TxConfig(ctx, gethcommon.Hash{}))
}
defer func() {
evmKeeper.Bank.StateDB = nil
}()
Comment on lines +195 to +201
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t clobber EVM bank state after the fallback

This defer always sets Bank.StateDB to nil, wiping any existing shared state. Track the original pointer and restore it just like the verify decorator.

-	stateDB := evmKeeper.Bank.StateDB
-	if stateDB == nil {
-		stateDB = evmKeeper.NewStateDB(ctx, evmKeeper.TxConfig(ctx, gethcommon.Hash{}))
-	}
-	defer func() {
-		evmKeeper.Bank.StateDB = nil
-	}()
+	origStateDB := evmKeeper.Bank.StateDB
+	stateDB := origStateDB
+	if stateDB == nil {
+		stateDB = evmKeeper.NewStateDB(ctx, evmKeeper.TxConfig(ctx, gethcommon.Hash{}))
+	}
+	defer func() {
+		evmKeeper.Bank.StateDB = origStateDB
+	}()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stateDB := evmKeeper.Bank.StateDB
if stateDB == nil {
stateDB = evmKeeper.NewStateDB(ctx, evmKeeper.TxConfig(ctx, gethcommon.Hash{}))
}
defer func() {
evmKeeper.Bank.StateDB = nil
}()
origStateDB := evmKeeper.Bank.StateDB
stateDB := origStateDB
if stateDB == nil {
stateDB = evmKeeper.NewStateDB(ctx, evmKeeper.TxConfig(ctx, gethcommon.Hash{}))
}
defer func() {
evmKeeper.Bank.StateDB = origStateDB
}()
🤖 Prompt for AI Agents
In app/ante/deductfee.go around lines 195 to 201, the defer unconditionally sets
evmKeeper.Bank.StateDB = nil which clobbers any preexisting shared StateDB;
instead capture the original StateDB pointer before the fallback (origStateDB :=
evmKeeper.Bank.StateDB), only set the fallback when nil, and in the defer
restore the original pointer (evmKeeper.Bank.StateDB = origStateDB) so you don't
wipe an existing shared state — follow the same save/restore pattern used in the
verify decorator.


evmObj := evmKeeper.NewEVM(ctx, evm.MOCK_GETH_MESSAGE, evmKeeper.GetEVMConfig(ctx), nil, stateDB)
wnibiBal, err := evmKeeper.ERC20().BalanceOf(wnibi.Address, eth.NibiruAddrToEthAddr(acc.GetAddress()), ctx, evmObj)
if err != nil {
return sdkioerrors.Wrapf(err, "failed to get WNIBI balance for account %s", acc.GetAddress())
}

feeCollector := eth.NibiruAddrToEthAddr(accountKeeper.GetModuleAddress(types.FeeCollectorName))
feesAmount := fees[0].Amount

sender := evm.Addrs{
Bech32: acc.GetAddress(),
Eth: eth.NibiruAddrToEthAddr(acc.GetAddress()),
}
if wnibiBal.Cmp(evm.NativeToWei(feesAmount.BigInt())) >= 0 {
nonce := evmKeeper.GetAccNonce(ctx, sender.Eth)
_, err = evmKeeper.ConvertEvmToCoinForWNIBI(
ctx, stateDB, wnibi, sender, accountKeeper.GetModuleAddress(types.FeeCollectorName),
sdkmath.NewIntFromBigInt(evm.NativeToWei(feesAmount.BigInt())),
nil,
)
if err != nil {
return sdkioerrors.Wrapf(err, "failed to transfer WNIBI from %s to %s", sender.Eth.Hex(), feeCollector.Hex())
}
if err := acc.SetSequence(nonce); err != nil {
return sdkioerrors.Wrapf(err, "failed to set sequence to %d", nonce)
}
accountKeeper.SetAccount(ctx, acc)
return nil
Comment on lines +216 to +230
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Do not set Cosmos account sequence from EVM nonce

Setting acc.SetSequence(nonce) will corrupt the Cosmos sequence for the fee payer (affects signature verification and replay protection). Remove the nonce fetch and sequence mutation.

-	if wnibiBal.Cmp(evm.NativeToWei(feesAmount.BigInt())) >= 0 {
-		nonce := evmKeeper.GetAccNonce(ctx, sender.Eth)
+	if wnibiBal.Cmp(evm.NativeToWei(feesAmount.BigInt())) >= 0 {
 		_, err = evmKeeper.ConvertEvmToCoinForWNIBI(
 			ctx, stateDB, wnibi, sender, accountKeeper.GetModuleAddress(types.FeeCollectorName),
 			sdkmath.NewIntFromBigInt(evm.NativeToWei(feesAmount.BigInt())),
 			nil,
 		)
 		if err != nil {
 			return sdkioerrors.Wrapf(err, "failed to transfer WNIBI from %s to %s", sender.Eth.Hex(), feeCollector.Hex())
 		}
-		if err := acc.SetSequence(nonce); err != nil {
-			return sdkioerrors.Wrapf(err, "failed to set sequence to %d", nonce)
-		}
-		accountKeeper.SetAccount(ctx, acc)
 		return nil
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if wnibiBal.Cmp(evm.NativeToWei(feesAmount.BigInt())) >= 0 {
nonce := evmKeeper.GetAccNonce(ctx, sender.Eth)
_, err = evmKeeper.ConvertEvmToCoinForWNIBI(
ctx, stateDB, wnibi, sender, accountKeeper.GetModuleAddress(types.FeeCollectorName),
sdkmath.NewIntFromBigInt(evm.NativeToWei(feesAmount.BigInt())),
nil,
)
if err != nil {
return sdkioerrors.Wrapf(err, "failed to transfer WNIBI from %s to %s", sender.Eth.Hex(), feeCollector.Hex())
}
if err := acc.SetSequence(nonce); err != nil {
return sdkioerrors.Wrapf(err, "failed to set sequence to %d", nonce)
}
accountKeeper.SetAccount(ctx, acc)
return nil
if wnibiBal.Cmp(evm.NativeToWei(feesAmount.BigInt())) >= 0 {
_, err = evmKeeper.ConvertEvmToCoinForWNIBI(
ctx, stateDB, wnibi, sender, accountKeeper.GetModuleAddress(types.FeeCollectorName),
sdkmath.NewIntFromBigInt(evm.NativeToWei(feesAmount.BigInt())),
nil,
)
if err != nil {
return sdkioerrors.Wrapf(err, "failed to transfer WNIBI from %s to %s", sender.Eth.Hex(), feeCollector.Hex())
}
return nil
}
🤖 Prompt for AI Agents
In app/ante/deductfee.go around lines 210 to 224, remove the logic that fetches
the EVM nonce and mutates the Cosmos account sequence: delete the nonce :=
evmKeeper.GetAccNonce(...) line and the block that calls acc.SetSequence(nonce)
and its error handling. Do not modify the Cosmos account's sequence from EVM
data; just perform the WNIBI transfer, keep accountKeeper.SetAccount(ctx, acc)
if needed, and return nil on success.

}
return sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "insufficient balance across supported gas tokens to cover %s", feesAmount)
}

// checkTxFeeWithValidatorMinGasPrices implements the default fee logic, where the minimum price per
// unit of gas is fixed and set by each validator, can the tx priority is computed from the gas price.
func checkTxFeeWithValidatorMinGasPrices(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, int64, error) {
feeTx, ok := tx.(sdk.FeeTx)
if !ok {
return nil, 0, sdkioerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx")
}

feeCoins := feeTx.GetFee()
gas := feeTx.GetGas()

// Ensure that the provided fees meet a minimum threshold for the validator,
// if this is a CheckTx. This is only for local mempool purposes, and thus
// is only ran on check tx.
if ctx.IsCheckTx() {
minGasPrices := ctx.MinGasPrices()
if !minGasPrices.IsZero() {
requiredFees := make(sdk.Coins, len(minGasPrices))

// Determine the required fees by multiplying each required minimum gas
// price by the gas limit, where fee = ceil(minGasPrice * gasLimit).
glDec := sdkmath.LegacyNewDec(int64(gas))
for i, gp := range minGasPrices {
fee := gp.Amount.Mul(glDec)
requiredFees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt())
}

if !feeCoins.IsAnyGTE(requiredFees) {
return nil, 0, sdkioerrors.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees)
}
}
}

priority := getTxPriority(feeCoins, int64(gas))
return feeCoins, priority, nil
}

// getTxPriority returns a naive tx priority based on the amount of the smallest denomination of the gas price
// provided in a transaction.
// NOTE: This implementation should be used with a great consideration as it opens potential attack vectors
// where txs with multiple coins could not be prioritize as expected.
func getTxPriority(fee sdk.Coins, gas int64) int64 {
var priority int64
for _, c := range fee {
p := int64(math.MaxInt64)
gasPrice := c.Amount.QuoRaw(gas)
if gasPrice.IsInt64() {
p = gasPrice.Int64()
}
if priority == 0 || p < priority {
priority = p
}
}

return priority
}
10 changes: 10 additions & 0 deletions app/ante/expected_keeper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ante

import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

// FeegrantKeeper defines the expected feegrant keeper.
type FeegrantKeeper interface {
UseGrantedFees(ctx sdk.Context, granter, grantee sdk.AccAddress, fee sdk.Coins, msgs []sdk.Msg) error
}
3 changes: 0 additions & 3 deletions app/ante/handler_opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
sdkioerrors "cosmossdk.io/errors"
wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
"github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
sdkante "github.com/cosmos/cosmos-sdk/x/auth/ante"
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
Expand Down Expand Up @@ -53,5 +52,3 @@ func (opts *AnteHandlerOptions) ValidateAndClean() error {
func AnteHandlerError(shortDesc string) error {
return sdkioerrors.Wrapf(sdkerrors.ErrLogic, "%s is required for AnteHandler", shortDesc)
}

type TxFeeChecker func(ctx sdk.Context, feeTx sdk.FeeTx) (sdk.Coins, int64, error)
Loading
Loading