From 93476e8592e99e662fd198e29ee2e96178936765 Mon Sep 17 00:00:00 2001 From: GuillemGarciaDev Date: Thu, 11 Sep 2025 16:53:52 +0200 Subject: [PATCH] feat(ante): add DynamicDiscountTxFeeChecker to apply discount percentage over whitelisted transaction messages --- Dockerfile | 2 +- Makefile | 8 +- app/ante/dynamic_discount_fee_checker.go | 100 ++++++++++ app/ante/dynamic_discount_fee_checker_test.go | 182 ++++++++++++++++++ app/app.go | 3 +- 5 files changed, 288 insertions(+), 7 deletions(-) create mode 100644 app/ante/dynamic_discount_fee_checker.go create mode 100644 app/ante/dynamic_discount_fee_checker_test.go diff --git a/Dockerfile b/Dockerfile index efae1a7..e6420e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ RUN make build FROM base AS integration RUN make lint # Unit tests -RUN make test-poa +RUN make test-unit # Integration tests RUN make test-integration # Simulation tests diff --git a/Makefile b/Makefile index 8953692..45d1ab2 100644 --- a/Makefile +++ b/Makefile @@ -132,7 +132,7 @@ lint-fix: ### Testing ### ############################################################################### EXCLUDED_POA_PACKAGES=$(shell go list ./x/poa/... | grep -v /x/poa/testutil | grep -v /x/poa/client | grep -v /x/poa/simulation | grep -v /x/poa/types) -EXCLUDED_UNIT_PACKAGES=$(shell go list ./... | grep -v tests | grep -v testutil | grep -v tools | grep -v app | grep -v docs | grep -v cmd | grep -v /x/poa/testutil | grep -v /x/poa/client | grep -v /x/poa/simulation | grep -v /x/poa/types) +EXCLUDED_UNIT_PACKAGES=$(shell go list ./... | grep -v tests | grep -v legacy | grep -v testutil | grep -v tools | grep -v docs | grep -v cmd | grep -v /x/poa/testutil | grep -v /x/poa/client | grep -v /x/poa/simulation | grep -v /x/poa/types) mocks: @echo "--> Installing mockgen" @@ -146,9 +146,9 @@ test-integration: @echo "--> Running integration testsuite" @go test -mod=readonly -tags=test -v ./tests/integration -test-poa: - @echo "--> Running POA tests" - @go test $(EXCLUDED_POA_PACKAGES) +test-unit: + @echo "--> Running unit tests" + @go test $(EXCLUDED_UNIT_PACKAGES) test-sim-benchmark-simulation: @echo "Running simulation invariant benchmarks..." diff --git a/app/ante/dynamic_discount_fee_checker.go b/app/ante/dynamic_discount_fee_checker.go new file mode 100644 index 0000000..7830507 --- /dev/null +++ b/app/ante/dynamic_discount_fee_checker.go @@ -0,0 +1,100 @@ +package ante + +import ( + "fmt" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/evm/ante/evm" + anteinterfaces "github.com/cosmos/evm/ante/interfaces" + evmtypes "github.com/cosmos/evm/x/vm/types" +) + +const ( + MinDiscountValue int64 = 0 + MaxDiscountValue int64 = 100 +) + +type Discount int64 + +func (d Discount) Int64() int64 { + return int64(d) +} + +func (d Discount) Int() sdkmath.Int { + return sdkmath.NewInt(int64(d)) +} + +func (d Discount) IsZero() bool { + return d.Int64() == 0 +} + +func (d Discount) IsValid() bool { + intD := d.Int64() + return intD >= MinDiscountValue && intD <= MaxDiscountValue +} + +func NewDynamicDiscountTxFeeChecker(keeper anteinterfaces.FeeMarketKeeper, discount Discount) ante.TxFeeChecker { + return func(ctx sdk.Context, tx sdk.Tx) (sdk.Coins, int64, error) { + feeTx, ok := tx.(sdk.FeeTx) + + denom := evmtypes.GetEVMCoinDenom() + ethCfg := evmtypes.GetEthChainConfig() + + if !ok { + return nil, 0, fmt.Errorf(" Tx must be a FeeTx") + } + + fee, priority, err := evm.FeeChecker(ctx, keeper, denom, ethCfg, feeTx) + if err != nil { + return nil, 0, err + } + + if !IsDiscountApplicable(tx) { + return fee, priority, nil + } + + return ApplyFeeDiscount(fee, priority, discount, denom) + } +} + +func IsDiscountApplicable(tx sdk.Tx) bool { + for _, msg := range tx.GetMsgs() { + if _, ok := msg.(*banktypes.MsgSend); ok { + return true + } + } + return false +} + +func ApplyFeeDiscount(fee sdk.Coins, priority int64, discount Discount, denom string) (sdk.Coins, int64, error) { + found, feeCoin := fee.Find(denom) + if !found { + return fee, priority, fmt.Errorf("fee not found for denom: %s", denom) + } + + if !discount.IsValid() || discount.IsZero() { + return fee, priority, fmt.Errorf("invalid discount: %d", discount) + } + + if fee.IsZero() { + return fee, priority, nil + } + + discountAmt := feeCoin.Amount.Mul(discount.Int()).Quo(sdkmath.NewInt(100)) + discountedFeeAmt := feeCoin.Amount.Sub(discountAmt) + + priorityInt := sdkmath.NewInt(priority) + discountedPriority := priorityInt.Add(priorityInt.Mul(discount.Int()).Quo(sdkmath.NewInt(100))) + + discountedFee := sdk.Coins{ + { + Denom: denom, + Amount: discountedFeeAmt, + }, + } + + return discountedFee, discountedPriority.Int64(), nil +} diff --git a/app/ante/dynamic_discount_fee_checker_test.go b/app/ante/dynamic_discount_fee_checker_test.go new file mode 100644 index 0000000..a8ccf55 --- /dev/null +++ b/app/ante/dynamic_discount_fee_checker_test.go @@ -0,0 +1,182 @@ +package ante + +import ( + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + poatypes "github.com/xrplevm/node/v9/x/poa/types" + "google.golang.org/protobuf/proto" +) + +type mockTx struct { + msgs []sdk.Msg +} + +// Implement sdk.Tx interface +func (m mockTx) GetMsgs() []sdk.Msg { return m.msgs } +func (m mockTx) GetMsgsV2() ([]proto.Message, error) { return nil, nil } +func (m mockTx) ValidateBasic() error { return nil } + +func TestIsDiscountApplicable(t *testing.T) { + // Helper type to mock sdk.Tx + + tt := []struct { + name string + tx sdk.Tx + expectedResult bool + }{ + { + name: "should return true if a discountable message is found", + tx: mockTx{msgs: []sdk.Msg{&banktypes.MsgSend{}}}, + expectedResult: true, + }, + { + name: "should return false if no messages", + tx: mockTx{msgs: []sdk.Msg{}}, + expectedResult: false, + }, + { + name: "should return false if no discountable message is found", + tx: mockTx{msgs: []sdk.Msg{&poatypes.MsgAddValidator{}}}, + expectedResult: false, + }, + { + name: "should return true if at least one discountable message is found among others", + tx: mockTx{msgs: []sdk.Msg{&poatypes.MsgAddValidator{}, &banktypes.MsgSend{}}}, + expectedResult: true, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + result := IsDiscountApplicable(tc.tx) + if result != tc.expectedResult { + t.Errorf("expected %v, got %v", tc.expectedResult, result) + } + }) + } +} + +func TestApplyFeeDiscount(t *testing.T) { + denom := "token" + + discount := Discount(10) + zeroFeeCoins := sdk.Coins{sdk.NewInt64Coin(denom, 0)} + feeCoins := sdk.Coins{sdk.NewInt64Coin(denom, 100)} + + tt := []struct { + name string + fee sdk.Coins + priority int64 + discount Discount + denom string + expectedFee sdk.Coins + expectedPriority int64 + expectedErr bool + errContains string + }{ + { + name: "should return an error if no fee denom is found", + fee: feeCoins, + denom: "unknown", + expectedErr: true, + errContains: "fee not found for denom", + }, + { + name: "should return an error if the discount is not valid (lower boundaries)", + fee: feeCoins, + denom: denom, + discount: Discount(-1), + expectedErr: true, + errContains: "invalid discount", + }, + { + name: "should return an error if the discount is not valid (upper boundaries)", + fee: feeCoins, + denom: denom, + discount: Discount(101), + expectedErr: true, + errContains: "invalid discount", + }, + { + name: "should return an error if the discount is zero", + fee: zeroFeeCoins, + denom: denom, + discount: Discount(0), + expectedErr: true, + errContains: "invalid discount", + }, + { + name: "should return the same fee and priority if fee is zero", + fee: zeroFeeCoins, + priority: 10, + denom: denom, + discount: discount, + expectedFee: sdk.Coins{sdk.NewInt64Coin("token", 0)}, + expectedPriority: 10, + }, + { + name: "should return the discounted fee and priority (fee=5, priority=5, discount=10)", + fee: sdk.Coins{sdk.NewInt64Coin(denom, 5)}, + priority: 5, + denom: denom, + discount: discount, + expectedFee: sdk.Coins{sdk.NewInt64Coin(denom, 5)}, + expectedPriority: 5, + }, + { + name: "should return the discounted fee and priority (fee=10, priority=10, discount=10)", + fee: sdk.Coins{sdk.NewInt64Coin("token", 10)}, + priority: 10, + denom: denom, + discount: discount, + expectedFee: sdk.Coins{sdk.NewInt64Coin("token", 9)}, + expectedPriority: 11, + }, + { + name: "should return the discounted fee and priority (fee=100, priority=100, discount=10)", + fee: feeCoins, + priority: 100, + denom: denom, + discount: discount, + expectedFee: sdk.Coins{sdk.NewInt64Coin("token", 90)}, + expectedPriority: 110, + }, + { + name: "should return the discounted fee and priority (fee=100, priority=0, discount=10)", + fee: feeCoins, + priority: 0, + denom: denom, + discount: discount, + expectedFee: sdk.Coins{sdk.NewInt64Coin("token", 90)}, + expectedPriority: 0, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + fee, priority, err := ApplyFeeDiscount(tc.fee, tc.priority, tc.discount, tc.denom) + + if tc.expectedErr { + if err == nil { + t.Errorf("expected error, got nil") + } + if !strings.Contains(err.Error(), tc.errContains) { + t.Errorf("expected %s, got %s", tc.errContains, err.Error()) + } + } else { + if !tc.expectedErr && err != nil { + t.Errorf("expected nil error, got %v", err) + } + if tc.expectedPriority != priority { + t.Errorf("expected %d, got %d", tc.expectedPriority, priority) + } + if !tc.expectedFee.Equal(fee) { + t.Errorf("expected %v, got %v", tc.expectedFee, fee) + } + } + }) + } +} diff --git a/app/app.go b/app/app.go index d42ce2c..2c4d2fc 100644 --- a/app/app.go +++ b/app/app.go @@ -25,7 +25,6 @@ import ( distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" paramproposal "github.com/cosmos/cosmos-sdk/x/params/types/proposal" - ethante "github.com/cosmos/evm/ante/evm" "github.com/xrplevm/node/v9/app/ante" evmante "github.com/cosmos/evm/ante" @@ -888,7 +887,7 @@ func (app *App) setAnteHandler(txConfig client.TxConfig, maxGasWanted uint64) { SignModeHandler: txConfig.SignModeHandler(), SigGasConsumer: ante.SigVerificationGasConsumer, MaxTxGasWanted: maxGasWanted, - TxFeeChecker: ethante.NewDynamicFeeChecker(app.FeeMarketKeeper), + TxFeeChecker: ante.NewDynamicDiscountTxFeeChecker(app.FeeMarketKeeper, 50), StakingKeeper: app.StakingKeeper, DistributionKeeper: app.DistrKeeper, ExtraDecorator: poaante.NewPoaDecorator(),