diff --git a/app/app.go b/app/app.go index feebb451e..21375a22e 100644 --- a/app/app.go +++ b/app/app.go @@ -20,6 +20,7 @@ import ( tmos "github.com/cometbft/cometbft/libs/os" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdkmath "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" nodeservice "github.com/cosmos/cosmos-sdk/client/grpc/node" @@ -125,6 +126,65 @@ type TerraApp struct { configurator module.Configurator } +func (app *TerraApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { + res := app.BaseApp.CheckTx(req) + + // the ctx here is just for the logger, so it might be okay to remove it + // and the logging in the event parsing + ctx := app.NewContext(true, tmproto.Header{}) + + // fetch consumed tax gas from events + taxGas := sdkmath.ZeroInt() + for _, event := range res.Events { + if event.Type == "tax2gas" { + for _, attr := range event.Attributes { + if attr.Key == "tax_gas" { + value, ok := sdkmath.NewIntFromString(attr.Value) + if !ok { + ctx.Logger().Error("failed to parse tax gas from events", "value", attr.Value) + continue + } + + taxGas = taxGas.Add(value) + } + } + } + } + + if taxGas.IsZero() { + return res + } + + gasWanted := uint64(res.GasWanted) + gasUsed := uint64(res.GasUsed) + subTaxGas := taxGas.Uint64() + + // check how many times the gas used fits into the gas wanted + // if it doesn't fit, we need to adjust the gas wanted + multiple := sdkmath.LegacyNewDec(res.GasWanted).Quo(sdkmath.LegacyNewDec(res.GasUsed).Add(taxGas.ToLegacyDec())) + + // we have a multiplier, so we know approximately how often tax gas can be subtracted + // we should assume everything >= 0.9 should be subtracted. Using >= 1 would have issues with approximations + if multiple.GTE(sdkmath.LegacyNewDecWithPrec(9, 1)) { + // adjust gas wanted by the tax gas + subTaxGas = taxGas.ToLegacyDec().Mul(multiple).TruncateInt().Uint64() + } + + if gasWanted > subTaxGas { + gasWanted -= subTaxGas + } + + // maxGas := app.BaseApp.GetConsensusParams(ctx).Block.MaxGas + ctx.Logger().Info("CheckTx", "GasWanted", res.GasWanted, "GasUsed", res.GasUsed, "TaxGas", taxGas, "GasWantedAdjusted", gasWanted, "Multiple", multiple) + + // if the gas wanted is still higher than the gas used, we can adjust the gas wanted + if gasWanted >= gasUsed { + res.GasWanted = int64(gasWanted) + } + + return res +} + func init() { userHomeDir, err := os.UserHomeDir() if err != nil { diff --git a/custom/auth/ante/ante.go b/custom/auth/ante/ante.go index ddfcde150..8b1189dab 100644 --- a/custom/auth/ante/ante.go +++ b/custom/auth/ante/ante.go @@ -80,8 +80,9 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { } return sdk.ChainAnteDecorators( - ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first - wasmkeeper.NewLimitSimulationGasDecorator(options.WasmConfig.SimulationGasLimit), + // ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first + // wasmkeeper.NewLimitSimulationGasDecorator(options.WasmConfig.SimulationGasLimit), + tax2gaskeeper.NewTax2GasDecorator(options.WasmConfig.SimulationGasLimit), wasmkeeper.NewCountTXDecorator(options.TXCounterStoreKey), ante.NewExtensionOptionsDecorator(options.ExtensionOptionChecker), ante.NewValidateBasicDecorator(), diff --git a/custom/wasm/keeper/handler_plugin.go b/custom/wasm/keeper/handler_plugin.go index bff98dae1..3e7450593 100644 --- a/custom/wasm/keeper/handler_plugin.go +++ b/custom/wasm/keeper/handler_plugin.go @@ -81,6 +81,11 @@ func (h SDKMessageHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddr return nil, nil, err } + gasMeter, ok := ctx.GasMeter().(*tax2gastypes.Tax2GasMeter) + if !ok { + return nil, nil, errorsmod.Wrap(sdkerrors.ErrInvalidType, "invalid gas meter") + } + gasPrices, ok := ctx.Value(tax2gastypes.FinalGasPrices).(sdk.DecCoins) if !ok { gasPrices = h.tax2gaskeeper.GetGasPrices(ctx) @@ -96,7 +101,9 @@ func (h SDKMessageHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddr if err != nil { return nil, nil, err } - ctx.TaxGasMeter().ConsumeGas(taxGas, "tax gas") + + // Consume tax gas + gasMeter.ConsumeTax(taxGas, "tax gas") events = eventManager.Events() } diff --git a/go.mod b/go.mod index 7493ec5d6..298094426 100644 --- a/go.mod +++ b/go.mod @@ -229,7 +229,7 @@ replace ( // use cometbft github.com/cometbft/cometbft => github.com/classic-terra/cometbft v0.37.4-terra1 github.com/cometbft/cometbft-db => github.com/cometbft/cometbft-db v0.8.0 - github.com/cosmos/cosmos-sdk => github.com/classic-terra/cosmos-sdk v0.47.10-terra.1.0.20240731055430-cf7f52e8ee42 + github.com/cosmos/cosmos-sdk => github.com/classic-terra/cosmos-sdk v0.47.10-terra.1 github.com/cosmos/ibc-go/v7 => github.com/classic-terra/ibc-go/v7 v7.4.0-terra github.com/cosmos/ledger-cosmos-go => github.com/terra-money/ledger-terra-go v0.11.2 // replace goleveldb to optimized one diff --git a/go.sum b/go.sum index 63ebf74c2..2e4d7bee0 100644 --- a/go.sum +++ b/go.sum @@ -350,8 +350,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/classic-terra/cometbft v0.37.4-terra1 h1:eT5B2n5KKi5WVW+3ZNOVTmtfKKaZrXOLX9G80m9mhZo= github.com/classic-terra/cometbft v0.37.4-terra1/go.mod h1:vFqj7Qe3uFFJvHZleTJPQDmJ/WscXHi4rKWqiCAaNZk= -github.com/classic-terra/cosmos-sdk v0.47.10-terra.1.0.20240731055430-cf7f52e8ee42 h1:Dr00n/hlWF4biEN/i2MwmNk3iOZ1e+PUraf72gNEqYU= -github.com/classic-terra/cosmos-sdk v0.47.10-terra.1.0.20240731055430-cf7f52e8ee42/go.mod h1:4mBvTB8zevoeTuQufWwTcNnthGG2afXO+9D42BKzlRo= +github.com/classic-terra/cosmos-sdk v0.47.10-terra.1 h1:ek0vQ435fpeP3xGhszDO2yMIRy5XGMj9MCTlvpMUIkw= +github.com/classic-terra/cosmos-sdk v0.47.10-terra.1/go.mod h1:4mBvTB8zevoeTuQufWwTcNnthGG2afXO+9D42BKzlRo= github.com/classic-terra/goleveldb v0.0.0-20230914223247-2b28f6655121 h1:fjpWDB0hm225wYg9vunyDyTH8ftd5xEUgINJKidj+Tw= github.com/classic-terra/goleveldb v0.0.0-20230914223247-2b28f6655121/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/classic-terra/ibc-go/v7 v7.4.0-terra h1:hawaq62XKlxyc8xLyIcc6IujDDEbqDBU+2U15SF+hj8= diff --git a/wasmbinding/test/tax_test.go b/wasmbinding/test/tax_test.go index 6d53c3494..e1da5c604 100644 --- a/wasmbinding/test/tax_test.go +++ b/wasmbinding/test/tax_test.go @@ -6,12 +6,14 @@ import ( wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasmvmtypes "github.com/CosmWasm/wasmvm/types" core "github.com/classic-terra/core/v3/types" + tax2gastypes "github.com/classic-terra/core/v3/x/tax2gas/types" sdk "github.com/cosmos/cosmos-sdk/types" ) // go test -v -run ^TestWasmTestSuite/TestTax$ github.com/classic-terra/core/v3/wasmbinding/test func (s *WasmTestSuite) TestTax() { s.SetupTest() + s.Ctx = s.Ctx.WithGasMeter(tax2gastypes.NewTax2GasMeter(s.Ctx.GasMeter().Limit(), false)) taxRate := sdk.NewDecWithPrec(11, 2) // 11% s.App.TreasuryKeeper.SetTaxRate(s.Ctx, taxRate) // 11% diff --git a/x/tax2gas/ante/ante.go b/x/tax2gas/ante/ante.go index 1621943b8..c494ca570 100644 --- a/x/tax2gas/ante/ante.go +++ b/x/tax2gas/ante/ante.go @@ -99,9 +99,7 @@ func (fd FeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, nex // the tax gas that user need to pay priority = int64(math.MaxInt64) if !isOracleTx { - if taxGas.IsInt64() { - priority = taxGas.Int64() - } + priority = int64(1) } } @@ -115,7 +113,11 @@ func (fd FeeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, nex WithValue(types.TaxGas, taxGas). WithValue(types.FinalGasPrices, gasPrices) if !taxGas.IsZero() { - newCtx.TaxGasMeter().ConsumeGas(taxGas, "ante handler taxGas") + gasMeter, ok := ctx.GasMeter().(*types.Tax2GasMeter) + if !ok { + return ctx, errorsmod.Wrap(sdkerrors.ErrInvalidType, "invalid gas meter") + } + gasMeter.ConsumeTax(taxGas, "ante handler taxGas") } newCtx = newCtx.WithValue(types.AnteConsumedGas, gasConsumed) if paidDenom != "" { diff --git a/x/tax2gas/ante/ante_test.go b/x/tax2gas/ante/ante_test.go index ca2f73318..13038cf09 100644 --- a/x/tax2gas/ante/ante_test.go +++ b/x/tax2gas/ante/ante_test.go @@ -64,6 +64,7 @@ func (suite *AnteTestSuite) SetupTest(isCheckTx bool) { tempDir := suite.T().TempDir() suite.app, suite.ctx = createTestApp(isCheckTx, tempDir) suite.ctx = suite.ctx.WithBlockHeight(1) + suite.ctx = suite.ctx.WithGasMeter(tax2gastypes.NewTax2GasMeter(suite.ctx.GasMeter().Limit(), false)) // Set up TxConfig. encodingConfig := suite.SetupEncoding() diff --git a/x/tax2gas/ante/fee_test.go b/x/tax2gas/ante/fee_test.go index d6320e3cc..1ce2e77df 100644 --- a/x/tax2gas/ante/fee_test.go +++ b/x/tax2gas/ante/fee_test.go @@ -27,6 +27,7 @@ import ( ibctesting "github.com/cosmos/ibc-go/v7/testing" wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + tax2gastypes "github.com/classic-terra/core/v3/x/tax2gas/types" ) var ( @@ -436,6 +437,7 @@ func (s *AnteTestSuite) TestDeductFeeDecorator() { s.Run(tc.name, func() { tc.mallate() s.ctx = s.app.BaseApp.NewContext(tc.checkTx, tmproto.Header{}) + s.ctx = s.ctx.WithGasMeter(tax2gastypes.NewTax2GasMeter(s.ctx.GasMeter().Limit(), false)) _, err = antehandler(s.ctx, tx, tc.simulation) diff --git a/x/tax2gas/keeper/ante.go b/x/tax2gas/keeper/ante.go new file mode 100644 index 000000000..a58844b80 --- /dev/null +++ b/x/tax2gas/keeper/ante.go @@ -0,0 +1,122 @@ +package keeper + +import ( + "fmt" + + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + types "github.com/classic-terra/core/v3/x/tax2gas/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + ante "github.com/cosmos/cosmos-sdk/x/auth/ante" +) + +// this keeper has been taken from the wasmd package + +// Tax2GasDecorator ante decorator to limit gas in simulation calls +type Tax2GasDecorator struct { + gasLimit *sdk.Gas +} + +// NewTax2GasDecorator constructor accepts nil value to fallback to block gas limit. +func NewTax2GasDecorator(gasLimit *sdk.Gas) *Tax2GasDecorator { + if gasLimit != nil && *gasLimit == 0 { + panic("gas limit must not be zero") + } + + return &Tax2GasDecorator{gasLimit: gasLimit} +} + +// AnteHandle that limits the maximum gas available in simulations only. +// A custom max value can be configured and will be applied when set. The value should not +// exceed the max block gas limit. +// Different values on nodes are not consensus breaking as they affect only +// simulations but may have effect on client user experience. +// +// When no custom value is set then the max block gas is used as default limit. +func (d Tax2GasDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + gasTx, ok := tx.(ante.GasTx) + if !ok { + // Set a gas meter with limit 0 as to prevent an infinite gas meter attack + // during runTx. + newCtx = SetGasMeter(simulate, ctx, 0) + return newCtx, sdkerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must be GasTx") + } + + ctx.Logger().Debug("Tax2GasDecorator.AnteHandle", "simulate", simulate, "gaswanted", gasTx.GetGas()) + + newCtx = ctx // default to old context for defer handling + + // gas := gasTx.GetGas() + // newCtx = SetGasMeter(simulate, ctx, gas) + + // Decorator will catch an OutOfGasPanic caused in the next antehandler + // AnteHandlers must have their own defer/recover in order for the BaseApp + // to know how much gas was used! This is because the GasMeter is created in + // the AnteHandler, but if it panics the context won't be set properly in + // runTx's recover call. + defer func() { + if r := recover(); r != nil { + switch rType := r.(type) { + case sdk.ErrorOutOfGas: + log := fmt.Sprintf( + "out of gas in location: %v; gasWanted: %d, gasUsed: %d", + rType.Descriptor, gasTx.GetGas(), newCtx.GasMeter().GasConsumed()) + + err = sdkerrors.Wrap(sdkerrors.ErrOutOfGas, log) + default: + panic(r) + } + } + }() + + if !simulate { + // Wasm code is not executed in checkTX so that we don't need to limit it further. + // Tendermint rejects the TX afterwards when the tx.gas > max block gas. + // On deliverTX we rely on the tendermint/sdk mechanics that ensure + // tx has gas set and gas < max block gas + if ctx.BlockHeight() == 0 { + return next(ctx.WithGasMeter(types.NewTax2GasMeter(0, true)), tx, simulate) + } + + return next(ctx.WithGasMeter(types.NewTax2GasMeter(gasTx.GetGas(), false)), tx, simulate) + } + + // apply custom node gas limit + if d.gasLimit != nil { + return next(ctx.WithGasMeter(types.NewTax2GasMeter(*d.gasLimit, false)), tx, simulate) + } + + // default to max block gas when set, to be on the safe side + if maxGas := ctx.ConsensusParams().GetBlock().MaxGas; maxGas > 0 { + return next(ctx.WithGasMeter(types.NewTax2GasMeter(sdk.Gas(maxGas), false)), tx, simulate) + } + + // if no limit is set anywhere, we make it an infinite gas meter, like in the upstream modules + return next(ctx.WithGasMeter(types.NewTax2GasMeter(0, true)), tx, simulate) +} + +// GasRegisterDecorator ante decorator to store gas register in the context +type GasRegisterDecorator struct { + gasRegister wasmtypes.GasRegister +} + +// NewGasRegisterDecorator constructor. +func NewGasRegisterDecorator(gr wasmtypes.GasRegister) *GasRegisterDecorator { + return &GasRegisterDecorator{gasRegister: gr} +} + +// AnteHandle adds the gas register to the context. +func (g GasRegisterDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + return next(wasmtypes.WithGasRegister(ctx, g.gasRegister), tx, simulate) +} + +// SetGasMeter returns a new context with a gas meter set from a given context. +func SetGasMeter(simulate bool, ctx sdk.Context, gasLimit uint64) sdk.Context { + // In various cases such as simulation and during the genesis block, we do not + // meter any gas utilization. + if simulate || ctx.BlockHeight() == 0 { + return ctx.WithGasMeter(types.NewTax2GasMeter(0, true)) + } + + return ctx.WithGasMeter(types.NewTax2GasMeter(gasLimit, false)) +} diff --git a/x/tax2gas/post/fee_test.go b/x/tax2gas/post/fee_test.go new file mode 100644 index 000000000..3ccce564d --- /dev/null +++ b/x/tax2gas/post/fee_test.go @@ -0,0 +1,168 @@ +package post_test + +import ( + oracle "github.com/classic-terra/core/v3/x/oracle/types" + "github.com/classic-terra/core/v3/x/tax2gas/ante" + "github.com/classic-terra/core/v3/x/tax2gas/post" + tax2gastypes "github.com/classic-terra/core/v3/x/tax2gas/types" + "github.com/classic-terra/core/v3/x/tax2gas/utils" + "github.com/classic-terra/core/v3/x/treasury/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/bank/testutil" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +func (s *PostTestSuite) TestDeductFeeDecorator() { + anteConsumedFee := int64(207566) + postConsumedFee := int64(1368041) + + testCases := []struct { + name string + simulation bool + checkTx bool + setupFunc func(addr sdk.AccAddress, tx signing.Tx) + expectedOracle sdk.Coins + expectedCp sdk.Coins + expectedBurn sdk.Coins + expFail bool + expErrMsg string + }{ + { + name: "happy case", + setupFunc: func(addr sdk.AccAddress, tx signing.Tx) { + s.setupTestCase(addr, 100000000, 100000, anteConsumedFee+500022) + }, + // amount: 100000000 + // tax(0.5%): 500022 + // use default value + // burn: tax * 0.9 + // distributtion: tax * 0.1 = 500022 + // cp: 2% of distribution: 499994 * 0.1 * 0.02 ~ 1000 + // oracle: distribution - oracle = 49002 + expectedBurn: sdk.NewCoins(sdk.NewInt64Coin("uluna", 450020)), + expectedCp: sdk.NewCoins(sdk.NewInt64Coin("uluna", 1000)), + expectedOracle: sdk.NewCoins(sdk.NewInt64Coin("uluna", 49002)), + }, + { + name: "not enough fee", + setupFunc: func(addr sdk.AccAddress, tx signing.Tx) { + s.setupTestCase(addr, 100000000, 100000, 600000) + }, + expFail: true, + }, + { + name: "combine ante + post, not enough fee", + setupFunc: func(addr sdk.AccAddress, tx signing.Tx) { + s.setupCombineAnteAndPost(addr, tx, 100000, 7328, 707559) + }, + expFail: true, + }, + { + name: "combine ante + post, enough fee", + setupFunc: func(addr sdk.AccAddress, tx signing.Tx) { + s.setupCombineAnteAndPost(addr, tx, 100000000, 100000, anteConsumedFee+postConsumedFee+500022) + }, + expFail: false, + expectedOracle: sdk.NewCoins(sdk.NewInt64Coin("uluna", 49002)), + expectedCp: sdk.NewCoins(sdk.NewInt64Coin("uluna", 1000)), + expectedBurn: sdk.NewCoins(sdk.NewInt64Coin("uluna", 450020)), + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + // Reset the entire app and context for each test case + s.SetupTest(true) + s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() + deco := post.NewTax2GasPostDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.DistrKeeper, s.app.Tax2gasKeeper) + posthandler := sdk.ChainPostDecorators(deco) + + // Generate a new address for each test case + priv1, _, addr1 := testdata.KeyTestPubAddr() + + // Fund the account + coins := sdk.NewCoins(sdk.NewCoin("uluna", sdk.NewInt(10000000))) + testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) + + // Create a new test transaction + privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} + tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) + s.Require().NoError(err) + + tc.setupFunc(addr1, tx) + + _, err = posthandler(s.ctx, tx, tc.simulation, true) + s.assertTestCase(tc, err) + }) + } +} + +func (s *PostTestSuite) setupTestCase(addr sdk.AccAddress, sendAmount, gasLimit, feeAmount int64) { + msg := banktypes.NewMsgSend(addr, addr, sdk.NewCoins(sdk.NewCoin("uluna", sdk.NewInt(sendAmount)))) + gm := tax2gastypes.NewTax2GasMeter(s.ctx.GasMeter().Limit(), false) + s.setupTax2GasMeter(gm, msg) + s.ctx = s.ctx.WithGasMeter(gm) + s.ctx = s.ctx.WithValue(tax2gastypes.AnteConsumedGas, uint64(7328)) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + s.txBuilder.SetGasLimit(uint64(gasLimit)) + s.ctx = s.ctx.WithValue(tax2gastypes.PaidDenom, "uluna") + s.txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewInt64Coin("uluna", feeAmount))) +} + +func (s *PostTestSuite) setupCombineAnteAndPost(addr sdk.AccAddress, tx signing.Tx, sendAmount, gasLimit, feeAmount int64) { + msg := banktypes.NewMsgSend(addr, addr, sdk.NewCoins(sdk.NewCoin("uluna", sdk.NewInt(sendAmount)))) + gm := tax2gastypes.NewTax2GasMeter(s.ctx.GasMeter().Limit(), false) + // s.setupTax2GasMeter(gm, msg) + mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.Tax2gasKeeper) + s.txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewInt64Coin("uluna", feeAmount))) + s.txBuilder.SetGasLimit(uint64(gasLimit)) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + s.ctx = s.ctx.WithGasMeter(gm) + + antehandler := sdk.ChainAnteDecorators(mfd) + newCtx, _ := antehandler(s.ctx, tx, false) + distrBalance := s.app.BankKeeper.GetBalance(s.ctx, s.app.AccountKeeper.GetModuleAddress(authtypes.FeeCollectorName), "uluna") + + // msg send consume 7328 gas, uluna gas price: 28.325 + // expect only this consumed gas is transferred to fee collector account after ante. + s.Require().Equal(sdk.NewDecWithPrec(7328, 0).Mul(sdk.NewDecWithPrec(28325, 3)).RoundInt64(), distrBalance.Amount.Int64()) + s.ctx = newCtx +} + +func (s *PostTestSuite) setupTax2GasMeter(gm sdk.GasMeter, msg sdk.Msg) { + burnTaxRate := s.app.Tax2gasKeeper.GetBurnTaxRate(s.ctx) + lunaGasPrice := sdk.NewDecCoinFromDec("uluna", sdk.NewDecWithPrec(28325, 3)) + taxes := utils.FilterMsgAndComputeTax(s.ctx, s.app.TreasuryKeeper, burnTaxRate, msg) + taxGas, _ := utils.ComputeGas(sdk.DecCoins{lunaGasPrice}, taxes) + gm.(*tax2gastypes.Tax2GasMeter).ConsumeTax(taxGas, "tax") +} + +func (s *PostTestSuite) assertTestCase(tc struct { + name string + simulation bool + checkTx bool + setupFunc func(addr sdk.AccAddress, tx signing.Tx) + expectedOracle sdk.Coins + expectedCp sdk.Coins + expectedBurn sdk.Coins + expFail bool + expErrMsg string +}, err error, +) { + currentOracle := s.app.BankKeeper.GetBalance(s.ctx, s.app.AccountKeeper.GetModuleAddress(oracle.ModuleName), "uluna") + currentCp := s.app.DistrKeeper.GetFeePoolCommunityCoins(s.ctx).AmountOf("uluna") + currentBurn := s.app.BankKeeper.GetBalance(s.ctx, s.app.AccountKeeper.GetModuleAddress(types.BurnModuleName), "uluna") + if tc.expFail { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.expErrMsg) + } else { + s.Require().NoError(err) + } + s.Require().Equal(tc.expectedOracle.AmountOf("uluna").Int64(), currentOracle.Amount.Int64()) + s.Require().Equal(tc.expectedCp.AmountOf("uluna").Int64(), currentCp.RoundInt64()) + s.Require().Equal(tc.expectedBurn.AmountOf("uluna").Int64(), currentBurn.Amount.Int64()) +} diff --git a/x/tax2gas/post/post.go b/x/tax2gas/post/post.go index 8435ecfe7..e28daf4d8 100644 --- a/x/tax2gas/post/post.go +++ b/x/tax2gas/post/post.go @@ -1,6 +1,8 @@ package post import ( + "fmt" + sdkmath "cosmossdk.io/math" errorsmod "cosmossdk.io/errors" @@ -35,6 +37,11 @@ func NewTax2GasPostDecorator(accountKeeper ante.AccountKeeper, bankKeeper types. } func (tgd Tax2gasPostDecorator) PostHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, success bool, next sdk.PostHandler) (sdk.Context, error) { + gasMeter, ok := ctx.GasMeter().(*types.Tax2GasMeter) + if !ok { + return ctx, fmt.Errorf("expected Tax2GasMeter, got %T", ctx.GasMeter()) + } + feeTx, ok := tx.(sdk.FeeTx) if !ok { return ctx, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") @@ -77,10 +84,13 @@ func (tgd Tax2gasPostDecorator) PostHandle(ctx sdk.Context, tx sdk.Tx, simulate if !simulate { // Deduct feeCoins with paid amount + if paidAmount.Ceil().RoundInt().Int64() > feeCoins.AmountOf(paidDenom).Int64() { + return ctx, errorsmod.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fee: %s", feeCoins) + } feeCoins = feeCoins.Sub(sdk.NewCoin(paidDenom, paidAmount.Ceil().RoundInt())) } - taxGas := ctx.TaxGasMeter().GasConsumed() + taxGas := gasMeter.TaxConsumed() // we consume the gas here as we need to calculate the tax for consumed gas // if the gas overflow, then that means the tx can't be estimates as normal way @@ -92,11 +102,16 @@ func (tgd Tax2gasPostDecorator) PostHandle(ctx sdk.Context, tx sdk.Tx, simulate // Check if gas not overflow if totalGasConsumed+taxGasUint64 >= totalGasConsumed && totalGasConsumed+taxGasUint64 >= taxGasUint64 { if simulate { - ctx.GasMeter().ConsumeGas(taxGasUint64, "consume tax gas") + gasMeter.DisableGasLimitEnforcement() + defer gasMeter.EnableGasLimitEnforcement() + gasMeter.ConsumeGas(taxGasUint64, "consume tax gas") } } } + // add the consumed tax gas to the transaction logs + ctx.EventManager().EmitEvent(sdk.NewEvent("tax2gas", sdk.NewAttribute("tax_gas", taxGas.String()))) + // Deduct the gas consumed amount spent on ante handler totalGasRemaining := sdkmath.NewInt(int64(totalGasConsumed - anteConsumedGas)).Add(taxGas) @@ -146,12 +161,11 @@ func (tgd Tax2gasPostDecorator) PostHandle(ctx sdk.Context, tx sdk.Tx, simulate // First, we will deduct the fees covered taxGas and handle BurnTaxSplit taxes, payableFees, gasRemaining := tax2gasutils.CalculateTaxesAndPayableFee(gasPrices, feeCoins, taxGas, totalGasRemaining) - if !simulate && !ctx.IsCheckTx() && gasRemaining.IsPositive() { + if !simulate && gasRemaining.IsPositive() { gasRemainingFees, err := tax2gasutils.ComputeFeesOnGasConsumed(tx, gasPrices, gasRemaining) if err != nil { return ctx, err } - return ctx, errorsmod.Wrapf(sdkerrors.ErrInsufficientFee, "fees are not enough to pay for gas, need to cover %s gas more, which equal to %q ", gasRemaining.String(), gasRemainingFees) } feePayerAccount := tgd.accountKeeper.GetAccount(ctx, feePayer) diff --git a/x/tax2gas/post/post_test.go b/x/tax2gas/post/post_test.go new file mode 100644 index 000000000..0f1de0f3e --- /dev/null +++ b/x/tax2gas/post/post_test.go @@ -0,0 +1,134 @@ +package post_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + dbm "github.com/cometbft/cometbft-db" + "github.com/cometbft/cometbft/libs/log" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + xauthsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + + terraapp "github.com/classic-terra/core/v3/app" + tax2gastypes "github.com/classic-terra/core/v3/x/tax2gas/types" + treasurytypes "github.com/classic-terra/core/v3/x/treasury/types" +) + +type PostTestSuite struct { + suite.Suite + + app *terraapp.TerraApp + // anteHandler sdk.AnteHandler + ctx sdk.Context + clientCtx client.Context + txBuilder client.TxBuilder +} + +// returns context and app with params set on account keeper +func createTestApp(isCheckTx bool, tempDir string) (*terraapp.TerraApp, sdk.Context) { + // TODO: we need to feed in custom binding here? + var wasmOpts []wasmkeeper.Option + app := terraapp.NewTerraApp( + log.NewNopLogger(), dbm.NewMemDB(), nil, true, map[int64]bool{}, + tempDir, terraapp.MakeEncodingConfig(), + simtestutil.EmptyAppOptions{}, wasmOpts, + ) + ctx := app.BaseApp.NewContext(isCheckTx, tmproto.Header{}) + app.AccountKeeper.SetParams(ctx, authtypes.DefaultParams()) + app.TreasuryKeeper.SetParams(ctx, treasurytypes.DefaultParams()) + app.DistrKeeper.SetParams(ctx, distributiontypes.DefaultParams()) + app.DistrKeeper.SetFeePool(ctx, distributiontypes.InitialFeePool()) + tax2gasParams := tax2gastypes.DefaultParams() + tax2gasParams.Enabled = true + app.Tax2gasKeeper.SetParams(ctx, tax2gasParams) + + return app, ctx +} + +// SetupTest setups a new test, with new app, context, and anteHandler. +func (suite *PostTestSuite) SetupTest(isCheckTx bool) { + tempDir := suite.T().TempDir() + suite.app, suite.ctx = createTestApp(isCheckTx, tempDir) + suite.ctx = suite.ctx.WithBlockHeight(1) + suite.ctx = suite.ctx.WithGasMeter(tax2gastypes.NewTax2GasMeter(suite.ctx.GasMeter().Limit(), false)) + + // Set up TxConfig. + encodingConfig := suite.SetupEncoding() + + suite.clientCtx = client.Context{}. + WithTxConfig(encodingConfig.TxConfig) +} + +func (suite *PostTestSuite) SetupEncoding() testutil.TestEncodingConfig { + encodingConfig := testutil.MakeTestEncodingConfig() + // We're using TestMsg encoding in some tests, so register it here. + encodingConfig.Amino.RegisterConcrete(&testdata.TestMsg{}, "testdata.TestMsg", nil) + testdata.RegisterInterfaces(encodingConfig.InterfaceRegistry) + + return encodingConfig +} + +// CreateTestTx is a helper function to create a tx given multiple inputs. +func (suite *PostTestSuite) CreateTestTx(privs []cryptotypes.PrivKey, accNums []uint64, accSeqs []uint64, chainID string) (xauthsigning.Tx, error) { + // First round: we gather all the signer infos. We use the "set empty + // signature" hack to do that. + var sigsV2 []signing.SignatureV2 + for i, priv := range privs { + sigV2 := signing.SignatureV2{ + PubKey: priv.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: suite.clientCtx.TxConfig.SignModeHandler().DefaultMode(), + Signature: nil, + }, + Sequence: accSeqs[i], + } + + sigsV2 = append(sigsV2, sigV2) + } + err := suite.txBuilder.SetSignatures(sigsV2...) + if err != nil { + return nil, err + } + + // Second round: all signer infos are set, so each signer can sign. + sigsV2 = []signing.SignatureV2{} + for i, priv := range privs { + signerData := xauthsigning.SignerData{ + ChainID: chainID, + AccountNumber: accNums[i], + Sequence: accSeqs[i], + } + sigV2, err := tx.SignWithPrivKey( + suite.clientCtx.TxConfig.SignModeHandler().DefaultMode(), signerData, + suite.txBuilder, priv, suite.clientCtx.TxConfig, accSeqs[i]) + if err != nil { + return nil, err + } + + sigsV2 = append(sigsV2, sigV2) + } + err = suite.txBuilder.SetSignatures(sigsV2...) + if err != nil { + return nil, err + } + + return suite.txBuilder.GetTx(), nil +} + +func TestPostTestSuite(t *testing.T) { + suite.Run(t, new(PostTestSuite)) +} diff --git a/x/tax2gas/types/gas.go b/x/tax2gas/types/gas.go new file mode 100644 index 000000000..fc354fe98 --- /dev/null +++ b/x/tax2gas/types/gas.go @@ -0,0 +1,155 @@ +package types + +import ( + "fmt" + "math" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type ErrorNegativeGasConsumed struct { + Descriptor string +} + +type Tax2GasMeter struct { + limit sdk.Gas + consumed sdk.Gas + taxConsumed sdkmath.Int + limitEnforced bool + infinite bool +} + +// NewGasMeter returns a reference to a new Tax2GasMeter. +func NewTax2GasMeter(limit sdk.Gas, infinite bool) sdk.GasMeter { + return &Tax2GasMeter{ + limit: limit, + consumed: 0, + taxConsumed: sdkmath.ZeroInt(), + limitEnforced: true, + infinite: infinite, + } +} + +// GasConsumed returns the gas consumed from the GasMeter. +func (g *Tax2GasMeter) GasConsumed() sdk.Gas { + return g.consumed +} + +// TaxConsumed returns the tax consumed from the GasMeter. +func (g *Tax2GasMeter) TaxConsumed() sdkmath.Int { + return g.taxConsumed +} + +// GasRemaining returns the gas left in the GasMeter. +func (g *Tax2GasMeter) GasRemaining() sdk.Gas { + if g.infinite { + return math.MaxUint64 + } + + if g.IsPastLimit() { + return 0 + } + return g.limit - g.consumed +} + +// Limit returns the gas limit of the GasMeter. +func (g *Tax2GasMeter) Limit() sdk.Gas { + if g.infinite { + return math.MaxUint64 + } + + return g.limit +} + +// GasConsumedToLimit returns the gas limit if gas consumed is past the limit, +// otherwise it returns the consumed gas. +// +// NOTE: This behavior is only called when recovering from panic when +// BlockGasMeter consumes gas past the limit. +func (g *Tax2GasMeter) GasConsumedToLimit() sdk.Gas { + if g.infinite { + return g.consumed + } + + if g.limitEnforced { + if g.IsPastLimit() { + return g.limit + } + return g.consumed + } + + // When limit enforcement is disabled, return the limit to prevent panics in consumeBlockGas + return g.limit +} + +// addUint64Overflow performs the addition operation on two uint64 integers and +// returns a boolean on whether or not the result overflows. +func addUint64Overflow(a, b uint64) (uint64, bool) { + if math.MaxUint64-a < b { + return 0, true + } + + return a + b, false +} + +// ConsumeGas adds the given amount of gas to the gas consumed and panics if it overflows the limit or out of gas. +func (g *Tax2GasMeter) ConsumeGas(amount sdk.Gas, descriptor string) { + var overflow bool + g.consumed, overflow = addUint64Overflow(g.consumed, amount) + if overflow { + g.consumed = math.MaxUint64 + panic(sdk.ErrorGasOverflow{Descriptor: descriptor}) + } + + if !g.infinite && g.limitEnforced && g.consumed > g.limit { + panic(sdk.ErrorOutOfGas{Descriptor: descriptor}) + } +} + +func (g *Tax2GasMeter) ConsumeTax(amount sdkmath.Int, descriptor string) { + if amount.IsNegative() { + panic(ErrorNegativeGasConsumed{Descriptor: descriptor}) + } + + g.taxConsumed = g.taxConsumed.Add(amount) +} + +// RefundGas will deduct the given amount from the gas consumed. If the amount is greater than the +// gas consumed, the function will panic. +// +// Use case: This functionality enables refunding gas to the transaction or block gas pools so that +// EVM-compatible chains can fully support the go-ethereum StateDb interface. +// See https://github.com/cosmos/cosmos-sdk/pull/9403 for reference. +func (g *Tax2GasMeter) RefundGas(amount sdk.Gas, descriptor string) { + if g.consumed < amount { + panic(ErrorNegativeGasConsumed{Descriptor: descriptor}) + } + + g.consumed -= amount +} + +// IsPastLimit returns true if gas consumed is past limit, otherwise it returns false. +func (g *Tax2GasMeter) IsPastLimit() bool { + return !g.infinite && g.limitEnforced && g.consumed > g.limit +} + +// IsOutOfGas returns true if gas consumed is greater than or equal to gas limit, otherwise it returns false. +func (g *Tax2GasMeter) IsOutOfGas() bool { + return !g.infinite && g.limitEnforced && g.consumed >= g.limit +} + +// String returns the Tax2GasMeter's gas limit and gas consumed. +func (g *Tax2GasMeter) String() string { + return fmt.Sprintf("Tax2GasMeter:\n limit: %d\n consumed: %d", g.limit, g.consumed) +} + +// EnableGasLimitEnforcement enables the gas limit enforcement. +func (g *Tax2GasMeter) EnableGasLimitEnforcement() { + g.limitEnforced = true +} + +// DisableGasLimitEnforcement disables the gas limit enforcement. +func (g *Tax2GasMeter) DisableGasLimitEnforcement() { + g.limitEnforced = false +}