Skip to content

Commit

Permalink
evm/fee: migrated fee modules & wip migrating evm module
Browse files Browse the repository at this point in the history
  • Loading branch information
dumbeng committed Aug 13, 2024
1 parent dacf1ba commit a1919c2
Show file tree
Hide file tree
Showing 174 changed files with 27,252 additions and 112 deletions.
88 changes: 88 additions & 0 deletions app/ante/cosmos/authz_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package cosmos

import (
"fmt"

errorsmod "cosmossdk.io/errors"
cosmos "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/authz"
)

// maxNestedMsgs defines a cap for the number of nested messages on a MsgExec message
const maxNestedMsgs = 7

// AuthzLimiterDecorator blocks certain msg types from being granted or executed
// within the authorization module.
type AuthzLimiterDecorator struct {
// disabledMsgTypes is the type urls of the msgs to block.
disabledMsgTypes []string
}

// NewAuthzLimiterDecorator creates a decorator to block certain msg types from being granted or executed within authz.
func NewAuthzLimiterDecorator(disabledMsgTypes ...string) AuthzLimiterDecorator {
return AuthzLimiterDecorator{
disabledMsgTypes: disabledMsgTypes,
}
}

func (ald AuthzLimiterDecorator) AnteHandle(ctx cosmos.Context, tx cosmos.Tx, simulate bool, next cosmos.AnteHandler) (newCtx cosmos.Context, err error) {
if err := ald.checkDisabledMsgs(tx.GetMsgs(), false, 1); err != nil {
return ctx, errorsmod.Wrapf(errortypes.ErrUnauthorized, err.Error())
}
return next(ctx, tx, simulate)
}

// checkDisabledMsgs iterates through the msgs and returns an error if it finds any unauthorized msgs.
//
// When searchOnlyInAuthzMsgs is enabled, only authz MsgGrant and MsgExec are blocked, if they contain unauthorized msg types.
// Otherwise any msg matching the disabled types are blocked, regardless of being in an authz msg or not.
//
// This method is recursive as MsgExec's can wrap other MsgExecs. The check for nested messages is performed up to the
// maxNestedMsgs threshold. If there are more than that limit, it returns an error
func (ald AuthzLimiterDecorator) checkDisabledMsgs(msgs []cosmos.Msg, isAuthzInnerMsg bool, nestedLvl int) error {
if nestedLvl >= maxNestedMsgs {
return fmt.Errorf("found more nested msgs than permited. Limit is : %d", maxNestedMsgs)
}
for _, msg := range msgs {
switch msg := msg.(type) {
case *authz.MsgExec:
innerMsgs, err := msg.GetMessages()
if err != nil {
return err
}
nestedLvl++
if err := ald.checkDisabledMsgs(innerMsgs, true, nestedLvl); err != nil {
return err
}
case *authz.MsgGrant:
authorization, err := msg.GetAuthorization()
if err != nil {
return err
}

url := authorization.MsgTypeURL()
if ald.isDisabledMsg(url) {
return fmt.Errorf("found disabled msg type: %s", url)
}
default:
url := cosmos.MsgTypeURL(msg)
if isAuthzInnerMsg && ald.isDisabledMsg(url) {
return fmt.Errorf("found disabled msg type: %s", url)
}
}
}
return nil
}

// isDisabledMsg returns true if the given message is in the list of restricted
// messages from the AnteHandler.
func (ald AuthzLimiterDecorator) isDisabledMsg(msgTypeURL string) bool {
for _, disabledType := range ald.disabledMsgTypes {
if msgTypeURL == disabledType {
return true
}
}

return false
}
224 changes: 224 additions & 0 deletions app/ante/cosmos/fee_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package cosmos

import (
"fmt"
"math"

errorsmod "cosmossdk.io/errors"
cosmos "github.com/cosmos/cosmos-sdk/types"
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/artela-network/artela-rollkit/app/ante/utils"
)

// DeductFeeDecorator deducts fees from the first signer of the tx.
// If the first signer does not have the funds to pay for the fees,
// and does not have enough unclaimed staking rewards, then return
// with InsufficientFunds error.
// The next AnteHandler is called if fees are successfully deducted.
//
// CONTRACT: Tx must implement FeeTx interface to use DeductFeeDecorator
type DeductFeeDecorator struct {
accountKeeper authante.AccountKeeper
bankKeeper BankKeeper
distributionKeeper anteutils.DistributionKeeper
feegrantKeeper authante.FeegrantKeeper
stakingKeeper anteutils.StakingKeeper
txFeeChecker anteutils.TxFeeChecker
}

// NewDeductFeeDecorator returns a new DeductFeeDecorator.
func NewDeductFeeDecorator(
ak authante.AccountKeeper,
bk BankKeeper,
dk anteutils.DistributionKeeper,
fk authante.FeegrantKeeper,
sk anteutils.StakingKeeper,
tfc anteutils.TxFeeChecker,
) DeductFeeDecorator {
if tfc == nil {
tfc = checkTxFeeWithValidatorMinGasPrices
}

return DeductFeeDecorator{
accountKeeper: ak,
bankKeeper: bk,
distributionKeeper: dk,
feegrantKeeper: fk,
stakingKeeper: sk,
txFeeChecker: tfc,
}
}

// AnteHandle ensures that the transaction contains valid fee requirements and tries to deduct those
// from the account balance or unclaimed staking rewards, which the transaction sender might have.
func (dfd DeductFeeDecorator) AnteHandle(ctx cosmos.Context, tx cosmos.Tx, simulate bool, next cosmos.AnteHandler) (cosmos.Context, error) {
feeTx, ok := tx.(cosmos.FeeTx)
if !ok {
return ctx, errorsmod.Wrap(errortypes.ErrTxDecode, "Tx must be a FeeTx")
}

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

var (
priority int64
err error
)

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

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

if err = dfd.deductFee(ctx, tx, fee, feePayer, feeGranter); err != nil {
return ctx, err
}

newCtx := ctx.WithPriority(priority)

return next(newCtx, tx, simulate)
}

// deductFee checks if the fee payer has enough funds to pay for the fees and deducts them.
// If the spendable balance is not enough, it tries to claim enough staking rewards to cover the fees.
func (dfd DeductFeeDecorator) deductFee(ctx cosmos.Context, sdkTx cosmos.Tx, fees cosmos.Coins, feePayer, feeGranter cosmos.AccAddress) error {
if fees.IsZero() {
return nil
}

if addr := dfd.accountKeeper.GetModuleAddress(authtypes.FeeCollectorName); addr == nil {
return fmt.Errorf("fee collector module account (%s) has not been set", authtypes.FeeCollectorName)
}

// by default, deduct fees from feePayer address
deductFeesFrom := feePayer

// if feegranter is set, then deduct the fee from the feegranter account.
// this works only when feegrant is enabled.
if feeGranter != nil {
if dfd.feegrantKeeper == nil {
return errortypes.ErrInvalidRequest.Wrap("fee grants are not enabled")
}

if !feeGranter.Equals(feePayer) {
err := dfd.feegrantKeeper.UseGrantedFees(ctx, feeGranter, feePayer, fees, sdkTx.GetMsgs())
if err != nil {
return errorsmod.Wrapf(err, "%s does not not allow to pay fees for %s", feeGranter, feePayer)
}
}

deductFeesFrom = feeGranter
}

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

// TODO mark 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)
// }

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

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.
//
//nolint:unused
func deductFeesFromBalanceOrUnclaimedStakingRewards(
ctx cosmos.Context, dfd DeductFeeDecorator, deductFeesFromAcc authtypes.AccountI, fees cosmos.Coins,
) error {
if err := anteutils.ClaimStakingRewardsIfNecessary(
ctx, dfd.bankKeeper, dfd.distributionKeeper, dfd.stakingKeeper, deductFeesFromAcc.GetAddress(), fees,
); err != nil {
return err
}

// TODO mark return authante.DeductFees(dfd.bankKeeper, ctx, deductFeesFromAcc, fees)
return nil
}

// 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 cosmos.Context, feeTx cosmos.FeeTx) (cosmos.Coins, int64, error) {
feeCoins := feeTx.GetFee()
gas := feeTx.GetGas()

// Ensure that the provided fees meets a minimum threshold for the validator,
// if this is a CheckTx. This is only for local mempool purposes, and thus
// is only ran on CheckTx.
if ctx.IsCheckTx() {
if err := checkFeeCoinsAgainstMinGasPrices(ctx, feeCoins, gas); err != nil {
return nil, 0, err
}
}

priority := getTxPriority(feeCoins, int64(gas)) // #nosec G701 -- gosec warning about integer overflow is not relevant here
return feeCoins, priority, nil
}

// checkFeeCoinsAgainstMinGasPrices checks if the provided fee coins are greater than or equal to the
// required fees, that are based on the minimum gas prices and the gas. If not, it will return an error.
func checkFeeCoinsAgainstMinGasPrices(ctx cosmos.Context, feeCoins cosmos.Coins, gas uint64) error {
minGasPrices := ctx.MinGasPrices()
if minGasPrices.IsZero() {
return nil
}

requiredFees := make(cosmos.Coins, len(minGasPrices))

// Determine the required fees by multiplying each required minimum gas
// price by the gas limit, where fee = ceil(minGasPrice * gasLimit).
glDec := cosmos.NewDec(int64(gas)) // #nosec G701 -- gosec warning about integer overflow is not relevant here
for i, gp := range minGasPrices {
fee := gp.Amount.Mul(glDec)
requiredFees[i] = cosmos.NewCoin(gp.Denom, fee.Ceil().RoundInt())
}

if !feeCoins.IsAnyGTE(requiredFees) {
return errorsmod.Wrapf(errortypes.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees)
}

return 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(fees cosmos.Coins, gas int64) int64 {
var priority int64
for _, c := range fees {
p := int64(math.MaxInt64)
gasPrice := c.Amount.QuoRaw(gas)
if gasPrice.IsInt64() {
p = gasPrice.Int64()
}
if priority == 0 || p < priority {
priority = p
}
}

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

import (
"math/big"

errorsmod "cosmossdk.io/errors"
cosmos "github.com/cosmos/cosmos-sdk/types"
errortypes "github.com/cosmos/cosmos-sdk/types/errors"

"github.com/artela-network/artela-rollkit/app/interfaces"
)

// MinGasPriceDecorator will check if the transaction's fee is at least as large
// as the MinGasPrices param. If fee is too low, decorator returns error and tx
// is rejected. This applies for both CheckTx and DeliverTx
// If fee is high enough, then call next AnteHandler
// CONTRACT: Tx must implement FeeTx to use MinGasPriceDecorator
type MinGasPriceDecorator struct {
feesKeeper interfaces.FeeKeeper
evmKeeper interfaces.EVMKeeper
}

// NewMinGasPriceDecorator creates a new MinGasPriceDecorator instance used only for
// Cosmos transactions.
func NewMinGasPriceDecorator(fk interfaces.FeeKeeper, ek interfaces.EVMKeeper) MinGasPriceDecorator {
return MinGasPriceDecorator{feesKeeper: fk, evmKeeper: ek}
}

func (mpd MinGasPriceDecorator) AnteHandle(ctx cosmos.Context, tx cosmos.Tx, simulate bool, next cosmos.AnteHandler) (newCtx cosmos.Context, err error) {
feeTx, ok := tx.(cosmos.FeeTx)
if !ok {
return ctx, errorsmod.Wrapf(errortypes.ErrInvalidType, "invalid transaction type %T, expected cosmos.FeeTx", tx)
}

minGasPrice := mpd.feesKeeper.GetParams(ctx).MinGasPrice

// Short-circuit if min gas price is 0 or if simulating
if minGasPrice.IsZero() || simulate {
return next(ctx, tx, simulate)
}
evmParams := mpd.evmKeeper.GetParams(ctx)
evmDenom := evmParams.GetEvmDenom()
minGasPrices := cosmos.DecCoins{
{
Denom: evmDenom,
Amount: minGasPrice,
},
}

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

requiredFees := make(cosmos.Coins, 0)

// Determine the required fees by multiplying each required minimum gas
// price by the gas limit, where fee = ceil(minGasPrice * gasLimit).
gasLimit := cosmos.NewDecFromBigInt(new(big.Int).SetUint64(gas))

for _, gp := range minGasPrices {
fee := gp.Amount.Mul(gasLimit).Ceil().RoundInt()
if fee.IsPositive() {
requiredFees = requiredFees.Add(cosmos.Coin{Denom: gp.Denom, Amount: fee})
}
}

// Fees not provided (or flag "auto"). Then use the base fee to make the check pass
if feeCoins == nil {
return ctx, errorsmod.Wrapf(errortypes.ErrInsufficientFee,
"fee not provided. Please use the --fees flag or the --gas-price flag along with the --gas flag to estimate the fee. The minimun global fee for this tx is: %s",
requiredFees)
}

if !feeCoins.IsAnyGTE(requiredFees) {
return ctx, errorsmod.Wrapf(errortypes.ErrInsufficientFee,
"provided fee < minimum global fee (%s < %s). Please increase the gas price.",
feeCoins,
requiredFees)
}

return next(ctx, tx, simulate)
}
Loading

0 comments on commit a1919c2

Please sign in to comment.