Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom denom for gas fees #42

Merged
merged 7 commits into from
Dec 12, 2024
Merged
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
30 changes: 13 additions & 17 deletions app/ante/cosmos/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"

anteutils "github.com/evmos/evmos/v12/app/ante/utils"
)

Expand All @@ -38,6 +39,7 @@ import (
type DeductFeeDecorator struct {
accountKeeper authante.AccountKeeper
bankKeeper BankKeeper
erc20Keeper ERC20Keeper
distributionKeeper anteutils.DistributionKeeper
feegrantKeeper authante.FeegrantKeeper
stakingKeeper anteutils.StakingKeeper
Expand All @@ -48,6 +50,7 @@ type DeductFeeDecorator struct {
func NewDeductFeeDecorator(
ak authante.AccountKeeper,
bk BankKeeper,
ek ERC20Keeper,
dk anteutils.DistributionKeeper,
fk authante.FeegrantKeeper,
sk anteutils.StakingKeeper,
Expand All @@ -60,6 +63,7 @@ func NewDeductFeeDecorator(
return DeductFeeDecorator{
accountKeeper: ak,
bankKeeper: bk,
erc20Keeper: ek,
distributionKeeper: dk,
feegrantKeeper: fk,
stakingKeeper: sk,
Expand Down Expand Up @@ -140,9 +144,15 @@ func (dfd DeductFeeDecorator) deductFee(ctx sdk.Context, sdkTx sdk.Tx, fees sdk.
return errortypes.ErrUnknownAddress.Wrapf("fee payer address: %s does not exist", deductFeesFrom)
}

// deduct the fees
if err := deductFeesFromBalanceOrUnclaimedStakingRewards(ctx, dfd, deductFeesFromAcc, fees); err != nil {
return fmt.Errorf("insufficient funds and failed to claim sufficient staking rewards to pay for fees: %w", err)
// If the account balance is not sufficient, try to withdraw enough staking rewards
err := anteutils.ClaimStakingRewardsIfNecessary(ctx, dfd.bankKeeper, dfd.distributionKeeper, dfd.stakingKeeper, deductFeesFromAcc.GetAddress(), fees)
if err != nil {
return fmt.Errorf("insufficient funds and failed to claim sufficient staking rewards: %w", err)
}

err = anteutils.DeductFees(dfd.bankKeeper, dfd.erc20Keeper, ctx, deductFeesFromAcc, fees)
if err != nil {
return errorsmod.Wrapf(err, "failed to deduct transaction costs from user balance")
}

events := sdk.Events{
Expand All @@ -157,20 +167,6 @@ func (dfd DeductFeeDecorator) deductFee(ctx sdk.Context, sdkTx sdk.Tx, fees sdk.
return nil
}

// deductFeesFromBalanceOrUnclaimedStakingRewards tries to deduct the fees from the account balance.
// If the account balance is not enough, it tries to claim enough staking rewards to cover the fees.
func deductFeesFromBalanceOrUnclaimedStakingRewards(
ctx sdk.Context, dfd DeductFeeDecorator, deductFeesFromAcc authtypes.AccountI, fees sdk.Coins,
) error {
if err := anteutils.ClaimStakingRewardsIfNecessary(
ctx, dfd.bankKeeper, dfd.distributionKeeper, dfd.stakingKeeper, deductFeesFromAcc.GetAddress(), fees,
); err != nil {
return err
}

return authante.DeductFees(dfd.bankKeeper, ctx, deductFeesFromAcc, fees)
}

// checkTxFeeWithValidatorMinGasPrices implements the default fee logic, where the minimum price per
// unit of gas is fixed and set by each validator, and the tx priority is computed from the gas price.
func checkTxFeeWithValidatorMinGasPrices(ctx sdk.Context, feeTx sdk.FeeTx) (sdk.Coins, int64, error) {
Expand Down
195 changes: 193 additions & 2 deletions app/ante/cosmos/fees_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,29 @@ package cosmos_test

import (
"fmt"
"math/big"
"time"

"cosmossdk.io/math"
sdktestutil "github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/cosmos-sdk/x/feegrant"
"github.com/ethereum/go-ethereum/common"

cosmosante "github.com/evmos/evmos/v12/app/ante/cosmos"
"github.com/evmos/evmos/v12/app/ante/evm"
"github.com/evmos/evmos/v12/contracts"
"github.com/evmos/evmos/v12/testutil"
testutiltx "github.com/evmos/evmos/v12/testutil/tx"
"github.com/evmos/evmos/v12/utils"
erc20types "github.com/evmos/evmos/v12/x/erc20/types"
inflationtypes "github.com/evmos/evmos/v12/x/inflation/types"
)

const ibcBase = "ibc/7B2A4F6E798182988D77B6B884919AF617A73503FDAC27C916CD7A69A69013CF"

func (suite *AnteTestSuite) TestDeductFeeDecorator() {
var (
dfd cosmosante.DeductFeeDecorator
Expand All @@ -23,6 +34,7 @@ func (suite *AnteTestSuite) TestDeductFeeDecorator() {
fgAddr, _ = testutiltx.NewAccAddressAndKey()
initBalance = sdk.NewInt(1e18)
lowGasPrice = math.NewInt(1)
bigGasPrice = math.NewInt(1e12)
zero = sdk.ZeroInt()
)

Expand All @@ -33,6 +45,7 @@ func (suite *AnteTestSuite) TestDeductFeeDecorator() {
rewards math.Int
gas uint64
gasPrice *math.Int
gasDenom string
feeGranter sdk.AccAddress
checkTx bool
simulate bool
Expand Down Expand Up @@ -268,10 +281,181 @@ func (suite *AnteTestSuite) TestDeductFeeDecorator() {

// remove the feegrant keeper from the decorator
dfd = cosmosante.NewDeductFeeDecorator(
suite.app.AccountKeeper, suite.app.BankKeeper, suite.app.DistrKeeper, nil, suite.app.StakingKeeper, nil,
suite.app.AccountKeeper, suite.app.BankKeeper, suite.app.Erc20Keeper, suite.app.DistrKeeper, nil, suite.app.StakingKeeper, nil,
)
},
},
{
name: "success - IBC gas denom",
balance: zero,
rewards: zero,
gas: 10_000,
gasPrice: &bigGasPrice,
gasDenom: ibcBase,
checkTx: false,
simulate: false,
expPass: true,
errContains: "",
malleate: func() {
// update evm params to use IBC denom as gas denom
params := suite.app.EvmKeeper.GetParams(suite.ctx)
params.GasDenom = ibcBase
err := suite.app.EvmKeeper.SetParams(suite.ctx, params)
suite.Require().NoError(err)

// register IBC denom
metadataIbc := banktypes.Metadata{
Description: "ATOM IBC voucher (channel 14)",
Base: ibcBase,
// NOTE: Denom units MUST be increasing
DenomUnits: []*banktypes.DenomUnit{
{
Denom: ibcBase,
Exponent: 0,
},
},
Name: "ATOM channel-14",
Symbol: "ibcATOM-14",
Display: ibcBase,
}

// initial IBC denom
err = suite.app.BankKeeper.MintCoins(suite.ctx, inflationtypes.ModuleName, sdk.Coins{sdk.NewInt64Coin(metadataIbc.Base, 1)})
suite.Require().NoError(err)

// register ERC20 representation of IBC denom
_, err = suite.app.Erc20Keeper.RegisterCoin(suite.ctx, metadataIbc)
suite.Require().NoError(err)

// fund sender's SDK account: mint IBC coins and convert it to ERC20 tokens
coin := sdk.NewCoin(ibcBase, sdk.NewInt(1e18))
coins := sdk.NewCoins(coin)
sender := sdk.AccAddress(addr.Bytes())
err = testutil.FundAccount(suite.ctx, suite.app.BankKeeper, sender, coins)
suite.Require().NoError(err)

// check that SDK balance is funded with IBC coins
senderBalances := suite.app.BankKeeper.GetBalance(suite.ctx, sender, ibcBase)
suite.Require().True(senderBalances.IsPositive())

// unsure that the fee collector balance is empty initially
feeCollectorInitialBalance := suite.app.BankKeeper.GetBalance(suite.ctx, authtypes.NewModuleAddress(authtypes.FeeCollectorName), ibcBase)
suite.Require().True(feeCollectorInitialBalance.IsZero())

// for this case to work properly, we need to set a dynamic fee checker
dfd = cosmosante.NewDeductFeeDecorator(
suite.app.AccountKeeper,
suite.app.BankKeeper,
suite.app.Erc20Keeper,
suite.app.DistrKeeper,
suite.app.FeeGrantKeeper,
suite.app.StakingKeeper,
evm.NewDynamicFeeChecker(suite.app.EvmKeeper),
)
},
postCheck: func() {
// check the fee collector balance, it should be positive (initially it was empty)
balance := suite.app.BankKeeper.GetBalance(suite.ctx, authtypes.NewModuleAddress(authtypes.FeeCollectorName), ibcBase)
suite.Require().True(balance.IsPositive())
},
},
{
name: "success - IBC gas denom, not enough SDK coins, enough ERC20 tokens",
balance: zero,
rewards: zero,
gas: 10_000,
gasPrice: &bigGasPrice,
gasDenom: ibcBase,
checkTx: false,
simulate: false,
expPass: true,
errContains: "",
malleate: func() {
// update evm params to use IBC denom as gas denom
params := suite.app.EvmKeeper.GetParams(suite.ctx)
params.GasDenom = ibcBase
err := suite.app.EvmKeeper.SetParams(suite.ctx, params)
suite.Require().NoError(err)

// register IBC denom
metadataIbc := banktypes.Metadata{
Description: "ATOM IBC voucher (channel 14)",
Base: ibcBase,
// NOTE: Denom units MUST be increasing
DenomUnits: []*banktypes.DenomUnit{
{
Denom: ibcBase,
Exponent: 0,
},
},
Name: "ATOM channel-14",
Symbol: "ibcATOM-14",
Display: ibcBase,
}

// initial IBC denom
err = suite.app.BankKeeper.MintCoins(suite.ctx, inflationtypes.ModuleName, sdk.Coins{sdk.NewInt64Coin(metadataIbc.Base, 1)})
suite.Require().NoError(err)

// register ERC20 representation of IBC denom
tp, err := suite.app.Erc20Keeper.RegisterCoin(suite.ctx, metadataIbc)
suite.Require().NoError(err)

// fund sender's eth account: mint IBC coins and convert it to ERC20 tokens
coin := sdk.NewCoin(ibcBase, sdk.NewInt(1e16))
coins := sdk.NewCoins(coin)
sender := sdk.AccAddress(addr.Bytes())
err = testutil.FundAccount(suite.ctx, suite.app.BankKeeper, sender, coins)
suite.Require().NoError(err)
msg := erc20types.NewMsgConvertCoin(
coin,
common.BytesToAddress(sender.Bytes()),
sender,
)
_, err = suite.app.Erc20Keeper.ConvertCoin(sdk.WrapSDKContext(suite.ctx), msg)
suite.Require().NoError(err)

// now the sender's eth balance is funded with ERC20 tokens
senderBalance := suite.app.Erc20Keeper.BalanceOf(suite.ctx, contracts.ERC20MinterBurnerDecimalsContract.ABI, tp.GetERC20Contract(), common.BytesToAddress(addr.Bytes()))
suite.Require().NotNil(senderBalance)
suite.Require().Equal(senderBalance.Cmp(big.NewInt(1e16)), 0) // == 1e16

// while SDK balance is empty
senderBalances := suite.app.BankKeeper.GetAllBalances(suite.ctx, sender)
suite.Require().True(senderBalances.IsZero())

// ensure that the fee collector balance is empty initially
feeCollectorInitialBalance := suite.app.BankKeeper.GetBalance(suite.ctx, authtypes.NewModuleAddress(authtypes.FeeCollectorName), ibcBase)
suite.Require().True(feeCollectorInitialBalance.IsZero())

// for this case to work properly, we need to set a dynamic fee checker
dfd = cosmosante.NewDeductFeeDecorator(
suite.app.AccountKeeper,
suite.app.BankKeeper,
suite.app.Erc20Keeper,
suite.app.DistrKeeper,
suite.app.FeeGrantKeeper,
suite.app.StakingKeeper,
evm.NewDynamicFeeChecker(suite.app.EvmKeeper),
)
},
postCheck: func() {
// get the token pair for the IBC denom
tpID := suite.app.Erc20Keeper.GetTokenPairID(suite.ctx, ibcBase)
suite.Require().NotNil(tpID)
tp, found := suite.app.Erc20Keeper.GetTokenPair(suite.ctx, tpID)
suite.Require().True(found)

// check the sender's ERC20 balance, it should be less than the initial balance
senderBalance := suite.app.Erc20Keeper.BalanceOf(suite.ctx, contracts.ERC20MinterBurnerDecimalsContract.ABI, tp.GetERC20Contract(), common.BytesToAddress(addr.Bytes()))
suite.Require().NotNil(senderBalance)
suite.Require().Equal(senderBalance.Cmp(big.NewInt(1e16)), -1) // < 1e16

// check the fee collector balance, it should be positive (initially it was empty)
balance := suite.app.BankKeeper.GetBalance(suite.ctx, authtypes.NewModuleAddress(authtypes.FeeCollectorName), ibcBase)
suite.Require().True(balance.IsPositive())
},
},
}

// Test execution
Expand All @@ -281,7 +465,13 @@ func (suite *AnteTestSuite) TestDeductFeeDecorator() {

// Create a new DeductFeeDecorator
dfd = cosmosante.NewDeductFeeDecorator(
suite.app.AccountKeeper, suite.app.BankKeeper, suite.app.DistrKeeper, suite.app.FeeGrantKeeper, suite.app.StakingKeeper, nil,
suite.app.AccountKeeper,
suite.app.BankKeeper,
suite.app.Erc20Keeper,
suite.app.DistrKeeper,
suite.app.FeeGrantKeeper,
suite.app.StakingKeeper,
nil,
)

// prepare the testcase
Expand All @@ -302,6 +492,7 @@ func (suite *AnteTestSuite) TestDeductFeeDecorator() {
GasPrice: tc.gasPrice,
FeeGranter: tc.feeGranter,
Msgs: []sdk.Msg{msg},
GasDenom: tc.gasDenom,
}

if tc.malleate != nil {
Expand Down
11 changes: 9 additions & 2 deletions app/ante/cosmos/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@

package cosmos

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

// BankKeeper defines the exposed interface for using functionality of the bank keeper
// in the context of the cosmos AnteHandler package.
type BankKeeper interface {
GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin
SendCoins(ctx sdk.Context, from, to sdk.AccAddress, amt sdk.Coins) error
GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
}

type ERC20Keeper interface {
TryConvertErc20Sdk(ctx sdk.Context, sender, receiver sdk.AccAddress, denom string, amount math.Int) error
}
6 changes: 3 additions & 3 deletions app/ante/cosmos/min_price.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"

evmante "github.com/evmos/evmos/v12/app/ante/evm"
)

Expand Down Expand Up @@ -52,11 +53,10 @@ func (mpd MinGasPriceDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate
if minGasPrice.IsZero() || simulate {
return next(ctx, tx, simulate)
}
evmParams := mpd.evmKeeper.GetParams(ctx)
evmDenom := evmParams.GetEvmDenom()
gasDenom := mpd.evmKeeper.GetParams(ctx).GasDenom
VictorTrustyDev marked this conversation as resolved.
Show resolved Hide resolved
minGasPrices := sdk.DecCoins{
{
Denom: evmDenom,
Denom: gasDenom,
Amount: minGasPrice,
},
}
Expand Down
Loading