From 94022e1102277df26dc15cd27a602588d00539a3 Mon Sep 17 00:00:00 2001 From: Lorenz Herzberger <64837895+LaurentMontBlanc@users.noreply.github.com> Date: Tue, 3 Oct 2023 09:01:39 +0200 Subject: [PATCH] 100 set fees to be 1 plmnt for a tx (#114) * replace DeductFeeDecorator and adjust e2e tests * reorganize decorators and add comments --------- Signed-off-by: Lorenz Herzberger --- app/ante/ante.go | 50 +------- app/ante/check_machine_decorator.go | 51 +++++++++ app/ante/deduct_fee_decorator.go | 172 ++++++++++++++++++++++++++++ tests/e2e/dao/suite.go | 10 +- testutil/network/network.go | 2 +- testutil/sample/sample.go | 2 +- 6 files changed, 233 insertions(+), 54 deletions(-) create mode 100644 app/ante/check_machine_decorator.go create mode 100644 app/ante/deduct_fee_decorator.go diff --git a/app/ante/ante.go b/app/ante/ante.go index de07768..7527424 100644 --- a/app/ante/ante.go +++ b/app/ante/ante.go @@ -1,9 +1,6 @@ package ante import ( - assettypes "github.com/planetmint/planetmint-go/x/asset/types" - machinetypes "github.com/planetmint/planetmint-go/x/machine/types" - errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -13,49 +10,6 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) -type CheckMachineDecorator struct { - mk MachineKeeper -} - -func NewCheckMachineDecorator(mk MachineKeeper) CheckMachineDecorator { - return CheckMachineDecorator{ - mk: mk, - } -} - -func (cm CheckMachineDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { - for _, msg := range tx.GetMsgs() { - switch sdk.MsgTypeURL(msg) { - case "/planetmintgo.asset.MsgNotarizeAsset": - notarizeMsg, ok := msg.(*assettypes.MsgNotarizeAsset) - if ok { - _, found := cm.mk.GetMachineIndexByAddress(ctx, notarizeMsg.GetCreator()) - if !found { - return ctx, errorsmod.Wrapf(machinetypes.ErrMachineNotFound, "error during CheckTx or ReCheckTx") - } - } - case "/planetmintgo.machine.MsgAttestMachine": - attestMsg, ok := msg.(*machinetypes.MsgAttestMachine) - if ok { - if attestMsg.GetCreator() != attestMsg.Machine.GetAddress() { - return ctx, errorsmod.Wrapf(machinetypes.ErrMachineIsNotCreator, "error during CheckTx or ReCheckTx") - } - _, activated, found := cm.mk.GetTrustAnchor(ctx, attestMsg.Machine.MachineId) - if !found { - return ctx, errorsmod.Wrapf(machinetypes.ErrTrustAnchorNotFound, "error during CheckTx or ReCheckTx") - } - if activated { - return ctx, errorsmod.Wrapf(machinetypes.ErrTrustAnchorAlreadyInUse, "error during CheckTx or ReCheckTx") - } - } - default: - continue - } - } - - return next(ctx, tx, simulate) -} - // HandlerOptions are the options required for constructing a default SDK AnteHandler. type HandlerOptions struct { AccountKeeper AccountKeeper @@ -64,7 +18,7 @@ type HandlerOptions struct { FeegrantKeeper FeegrantKeeper SignModeHandler authsigning.SignModeHandler SigGasConsumer func(meter sdk.GasMeter, sig signing.SignatureV2, params authtypes.Params) error - TxFeeChecker ante.TxFeeChecker + TxFeeChecker TxFeeChecker MachineKeeper MachineKeeper } @@ -96,7 +50,7 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { ante.NewValidateMemoDecorator(options.AccountKeeper), NewCheckMachineDecorator(options.MachineKeeper), ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), - ante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), + NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), ante.NewSetPubKeyDecorator(options.AccountKeeper), // SetPubKeyDecorator must be called before all signature verification decorators ante.NewValidateSigCountDecorator(options.AccountKeeper), ante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer), diff --git a/app/ante/check_machine_decorator.go b/app/ante/check_machine_decorator.go new file mode 100644 index 0000000..749ecb3 --- /dev/null +++ b/app/ante/check_machine_decorator.go @@ -0,0 +1,51 @@ +package ante + +import ( + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + assettypes "github.com/planetmint/planetmint-go/x/asset/types" + machinetypes "github.com/planetmint/planetmint-go/x/machine/types" +) + +type CheckMachineDecorator struct { + mk MachineKeeper +} + +func NewCheckMachineDecorator(mk MachineKeeper) CheckMachineDecorator { + return CheckMachineDecorator{ + mk: mk, + } +} + +func (cm CheckMachineDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + for _, msg := range tx.GetMsgs() { + switch sdk.MsgTypeURL(msg) { + case "/planetmintgo.asset.MsgNotarizeAsset": + notarizeMsg, ok := msg.(*assettypes.MsgNotarizeAsset) + if ok { + _, found := cm.mk.GetMachineIndexByAddress(ctx, notarizeMsg.GetCreator()) + if !found { + return ctx, errorsmod.Wrapf(machinetypes.ErrMachineNotFound, "error during CheckTx or ReCheckTx") + } + } + case "/planetmintgo.machine.MsgAttestMachine": + attestMsg, ok := msg.(*machinetypes.MsgAttestMachine) + if ok { + if attestMsg.GetCreator() != attestMsg.Machine.GetAddress() { + return ctx, errorsmod.Wrapf(machinetypes.ErrMachineIsNotCreator, "error during CheckTx or ReCheckTx") + } + _, activated, found := cm.mk.GetTrustAnchor(ctx, attestMsg.Machine.MachineId) + if !found { + return ctx, errorsmod.Wrapf(machinetypes.ErrTrustAnchorNotFound, "error during CheckTx or ReCheckTx") + } + if activated { + return ctx, errorsmod.Wrapf(machinetypes.ErrTrustAnchorAlreadyInUse, "error during CheckTx or ReCheckTx") + } + } + default: + continue + } + } + + return next(ctx, tx, simulate) +} diff --git a/app/ante/deduct_fee_decorator.go b/app/ante/deduct_fee_decorator.go new file mode 100644 index 0000000..25b2276 --- /dev/null +++ b/app/ante/deduct_fee_decorator.go @@ -0,0 +1,172 @@ +package ante + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// TxFeeChecker check if the provided fee is enough and returns the effective fee and tx priority, +// the effective fee should be deducted later, and the priority should be returned in abci response. +type TxFeeChecker func(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, error) + +// 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 AccountKeeper + bankKeeper authtypes.BankKeeper + feegrantKeeper FeegrantKeeper + txFeeChecker TxFeeChecker +} + +func NewDeductFeeDecorator(ak AccountKeeper, bk authtypes.BankKeeper, fk FeegrantKeeper, tfc TxFeeChecker) DeductFeeDecorator { + if tfc == nil { + tfc = checkTxFee + } + + return DeductFeeDecorator{ + accountKeeper: ak, + bankKeeper: bk, + feegrantKeeper: fk, + txFeeChecker: tfc, + } +} + +func checkTxFee(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, error) { + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return nil, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") + } + + feeCoins := feeTx.GetFee() + gas := feeTx.GetGas() + + if ctx.IsCheckTx() { + // min-gas-prices is used to determine which coins are to be deducted and if they are provided as fee + 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, errorsmod.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees) + } + } + } + + return feeCoins, nil +} + +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, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") + } + + if !simulate && ctx.BlockHeight() > 0 && feeTx.GetGas() == 0 { + return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidGasLimit, "must provide positive gas") + } + + var ( + err error + ) + + fee := feeTx.GetFee() + if !simulate { + fee, err = dfd.txFeeChecker(ctx, tx) + if err != nil { + return ctx, err + } + } + if err := dfd.checkDeductFee(ctx, tx, fee); err != nil { + return ctx, err + } + + return next(ctx, tx, simulate) +} + +func (dfd DeductFeeDecorator) checkDeductFee(ctx sdk.Context, sdkTx sdk.Tx, fee sdk.Coins) error { + feeTx, ok := sdkTx.(sdk.FeeTx) + if !ok { + return errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") + } + + if addr := dfd.accountKeeper.GetModuleAddress(authtypes.FeeCollectorName); addr == nil { + return fmt.Errorf("fee collector module account (%s) has not been set", 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 errorsmod.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 := dfd.deductFees(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 (dfd DeductFeeDecorator) deductFees(bankKeeper authtypes.BankKeeper, ctx sdk.Context, acc authtypes.AccountI, fees sdk.Coins) error { + // check if exactly one fee is provided and is greater than 0 + if !fees.IsValid() && len(fees) == 1 { + return errorsmod.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees) + } + + // take exactly on token as fee no matter what is provided + denoms := fees.Denoms() + feesToCollect := sdk.Coins{sdk.NewCoin(denoms[0], sdk.OneInt())} + + err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), authtypes.FeeCollectorName, feesToCollect) + if err != nil { + return errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds, err.Error()) + } + + return nil +} diff --git a/tests/e2e/dao/suite.go b/tests/e2e/dao/suite.go index 7dc5900..2cb16b2 100644 --- a/tests/e2e/dao/suite.go +++ b/tests/e2e/dao/suite.go @@ -2,6 +2,7 @@ package dao import ( "fmt" + "github.com/planetmint/planetmint-go/config" "github.com/planetmint/planetmint-go/testutil/network" "github.com/planetmint/planetmint-go/testutil/sample" @@ -119,17 +120,18 @@ func (s *E2ETestSuite) TestDistributeCollectedFees() { err = s.network.WaitForNextBlock() s.Require().NoError(err) - // assert that alice has 6 of 20 paid fee tokens based on 5000 stake of 15000 total stake + // assert that alice has 0 of 2 paid fee tokens based on 5000 stake of 15000 total stake out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, bank.GetBalancesCmd(), []string{ aliceAddr.String(), }) - assert.Contains(s.T(), out.String(), "6") + assert.NotContains(s.T(), out.String(), "node0token") s.Require().NoError(err) - // assert that bob has 13 of 20 paid fee tokens based on 10000 stake of 15000 total stake + // assert that bob has 1 of 2 paid fee tokens based on 10000 stake of 15000 total stake out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, bank.GetBalancesCmd(), []string{ bobAddr.String(), }) - assert.Contains(s.T(), out.String(), "13") + assert.Contains(s.T(), out.String(), "1") + assert.Contains(s.T(), out.String(), "node0token") s.Require().NoError(err) } diff --git a/testutil/network/network.go b/testutil/network/network.go index 4374c36..46b72b2 100644 --- a/testutil/network/network.go +++ b/testutil/network/network.go @@ -79,7 +79,7 @@ func DefaultConfig() network.Config { ChainID: chainID, NumValidators: 1, BondDenom: sdk.DefaultBondDenom, - MinGasPrices: fmt.Sprintf("0.000006%s", sdk.DefaultBondDenom), + MinGasPrices: fmt.Sprintf("0.000003%s", sdk.DefaultBondDenom), AccountTokens: sdk.TokensFromConsensusPower(1000, sdk.DefaultPowerReduction), StakingTokens: sdk.TokensFromConsensusPower(500, sdk.DefaultPowerReduction), BondedTokens: sdk.TokensFromConsensusPower(100, sdk.DefaultPowerReduction), diff --git a/testutil/sample/sample.go b/testutil/sample/sample.go index 37792c7..d97089a 100644 --- a/testutil/sample/sample.go +++ b/testutil/sample/sample.go @@ -29,7 +29,7 @@ const Name = "machine" const Amount = "1000stake" // Fees is the amount of fees to use in tests -const Fees = "2stake" +const Fees = "1stake" // DefaultDerivationPath is the BIP44Prefix for PLMNT (see https://github.com/satoshilabs/slips/blob/master/slip-0044.md) const DefaultDerivationPath = "m/44'/8680'/0'/0/0"