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: denom resolver #53

Merged
merged 20 commits into from
May 14, 2024
8 changes: 2 additions & 6 deletions proto/feemarket/feemarket/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ message BaseFeeRequest {}

// StateResponse is the response type for the Query/BaseFee RPC method.
message BaseFeeResponse {
repeated cosmos.base.v1beta1.Coin fees = 1 [
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true,
(amino.encoding) = "legacy_coins",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
cosmos.base.v1beta1.Coin fees = 1
[ (gogoproto.nullable) = false, (amino.dont_omitempty) = true ];
}
6 changes: 5 additions & 1 deletion tests/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import (

feemarketmodule "github.com/skip-mev/feemarket/x/feemarket"
feemarketkeeper "github.com/skip-mev/feemarket/x/feemarket/keeper"
feemarkettypes "github.com/skip-mev/feemarket/x/feemarket/types"
)

const (
Expand Down Expand Up @@ -182,7 +183,7 @@ func New(
// authtypes.RandomGenesisAccountsFn(simulation.RandomGenesisAccounts),

// For providing a custom a base account type add it below.
// By default the auth module uses authtypes.ProtoBaseAccount().
// By default, the auth module uses authtypes.ProtoBaseAccount().
//
// func() authtypes.AccountI { return authtypes.ProtoBaseAccount() },

Expand Down Expand Up @@ -251,6 +252,9 @@ func New(

app.App = appBuilder.Build(logger, db, traceStore, baseAppOptions...)

// set denom resolver to test variant.
app.FeeMarketKeeper.SetDenomResolver(&feemarkettypes.TestDenomResolver{})

// Create a global ante handler that will be called on each transaction when
// proposals are being built and verified.
anteHandlerOptions := ante.HandlerOptions{
Expand Down
1 change: 1 addition & 0 deletions testutils/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func FeeMarket(
initializer.Codec,
storeKey,
authKeeper,
&feemarkettypes.TestDenomResolver{},
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
)
}
3 changes: 2 additions & 1 deletion x/feemarket/ante/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ type BankKeeper interface {
//go:generate mockery --name FeeMarketKeeper --filename mock_feemarket_keeper.go
type FeeMarketKeeper interface {
GetState(ctx sdk.Context) (feemarkettypes.State, error)
GetMinGasPrices(ctx sdk.Context) (sdk.Coins, error)
GetParams(ctx sdk.Context) (feemarkettypes.Params, error)
SetState(ctx sdk.Context, state feemarkettypes.State) error
SetParams(ctx sdk.Context, params feemarkettypes.Params) error
GetMinGasPrice(ctx sdk.Context) (sdk.Coin, error)
GetDenomResolver() feemarkettypes.DenomResolver
}
105 changes: 61 additions & 44 deletions x/feemarket/ante/fee.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

feemarkettypes "github.com/skip-mev/feemarket/x/feemarket/types"
)

// FeeMarketCheckDecorator checks sufficient fees from the fee payer based off of the current
Expand All @@ -27,6 +29,8 @@ func NewFeeMarketCheckDecorator(fmk FeeMarketKeeper) FeeMarketCheckDecorator {

// AnteHandle checks if the tx provides sufficient fee to cover the required fee from the fee market.
func (dfd FeeMarketCheckDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) {
var feeCoin sdk.Coin

// GenTx consume no fee
if ctx.BlockHeight() == 0 {
return next(ctx, tx, simulate)
Expand All @@ -41,96 +45,109 @@ func (dfd FeeMarketCheckDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula
return ctx, sdkerrors.ErrInvalidGasLimit.Wrapf("must provide positive gas")
}

minGasPrices, err := dfd.feemarketKeeper.GetMinGasPrices(ctx)
requiredBaseFee, err := dfd.feemarketKeeper.GetMinGasPrice(ctx)
if err != nil {
return ctx, errorsmod.Wrapf(err, "unable to get fee market state")
}

fee := feeTx.GetFee()
feeCoins := feeTx.GetFee()
gas := feeTx.GetGas() // use provided gas limit

if len(feeCoins) > 1 {
return ctx, feemarkettypes.ErrTooManyFeeCoins
}

// If there is a fee attached to the tx, make sure the fee denom is a denom accepted by the chain
if len(feeCoins) == 1 {
feeDenom := feeCoins.GetDenomByIndex(0)
if feeDenom != requiredBaseFee.Denom {
return ctx, err
}
}

ctx.Logger().Info("fee deduct ante handle",
"min gas prices", minGasPrices,
"fee", fee,
"min gas prices", requiredBaseFee,
"fee", feeCoins,
"gas limit", gas,
)

if !simulate {
fee, _, err = CheckTxFees(ctx, minGasPrices, feeTx, true)
feeCoin, _, err = CheckTxFee(ctx, requiredBaseFee, feeTx, true, dfd.feemarketKeeper.GetDenomResolver())
if err != nil {
return ctx, errorsmod.Wrapf(err, "error checking fee")
}
}

minGasPricesDecCoins := sdk.NewDecCoinsFromCoins(minGasPrices...)
newCtx := ctx.WithPriority(getTxPriority(fee, int64(gas))).WithMinGasPrices(minGasPricesDecCoins)
minGasPricesDecCoin := sdk.NewDecCoinFromCoin(requiredBaseFee)
newCtx := ctx.WithPriority(getTxPriority(feeCoin, int64(gas))).WithMinGasPrices(sdk.NewDecCoins(minGasPricesDecCoin))
return next(newCtx, tx, simulate)
}

// CheckTxFees implements the logic for the fee market to check if a Tx has provided sufficient
// CheckTxFee implements the logic for the fee market to check if a Tx has provided sufficient
// fees given the current state of the fee market. Returns an error if insufficient fees.
func CheckTxFees(ctx sdk.Context, minFees sdk.Coins, feeTx sdk.FeeTx, isCheck bool) (feeCoins sdk.Coins, tip sdk.Coins, err error) {
minFeesDecCoins := sdk.NewDecCoinsFromCoins(minFees...)
feeCoins = feeTx.GetFee()
func CheckTxFee(ctx sdk.Context, minFee sdk.Coin, feeTx sdk.FeeTx, isCheck bool, resolver feemarkettypes.DenomResolver) (feeCoin sdk.Coin, tip sdk.Coin, err error) {
minFeesDecCoin := sdk.NewDecCoinFromCoin(minFee)
feeCoin = feeTx.GetFee()[0]

feeCoin, err = resolver.ConvertToBaseToken(ctx, feeCoin, minFee.Denom)
if err != nil {
return sdk.Coin{}, sdk.Coin{}, err
}

// Ensure that the provided fees meet the minimum
minGasPrices := minFeesDecCoins
if !minGasPrices.IsZero() {
requiredFees := make(sdk.Coins, len(minGasPrices))
consumedFees := make(sdk.Coins, len(minGasPrices))
minGasPrice := minFeesDecCoin
if !minGasPrice.IsZero() {
var requiredFee sdk.Coin
var consumedFee sdk.Coin

// Determine the required fees by multiplying each required minimum gas
// price by the gas, where fee = ceil(minGasPrice * gas).
gasConsumed := int64(ctx.GasMeter().GasConsumed())
gcDec := sdkmath.LegacyNewDec(gasConsumed)
glDec := sdkmath.LegacyNewDec(int64(feeTx.GetGas()))

for i, gp := range minGasPrices {
consumedFee := gp.Amount.Mul(gcDec)
limitFee := gp.Amount.Mul(glDec)
consumedFees[i] = sdk.NewCoin(gp.Denom, consumedFee.Ceil().RoundInt())
requiredFees[i] = sdk.NewCoin(gp.Denom, limitFee.Ceil().RoundInt())
}

if !feeCoins.IsAnyGTE(requiredFees) {
return nil, nil, sdkerrors.ErrInsufficientFee.Wrapf(
"got: %s required: %s, minGasPrices: %s, gas: %d",
feeCoins,
requiredFees,
minGasPrices,
consumedFeeAmount := minGasPrice.Amount.Mul(gcDec)
limitFee := minGasPrice.Amount.Mul(glDec)
consumedFee = sdk.NewCoin(minGasPrice.Denom, consumedFeeAmount.Ceil().RoundInt())
requiredFee = sdk.NewCoin(minGasPrice.Denom, limitFee.Ceil().RoundInt())

if feeCoin.Denom != requiredFee.Denom || !feeCoin.IsGTE(requiredFee) {
return sdk.Coin{}, sdk.Coin{}, sdkerrors.ErrInsufficientFee.Wrapf(
"got: %s required: %s, minGasPrice: %s, gas: %d",
feeCoin,
requiredFee,
minGasPrice,
gasConsumed,
)
}

if isCheck {
// set fee coins to be required amount if checking
feeCoins = requiredFees
feeCoin = requiredFee
} else {
// tip is the difference between feeCoins and the required fees
tip = feeCoins.Sub(requiredFees...)
// set fee coins to be ONLY the consumed amount if we are calculated consumed fee to deduct
feeCoins = consumedFees
// tip is the difference between feeCoin and the required fee
tip = feeCoin.Sub(requiredFee)
// set fee coin to be ONLY the consumed amount if we are calculated consumed fee to deduct
feeCoin = consumedFee
}
}

return feeCoins, tip, nil
return feeCoin, tip, 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 prioritized as expected.
func getTxPriority(fee sdk.Coins, gas int64) int64 {
func getTxPriority(fee sdk.Coin, 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
}
p := int64(math.MaxInt64)
gasPrice := fee.Amount.QuoRaw(gas)
if gasPrice.IsInt64() {
p = gasPrice.Int64()
}
if priority == 0 || p < priority {
priority = p
}

return priority
Expand Down
34 changes: 34 additions & 0 deletions x/feemarket/ante/fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func TestAnteHandle(t *testing.T) {
gasLimit := antesuite.NewTestGasLimit()
validFeeAmount := types.DefaultMinBaseFee.MulRaw(int64(gasLimit))
validFee := sdk.NewCoins(sdk.NewCoin("stake", validFeeAmount))
validFeeDifferentDenom := sdk.NewCoins(sdk.NewCoin("atom", validFeeAmount))

testCases := []antesuite.TestCase{
{
Expand All @@ -37,6 +38,23 @@ func TestAnteHandle(t *testing.T) {
ExpPass: false,
ExpErr: sdkerrors.ErrInvalidGasLimit,
},
{
Name: "0 gas given should fail with resolvable denom",
Malleate: func(suite *antesuite.TestSuite) antesuite.TestCaseArgs {
accs := suite.CreateTestAccounts(1)

return antesuite.TestCaseArgs{
Msgs: []sdk.Msg{testdata.NewTestMsg(accs[0].Account.GetAddress())},
GasLimit: 0,
FeeAmount: validFeeDifferentDenom,
}
},
RunAnte: true,
RunPost: false,
Simulate: false,
ExpPass: false,
ExpErr: sdkerrors.ErrInvalidGasLimit,
},
{
Name: "signer has enough funds, should pass",
Malleate: func(suite *antesuite.TestSuite) antesuite.TestCaseArgs {
Expand All @@ -53,6 +71,22 @@ func TestAnteHandle(t *testing.T) {
ExpPass: true,
ExpErr: nil,
},
{
Name: "signer has enough funds in resolvable denom, should pass",
Malleate: func(suite *antesuite.TestSuite) antesuite.TestCaseArgs {
accs := suite.CreateTestAccounts(1)
return antesuite.TestCaseArgs{
Msgs: []sdk.Msg{testdata.NewTestMsg(accs[0].Account.GetAddress())},
GasLimit: gasLimit,
FeeAmount: validFeeDifferentDenom,
}
},
RunAnte: true,
RunPost: false,
Simulate: false,
ExpPass: true,
ExpErr: nil,
},
}

for _, tc := range testCases {
Expand Down
Loading
Loading