From ba7dbbfe2896de782a00b6bc7deaeaa84468538c Mon Sep 17 00:00:00 2001 From: Anh Minh <1phamminh0811@gmail.com> Date: Thu, 8 Aug 2024 18:07:48 +0700 Subject: [PATCH 1/7] Use wasmvm v1.5.3 --- app/app.go | 2 ++ app/upgrades/v8_2/constants.go | 13 +++++++++++++ app/upgrades/v8_2/upgrades.go | 21 +++++++++++++++++++++ go.mod | 4 ++-- go.sum | 8 ++++---- 5 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 app/upgrades/v8_2/constants.go create mode 100644 app/upgrades/v8_2/upgrades.go diff --git a/app/app.go b/app/app.go index 823d41c85..88268476a 100644 --- a/app/app.go +++ b/app/app.go @@ -56,6 +56,7 @@ import ( v7_1 "github.com/classic-terra/core/v3/app/upgrades/v7_1" v8 "github.com/classic-terra/core/v3/app/upgrades/v8" v8_1 "github.com/classic-terra/core/v3/app/upgrades/v8_1" + v8_2 "github.com/classic-terra/core/v3/app/upgrades/v8_2" customante "github.com/classic-terra/core/v3/custom/auth/ante" custompost "github.com/classic-terra/core/v3/custom/auth/post" @@ -87,6 +88,7 @@ var ( v7_1.Upgrade, v8.Upgrade, v8_1.Upgrade, + v8_2.Upgrade, } // Forks defines forks to be applied to the network diff --git a/app/upgrades/v8_2/constants.go b/app/upgrades/v8_2/constants.go new file mode 100644 index 000000000..9a3739067 --- /dev/null +++ b/app/upgrades/v8_2/constants.go @@ -0,0 +1,13 @@ +//nolint:revive +package v8_2 + +import ( + "github.com/classic-terra/core/v3/app/upgrades" +) + +const UpgradeName = "v8_2" + +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateV82UpgradeHandler, +} diff --git a/app/upgrades/v8_2/upgrades.go b/app/upgrades/v8_2/upgrades.go new file mode 100644 index 000000000..4182109c0 --- /dev/null +++ b/app/upgrades/v8_2/upgrades.go @@ -0,0 +1,21 @@ +//nolint:revive +package v8_2 + +import ( + "github.com/classic-terra/core/v3/app/keepers" + "github.com/classic-terra/core/v3/app/upgrades" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" +) + +func CreateV82UpgradeHandler( + mm *module.Manager, + cfg module.Configurator, + _ upgrades.BaseAppParamManager, + keepers *keepers.AppKeepers, +) upgradetypes.UpgradeHandler { + return func(ctx sdk.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + return mm.RunMigrations(ctx, cfg, fromVM) + } +} diff --git a/go.mod b/go.mod index c0b02b491..a3fdda590 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( cosmossdk.io/math v1.3.0 cosmossdk.io/simapp v0.0.0-20230602123434-616841b9704d github.com/CosmWasm/wasmd v0.45.0 - github.com/CosmWasm/wasmvm v1.5.2 + github.com/CosmWasm/wasmvm v1.5.3 github.com/cometbft/cometbft v0.37.4 github.com/cometbft/cometbft-db v0.8.0 github.com/cosmos/cosmos-sdk v0.47.10 @@ -225,7 +225,7 @@ replace ( ) replace ( - github.com/CosmWasm/wasmd => github.com/classic-terra/wasmd v0.45.0-terra.5 + github.com/CosmWasm/wasmd => github.com/classic-terra/wasmd v0.45.0-terra.6 // 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 diff --git a/go.sum b/go.sum index bfc5d875b..a25ec6c0b 100644 --- a/go.sum +++ b/go.sum @@ -222,8 +222,8 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg6 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= -github.com/CosmWasm/wasmvm v1.5.2 h1:+pKB1Mz9GZVt1vadxB+EDdD1FOz3dMNjIKq/58/lrag= -github.com/CosmWasm/wasmvm v1.5.2/go.mod h1:Q0bSEtlktzh7W2hhEaifrFp1Erx11ckQZmjq8FLCyys= +github.com/CosmWasm/wasmvm v1.5.3 h1:wcmkey/WkTGwCTHGBD+fRS3cbhhipR9q34kbCZUQSsc= +github.com/CosmWasm/wasmvm v1.5.3/go.mod h1:Q0bSEtlktzh7W2hhEaifrFp1Erx11ckQZmjq8FLCyys= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= @@ -356,8 +356,8 @@ github.com/classic-terra/goleveldb v0.0.0-20230914223247-2b28f6655121 h1:fjpWDB0 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= github.com/classic-terra/ibc-go/v7 v7.4.0-terra/go.mod h1:s0lxNkjVIqsb8AVltL0qhzxeLgOKvWZrknPuvgjlEQ8= -github.com/classic-terra/wasmd v0.45.0-terra.5 h1:0fuc4lS1Z0Egci6hwK4DZqOwF2l9fGsZuPTZ1akObFY= -github.com/classic-terra/wasmd v0.45.0-terra.5/go.mod h1:r/AxjzSLyrQbrANEsV/d+qf/vmCDFiAoVwWenGs23ZY= +github.com/classic-terra/wasmd v0.45.0-terra.6 h1:ytQsD5/aGGIb/CQ+AiP1GO8AxCZGX4IgijDP+jyShYE= +github.com/classic-terra/wasmd v0.45.0-terra.6/go.mod h1:TZFaZdgesx5DJtQzvZFFEZRglQtTYoUCK3ygpvA3NgU= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304= From 8a1ad12bfaf2ef96737b6aed892f0d794980c097 Mon Sep 17 00:00:00 2001 From: Anh Minh <1phamminh0811@gmail.com> Date: Thu, 8 Aug 2024 18:11:58 +0700 Subject: [PATCH 2/7] update old version --- contrib/updates/prepare_cosmovisor.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/updates/prepare_cosmovisor.sh b/contrib/updates/prepare_cosmovisor.sh index 703be14a9..17d983443 100644 --- a/contrib/updates/prepare_cosmovisor.sh +++ b/contrib/updates/prepare_cosmovisor.sh @@ -5,7 +5,7 @@ # These fields should be fetched automatically in the future # Need to do more upgrade to see upgrade patterns -OLD_VERSION=v3.0.3 +OLD_VERSION=v3.1.3 # this command will retrieve the folder with the largest number in format v SOFTWARE_UPGRADE_NAME=$(ls -d -- ./app/upgrades/v* | sort -Vr | head -n 1 | xargs basename) BUILDDIR=$1 From d8fd9f72edda19d69994126237340eae9528a2d6 Mon Sep 17 00:00:00 2001 From: Anh Minh <1phamminh0811@gmail.com> Date: Thu, 8 Aug 2024 18:19:10 +0700 Subject: [PATCH 3/7] use docker compose instead of docker-compose --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index f521db964..6b23e4ee3 100755 --- a/Makefile +++ b/Makefile @@ -336,21 +336,21 @@ localnet-start: localnet-stop build-linux -v /etc/shadow:/etc/shadow:ro \ classic-terra/terrad-env testnet --chain-id ${TESTNET_CHAINID} --v ${TESTNET_NVAL} -o . --starting-ip-address 192.168.10.2 --keyring-backend=test; \ fi - docker-compose up -d + docker compose up -d localnet-start-upgrade: localnet-upgrade-stop build-linux $(MAKE) -C contrib/updates build-cosmovisor-linux BUILDDIR=$(BUILDDIR) $(if $(shell $(DOCKER) inspect -f '{{ .Id }}' classic-terra/terrad-upgrade-env 2>/dev/null),$(info found image classic-terra/terrad-upgrade-env),$(MAKE) -C contrib/localnet terrad-upgrade-env) bash contrib/updates/prepare_cosmovisor.sh $(BUILDDIR) ${TESTNET_NVAL} ${TESTNET_CHAINID} - docker-compose -f ./contrib/updates/docker-compose.yml up -d + docker compose -f ./contrib/updates/docker-compose.yml up -d localnet-upgrade-stop: - docker-compose -f ./contrib/updates/docker-compose.yml down + docker compose -f ./contrib/updates/docker-compose.yml down rm -rf build/node* rm -rf build/gentxs localnet-stop: - docker-compose down + docker compose down rm -rf build/node* rm -rf build/gentxs From c5268e14ddbb82eafe372d0a6b71c300a21a59fb Mon Sep 17 00:00:00 2001 From: Inon Man <121477599+inon-man@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:13:12 +0900 Subject: [PATCH 4/7] build(deps): wasmvm v1.5.4 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a3fdda590..ad62abf92 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( cosmossdk.io/math v1.3.0 cosmossdk.io/simapp v0.0.0-20230602123434-616841b9704d github.com/CosmWasm/wasmd v0.45.0 - github.com/CosmWasm/wasmvm v1.5.3 + github.com/CosmWasm/wasmvm v1.5.4 github.com/cometbft/cometbft v0.37.4 github.com/cometbft/cometbft-db v0.8.0 github.com/cosmos/cosmos-sdk v0.47.10 diff --git a/go.sum b/go.sum index a25ec6c0b..f7f4f93b0 100644 --- a/go.sum +++ b/go.sum @@ -222,8 +222,8 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg6 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= -github.com/CosmWasm/wasmvm v1.5.3 h1:wcmkey/WkTGwCTHGBD+fRS3cbhhipR9q34kbCZUQSsc= -github.com/CosmWasm/wasmvm v1.5.3/go.mod h1:Q0bSEtlktzh7W2hhEaifrFp1Erx11ckQZmjq8FLCyys= +github.com/CosmWasm/wasmvm v1.5.4 h1:Opqy65ubJ8bMsT08dn85VjRdsLJVPIAgIXif92qOMGc= +github.com/CosmWasm/wasmvm v1.5.4/go.mod h1:Q0bSEtlktzh7W2hhEaifrFp1Erx11ckQZmjq8FLCyys= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= From 8aff783dcd2c68c847424cf443c891e7ade602aa Mon Sep 17 00:00:00 2001 From: Inon Man <121477599+inon-man@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:20:51 +0900 Subject: [PATCH 5/7] build(ci): mergify rules for backporting to release/v3.1.x --- .mergify.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index 11d7354c3..49af9739a 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,26 +1,32 @@ +defaults: + actions: + backport: + assignees: + - "{{ author }}" + queue_rules: - name: default conditions: - - "#approved-reviews-by>2" + - "#approved-reviews-by>1" + commit_message_template: | + {{ title }} (#{{ number }}) + {{ body }} pull_request_rules: - - name: automerge to main with label automerge and branch protection passing + - name: Automatic merge on approval to the main branch conditions: - - "#approved-reviews-by>2" + - "#approved-reviews-by>=1" - base=main - label=A:automerge actions: queue: name: default method: squash - commit_message_template: | - {{ title }} (#{{ number }}) - {{ body }} - - name: backport patches to v2.1.x branch + - name: Backport patches to release/v3.1.x branch conditions: - base=main - - label=backport/v2.1.x + - label=backport/v3.1.x actions: backport: branches: - - release/v2.1.x + - release/v3.1.x \ No newline at end of file From 29a5a45694a76591e3cb54d050537cf36090de58 Mon Sep 17 00:00:00 2001 From: Inon Man <121477599+inon-man@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:23:01 +0900 Subject: [PATCH 6/7] tests(localnet): upgrade test for core v3.1.4 --- contrib/updates/prepare_cosmovisor.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/updates/prepare_cosmovisor.sh b/contrib/updates/prepare_cosmovisor.sh index 17d983443..4fc2c50b7 100644 --- a/contrib/updates/prepare_cosmovisor.sh +++ b/contrib/updates/prepare_cosmovisor.sh @@ -5,7 +5,7 @@ # These fields should be fetched automatically in the future # Need to do more upgrade to see upgrade patterns -OLD_VERSION=v3.1.3 +OLD_VERSION=v3.1.4 # this command will retrieve the folder with the largest number in format v SOFTWARE_UPGRADE_NAME=$(ls -d -- ./app/upgrades/v* | sort -Vr | head -n 1 | xargs basename) BUILDDIR=$1 From ae9f72de6074975f71a5e323f7377356e5cb84d7 Mon Sep 17 00:00:00 2001 From: Anh Minh <1phamminh0811@gmail.com> Date: Tue, 13 Aug 2024 10:59:42 +0700 Subject: [PATCH 7/7] merge main and update wasmd --- app/app.go | 15 +- app/keepers/keepers.go | 18 +- app/modules.go | 7 + app/sim_test.go | 24 +- app/upgrades/v8_2/constants.go | 13 - app/upgrades/v9/constants.go | 20 + app/upgrades/{v8_2 => v9}/upgrades.go | 16 +- custom/auth/ante/ante.go | 8 +- custom/auth/ante/expected_keeper.go | 6 + custom/auth/ante/fee.go | 219 ---- custom/auth/ante/fee_tax.go | 128 --- custom/auth/ante/fee_test.go | 1005 ----------------- custom/auth/ante/integration_test.go | 220 ---- custom/auth/client/utils/feeutils.go | 143 +-- custom/auth/post/post.go | 13 +- custom/auth/tx/service.go | 11 +- custom/wasm/keeper/handler_plugin.go | 49 +- go.mod | 2 +- go.sum | 4 +- proto/terra/tax2gas/v1beta1/genesis.proto | 45 + proto/terra/tax2gas/v1beta1/query.proto | 31 + proto/terra/tax2gas/v1beta1/tx.proto | 38 + scripts/protocgen.sh | 3 - tests/e2e/configurer/chain/commands.go | 121 +- tests/e2e/containers/containers.go | 18 + tests/e2e/e2e_test.go | 421 ++++++- tests/e2e/initialization/config.go | 67 +- tests/e2e/scripts/forwarder.wasm | Bin 0 -> 167215 bytes tests/e2e/scripts/hermes_bootstrap.sh | 8 +- tests/interchaintest/setup.go | 4 + x/tax2gas/README.md | 27 + x/tax2gas/ante/ante.go | 267 +++++ x/tax2gas/ante/ante_test.go | 133 +++ x/tax2gas/ante/fee_test.go | 666 +++++++++++ .../tax2gas}/ante/testdata/hackatom.wasm | Bin x/tax2gas/client/cli/query.go | 89 ++ x/tax2gas/client/cli/tx.go | 21 + x/tax2gas/exported/alias.go | 12 + x/tax2gas/genesis.go | 14 + x/tax2gas/keeper/keeper.go | 83 ++ x/tax2gas/keeper/keeper_test.go | 122 ++ x/tax2gas/keeper/msg_server.go | 36 + x/tax2gas/keeper/msg_server_test.go | 60 + x/tax2gas/keeper/params.go | 34 + x/tax2gas/keeper/querier.go | 23 + x/tax2gas/keeper/querier_test.go | 14 + x/tax2gas/module.go | 122 ++ .../tax2gas/post/burntax.go | 6 +- x/tax2gas/post/post.go | 173 +++ x/tax2gas/types/codec.go | 39 + x/tax2gas/types/errors.go | 12 + x/tax2gas/types/events.go | 5 + x/tax2gas/types/expected_keeper.go | 42 + x/tax2gas/types/genesis.go | 17 + x/tax2gas/types/genesis.pb.go | 710 ++++++++++++ x/tax2gas/types/keys.go | 22 + x/tax2gas/types/msgs.go | 26 + x/tax2gas/types/params.go | 79 ++ x/tax2gas/types/query.pb.go | 860 ++++++++++++++ x/tax2gas/types/query.pb.gw.go | 218 ++++ x/tax2gas/types/tx.pb.go | 599 ++++++++++ x/tax2gas/utils/fee_tax.go | 147 +++ x/tax2gas/utils/utils.go | 102 ++ 63 files changed, 5598 insertions(+), 1859 deletions(-) delete mode 100644 app/upgrades/v8_2/constants.go create mode 100644 app/upgrades/v9/constants.go rename app/upgrades/{v8_2 => v9}/upgrades.go (51%) delete mode 100644 custom/auth/ante/fee.go delete mode 100644 custom/auth/ante/fee_tax.go delete mode 100644 custom/auth/ante/fee_test.go delete mode 100644 custom/auth/ante/integration_test.go create mode 100644 proto/terra/tax2gas/v1beta1/genesis.proto create mode 100644 proto/terra/tax2gas/v1beta1/query.proto create mode 100644 proto/terra/tax2gas/v1beta1/tx.proto create mode 100644 tests/e2e/scripts/forwarder.wasm create mode 100644 x/tax2gas/README.md create mode 100644 x/tax2gas/ante/ante.go create mode 100644 x/tax2gas/ante/ante_test.go create mode 100644 x/tax2gas/ante/fee_test.go rename {custom/auth => x/tax2gas}/ante/testdata/hackatom.wasm (100%) create mode 100644 x/tax2gas/client/cli/query.go create mode 100644 x/tax2gas/client/cli/tx.go create mode 100644 x/tax2gas/exported/alias.go create mode 100644 x/tax2gas/genesis.go create mode 100644 x/tax2gas/keeper/keeper.go create mode 100644 x/tax2gas/keeper/keeper_test.go create mode 100644 x/tax2gas/keeper/msg_server.go create mode 100644 x/tax2gas/keeper/msg_server_test.go create mode 100644 x/tax2gas/keeper/params.go create mode 100644 x/tax2gas/keeper/querier.go create mode 100644 x/tax2gas/keeper/querier_test.go create mode 100644 x/tax2gas/module.go rename custom/auth/ante/fee_burntax.go => x/tax2gas/post/burntax.go (94%) create mode 100644 x/tax2gas/post/post.go create mode 100644 x/tax2gas/types/codec.go create mode 100644 x/tax2gas/types/errors.go create mode 100644 x/tax2gas/types/events.go create mode 100644 x/tax2gas/types/expected_keeper.go create mode 100644 x/tax2gas/types/genesis.go create mode 100644 x/tax2gas/types/genesis.pb.go create mode 100644 x/tax2gas/types/keys.go create mode 100644 x/tax2gas/types/msgs.go create mode 100644 x/tax2gas/types/params.go create mode 100644 x/tax2gas/types/query.pb.go create mode 100644 x/tax2gas/types/query.pb.gw.go create mode 100644 x/tax2gas/types/tx.pb.go create mode 100644 x/tax2gas/utils/fee_tax.go create mode 100644 x/tax2gas/utils/utils.go diff --git a/app/app.go b/app/app.go index 88268476a..feebb451e 100644 --- a/app/app.go +++ b/app/app.go @@ -56,7 +56,7 @@ import ( v7_1 "github.com/classic-terra/core/v3/app/upgrades/v7_1" v8 "github.com/classic-terra/core/v3/app/upgrades/v8" v8_1 "github.com/classic-terra/core/v3/app/upgrades/v8_1" - v8_2 "github.com/classic-terra/core/v3/app/upgrades/v8_2" + v9 "github.com/classic-terra/core/v3/app/upgrades/v9" customante "github.com/classic-terra/core/v3/custom/auth/ante" custompost "github.com/classic-terra/core/v3/custom/auth/post" @@ -88,7 +88,7 @@ var ( v7_1.Upgrade, v8.Upgrade, v8_1.Upgrade, - v8_2.Upgrade, + v9.Upgrade, } // Forks defines forks to be applied to the network @@ -242,6 +242,7 @@ func NewTerraApp( TXCounterStoreKey: app.GetKey(wasmtypes.StoreKey), DyncommKeeper: app.DyncommKeeper, StakingKeeper: app.StakingKeeper, + Tax2Gaskeeper: app.Tax2gasKeeper, Cdc: app.appCodec, }, ) @@ -251,7 +252,13 @@ func NewTerraApp( postHandler, err := custompost.NewPostHandler( custompost.HandlerOptions{ - DyncommKeeper: app.DyncommKeeper, + AccountKeeper: app.AccountKeeper, + BankKeeper: app.BankKeeper, + FeegrantKeeper: app.FeeGrantKeeper, + DistrKeeper: app.DistrKeeper, + DyncommKeeper: app.DyncommKeeper, + TreasuryKeeper: app.TreasuryKeeper, + Tax2Gaskeeper: app.Tax2gasKeeper, }, ) if err != nil { @@ -400,7 +407,7 @@ func (app *TerraApp) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIC // RegisterTxService implements the Application.RegisterTxService method. func (app *TerraApp) RegisterTxService(clientCtx client.Context) { authtx.RegisterTxService(app.BaseApp.GRPCQueryRouter(), clientCtx, app.BaseApp.Simulate, app.interfaceRegistry) - customauthtx.RegisterTxService(app.BaseApp.GRPCQueryRouter(), clientCtx, app.TreasuryKeeper) + customauthtx.RegisterTxService(app.BaseApp.GRPCQueryRouter(), clientCtx, app.TreasuryKeeper, app.Tax2gasKeeper) } // RegisterTendermintService implements the Application.RegisterTendermintService method. diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index 406fd28c9..8f7ffee7e 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -66,6 +66,8 @@ import ( markettypes "github.com/classic-terra/core/v3/x/market/types" oraclekeeper "github.com/classic-terra/core/v3/x/oracle/keeper" oracletypes "github.com/classic-terra/core/v3/x/oracle/types" + tax2gasKeeper "github.com/classic-terra/core/v3/x/tax2gas/keeper" + tax2gasTypes "github.com/classic-terra/core/v3/x/tax2gas/types" treasurykeeper "github.com/classic-terra/core/v3/x/treasury/keeper" treasurytypes "github.com/classic-terra/core/v3/x/treasury/types" ) @@ -103,10 +105,10 @@ type AppKeepers struct { DyncommKeeper dyncommkeeper.Keeper IBCHooksKeeper *ibchookskeeper.Keeper ConsensusParamsKeeper consensusparamkeeper.Keeper - - Ics20WasmHooks *ibchooks.WasmHooks - IBCHooksWrapper *ibchooks.ICS4Middleware - TransferStack ibctransfer.IBCModule + Tax2gasKeeper tax2gasKeeper.Keeper + Ics20WasmHooks *ibchooks.WasmHooks + IBCHooksWrapper *ibchooks.ICS4Middleware + TransferStack ibctransfer.IBCModule // make scoped keepers public for test purposes ScopedIBCKeeper capabilitykeeper.ScopedKeeper @@ -156,6 +158,7 @@ func NewAppKeepers( treasurytypes.StoreKey, wasmtypes.StoreKey, dyncommtypes.StoreKey, + tax2gasTypes.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey) memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey) @@ -276,6 +279,12 @@ func NewAppKeepers( stakingtypes.NewMultiStakingHooks(appKeepers.DistrKeeper.Hooks(), appKeepers.SlashingKeeper.Hooks()), ) + appKeepers.Tax2gasKeeper = tax2gasKeeper.NewKeeper( + appCodec, + appKeepers.keys[tax2gasTypes.StoreKey], + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + // Create IBC Keeper appKeepers.IBCKeeper = ibckeeper.NewKeeper( appCodec, @@ -390,6 +399,7 @@ func NewAppKeepers( appKeepers.BankKeeper, appKeepers.TreasuryKeeper, appKeepers.AccountKeeper, + appKeepers.Tax2gasKeeper, appCodec, appKeepers.TransferKeeper, ) diff --git a/app/modules.go b/app/modules.go index a7dfb5cb2..f638fc110 100644 --- a/app/modules.go +++ b/app/modules.go @@ -25,6 +25,8 @@ import ( markettypes "github.com/classic-terra/core/v3/x/market/types" "github.com/classic-terra/core/v3/x/oracle" oracletypes "github.com/classic-terra/core/v3/x/oracle/types" + tax2gas "github.com/classic-terra/core/v3/x/tax2gas" + tax2gasTypes "github.com/classic-terra/core/v3/x/tax2gas/types" "github.com/classic-terra/core/v3/x/treasury" treasuryclient "github.com/classic-terra/core/v3/x/treasury/client" treasurytypes "github.com/classic-terra/core/v3/x/treasury/types" @@ -123,6 +125,7 @@ var ( customwasm.AppModuleBasic{}, ibcfee.AppModuleBasic{}, dyncomm.AppModuleBasic{}, + tax2gas.AppModuleBasic{}, ibchooks.AppModuleBasic{}, consensus.AppModuleBasic{}, ) @@ -184,6 +187,7 @@ func appModules( treasury.NewAppModule(appCodec, app.TreasuryKeeper), wasm.NewAppModule(appCodec, &app.WasmKeeper, app.StakingKeeper, app.AccountKeeper, app.BankKeeper, app.MsgServiceRouter(), app.GetSubspace(wasmtypes.ModuleName)), dyncomm.NewAppModule(appCodec, app.DyncommKeeper, app.StakingKeeper), + tax2gas.NewAppModule(appCodec, app.Tax2gasKeeper), ibchooks.NewAppModule(app.AccountKeeper), consensus.NewAppModule(appCodec, app.ConsensusParamsKeeper), crisis.NewAppModule(app.CrisisKeeper, skipGenesisInvariants, app.GetSubspace(crisistypes.ModuleName)), // always be last to make sure that it checks for all invariants and not only part of them @@ -250,6 +254,7 @@ func orderBeginBlockers() []string { markettypes.ModuleName, wasmtypes.ModuleName, dyncommtypes.ModuleName, + tax2gasTypes.ModuleName, // consensus module consensusparamtypes.ModuleName, } @@ -284,6 +289,7 @@ func orderEndBlockers() []string { markettypes.ModuleName, wasmtypes.ModuleName, dyncommtypes.ModuleName, + tax2gasTypes.ModuleName, // consensus module consensusparamtypes.ModuleName, } @@ -318,6 +324,7 @@ func orderInitGenesis() []string { treasurytypes.ModuleName, wasmtypes.ModuleName, dyncommtypes.ModuleName, + tax2gasTypes.ModuleName, // consensus module consensusparamtypes.ModuleName, } diff --git a/app/sim_test.go b/app/sim_test.go index 144b73f0a..a3904a5e2 100644 --- a/app/sim_test.go +++ b/app/sim_test.go @@ -21,10 +21,13 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" simcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" + + tax2gastypes "github.com/classic-terra/core/v3/x/tax2gas/types" ) // SimAppChainID hardcoded chainID for simulation @@ -142,16 +145,33 @@ func TestAppStateDeterminism(t *testing.T) { simtestutil.EmptyAppOptions{}, emptyWasmOpts, interBlockCacheOpt(), fauxMerkleModeOpt(), ) + appGenState := app.DefaultGenesis() + tax2gasGenState := tax2gastypes.GenesisState{} + err := app.AppCodec().UnmarshalJSON(appGenState[tax2gastypes.ModuleName], &tax2gasGenState) + require.NoError(t, err) + + tax2gasGenState.Params = tax2gastypes.Params{ + Enabled: true, + GasPrices: sdk.DecCoins{ + sdk.NewDecCoinFromDec("stake", sdk.NewDecWithPrec(0, 3)), + }, + } + newGenState := tax2gasGenState + bz, err := app.AppCodec().MarshalJSON(&newGenState) + require.NoError(t, err) + + appGenState[tax2gastypes.ModuleName] = bz + fmt.Printf( "running non-determinism simulation; seed %d: %d/%d, attempt: %d/%d\n", config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, ) - _, _, err := simulation.SimulateFromSeed( + _, _, err = simulation.SimulateFromSeed( t, os.Stdout, app.BaseApp, - AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), + AppStateFn(app.AppCodec(), app.SimulationManager(), appGenState), simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 simtestutil.SimulationOperations(app, app.AppCodec(), config), app.ModuleAccountAddrs(), diff --git a/app/upgrades/v8_2/constants.go b/app/upgrades/v8_2/constants.go deleted file mode 100644 index 9a3739067..000000000 --- a/app/upgrades/v8_2/constants.go +++ /dev/null @@ -1,13 +0,0 @@ -//nolint:revive -package v8_2 - -import ( - "github.com/classic-terra/core/v3/app/upgrades" -) - -const UpgradeName = "v8_2" - -var Upgrade = upgrades.Upgrade{ - UpgradeName: UpgradeName, - CreateUpgradeHandler: CreateV82UpgradeHandler, -} diff --git a/app/upgrades/v9/constants.go b/app/upgrades/v9/constants.go new file mode 100644 index 000000000..8bca8b61c --- /dev/null +++ b/app/upgrades/v9/constants.go @@ -0,0 +1,20 @@ +package v9 + +import ( + "github.com/classic-terra/core/v3/app/upgrades" + store "github.com/cosmos/cosmos-sdk/store/types" + + tax2gastypes "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +const UpgradeName = "v9" + +var Upgrade = upgrades.Upgrade{ + UpgradeName: UpgradeName, + CreateUpgradeHandler: CreateV9UpgradeHandler, + StoreUpgrades: store.StoreUpgrades{ + Added: []string{ + tax2gastypes.ModuleName, + }, + }, +} diff --git a/app/upgrades/v8_2/upgrades.go b/app/upgrades/v9/upgrades.go similarity index 51% rename from app/upgrades/v8_2/upgrades.go rename to app/upgrades/v9/upgrades.go index 4182109c0..fb67fa7dc 100644 --- a/app/upgrades/v8_2/upgrades.go +++ b/app/upgrades/v9/upgrades.go @@ -1,21 +1,31 @@ -//nolint:revive -package v8_2 +package v9 import ( "github.com/classic-terra/core/v3/app/keepers" "github.com/classic-terra/core/v3/app/upgrades" + tax2gastypes "github.com/classic-terra/core/v3/x/tax2gas/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" ) -func CreateV82UpgradeHandler( +func CreateV9UpgradeHandler( mm *module.Manager, cfg module.Configurator, _ upgrades.BaseAppParamManager, keepers *keepers.AppKeepers, ) upgradetypes.UpgradeHandler { return func(ctx sdk.Context, _ upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { + // set default oracle split + keepers.TreasuryKeeper.SetTaxRate(ctx, sdk.ZeroDec()) + + tax2gasParams := tax2gastypes.DefaultParams() + tax2gasParams.GasPrices = sdk.NewDecCoins( + sdk.NewDecCoinFromDec("uluna", sdk.NewDecWithPrec(28325, 3)), + sdk.NewDecCoinFromDec("uusd", sdk.NewDecWithPrec(75, 2)), + ) + tax2gasParams.MaxTotalBypassMinFeeMsgGasUsage = 200000 + keepers.Tax2gasKeeper.SetParams(ctx, tax2gasParams) return mm.RunMigrations(ctx, cfg, fromVM) } } diff --git a/custom/auth/ante/ante.go b/custom/auth/ante/ante.go index 75400c615..ddfcde150 100644 --- a/custom/auth/ante/ante.go +++ b/custom/auth/ante/ante.go @@ -12,6 +12,9 @@ import ( dyncommante "github.com/classic-terra/core/v3/x/dyncomm/ante" dyncommkeeper "github.com/classic-terra/core/v3/x/dyncomm/keeper" + tax2gasante "github.com/classic-terra/core/v3/x/tax2gas/ante" + tax2gaskeeper "github.com/classic-terra/core/v3/x/tax2gas/keeper" + tax2gastypes "github.com/classic-terra/core/v3/x/tax2gas/types" "github.com/cosmos/cosmos-sdk/codec" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" ibcante "github.com/cosmos/ibc-go/v7/modules/core/ante" @@ -26,7 +29,7 @@ type HandlerOptions struct { AccountKeeper ante.AccountKeeper BankKeeper BankKeeper ExtensionOptionChecker ante.ExtensionOptionChecker - FeegrantKeeper ante.FeegrantKeeper + FeegrantKeeper tax2gastypes.FeegrantKeeper OracleKeeper OracleKeeper TreasuryKeeper TreasuryKeeper SignModeHandler signing.SignModeHandler @@ -40,6 +43,7 @@ type HandlerOptions struct { TXCounterStoreKey storetypes.StoreKey DyncommKeeper dyncommkeeper.Keeper StakingKeeper *stakingkeeper.Keeper + Tax2Gaskeeper tax2gaskeeper.Keeper Cdc codec.BinaryCodec } @@ -88,7 +92,7 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { // MinInitialDepositDecorator prevents submitting governance proposal low initial deposit NewMinInitialDepositDecorator(options.GovKeeper, options.TreasuryKeeper), ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), - NewFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TreasuryKeeper, options.DistributionKeeper), + tax2gasante.NewFeeDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TreasuryKeeper, options.Tax2Gaskeeper), dyncommante.NewDyncommDecorator(options.Cdc, options.DyncommKeeper, options.StakingKeeper), // Do not add any other decorators below this point unless explicitly explain. diff --git a/custom/auth/ante/expected_keeper.go b/custom/auth/ante/expected_keeper.go index 05957c631..002a1f804 100644 --- a/custom/auth/ante/expected_keeper.go +++ b/custom/auth/ante/expected_keeper.go @@ -26,10 +26,12 @@ type OracleKeeper interface { // BankKeeper defines the contract needed for supply related APIs (noalias) type BankKeeper interface { + GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error SendCoins(ctx sdk.Context, from, to sdk.AccAddress, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error SendCoinsFromModuleToModule(ctx sdk.Context, senderModule string, recipientModule string, amt sdk.Coins) error + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error } type DistrKeeper interface { @@ -42,3 +44,7 @@ type DistrKeeper interface { type GovKeeper interface { GetDepositParams(ctx sdk.Context) govv1.DepositParams } + +type Tax2GasKeeper interface { + GetBurnTaxRate(ctx sdk.Context) (burnTaxRate sdk.Dec) +} diff --git a/custom/auth/ante/fee.go b/custom/auth/ante/fee.go deleted file mode 100644 index 8e8cf402c..000000000 --- a/custom/auth/ante/fee.go +++ /dev/null @@ -1,219 +0,0 @@ -package ante - -import ( - "fmt" - "math" - - errorsmod "cosmossdk.io/errors" - - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - "github.com/cosmos/cosmos-sdk/x/auth/ante" - "github.com/cosmos/cosmos-sdk/x/auth/types" -) - -// FeeDecorator deducts fees from the first signer of the tx -// If the first signer does not have the funds to pay for the fees, return with InsufficientFunds error -// Call next AnteHandler if fees successfully deducted -// CONTRACT: Tx must implement FeeTx interface to use DeductFeeDecorator -type FeeDecorator struct { - accountKeeper ante.AccountKeeper - bankKeeper BankKeeper - feegrantKeeper ante.FeegrantKeeper - treasuryKeeper TreasuryKeeper - distrKeeper DistrKeeper -} - -func NewFeeDecorator(ak ante.AccountKeeper, bk BankKeeper, fk ante.FeegrantKeeper, tk TreasuryKeeper, dk DistrKeeper) FeeDecorator { - return FeeDecorator{ - accountKeeper: ak, - bankKeeper: bk, - feegrantKeeper: fk, - treasuryKeeper: tk, - distrKeeper: dk, - } -} - -func (fd FeeDecorator) 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 ( - priority int64 - err error - ) - - msgs := feeTx.GetMsgs() - // Compute taxes - taxes := FilterMsgAndComputeTax(ctx, fd.treasuryKeeper, msgs...) - - if !simulate { - priority, err = fd.checkTxFee(ctx, tx, taxes) - if err != nil { - return ctx, err - } - } - - if err := fd.checkDeductFee(ctx, feeTx, taxes, simulate); err != nil { - return ctx, err - } - - newCtx := ctx.WithPriority(priority) - - return next(newCtx, tx, simulate) -} - -func (fd FeeDecorator) checkDeductFee(ctx sdk.Context, feeTx sdk.FeeTx, taxes sdk.Coins, simulate bool) error { - if addr := fd.accountKeeper.GetModuleAddress(types.FeeCollectorName); addr == nil { - return fmt.Errorf("fee collector module account (%s) has not been set", types.FeeCollectorName) - } - - fee := feeTx.GetFee() - 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 fd.feegrantKeeper == nil { - return sdkerrors.ErrInvalidRequest.Wrap("fee grants are not enabled") - } else if !feeGranter.Equals(feePayer) { - err := fd.feegrantKeeper.UseGrantedFees(ctx, feeGranter, feePayer, fee, feeTx.GetMsgs()) - if err != nil { - return errorsmod.Wrapf(err, "%s does not not allow to pay fees for %s", feeGranter, feePayer) - } - } - - deductFeesFrom = feeGranter - } - - deductFeesFromAcc := fd.accountKeeper.GetAccount(ctx, deductFeesFrom) - if deductFeesFromAcc == nil { - return sdkerrors.ErrUnknownAddress.Wrapf("fee payer address: %s does not exist", deductFeesFrom) - } - - feesOrTax := fee - - // deduct the fees - if fee.IsZero() && simulate { - feesOrTax = taxes - } - - if !feesOrTax.IsZero() { - err := DeductFees(fd.bankKeeper, ctx, deductFeesFromAcc, feesOrTax) - if err != nil { - return err - } - - if !taxes.IsZero() { - err := fd.BurnTaxSplit(ctx, taxes) - if err != nil { - return err - } - // Record tax proceeds - fd.treasuryKeeper.RecordEpochTaxProceeds(ctx, taxes) - } - } - - 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 DeductFees(bankKeeper types.BankKeeper, ctx sdk.Context, acc types.AccountI, fees sdk.Coins) error { - if !fees.IsValid() { - return errorsmod.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees) - } - - err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), types.FeeCollectorName, fees) - if err != nil { - return errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds, err.Error()) - } - - return nil -} - -// checkTxFee implements the default fee logic, where the minimum price per -// unit of gas is fixed and set by each validator, can the tx priority is computed from the gas price. -// Transaction with only oracle messages will skip gas fee check and will have the most priority. -// It also checks enough fee for treasury tax -func (fd FeeDecorator) checkTxFee(ctx sdk.Context, tx sdk.Tx, taxes sdk.Coins) (int64, error) { - feeTx, ok := tx.(sdk.FeeTx) - if !ok { - return 0, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") - } - - feeCoins := feeTx.GetFee() - gas := feeTx.GetGas() - msgs := feeTx.GetMsgs() - isOracleTx := isOracleTx(msgs) - - // Ensure that the provided fees meet a minimum threshold for the validator, - // if this is a CheckTx. This is only for local mempool purposes, and thus - // is only ran on check tx. - if ctx.IsCheckTx() && !isOracleTx { - requiredGasFees := sdk.Coins{} - minGasPrices := ctx.MinGasPrices() - if !minGasPrices.IsZero() { - requiredGasFees = 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 := sdk.NewDec(int64(gas)) - for i, gp := range minGasPrices { - fee := gp.Amount.Mul(glDec) - requiredGasFees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt()) - } - } - - requiredFees := requiredGasFees.Add(taxes...) - - // Check required fees - if !requiredFees.IsZero() && !feeCoins.IsAnyGTE(requiredFees) { - return 0, errorsmod.Wrapf(sdkerrors.ErrInsufficientFee, "insufficient fees; got: %q, required: %q = %q(gas) + %q(stability)", feeCoins, requiredFees, requiredGasFees, taxes) - } - } - - priority := int64(math.MaxInt64) - - if !isOracleTx { - priority = getTxPriority(feeCoins, int64(gas)) - } - - return priority, 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 prioritize as expected. -func getTxPriority(fee sdk.Coins, 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 - } - } - - return priority -} diff --git a/custom/auth/ante/fee_tax.go b/custom/auth/ante/fee_tax.go deleted file mode 100644 index 96a0e48d6..000000000 --- a/custom/auth/ante/fee_tax.go +++ /dev/null @@ -1,128 +0,0 @@ -package ante - -import ( - "regexp" - "strings" - - wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" - sdk "github.com/cosmos/cosmos-sdk/types" - authz "github.com/cosmos/cosmos-sdk/x/authz" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - - marketexported "github.com/classic-terra/core/v3/x/market/exported" - oracleexported "github.com/classic-terra/core/v3/x/oracle/exported" -) - -var IBCRegexp = regexp.MustCompile("^ibc/[a-fA-F0-9]{64}$") - -func isIBCDenom(denom string) bool { - return IBCRegexp.MatchString(strings.ToLower(denom)) -} - -// FilterMsgAndComputeTax computes the stability tax on messages. -func FilterMsgAndComputeTax(ctx sdk.Context, tk TreasuryKeeper, msgs ...sdk.Msg) sdk.Coins { - taxes := sdk.Coins{} - - for _, msg := range msgs { - switch msg := msg.(type) { - case *banktypes.MsgSend: - if !tk.HasBurnTaxExemptionAddress(ctx, msg.FromAddress, msg.ToAddress) { - taxes = taxes.Add(computeTax(ctx, tk, msg.Amount)...) - } - - case *banktypes.MsgMultiSend: - tainted := 0 - - for _, input := range msg.Inputs { - if tk.HasBurnTaxExemptionAddress(ctx, input.Address) { - tainted++ - } - } - - for _, output := range msg.Outputs { - if tk.HasBurnTaxExemptionAddress(ctx, output.Address) { - tainted++ - } - } - - if tainted != len(msg.Inputs)+len(msg.Outputs) { - for _, input := range msg.Inputs { - taxes = taxes.Add(computeTax(ctx, tk, input.Coins)...) - } - } - - case *marketexported.MsgSwapSend: - taxes = taxes.Add(computeTax(ctx, tk, sdk.NewCoins(msg.OfferCoin))...) - - case *wasmtypes.MsgInstantiateContract: - taxes = taxes.Add(computeTax(ctx, tk, msg.Funds)...) - - case *wasmtypes.MsgInstantiateContract2: - taxes = taxes.Add(computeTax(ctx, tk, msg.Funds)...) - - case *wasmtypes.MsgExecuteContract: - if !tk.HasBurnTaxExemptionContract(ctx, msg.Contract) { - taxes = taxes.Add(computeTax(ctx, tk, msg.Funds)...) - } - - case *authz.MsgExec: - messages, err := msg.GetMessages() - if err == nil { - taxes = taxes.Add(FilterMsgAndComputeTax(ctx, tk, messages...)...) - } - } - } - - return taxes -} - -// computes the stability tax according to tax-rate and tax-cap -func computeTax(ctx sdk.Context, tk TreasuryKeeper, principal sdk.Coins) sdk.Coins { - taxRate := tk.GetTaxRate(ctx) - if taxRate.Equal(sdk.ZeroDec()) { - return sdk.Coins{} - } - - taxes := sdk.Coins{} - - for _, coin := range principal { - if coin.Denom == sdk.DefaultBondDenom { - continue - } - - if isIBCDenom(coin.Denom) { - continue - } - - taxDue := sdk.NewDecFromInt(coin.Amount).Mul(taxRate).TruncateInt() - - // If tax due is greater than the tax cap, cap! - taxCap := tk.GetTaxCap(ctx, coin.Denom) - if taxDue.GT(taxCap) { - taxDue = taxCap - } - - if taxDue.Equal(sdk.ZeroInt()) { - continue - } - - taxes = taxes.Add(sdk.NewCoin(coin.Denom, taxDue)) - } - - return taxes -} - -func isOracleTx(msgs []sdk.Msg) bool { - for _, msg := range msgs { - switch msg.(type) { - case *oracleexported.MsgAggregateExchangeRatePrevote: - continue - case *oracleexported.MsgAggregateExchangeRateVote: - continue - default: - return false - } - } - - return true -} diff --git a/custom/auth/ante/fee_test.go b/custom/auth/ante/fee_test.go deleted file mode 100644 index ecd8a634f..000000000 --- a/custom/auth/ante/fee_test.go +++ /dev/null @@ -1,1005 +0,0 @@ -package ante_test - -import ( - "encoding/json" - "fmt" - "os" - "time" - - 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/types/query" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - authz "github.com/cosmos/cosmos-sdk/x/authz" - "github.com/cosmos/cosmos-sdk/x/bank/testutil" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - - wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" - wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" - "github.com/classic-terra/core/v3/custom/auth/ante" - core "github.com/classic-terra/core/v3/types" - markettypes "github.com/classic-terra/core/v3/x/market/types" - oracletypes "github.com/classic-terra/core/v3/x/oracle/types" -) - -func (s *AnteTestSuite) TestDeductFeeDecorator_ZeroGas() { - s.SetupTest(true) // setup - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.DistrKeeper) - antehandler := sdk.ChainAnteDecorators(mfd) - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - coins := sdk.NewCoins(sdk.NewCoin("atom", sdk.NewInt(300))) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) - - // msg and signatures - msg := testdata.NewTestMsg(addr1) - s.Require().NoError(s.txBuilder.SetMsgs(msg)) - - // set zero gas - s.txBuilder.SetGasLimit(0) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // Set IsCheckTx to true - s.ctx = s.ctx.WithIsCheckTx(true) - - _, err = antehandler(s.ctx, tx, false) - s.Require().Error(err) - - // zero gas is accepted in simulation mode - _, err = antehandler(s.ctx, tx, true) - s.Require().NoError(err) -} - -func (s *AnteTestSuite) TestEnsureMempoolFees() { - s.SetupTest(true) // setup - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.DistrKeeper) - antehandler := sdk.ChainAnteDecorators(mfd) - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - coins := sdk.NewCoins(sdk.NewCoin("atom", sdk.NewInt(300))) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) - - // msg and signatures - msg := testdata.NewTestMsg(addr1) - feeAmount := testdata.NewTestFeeAmount() - gasLimit := uint64(15) - s.Require().NoError(s.txBuilder.SetMsgs(msg)) - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // Set high gas price so standard test fee fails - atomPrice := sdk.NewDecCoinFromDec("atom", sdk.NewDec(20)) - highGasPrice := []sdk.DecCoin{atomPrice} - s.ctx = s.ctx.WithMinGasPrices(highGasPrice) - - // Set IsCheckTx to true - s.ctx = s.ctx.WithIsCheckTx(true) - - // antehandler errors with insufficient fees - _, err = antehandler(s.ctx, tx, false) - s.Require().NotNil(err, "Decorator should have errored on too low fee for local gasPrice") - - // antehandler should not error since we do not check minGasPrice in simulation mode - cacheCtx, _ := s.ctx.CacheContext() - _, err = antehandler(cacheCtx, tx, true) - s.Require().Nil(err, "Decorator should not have errored in simulation mode") - - // Set IsCheckTx to false - s.ctx = s.ctx.WithIsCheckTx(false) - - // antehandler should not error since we do not check minGasPrice in DeliverTx - _, err = antehandler(s.ctx, tx, false) - s.Require().Nil(err, "MempoolFeeDecorator returned error in DeliverTx") - - // Set IsCheckTx back to true for testing sufficient mempool fee - s.ctx = s.ctx.WithIsCheckTx(true) - - atomPrice = sdk.NewDecCoinFromDec("atom", sdk.NewDec(0).Quo(sdk.NewDec(100000))) - lowGasPrice := []sdk.DecCoin{atomPrice} - s.ctx = s.ctx.WithMinGasPrices(lowGasPrice) - - newCtx, err := antehandler(s.ctx, tx, false) - s.Require().Nil(err, "Decorator should not have errored on fee higher than local gasPrice") - // Priority is the smallest gas price amount in any denom. Since we have only 1 gas price - // of 10atom, the priority here is 10. - s.Require().Equal(int64(10), newCtx.Priority()) -} - -func (s *AnteTestSuite) TestDeductFees() { - s.SetupTest(true) // setup - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - - // msg and signatures - msg := testdata.NewTestMsg(addr1) - feeAmount := testdata.NewTestFeeAmount() - gasLimit := testdata.NewTestGasLimit() - s.Require().NoError(s.txBuilder.SetMsgs(msg)) - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // Set account with insufficient funds - acc := s.app.AccountKeeper.NewAccountWithAddress(s.ctx, addr1) - s.app.AccountKeeper.SetAccount(s.ctx, acc) - coins := sdk.NewCoins(sdk.NewCoin("atom", sdk.NewInt(10))) - err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) - s.Require().NoError(err) - - dfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.DistrKeeper) - antehandler := sdk.ChainAnteDecorators(dfd) - - _, err = antehandler(s.ctx, tx, false) - - s.Require().NotNil(err, "Tx did not error when fee payer had insufficient funds") - - // Set account with sufficient funds - s.app.AccountKeeper.SetAccount(s.ctx, acc) - err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin("atom", sdk.NewInt(200)))) - s.Require().NoError(err) - - _, err = antehandler(s.ctx, tx, false) - - s.Require().Nil(err, "Tx errored after account has been set with sufficient funds") -} - -func (s *AnteTestSuite) TestEnsureMempoolFeesSend() { - s.SetupTest(true) // setup - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.DistrKeeper) - antehandler := sdk.ChainAnteDecorators(mfd) - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - coins := sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, sdk.NewInt(1000000))) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) - - // msg and signatures - sendAmount := int64(1000000) - sendCoins := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, sendAmount)) - msg := banktypes.NewMsgSend(addr1, addr1, sendCoins) - - feeAmount := testdata.NewTestFeeAmount() - gasLimit := testdata.NewTestGasLimit() - s.Require().NoError(s.txBuilder.SetMsgs(msg)) - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // set zero gas prices - s.ctx = s.ctx.WithMinGasPrices(sdk.NewDecCoins()) - - // Set IsCheckTx to true - s.ctx = s.ctx.WithIsCheckTx(true) - - // antehandler errors with insufficient fees due to tax - _, err = antehandler(s.ctx, tx, false) - s.Require().Error(err, "Decorator should errored on low fee for local gasPrice + tax") - - tk := s.app.TreasuryKeeper - expectedTax := tk.GetTaxRate(s.ctx).MulInt64(sendAmount).TruncateInt() - if taxCap := tk.GetTaxCap(s.ctx, core.MicroSDRDenom); expectedTax.GT(taxCap) { - expectedTax = taxCap - } - - // set tax amount - feeAmount = sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, expectedTax)) - s.txBuilder.SetFeeAmount(feeAmount) - tx, err = s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // must pass with tax - _, err = antehandler(s.ctx, tx, false) - s.Require().NoError(err, "Decorator should not have errored on fee higher than local gasPrice") -} - -func (s *AnteTestSuite) TestEnsureMempoolFeesSwapSend() { - s.SetupTest(true) // setup - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.DistrKeeper) - antehandler := sdk.ChainAnteDecorators(mfd) - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - coins := sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, sdk.NewInt(1000000))) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) - - // msg and signatures - sendAmount := int64(1000000) - sendCoin := sdk.NewInt64Coin(core.MicroSDRDenom, sendAmount) - msg := markettypes.NewMsgSwapSend(addr1, addr1, sendCoin, core.MicroKRWDenom) - - feeAmount := testdata.NewTestFeeAmount() - gasLimit := testdata.NewTestGasLimit() - s.Require().NoError(s.txBuilder.SetMsgs(msg)) - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // set zero gas prices - s.ctx = s.ctx.WithMinGasPrices(sdk.NewDecCoins()) - - // Set IsCheckTx to true - s.ctx = s.ctx.WithIsCheckTx(true) - - // antehandler errors with insufficient fees due to tax - _, err = antehandler(s.ctx, tx, false) - s.Require().Error(err, "Decorator should errored on low fee for local gasPrice + tax") - - tk := s.app.TreasuryKeeper - expectedTax := tk.GetTaxRate(s.ctx).MulInt64(sendAmount).TruncateInt() - if taxCap := tk.GetTaxCap(s.ctx, core.MicroSDRDenom); expectedTax.GT(taxCap) { - expectedTax = taxCap - } - - // set tax amount - s.txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, expectedTax))) - tx, err = s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // must pass with tax - _, err = antehandler(s.ctx, tx, false) - s.Require().NoError(err, "Decorator should not have errored on fee higher than local gasPrice") -} - -func (s *AnteTestSuite) TestEnsureMempoolFeesMultiSend() { - s.SetupTest(true) // setup - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.DistrKeeper) - antehandler := sdk.ChainAnteDecorators(mfd) - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - coins := sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, sdk.NewInt(1000000))) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) - - // msg and signatures - sendAmount := int64(1000000) - sendCoins := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, sendAmount)) - msg := banktypes.NewMsgMultiSend( - []banktypes.Input{ - banktypes.NewInput(addr1, sendCoins), - banktypes.NewInput(addr1, sendCoins), - }, - []banktypes.Output{ - banktypes.NewOutput(addr1, sendCoins), - banktypes.NewOutput(addr1, sendCoins), - }, - ) - - feeAmount := testdata.NewTestFeeAmount() - gasLimit := testdata.NewTestGasLimit() - s.Require().NoError(s.txBuilder.SetMsgs(msg)) - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // set zero gas prices - s.ctx = s.ctx.WithMinGasPrices(sdk.NewDecCoins()) - - // Set IsCheckTx to true - s.ctx = s.ctx.WithIsCheckTx(true) - - // antehandler errors with insufficient fees due to tax - _, err = antehandler(s.ctx, tx, false) - s.Require().Error(err, "Decorator should errored on low fee for local gasPrice + tax") - - tk := s.app.TreasuryKeeper - expectedTax := tk.GetTaxRate(s.ctx).MulInt64(sendAmount).TruncateInt() - if taxCap := tk.GetTaxCap(s.ctx, core.MicroSDRDenom); expectedTax.GT(taxCap) { - expectedTax = taxCap - } - - // set tax amount - s.txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, expectedTax))) - tx, err = s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - _, err = antehandler(s.ctx, tx, false) - s.Require().Error(err, "Decorator should errored on low fee for local gasPrice + tax") - - // must pass with tax - s.txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, expectedTax.Add(expectedTax)))) - tx, err = s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - _, err = antehandler(s.ctx, tx, false) - s.Require().NoError(err, "Decorator should not have errored on fee higher than local gasPrice") -} - -func (s *AnteTestSuite) TestEnsureMempoolFeesInstantiateContract() { - s.SetupTest(true) // setup - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.DistrKeeper) - antehandler := sdk.ChainAnteDecorators(mfd) - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - coins := sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, sdk.NewInt(1000000))) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) - - // msg and signatures - sendAmount := int64(1000000) - sendCoins := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, sendAmount)) - msg := &wasmtypes.MsgInstantiateContract{ - Sender: addr1.String(), - Admin: addr1.String(), - CodeID: 0, - Msg: []byte{}, - Funds: sendCoins, - } - - feeAmount := testdata.NewTestFeeAmount() - gasLimit := testdata.NewTestGasLimit() - s.Require().NoError(s.txBuilder.SetMsgs(msg)) - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // set zero gas prices - s.ctx = s.ctx.WithMinGasPrices(sdk.NewDecCoins()) - - // Set IsCheckTx to true - s.ctx = s.ctx.WithIsCheckTx(true) - - // antehandler errors with insufficient fees due to tax - _, err = antehandler(s.ctx, tx, false) - s.Require().Error(err, "Decorator should errored on low fee for local gasPrice + tax") - - tk := s.app.TreasuryKeeper - expectedTax := tk.GetTaxRate(s.ctx).MulInt64(sendAmount).TruncateInt() - if taxCap := tk.GetTaxCap(s.ctx, core.MicroSDRDenom); expectedTax.GT(taxCap) { - expectedTax = taxCap - } - - // set tax amount - s.txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, expectedTax))) - tx, err = s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // must pass with tax - _, err = antehandler(s.ctx, tx, false) - s.Require().NoError(err, "Decorator should not have errored on fee higher than local gasPrice") -} - -func (s *AnteTestSuite) TestEnsureMempoolFeesExecuteContract() { - s.SetupTest(true) // setup - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.DistrKeeper) - antehandler := sdk.ChainAnteDecorators(mfd) - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - coins := sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, sdk.NewInt(1000000))) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) - - // msg and signatures - sendAmount := int64(1000000) - sendCoins := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, sendAmount)) - msg := &wasmtypes.MsgExecuteContract{ - Sender: addr1.String(), - Contract: addr1.String(), - Msg: []byte{}, - Funds: sendCoins, - } - - feeAmount := testdata.NewTestFeeAmount() - gasLimit := testdata.NewTestGasLimit() - s.Require().NoError(s.txBuilder.SetMsgs(msg)) - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // set zero gas prices - s.ctx = s.ctx.WithMinGasPrices(sdk.NewDecCoins()) - - // Set IsCheckTx to true - s.ctx = s.ctx.WithIsCheckTx(true) - - // antehandler errors with insufficient fees due to tax - _, err = antehandler(s.ctx, tx, false) - s.Require().Error(err, "Decorator should errored on low fee for local gasPrice + tax") - - tk := s.app.TreasuryKeeper - expectedTax := tk.GetTaxRate(s.ctx).MulInt64(sendAmount).TruncateInt() - if taxCap := tk.GetTaxCap(s.ctx, core.MicroSDRDenom); expectedTax.GT(taxCap) { - expectedTax = taxCap - } - - // set tax amount - s.txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, expectedTax))) - tx, err = s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // must pass with tax - _, err = antehandler(s.ctx, tx, false) - s.Require().NoError(err, "Decorator should not have errored on fee higher than local gasPrice") -} - -func (s *AnteTestSuite) TestEnsureMempoolFeesAuthzExec() { - s.SetupTest(true) // setup - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.DistrKeeper) - antehandler := sdk.ChainAnteDecorators(mfd) - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - coins := sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, sdk.NewInt(1000000))) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) - - // msg and signatures - sendAmount := int64(1000000) - sendCoins := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, sendAmount)) - msg := authz.NewMsgExec(addr1, []sdk.Msg{banktypes.NewMsgSend(addr1, addr1, sendCoins)}) - - feeAmount := testdata.NewTestFeeAmount() - gasLimit := testdata.NewTestGasLimit() - s.Require().NoError(s.txBuilder.SetMsgs(&msg)) - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // set zero gas prices - s.ctx = s.ctx.WithMinGasPrices(sdk.NewDecCoins()) - - // Set IsCheckTx to true - s.ctx = s.ctx.WithIsCheckTx(true) - - // antehandler errors with insufficient fees due to tax - _, err = antehandler(s.ctx, tx, false) - s.Require().Error(err, "Decorator should errored on low fee for local gasPrice + tax") - - tk := s.app.TreasuryKeeper - expectedTax := tk.GetTaxRate(s.ctx).MulInt64(sendAmount).TruncateInt() - if taxCap := tk.GetTaxCap(s.ctx, core.MicroSDRDenom); expectedTax.GT(taxCap) { - expectedTax = taxCap - } - - // set tax amount - s.txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, expectedTax))) - tx, err = s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // must pass with tax - _, err = antehandler(s.ctx, tx, false) - s.Require().NoError(err, "Decorator should not have errored on fee higher than local gasPrice") -} - -// go test -v -run ^TestAnteTestSuite/TestTaxExemption$ github.com/classic-terra/core/v3/custom/auth/ante -func (s *AnteTestSuite) TestTaxExemption() { - // keys and addresses - var privs []cryptotypes.PrivKey - var addrs []sdk.AccAddress - - // 0, 1: exemption - // 2, 3: normal - for i := 0; i < 4; i++ { - priv, _, addr := testdata.KeyTestPubAddr() - privs = append(privs, priv) - addrs = append(addrs, addr) - } - - // set send amount - sendAmt := int64(1000000) - sendCoin := sdk.NewInt64Coin(core.MicroSDRDenom, sendAmt) - feeAmt := int64(1000) - - cases := []struct { - name string - msgSigner cryptotypes.PrivKey - msgCreator func() []sdk.Msg - minFeeAmount int64 - expectProceeds int64 - }{ - { - name: "MsgSend(exemption -> exemption)", - msgSigner: privs[0], - msgCreator: func() []sdk.Msg { - var msgs []sdk.Msg - - msg1 := banktypes.NewMsgSend(addrs[0], addrs[1], sdk.NewCoins(sendCoin)) - msgs = append(msgs, msg1) - - return msgs - }, - minFeeAmount: 0, - expectProceeds: 0, - }, { - name: "MsgSend(normal -> normal)", - msgSigner: privs[2], - msgCreator: func() []sdk.Msg { - var msgs []sdk.Msg - - msg1 := banktypes.NewMsgSend(addrs[2], addrs[3], sdk.NewCoins(sendCoin)) - msgs = append(msgs, msg1) - - return msgs - }, - // tax this one hence burn amount is fee amount - minFeeAmount: feeAmt, - expectProceeds: feeAmt, - }, { - name: "MsgExec(MsgSend(normal -> normal))", - msgSigner: privs[2], - msgCreator: func() []sdk.Msg { - var msgs []sdk.Msg - - msg1 := authz.NewMsgExec(addrs[1], []sdk.Msg{banktypes.NewMsgSend(addrs[2], addrs[3], sdk.NewCoins(sendCoin))}) - msgs = append(msgs, &msg1) - - return msgs - }, - // tax this one hence burn amount is fee amount - minFeeAmount: feeAmt, - expectProceeds: feeAmt, - }, { - name: "MsgSend(exemption -> normal), MsgSend(exemption -> exemption)", - msgSigner: privs[0], - msgCreator: func() []sdk.Msg { - var msgs []sdk.Msg - - msg1 := banktypes.NewMsgSend(addrs[0], addrs[2], sdk.NewCoins(sendCoin)) - msgs = append(msgs, msg1) - msg2 := banktypes.NewMsgSend(addrs[0], addrs[1], sdk.NewCoins(sendCoin)) - msgs = append(msgs, msg2) - - return msgs - }, - // tax this one hence burn amount is fee amount - minFeeAmount: feeAmt, - expectProceeds: feeAmt, - }, { - name: "MsgSend(exemption -> exemption), MsgMultiSend(exemption -> normal, exemption -> exemption)", - msgSigner: privs[0], - msgCreator: func() []sdk.Msg { - var msgs []sdk.Msg - - msg1 := banktypes.NewMsgSend(addrs[0], addrs[1], sdk.NewCoins(sendCoin)) - msgs = append(msgs, msg1) - msg2 := banktypes.NewMsgMultiSend( - []banktypes.Input{ - { - Address: addrs[0].String(), - Coins: sdk.NewCoins(sendCoin), - }, - { - Address: addrs[0].String(), - Coins: sdk.NewCoins(sendCoin), - }, - }, - []banktypes.Output{ - { - Address: addrs[2].String(), - Coins: sdk.NewCoins(sendCoin), - }, - { - Address: addrs[1].String(), - Coins: sdk.NewCoins(sendCoin), - }, - }, - ) - msgs = append(msgs, msg2) - - return msgs - }, - minFeeAmount: feeAmt * 2, - expectProceeds: feeAmt * 2, - }, { - name: "MsgExecuteContract(exemption), MsgExecuteContract(normal)", - msgSigner: privs[3], - msgCreator: func() []sdk.Msg { - sendAmount := int64(1000000) - sendCoins := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, sendAmount)) - // get wasm code for wasm contract create and instantiate - wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") - s.Require().NoError(err) - per := wasmkeeper.NewDefaultPermissionKeeper(s.app.WasmKeeper) - // set wasm default params - s.app.WasmKeeper.SetParams(s.ctx, wasmtypes.DefaultParams()) - // wasm create - CodeID, _, err := per.Create(s.ctx, addrs[0], wasmCode, nil) - s.Require().NoError(err) - // params for contract init - r := wasmkeeper.HackatomExampleInitMsg{Verifier: addrs[0], Beneficiary: addrs[0]} - bz, err := json.Marshal(r) - s.Require().NoError(err) - // change block time for contract instantiate - s.ctx = s.ctx.WithBlockTime(time.Date(2020, time.April, 22, 12, 0, 0, 0, time.UTC)) - // instantiate contract then set the contract address to tax exemption - addr, _, err := per.Instantiate(s.ctx, CodeID, addrs[0], nil, bz, "my label", nil) - s.Require().NoError(err) - s.app.TreasuryKeeper.AddBurnTaxExemptionAddress(s.ctx, addr.String()) - // instantiate contract then not set to tax exemption - addr1, _, err := per.Instantiate(s.ctx, CodeID, addrs[0], nil, bz, "my label", nil) - s.Require().NoError(err) - - var msgs []sdk.Msg - // msg and signatures - msg1 := &wasmtypes.MsgExecuteContract{ - Sender: addrs[0].String(), - Contract: addr.String(), - Msg: []byte{}, - Funds: sendCoins, - } - msgs = append(msgs, msg1) - - msg2 := &wasmtypes.MsgExecuteContract{ - Sender: addrs[3].String(), - Contract: addr1.String(), - Msg: []byte{}, - Funds: sendCoins, - } - msgs = append(msgs, msg2) - return msgs - }, - minFeeAmount: feeAmt, - expectProceeds: feeAmt, - }, - } - - // there should be no coin in burn module - for _, c := range cases { - s.SetupTest(true) // setup - require := s.Require() - tk := s.app.TreasuryKeeper - ak := s.app.AccountKeeper - bk := s.app.BankKeeper - burnSplitRate := sdk.NewDecWithPrec(5, 1) - oracleSplitRate := sdk.ZeroDec() - - // Set burn split rate to 50% - // oracle split to 0% (oracle split is covered in another test) - tk.SetBurnSplitRate(s.ctx, burnSplitRate) - tk.SetOracleSplitRate(s.ctx, oracleSplitRate) - - fmt.Printf("CASE = %s \n", c.name) - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - tk.AddBurnTaxExemptionAddress(s.ctx, addrs[0].String()) - tk.AddBurnTaxExemptionAddress(s.ctx, addrs[1].String()) - - mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.DistrKeeper) - antehandler := sdk.ChainAnteDecorators(mfd) - - for i := 0; i < 4; i++ { - coins := sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, sdk.NewInt(10000000))) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addrs[i], coins) - } - - // msg and signatures - feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, c.minFeeAmount)) - gasLimit := testdata.NewTestGasLimit() - require.NoError(s.txBuilder.SetMsgs(c.msgCreator()...)) - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{c.msgSigner}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - require.NoError(err) - - _, err = antehandler(s.ctx, tx, false) - require.NoError(err) - - // check fee collector - feeCollector := ak.GetModuleAccount(s.ctx, authtypes.FeeCollectorName) - amountFee := bk.GetBalance(s.ctx, feeCollector.GetAddress(), core.MicroSDRDenom) - require.Equal(amountFee, sdk.NewCoin(core.MicroSDRDenom, sdk.NewDec(c.minFeeAmount).Mul(burnSplitRate).TruncateInt())) - - // check tax proceeds - taxProceeds := s.app.TreasuryKeeper.PeekEpochTaxProceeds(s.ctx) - require.Equal(taxProceeds, sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, sdk.NewInt(c.expectProceeds)))) - } -} - -// go test -v -run ^TestAnteTestSuite/TestBurnSplitTax$ github.com/classic-terra/core/v3/custom/auth/ante -func (s *AnteTestSuite) TestBurnSplitTax() { - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 0), sdk.ZeroDec(), sdk.NewDecWithPrec(5, 1)) // 100% distribute, 0% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 1), sdk.ZeroDec(), sdk.NewDecWithPrec(5, 1)) // 10% distribute, 0% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 2), sdk.ZeroDec(), sdk.NewDecWithPrec(5, 1)) // 0.1% distribute, 0% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(0, 0), sdk.ZeroDec(), sdk.NewDecWithPrec(5, 1)) // 0% distribute, 0% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 0), sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1)) // 100% distribute, 50% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1)) // 10% distribute, 50% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 2), sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1)) // 0.1% distribute, 50% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(0, 0), sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1)) // 0% distribute, 50% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 0), sdk.ZeroDec(), sdk.NewDecWithPrec(5, 1)) // 100% distribute, 0% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 1), sdk.ZeroDec(), sdk.NewDecWithPrec(5, 1)) // 10% distribute, 0% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 2), sdk.ZeroDec(), sdk.NewDecWithPrec(5, 1)) // 0.1% distribute, 0% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(0, 0), sdk.ZeroDec(), sdk.NewDecWithPrec(5, 1)) // 0% distribute, 0% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 0), sdk.OneDec(), sdk.NewDecWithPrec(5, 1)) // 100% distribute, 100% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 1), sdk.OneDec(), sdk.NewDecWithPrec(5, 1)) // 10% distribute, 100% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 2), sdk.OneDec(), sdk.NewDecWithPrec(5, 1)) // 0.1% distribute, 100% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(0, 0), sdk.OneDec(), sdk.NewDecWithPrec(5, 1)) // 0% distribute, 100% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 2), sdk.OneDec(), sdk.NewDecWithPrec(5, 2)) // 0.1% distribute, 100% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(0, 0), sdk.OneDec(), sdk.NewDecWithPrec(5, 2)) // 0% distribute, 100% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(1, 2), sdk.OneDec(), sdk.NewDecWithPrec(1, 1)) // 0.1% distribute, 100% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(0, 0), sdk.OneDec(), sdk.NewDecWithPrec(1, 2)) // 0% distribute, 100% to oracle - s.runBurnSplitTaxTest(sdk.NewDecWithPrec(-1, 1), sdk.ZeroDec(), sdk.NewDecWithPrec(5, 1)) // -10% distribute - invalid rate -} - -func (s *AnteTestSuite) runBurnSplitTaxTest(burnSplitRate sdk.Dec, oracleSplitRate sdk.Dec, communityTax sdk.Dec) { - s.SetupTest(true) // setup - require := s.Require() - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - ak := s.app.AccountKeeper - bk := s.app.BankKeeper - tk := s.app.TreasuryKeeper - dk := s.app.DistrKeeper - mfd := ante.NewFeeDecorator(ak, bk, s.app.FeeGrantKeeper, tk, dk) - antehandler := sdk.ChainAnteDecorators(mfd) - - // Set burn split tax - tk.SetBurnSplitRate(s.ctx, burnSplitRate) - tk.SetOracleSplitRate(s.ctx, oracleSplitRate) - taxRate := tk.GetTaxRate(s.ctx) - - // Set community tax - dkParams := dk.GetParams(s.ctx) - dkParams.CommunityTax = communityTax - dk.SetParams(s.ctx, dkParams) - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - coins := sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, sdk.NewInt(1000000))) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) - - // msg and signatures - sendAmount := int64(1000000) - sendCoins := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, sendAmount)) - msg := banktypes.NewMsgSend(addr1, addr1, sendCoins) - - gasLimit := testdata.NewTestGasLimit() - require.NoError(s.txBuilder.SetMsgs(msg)) - s.txBuilder.SetGasLimit(gasLimit) - expectedTax := tk.GetTaxRate(s.ctx).MulInt64(sendAmount).TruncateInt() - if taxCap := tk.GetTaxCap(s.ctx, core.MicroSDRDenom); expectedTax.GT(taxCap) { - expectedTax = taxCap - } - - // set tax amount - s.txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, expectedTax))) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - require.NoError(err) - - // set zero gas prices - s.ctx = s.ctx.WithMinGasPrices(sdk.NewDecCoins()) - - // Set IsCheckTx to true - s.ctx = s.ctx.WithIsCheckTx(true) - - feeCollector := ak.GetModuleAccount(s.ctx, authtypes.FeeCollectorName) - - amountFeeBefore := bk.GetAllBalances(s.ctx, feeCollector.GetAddress()) - - totalSupplyBefore, _, err := bk.GetPaginatedTotalSupply(s.ctx, &query.PageRequest{}) - require.NoError(err) - fmt.Printf( - "Before: TotalSupply %v, FeeCollector %v\n", - totalSupplyBefore, - amountFeeBefore, - ) - - // send tx to BurnTaxFeeDecorator antehandler - _, err = antehandler(s.ctx, tx, false) - require.NoError(err) - - // burn the burn account - tk.BurnCoinsFromBurnAccount(s.ctx) - - feeCollectorAfter := bk.GetAllBalances(s.ctx, ak.GetModuleAddress(authtypes.FeeCollectorName)) - oracleAfter := bk.GetAllBalances(s.ctx, ak.GetModuleAddress(oracletypes.ModuleName)) - taxes := ante.FilterMsgAndComputeTax(s.ctx, tk, msg) - communityPoolAfter, _ := dk.GetFeePoolCommunityCoins(s.ctx).TruncateDecimal() - if communityPoolAfter.IsZero() { - communityPoolAfter = sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, sdk.ZeroInt())) - } - - // burnTax := sdk.NewDecCoinsFromCoins(taxes...) - // in the burn tax split function, coins and not deccoins are used, which leads to rounding differences - // when comparing to the test with very small numbers, accordingly all deccoin calculations are changed to coins - burnTax := taxes - - if burnSplitRate.IsPositive() { - distributionDeltaCoins := burnSplitRate.MulInt(burnTax.AmountOf(core.MicroSDRDenom)).RoundInt() - applyCommunityTax := communityTax.Mul(oracleSplitRate.Quo(communityTax.Mul(oracleSplitRate).Sub(communityTax).Add(sdk.OneDec()))) - - expectedCommunityCoins := applyCommunityTax.MulInt(distributionDeltaCoins).RoundInt() - distributionDeltaCoins = distributionDeltaCoins.Sub(expectedCommunityCoins) - - expectedOracleCoins := oracleSplitRate.MulInt(distributionDeltaCoins).RoundInt() - expectedDistrCoins := distributionDeltaCoins.Sub(expectedOracleCoins) - - // expected: community pool 50% - fmt.Printf("-- sendCoins %+v, BurnTax %+v, BurnSplitRate %+v, OracleSplitRate %+v, CommunityTax %+v, CTaxApplied %+v, OracleCoins %+v, DistrCoins %+v\n", sendCoins.AmountOf(core.MicroSDRDenom), taxRate, burnSplitRate, oracleSplitRate, communityTax, applyCommunityTax, expectedOracleCoins, expectedDistrCoins) - require.Equal(feeCollectorAfter, sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, expectedDistrCoins))) - require.Equal(oracleAfter, sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, expectedOracleCoins))) - require.Equal(communityPoolAfter, sdk.NewCoins(sdk.NewCoin(core.MicroSDRDenom, expectedCommunityCoins))) - burnTax = burnTax.Sub(sdk.NewCoin(core.MicroSDRDenom, distributionDeltaCoins)).Sub(sdk.NewCoin(core.MicroSDRDenom, expectedCommunityCoins)) - } - - // check tax proceeds - // as end blocker has not been run here, we need to calculate it from the fee collector - addTaxFromFees := feeCollectorAfter.AmountOf(core.MicroSDRDenom) - if communityTax.IsPositive() { - addTaxFromFees = communityTax.Mul(sdk.NewDecFromInt(addTaxFromFees)).RoundInt() - } - expectedTaxProceeds := communityPoolAfter.AmountOf(core.MicroSDRDenom).Add(addTaxFromFees) - originalDistribution := sdk.ZeroDec() - if burnSplitRate.IsPositive() { - originalDistribution = burnSplitRate.Mul(sdk.NewDecFromInt(taxes.AmountOf(core.MicroSDRDenom))) - } - originalTaxProceeds := sdk.ZeroInt() - if communityTax.IsPositive() { - originalTaxProceeds = communityTax.Mul(originalDistribution).RoundInt() - } - // due to precision (roundInt) this can deviate up to 1 from the expected value - require.LessOrEqual(expectedTaxProceeds.Sub(originalTaxProceeds).Int64(), sdk.OneInt().Int64()) - - totalSupplyAfter, _, err := bk.GetPaginatedTotalSupply(s.ctx, &query.PageRequest{}) - require.NoError(err) - if !burnTax.Empty() { - // expected: total supply = tax - split tax - require.Equal( - totalSupplyBefore.Sub(totalSupplyAfter...), - burnTax, - ) - } - - fmt.Printf( - "After: TotalSupply %v, FeeCollector %v\n", - totalSupplyAfter, - feeCollectorAfter, - ) -} - -// go test -v -run ^TestAnteTestSuite/TestEnsureIBCUntaxed$ github.com/classic-terra/core/v3/custom/auth/ante -// TestEnsureIBCUntaxed tests that IBC transactions are not taxed, but fee is still deducted -func (s *AnteTestSuite) TestEnsureIBCUntaxed() { - s.SetupTest(true) // setup - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - mfd := ante.NewFeeDecorator( - s.app.AccountKeeper, - s.app.BankKeeper, - s.app.FeeGrantKeeper, - s.app.TreasuryKeeper, - s.app.DistrKeeper, - ) - antehandler := sdk.ChainAnteDecorators(mfd) - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - account := s.app.AccountKeeper.NewAccountWithAddress(s.ctx, addr1) - s.app.AccountKeeper.SetAccount(s.ctx, account) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, 1_000_000_000))) - - // msg and signatures - sendAmount := int64(1_000_000) - sendCoins := sdk.NewCoins(sdk.NewInt64Coin(core.OsmoIbcDenom, sendAmount)) - msg := banktypes.NewMsgSend(addr1, addr1, sendCoins) - - feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, 1_000_000)) - gasLimit := testdata.NewTestGasLimit() - s.Require().NoError(s.txBuilder.SetMsgs(msg)) - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // set zero gas prices - s.ctx = s.ctx.WithMinGasPrices(sdk.NewDecCoins()) - - // Set IsCheckTx to true - s.ctx = s.ctx.WithIsCheckTx(true) - - // IBC must pass without burn - _, err = antehandler(s.ctx, tx, false) - s.Require().NoError(err, "Decorator should not have errored on IBC denoms") - - // check if tax proceeds are empty - taxProceeds := s.app.TreasuryKeeper.PeekEpochTaxProceeds(s.ctx) - s.Require().True(taxProceeds.Empty()) -} - -// go test -v -run ^TestAnteTestSuite/TestOracleZeroFee$ github.com/classic-terra/core/v3/custom/auth/ante -func (s *AnteTestSuite) TestOracleZeroFee() { - s.SetupTest(true) // setup - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - mfd := ante.NewFeeDecorator( - s.app.AccountKeeper, - s.app.BankKeeper, - s.app.FeeGrantKeeper, - s.app.TreasuryKeeper, - s.app.DistrKeeper, - ) - antehandler := sdk.ChainAnteDecorators(mfd) - - // keys and addresses - priv1, _, addr1 := testdata.KeyTestPubAddr() - account := s.app.AccountKeeper.NewAccountWithAddress(s.ctx, addr1) - s.app.AccountKeeper.SetAccount(s.ctx, account) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, 1_000_000_000))) - - // new val - val, err := stakingtypes.NewValidator(sdk.ValAddress(addr1), priv1.PubKey(), stakingtypes.Description{}) - s.Require().NoError(err) - s.app.StakingKeeper.SetValidator(s.ctx, val) - - // msg and signatures - - // MsgAggregateExchangeRatePrevote - msg := oracletypes.NewMsgAggregateExchangeRatePrevote(oracletypes.GetAggregateVoteHash("salt", "exchange rates", val.GetOperator()), addr1, val.GetOperator()) - s.txBuilder.SetMsgs(msg) - s.txBuilder.SetGasLimit(testdata.NewTestGasLimit()) - s.txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, 0))) - privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - _, err = antehandler(s.ctx, tx, false) - s.Require().NoError(err) - - // check fee collector empty - balances := s.app.BankKeeper.GetAllBalances(s.ctx, s.app.AccountKeeper.GetModuleAddress(authtypes.FeeCollectorName)) - s.Require().Equal(sdk.Coins{}, balances) - - // MsgAggregateExchangeRateVote - msg1 := oracletypes.NewMsgAggregateExchangeRateVote("salt", "exchange rates", addr1, val.GetOperator()) - s.txBuilder.SetMsgs(msg1) - tx, err = s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - _, err = antehandler(s.ctx, tx, false) - s.Require().NoError(err) - - // check fee collector empty - balances = s.app.BankKeeper.GetAllBalances(s.ctx, s.app.AccountKeeper.GetModuleAddress(authtypes.FeeCollectorName)) - s.Require().Equal(sdk.Coins{}, balances) -} diff --git a/custom/auth/ante/integration_test.go b/custom/auth/ante/integration_test.go deleted file mode 100644 index ffadd5983..000000000 --- a/custom/auth/ante/integration_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package ante_test - -import ( - "fmt" - - 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/ante" - "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" - - wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" - - customante "github.com/classic-terra/core/v3/custom/auth/ante" - core "github.com/classic-terra/core/v3/types" - treasurytypes "github.com/classic-terra/core/v3/x/treasury/types" -) - -// go test -v -run ^TestAnteTestSuite/TestIntegrationTaxExemption$ github.com/classic-terra/core/v3/custom/auth/ante -func (s *AnteTestSuite) TestIntegrationTaxExemption() { - // keys and addresses - var privs []cryptotypes.PrivKey - var addrs []sdk.AccAddress - - // 0, 1: exemption - // 2, 3: normal - for i := 0; i < 4; i++ { - priv, _, addr := testdata.KeyTestPubAddr() - privs = append(privs, priv) - addrs = append(addrs, addr) - } - - // set send amount - sendAmt := int64(1_000_000) - sendCoin := sdk.NewInt64Coin(core.MicroSDRDenom, sendAmt) - feeAmt := int64(1000) - - cases := []struct { - name string - msgSigner cryptotypes.PrivKey - msgCreator func() []sdk.Msg - expectedFeeAmount int64 - }{ - { - name: "MsgSend(exemption -> exemption)", - msgSigner: privs[0], - msgCreator: func() []sdk.Msg { - var msgs []sdk.Msg - - msg1 := banktypes.NewMsgSend(addrs[0], addrs[1], sdk.NewCoins(sendCoin)) - msgs = append(msgs, msg1) - - return msgs - }, - expectedFeeAmount: 0, - }, { - name: "MsgSend(normal -> normal)", - msgSigner: privs[2], - msgCreator: func() []sdk.Msg { - var msgs []sdk.Msg - - msg1 := banktypes.NewMsgSend(addrs[2], addrs[3], sdk.NewCoins(sendCoin)) - msgs = append(msgs, msg1) - - return msgs - }, - // tax this one hence burn amount is fee amount - expectedFeeAmount: feeAmt, - }, { - name: "MsgSend(exemption -> normal), MsgSend(exemption -> exemption)", - msgSigner: privs[0], - msgCreator: func() []sdk.Msg { - var msgs []sdk.Msg - - msg1 := banktypes.NewMsgSend(addrs[0], addrs[2], sdk.NewCoins(sendCoin)) - msgs = append(msgs, msg1) - msg2 := banktypes.NewMsgSend(addrs[0], addrs[1], sdk.NewCoins(sendCoin)) - msgs = append(msgs, msg2) - - return msgs - }, - // tax this one hence burn amount is fee amount - expectedFeeAmount: feeAmt, - }, { - name: "MsgSend(exemption -> exemption), MsgMultiSend(exemption -> normal, exemption)", - msgSigner: privs[0], - msgCreator: func() []sdk.Msg { - var msgs []sdk.Msg - - msg1 := banktypes.NewMsgSend(addrs[0], addrs[1], sdk.NewCoins(sendCoin)) - msgs = append(msgs, msg1) - msg2 := banktypes.NewMsgMultiSend( - []banktypes.Input{ - { - Address: addrs[0].String(), - Coins: sdk.NewCoins(sendCoin.Add(sendCoin)), - }, - }, - []banktypes.Output{ - { - Address: addrs[2].String(), - Coins: sdk.NewCoins(sendCoin), - }, - { - Address: addrs[1].String(), - Coins: sdk.NewCoins(sendCoin), - }, - }, - ) - msgs = append(msgs, msg2) - - return msgs - }, - expectedFeeAmount: feeAmt * 2, - }, - } - - for _, c := range cases { - s.SetupTest(true) // setup - tk := s.app.TreasuryKeeper - ak := s.app.AccountKeeper - bk := s.app.BankKeeper - dk := s.app.DistrKeeper - wk := s.app.WasmKeeper - - // Set burn split rate to 50% - // fee amount should be 500, 50% of 10000 - burnSplitRate := sdk.NewDecWithPrec(5, 1) - tk.SetBurnSplitRate(s.ctx, burnSplitRate) // 50% - - feeCollector := ak.GetModuleAccount(s.ctx, types.FeeCollectorName) - burnModule := ak.GetModuleAccount(s.ctx, treasurytypes.BurnModuleName) - - encodingConfig := s.SetupEncoding() - wasmConfig := wasmtypes.DefaultWasmConfig() - antehandler, err := customante.NewAnteHandler( - customante.HandlerOptions{ - AccountKeeper: ak, - BankKeeper: bk, - WasmKeeper: &wk, - FeegrantKeeper: s.app.FeeGrantKeeper, - OracleKeeper: s.app.OracleKeeper, - TreasuryKeeper: s.app.TreasuryKeeper, - SigGasConsumer: ante.DefaultSigVerificationGasConsumer, - SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), - IBCKeeper: *s.app.IBCKeeper, - DistributionKeeper: dk, - WasmConfig: &wasmConfig, - TXCounterStoreKey: s.app.GetKey(wasmtypes.StoreKey), - }, - ) - s.Require().NoError(err) - - for i := 0; i < 4; i++ { - coins := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, 1_000_000)) - testutil.FundAccount(s.app.BankKeeper, s.ctx, addrs[i], coins) - } - - s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() - - tk.AddBurnTaxExemptionAddress(s.ctx, addrs[0].String()) - tk.AddBurnTaxExemptionAddress(s.ctx, addrs[1].String()) - - s.Run(c.name, func() { - // case 1 provides zero fee so not enough fee - // case 2 provides enough fee - feeCases := []int64{0, feeAmt} - for i := 0; i < 1; i++ { - feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, feeCases[i])) - gasLimit := testdata.NewTestGasLimit() - s.Require().NoError(s.txBuilder.SetMsgs(c.msgCreator()...)) - s.txBuilder.SetFeeAmount(feeAmount) - s.txBuilder.SetGasLimit(gasLimit) - - privs, accNums, accSeqs := []cryptotypes.PrivKey{c.msgSigner}, []uint64{3}, []uint64{0} - tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) - s.Require().NoError(err) - - // set zero gas prices - s.ctx = s.ctx.WithMinGasPrices(sdk.NewDecCoins()) - - feeCollectorBefore := bk.GetBalance(s.ctx, feeCollector.GetAddress(), core.MicroSDRDenom) - burnBefore := bk.GetBalance(s.ctx, burnModule.GetAddress(), core.MicroSDRDenom) - communityBefore := dk.GetFeePool(s.ctx).CommunityPool.AmountOf(core.MicroSDRDenom) - supplyBefore := bk.GetSupply(s.ctx, core.MicroSDRDenom) - - _, err = antehandler(s.ctx, tx, false) - if i == 0 && c.expectedFeeAmount != 0 { - s.Require().EqualError(err, fmt.Sprintf( - "insufficient fees; got: \"\", required: \"%dusdr\" = \"\"(gas) + \"%dusdr\"(stability): insufficient fee", - c.expectedFeeAmount, c.expectedFeeAmount)) - } else { - s.Require().NoError(err) - } - - feeCollectorAfter := bk.GetBalance(s.ctx, feeCollector.GetAddress(), core.MicroSDRDenom) - burnAfter := bk.GetBalance(s.ctx, burnModule.GetAddress(), core.MicroSDRDenom) - communityAfter := dk.GetFeePool(s.ctx).CommunityPool.AmountOf(core.MicroSDRDenom) - supplyAfter := bk.GetSupply(s.ctx, core.MicroSDRDenom) - - if i == 0 { - s.Require().Equal(feeCollectorBefore, feeCollectorAfter) - s.Require().Equal(burnBefore, burnAfter) - s.Require().Equal(communityBefore, communityAfter) - s.Require().Equal(supplyBefore, supplyAfter) - } - - if i == 1 { - s.Require().Equal(feeCollectorBefore, feeCollectorAfter) - splitAmount := burnSplitRate.MulInt64(c.expectedFeeAmount).TruncateInt() - s.Require().Equal(burnBefore, burnAfter.AddAmount(splitAmount)) - s.Require().Equal(communityBefore, communityAfter.Add(sdk.NewDecFromInt(splitAmount))) - s.Require().Equal(supplyBefore, supplyAfter.SubAmount(splitAmount)) - } - } - }) - } -} diff --git a/custom/auth/client/utils/feeutils.go b/custom/auth/client/utils/feeutils.go index cd45132ce..f99d5ee45 100644 --- a/custom/auth/client/utils/feeutils.go +++ b/custom/auth/client/utils/feeutils.go @@ -1,22 +1,12 @@ package utils import ( - "context" - - "cosmossdk.io/math" "github.com/spf13/pflag" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/tx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" - "github.com/cosmos/cosmos-sdk/x/authz" - - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - - wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" - marketexported "github.com/classic-terra/core/v3/x/market/exported" - treasuryexported "github.com/classic-terra/core/v3/x/treasury/exported" ) type ( @@ -62,13 +52,9 @@ func ComputeFeesWithCmd( gas = adj } - // Computes taxes of the msgs - taxes, err := FilterMsgAndComputeTax(clientCtx, msgs...) - if err != nil { - return nil, err - } - - fees := txf.Fees().Add(taxes...) + // As the tax is already converted to gas when simulating, + // we don't need to calculate tax anymore + fees := txf.Fees() gasPrices := txf.GasPrices() if !gasPrices.IsZero() { @@ -96,129 +82,6 @@ func ComputeFeesWithCmd( }, nil } -// FilterMsgAndComputeTax computes the stability tax on MsgSend and MsgMultiSend. -func FilterMsgAndComputeTax(clientCtx client.Context, msgs ...sdk.Msg) (taxes sdk.Coins, err error) { - taxRate, err := queryTaxRate(clientCtx) - if err != nil { - return nil, err - } - - for _, msg := range msgs { - switch msg := msg.(type) { - case *banktypes.MsgSend: - tax, err := computeTax(clientCtx, taxRate, msg.Amount) - if err != nil { - return nil, err - } - - taxes = taxes.Add(tax...) - - case *banktypes.MsgMultiSend: - for _, input := range msg.Inputs { - tax, err := computeTax(clientCtx, taxRate, input.Coins) - if err != nil { - return nil, err - } - - taxes = taxes.Add(tax...) - } - - case *authz.MsgExec: - messages, err := msg.GetMessages() - if err != nil { - panic(err) - } - - tax, err := FilterMsgAndComputeTax(clientCtx, messages...) - if err != nil { - return nil, err - } - - taxes = taxes.Add(tax...) - - case *marketexported.MsgSwapSend: - tax, err := computeTax(clientCtx, taxRate, sdk.NewCoins(msg.OfferCoin)) - if err != nil { - return nil, err - } - - taxes = taxes.Add(tax...) - - case *wasmtypes.MsgInstantiateContract: - tax, err := computeTax(clientCtx, taxRate, msg.Funds) - if err != nil { - return nil, err - } - - taxes = taxes.Add(tax...) - - case *wasmtypes.MsgInstantiateContract2: - tax, err := computeTax(clientCtx, taxRate, msg.Funds) - if err != nil { - return nil, err - } - - taxes = taxes.Add(tax...) - - case *wasmtypes.MsgExecuteContract: - tax, err := computeTax(clientCtx, taxRate, msg.Funds) - if err != nil { - return nil, err - } - - taxes = taxes.Add(tax...) - } - } - - return taxes, nil -} - -// computes the stability tax according to tax-rate and tax-cap -func computeTax(clientCtx client.Context, taxRate sdk.Dec, principal sdk.Coins) (taxes sdk.Coins, err error) { - for _, coin := range principal { - - taxCap, err := queryTaxCap(clientCtx, coin.Denom) - if err != nil { - return nil, err - } - - taxDue := sdk.NewDecFromInt(coin.Amount).Mul(taxRate).TruncateInt() - - // If tax due is greater than the tax cap, cap! - if taxDue.GT(taxCap) { - taxDue = taxCap - } - - if taxDue.Equal(sdk.ZeroInt()) { - continue - } - - taxes = taxes.Add(sdk.NewCoin(coin.Denom, taxDue)) - } - - return -} - -func queryTaxRate(clientCtx client.Context) (sdk.Dec, error) { - queryClient := treasuryexported.NewQueryClient(clientCtx) - - res, err := queryClient.TaxRate(context.Background(), &treasuryexported.QueryTaxRateRequest{}) - if err != nil { - return sdk.ZeroDec(), err - } - return res.TaxRate, err -} - -func queryTaxCap(clientCtx client.Context, denom string) (math.Int, error) { - queryClient := treasuryexported.NewQueryClient(clientCtx) - - res, err := queryClient.TaxCap(context.Background(), &treasuryexported.QueryTaxCapRequest{Denom: denom}) - if err != nil { - return sdk.NewInt(0), err - } - return res.TaxCap, err -} - // prepareFactory ensures the account defined by ctx.GetFromAddress() exists and // if the account number and/or the account sequence number are zero (not set), // they will be queried for and set on the provided Factory. A new Factory with diff --git a/custom/auth/post/post.go b/custom/auth/post/post.go index 95f0f1867..5183346cc 100644 --- a/custom/auth/post/post.go +++ b/custom/auth/post/post.go @@ -3,12 +3,22 @@ package post import ( dyncommkeeper "github.com/classic-terra/core/v3/x/dyncomm/keeper" dyncommpost "github.com/classic-terra/core/v3/x/dyncomm/post" + tax2gaskeeper "github.com/classic-terra/core/v3/x/tax2gas/keeper" + tax2gaspost "github.com/classic-terra/core/v3/x/tax2gas/post" + tax2gastypes "github.com/classic-terra/core/v3/x/tax2gas/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/ante" ) // HandlerOptions are the options required for constructing a default SDK AnteHandler. type HandlerOptions struct { - DyncommKeeper dyncommkeeper.Keeper + AccountKeeper ante.AccountKeeper + BankKeeper tax2gastypes.BankKeeper + FeegrantKeeper tax2gastypes.FeegrantKeeper + DyncommKeeper dyncommkeeper.Keeper + TreasuryKeeper tax2gastypes.TreasuryKeeper + DistrKeeper tax2gastypes.DistrKeeper + Tax2Gaskeeper tax2gaskeeper.Keeper } // NewPostHandler returns an PostHandler that checks and set target @@ -16,5 +26,6 @@ type HandlerOptions struct { func NewPostHandler(options HandlerOptions) (sdk.PostHandler, error) { return sdk.ChainPostDecorators( dyncommpost.NewDyncommPostDecorator(options.DyncommKeeper), + tax2gaspost.NewTax2GasPostDecorator(options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TreasuryKeeper, options.DistrKeeper, options.Tax2Gaskeeper), ), nil } diff --git a/custom/auth/tx/service.go b/custom/auth/tx/service.go index 63710d04e..f227a8164 100644 --- a/custom/auth/tx/service.go +++ b/custom/auth/tx/service.go @@ -9,6 +9,7 @@ import ( "google.golang.org/grpc/status" customante "github.com/classic-terra/core/v3/custom/auth/ante" + tax2gasUtils "github.com/classic-terra/core/v3/x/tax2gas/utils" "github.com/cosmos/cosmos-sdk/client" codectypes "github.com/cosmos/cosmos-sdk/codec/types" @@ -21,13 +22,15 @@ var _ ServiceServer = txServer{} type txServer struct { clientCtx client.Context treasuryKeeper customante.TreasuryKeeper + tax2gasKeeper customante.Tax2GasKeeper } // NewTxServer creates a new Tx service server. -func NewTxServer(clientCtx client.Context, treasuryKeeper customante.TreasuryKeeper) ServiceServer { +func NewTxServer(clientCtx client.Context, treasuryKeeper customante.TreasuryKeeper, tax2gasKeeper customante.Tax2GasKeeper) ServiceServer { return txServer{ clientCtx: clientCtx, treasuryKeeper: treasuryKeeper, + tax2gasKeeper: tax2gasKeeper, } } @@ -52,7 +55,8 @@ func (ts txServer) ComputeTax(c context.Context, req *ComputeTaxRequest) (*Compu return nil, status.Errorf(codes.InvalidArgument, "empty txBytes is not allowed") } - taxAmount := customante.FilterMsgAndComputeTax(ctx, ts.treasuryKeeper, msgs...) + burnTaxRate := ts.tax2gasKeeper.GetBurnTaxRate(ctx) + taxAmount := tax2gasUtils.FilterMsgAndComputeTax(ctx, ts.treasuryKeeper, burnTaxRate, msgs...) return &ComputeTaxResponse{ TaxAmount: taxAmount, }, nil @@ -63,10 +67,11 @@ func RegisterTxService( qrt gogogrpc.Server, clientCtx client.Context, treasuryKeeper customante.TreasuryKeeper, + tax2gasKeeper customante.Tax2GasKeeper, ) { RegisterServiceServer( qrt, - NewTxServer(clientCtx, treasuryKeeper), + NewTxServer(clientCtx, treasuryKeeper, tax2gasKeeper), ) } diff --git a/custom/wasm/keeper/handler_plugin.go b/custom/wasm/keeper/handler_plugin.go index f3a24801b..bff98dae1 100644 --- a/custom/wasm/keeper/handler_plugin.go +++ b/custom/wasm/keeper/handler_plugin.go @@ -1,20 +1,22 @@ package keeper import ( - "github.com/classic-terra/core/v3/custom/auth/ante" - treasurykeeper "github.com/classic-terra/core/v3/x/treasury/keeper" - - wasmvmtypes "github.com/CosmWasm/wasmvm/types" + errorsmod "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/baseapp" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - cosmosante "github.com/cosmos/cosmos-sdk/x/auth/ante" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" bankKeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + tax2gaskeeper "github.com/classic-terra/core/v3/x/tax2gas/keeper" + tax2gastypes "github.com/classic-terra/core/v3/x/tax2gas/types" + tax2gasutils "github.com/classic-terra/core/v3/x/tax2gas/utils" + treasurykeeper "github.com/classic-terra/core/v3/x/treasury/keeper" ) // msgEncoder is an extension point to customize encodings @@ -35,6 +37,7 @@ type SDKMessageHandler struct { treasuryKeeper treasurykeeper.Keeper accountKeeper authkeeper.AccountKeeper bankKeeper bankKeeper.Keeper + tax2gaskeeper tax2gaskeeper.Keeper } func NewMessageHandler( @@ -45,6 +48,7 @@ func NewMessageHandler( bankKeeper bankKeeper.Keeper, treasuryKeeper treasurykeeper.Keeper, accountKeeper authkeeper.AccountKeeper, + tax2gaskeeper tax2gaskeeper.Keeper, unpacker codectypes.AnyUnpacker, portSource wasmtypes.ICS20TransferPortSource, customEncoders ...*wasmkeeper.MessageEncoders, @@ -54,19 +58,20 @@ func NewMessageHandler( encoders = encoders.Merge(e) } return wasmkeeper.NewMessageHandlerChain( - NewSDKMessageHandler(router, encoders, treasuryKeeper, accountKeeper, bankKeeper), + NewSDKMessageHandler(router, encoders, treasuryKeeper, accountKeeper, bankKeeper, tax2gaskeeper), wasmkeeper.NewIBCRawPacketHandler(ics4Wrapper, channelKeeper, capabilityKeeper), wasmkeeper.NewBurnCoinMessageHandler(bankKeeper), ) } -func NewSDKMessageHandler(router MessageRouter, encoders msgEncoder, treasuryKeeper treasurykeeper.Keeper, accountKeeper authkeeper.AccountKeeper, bankKeeper bankKeeper.Keeper) SDKMessageHandler { +func NewSDKMessageHandler(router MessageRouter, encoders msgEncoder, treasuryKeeper treasurykeeper.Keeper, accountKeeper authkeeper.AccountKeeper, bankKeeper bankKeeper.Keeper, tax2gaskeeper tax2gaskeeper.Keeper) SDKMessageHandler { return SDKMessageHandler{ router: router, encoders: encoders, treasuryKeeper: treasuryKeeper, accountKeeper: accountKeeper, bankKeeper: bankKeeper, + tax2gaskeeper: tax2gaskeeper, } } @@ -76,17 +81,25 @@ func (h SDKMessageHandler) DispatchMsg(ctx sdk.Context, contractAddr sdk.AccAddr return nil, nil, err } + gasPrices, ok := ctx.Value(tax2gastypes.FinalGasPrices).(sdk.DecCoins) + if !ok { + gasPrices = h.tax2gaskeeper.GetGasPrices(ctx) + } for _, sdkMsg := range sdkMsgs { - // Charge tax on result msg - taxes := ante.FilterMsgAndComputeTax(ctx, h.treasuryKeeper, sdkMsg) - if !taxes.IsZero() { - eventManager := sdk.NewEventManager() - contractAcc := h.accountKeeper.GetAccount(ctx, contractAddr) - if err := cosmosante.DeductFees(h.bankKeeper, ctx.WithEventManager(eventManager), contractAcc, taxes); err != nil { - return nil, nil, err - } + if h.tax2gaskeeper.IsEnabled(ctx) { + burnTaxRate := h.tax2gaskeeper.GetBurnTaxRate(ctx) + taxes := tax2gasutils.FilterMsgAndComputeTax(ctx, h.treasuryKeeper, burnTaxRate, sdkMsg) + if !taxes.IsZero() { + eventManager := sdk.NewEventManager() - events = eventManager.Events() + taxGas, err := tax2gasutils.ComputeGas(gasPrices, taxes) + if err != nil { + return nil, nil, err + } + ctx.TaxGasMeter().ConsumeGas(taxGas, "tax gas") + + events = eventManager.Events() + } } res, err := h.handleSdkMessage(ctx, contractAddr, sdkMsg) @@ -112,7 +125,7 @@ func (h SDKMessageHandler) handleSdkMessage(ctx sdk.Context, contractAddr sdk.Ad // make sure this account can send it for _, acct := range msg.GetSigners() { if !acct.Equals(contractAddr) { - return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "contract doesn't have permission") + return nil, errorsmod.Wrap(sdkerrors.ErrUnauthorized, "contract doesn't have permission") } } @@ -127,5 +140,5 @@ func (h SDKMessageHandler) handleSdkMessage(ctx sdk.Context, contractAddr sdk.Ad // proto messages and has registered all `Msg services`, then this // path should never be called, because all those Msgs should be // registered within the `msgServiceRouter` already. - return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "can't route message %+v", msg) + return nil, errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "can't route message %+v", msg) } diff --git a/go.mod b/go.mod index ad62abf92..61ed3c766 100644 --- a/go.mod +++ b/go.mod @@ -225,7 +225,7 @@ replace ( ) replace ( - github.com/CosmWasm/wasmd => github.com/classic-terra/wasmd v0.45.0-terra.6 + github.com/CosmWasm/wasmd => github.com/classic-terra/wasmd v0.45.0-terra.7 // 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 diff --git a/go.sum b/go.sum index f7f4f93b0..cbb8754d9 100644 --- a/go.sum +++ b/go.sum @@ -356,8 +356,8 @@ github.com/classic-terra/goleveldb v0.0.0-20230914223247-2b28f6655121 h1:fjpWDB0 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= github.com/classic-terra/ibc-go/v7 v7.4.0-terra/go.mod h1:s0lxNkjVIqsb8AVltL0qhzxeLgOKvWZrknPuvgjlEQ8= -github.com/classic-terra/wasmd v0.45.0-terra.6 h1:ytQsD5/aGGIb/CQ+AiP1GO8AxCZGX4IgijDP+jyShYE= -github.com/classic-terra/wasmd v0.45.0-terra.6/go.mod h1:TZFaZdgesx5DJtQzvZFFEZRglQtTYoUCK3ygpvA3NgU= +github.com/classic-terra/wasmd v0.45.0-terra.7 h1:s2Wx2kWmsajiYP0cHADsbD2dYg0JAZSwd2JeeEEpTZM= +github.com/classic-terra/wasmd v0.45.0-terra.7/go.mod h1:dF31qSPGrHgGPreCCIbAtw/YnJT0Gi997sAA5ZllPSI= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304= diff --git a/proto/terra/tax2gas/v1beta1/genesis.proto b/proto/terra/tax2gas/v1beta1/genesis.proto new file mode 100644 index 000000000..799f6101b --- /dev/null +++ b/proto/terra/tax2gas/v1beta1/genesis.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; +package terra.tax2gas.v1beta1; + +import "gogoproto/gogo.proto"; +import "cosmos/base/v1beta1/coin.proto"; +import "amino/amino.proto"; + +option go_package = "github.com/classic-terra/core/v3/x/tax2gas/types"; + +message Params { + option (gogoproto.goproto_stringer) = true; + option (amino.name) = "terra/x/tax2gas/Params"; + + repeated cosmos.base.v1beta1.DecCoin gas_prices = 1 [ + (gogoproto.moretags) = "yaml:\"gas_prices\"", + (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.DecCoins", + (gogoproto.nullable) = false, + (amino.dont_omitempty) = true + ]; + + string burn_tax_rate = 2 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; + + bool enabled = 3; + + // bypass_min_fee_msg_types defines a list of message type urls + // that are free of fee charge. + repeated string bypass_min_fee_msg_types = 4 [ + (gogoproto.jsontag) = "bypass_min_fee_msg_types,omitempty", + (gogoproto.moretags) = "yaml:\"bypass_min_fee_msg_types\"" + ]; + + // max_total_bypass_min_fee_msg_gas_usage defines the total maximum gas usage + // allowed for a transaction containing only messages of types in bypass_min_fee_msg_types + // to bypass fee charge. + uint64 max_total_bypass_min_fee_msg_gas_usage = 5; +} + +// GenesisState defines the tax2gas module's genesis state. +message GenesisState { + // params is the container of tax2gas parameters. + Params params = 1 [(gogoproto.nullable) = false]; +} \ No newline at end of file diff --git a/proto/terra/tax2gas/v1beta1/query.proto b/proto/terra/tax2gas/v1beta1/query.proto new file mode 100644 index 000000000..bb4d635ab --- /dev/null +++ b/proto/terra/tax2gas/v1beta1/query.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; +package terra.tax2gas.v1beta1; + +import "gogoproto/gogo.proto"; +import "google/api/annotations.proto"; +import "terra/tax2gas/v1beta1/genesis.proto"; + +option go_package = "github.com/classic-terra/core/v3/x/tax2gas/types"; + +service Query { + rpc Params(QueryParamsRequest) returns (QueryParamsResponse) { + option (google.api.http).get = "/terra/tax2gas/Params"; + } + rpc BurnTaxRate(QueryBurnTaxRateRequest) returns (QueryBurnTaxRateResponse) { + option (google.api.http).get = "/terra/tax2gas/BurnTaxRate"; + } +} + +//=============================== Params +message QueryParamsRequest {} +message QueryParamsResponse { + Params params = 1 [(gogoproto.nullable) = false]; +} + +message QueryBurnTaxRateRequest {} +message QueryBurnTaxRateResponse { + string burn_tax_rate = 1 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; +} \ No newline at end of file diff --git a/proto/terra/tax2gas/v1beta1/tx.proto b/proto/terra/tax2gas/v1beta1/tx.proto new file mode 100644 index 000000000..87d8f61c1 --- /dev/null +++ b/proto/terra/tax2gas/v1beta1/tx.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; +package terra.tax2gas.v1beta1; + +import "gogoproto/gogo.proto"; +import "cosmos/msg/v1/msg.proto"; +import "amino/amino.proto"; +import "cosmos_proto/cosmos.proto"; +import "terra/tax2gas/v1beta1/genesis.proto"; + +option go_package = "github.com/classic-terra/core/v3/x/tax2gas/types"; + +service Msg { + option (cosmos.msg.v1.service) = true; + + rpc UpdateParams(MsgUpdateParams) returns (MsgUpdateParamsResponse); +} + +// MsgUpdateParams is the Msg/UpdateParams request type. +// +// Since: cosmos-sdk 0.47 +message MsgUpdateParams { + option (cosmos.msg.v1.signer) = "authority"; + + // authority is the address that controls the module (defaults to x/gov unless overwritten). + string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + option (amino.name) = "terra/x/tax2gas/MsgUpdateParams"; + + // params defines the x/tax2gas parameters to update. + // + // NOTE: All parameters must be supplied. + Params params = 2 [(gogoproto.nullable) = false, (amino.dont_omitempty) = true]; +} + +// MsgUpdateParamsResponse defines the response structure for executing a +// MsgUpdateParams message. +// +// Since: cosmos-sdk 0.47 +message MsgUpdateParamsResponse {} \ No newline at end of file diff --git a/scripts/protocgen.sh b/scripts/protocgen.sh index bfcae1a32..674c4958a 100755 --- a/scripts/protocgen.sh +++ b/scripts/protocgen.sh @@ -2,9 +2,6 @@ set -eo pipefail -# get protoc executions -go get github.com/regen-network/cosmos-proto/protoc-gen-gocosmos 2>/dev/null - echo "Generating gogo proto code" cd proto proto_dirs=$(find terra -path -prune -o -name '*.proto' -print0 | xargs -0 -n1 dirname | sort | uniq) diff --git a/tests/e2e/configurer/chain/commands.go b/tests/e2e/configurer/chain/commands.go index c1e96bac5..f419217a5 100644 --- a/tests/e2e/configurer/chain/commands.go +++ b/tests/e2e/configurer/chain/commands.go @@ -25,18 +25,25 @@ import ( func (n *NodeConfig) StoreWasmCode(wasmFile, from string) { n.LogActionF("storing wasm code from file %s", wasmFile) - cmd := []string{"terrad", "tx", "wasm", "store", wasmFile, fmt.Sprintf("--from=%s", from)} + cmd := []string{"terrad", "tx", "wasm", "store", wasmFile, fmt.Sprintf("--from=%s", from), "--fees=10uluna", "--gas=2000000"} _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) require.NoError(n.t, err) n.LogActionF("successfully stored") } -func (n *NodeConfig) InstantiateWasmContract(codeID, initMsg, amount, from string) { +func (n *NodeConfig) InstantiateWasmContract(codeID, initMsg, amount, from string, feeDenoms []string, fees ...sdk.Coin) { n.LogActionF("instantiating wasm contract %s with %s", codeID, initMsg) cmd := []string{"terrad", "tx", "wasm", "instantiate", codeID, initMsg, fmt.Sprintf("--from=%s", from), "--no-admin", "--label=ratelimit"} if amount != "" { cmd = append(cmd, fmt.Sprintf("--amount=%s", amount)) } + if len(fees) == 0 { + gasPrices := feeDenomsToGasPrices(feeDenoms) + cmd = append(cmd, "--gas", "auto", "--gas-adjustment=1.2", fmt.Sprintf("--gas-prices=%s", gasPrices)) + } else { + feeCoins := sdk.NewCoins(fees...) + cmd = append(cmd, "--fees", feeCoins.String(), "--gas", "auto", "--gas-adjustment=1.2") + } n.LogActionF(strings.Join(cmd, " ")) _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) @@ -45,7 +52,7 @@ func (n *NodeConfig) InstantiateWasmContract(codeID, initMsg, amount, from strin n.LogActionF("successfully initialized") } -func (n *NodeConfig) Instantiate2WasmContract(codeID, initMsg, salt, amount, fee, gas, from string) { +func (n *NodeConfig) Instantiate2WasmContract(codeID, initMsg, salt, amount, from string, feeDenoms []string) { n.LogActionF("instantiating wasm contract %s with %s", codeID, initMsg) encodedSalt := make([]byte, hex.EncodedLen(len([]byte(salt)))) hex.Encode(encodedSalt, []byte(salt)) @@ -53,33 +60,45 @@ func (n *NodeConfig) Instantiate2WasmContract(codeID, initMsg, salt, amount, fee if amount != "" { cmd = append(cmd, fmt.Sprintf("--amount=%s", amount)) } - if fee != "" { - cmd = append(cmd, fmt.Sprintf("--fees=%s", fee)) - } - if gas != "" { - cmd = append(cmd, fmt.Sprintf("--gas=%s", gas)) - } + gasPrices := feeDenomsToGasPrices(feeDenoms) + cmd = append(cmd, "--gas", "auto", "--gas-adjustment=1.2", fmt.Sprintf("--gas-prices=%s", gasPrices)) + n.LogActionF(strings.Join(cmd, " ")) _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) require.NoError(n.t, err) n.LogActionF("successfully initialized") } -func (n *NodeConfig) WasmExecute(contract, execMsg, amount, fee, from string) { +func (n *NodeConfig) WasmExecute(contract, execMsg, amount, from string, feeDenoms []string) { n.LogActionF("executing %s on wasm contract %s from %s", execMsg, contract, from) cmd := []string{"terrad", "tx", "wasm", "execute", contract, execMsg, fmt.Sprintf("--from=%s", from)} if amount != "" { cmd = append(cmd, fmt.Sprintf("--amount=%s", amount)) } - if fee != "" { - cmd = append(cmd, fmt.Sprintf("--fees=%s", fee)) - } + gasPrices := feeDenomsToGasPrices(feeDenoms) + cmd = append(cmd, "--gas", "auto", "--gas-adjustment=1.2", fmt.Sprintf("--gas-prices=%s", gasPrices)) + n.LogActionF(strings.Join(cmd, " ")) _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) require.NoError(n.t, err) n.LogActionF("successfully executed") } +func (n *NodeConfig) WasmExecuteError(contract, execMsg, amount, from string, feeDenoms []string) { + n.LogActionF("executing %s on wasm contract %s from %s", execMsg, contract, from) + cmd := []string{"terrad", "tx", "wasm", "execute", contract, execMsg, fmt.Sprintf("--from=%s", from)} + if amount != "" { + cmd = append(cmd, fmt.Sprintf("--amount=%s", amount)) + } + gasPrices := feeDenomsToGasPrices(feeDenoms) + cmd = append(cmd, "--gas", "auto", "--gas-adjustment=1.2", fmt.Sprintf("--gas-prices=%s", gasPrices)) + + n.LogActionF(strings.Join(cmd, " ")) + _, _, err := n.containerManager.ExecTxCmdError(n.t, n.chainID, n.Name, cmd, "", true) + require.NoError(n.t, err) + n.LogActionF("executed failed") +} + // QueryParams extracts the params for a given subspace and key. This is done generically via json to avoid having to // specify the QueryParamResponse type (which may not exist for all params). func (n *NodeConfig) QueryParams(subspace, key string, result any) { @@ -124,13 +143,13 @@ func (n *NodeConfig) SubmitAddBurnTaxExemptionAddressProposal(addresses []string "add-burn-tax-exemption-address", strings.Join(addresses, ","), "--title=\"burn tax exemption address\"", "--description=\"\"burn tax exemption address", + "--gas", "300000", "--gas-prices", "1uluna", fmt.Sprintf("--from=%s", walletName), } resp, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) require.NoError(n.t, err) - fmt.Println("resp: ", resp.String()) proposalID, err := extractProposalIDFromResponse(resp.String()) require.NoError(n.t, err) @@ -152,7 +171,7 @@ func (n *NodeConfig) FailIBCTransfer(from, recipient, amount string) { func (n *NodeConfig) SendIBCTransfer(from, recipient, amount, memo string) { n.LogActionF("IBC sending %s from %s to %s. memo: %s", amount, from, recipient, memo) - cmd := []string{"terrad", "tx", "ibc-transfer", "transfer", "transfer", "channel-0", recipient, amount, fmt.Sprintf("--from=%s", from), "--memo", memo} + cmd := []string{"terrad", "tx", "ibc-transfer", "transfer", "transfer", "channel-0", recipient, amount, fmt.Sprintf("--from=%s", from), "--memo", memo, "--fees=10uluna"} _, _, err := n.containerManager.ExecTxCmdWithSuccessString(n.t, n.chainID, n.Name, cmd, "\"code\":0") require.NoError(n.t, err) @@ -171,7 +190,7 @@ func (n *NodeConfig) SubmitTextProposal(text string, initialDeposit sdk.Coin) { func (n *NodeConfig) DepositProposal(proposalNumber int) { n.LogActionF("depositing on proposal: %d", proposalNumber) deposit := sdk.NewCoin(initialization.TerraDenom, sdk.NewInt(20*assets.MicroUnit)).String() - cmd := []string{"terrad", "tx", "gov", "deposit", fmt.Sprintf("%d", proposalNumber), deposit, "--from=val"} + cmd := []string{"terrad", "tx", "gov", "deposit", fmt.Sprintf("%d", proposalNumber), deposit, "--from=val", "--gas", "300000", "--fees", "10000000uluna"} _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) require.NoError(n.t, err) n.LogActionF("successfully deposited on proposal %d", proposalNumber) @@ -179,7 +198,7 @@ func (n *NodeConfig) DepositProposal(proposalNumber int) { func (n *NodeConfig) VoteYesProposal(from string, proposalNumber int) { n.LogActionF("voting yes on proposal: %d", proposalNumber) - cmd := []string{"terrad", "tx", "gov", "vote", fmt.Sprintf("%d", proposalNumber), "yes", fmt.Sprintf("--from=%s", from)} + cmd := []string{"terrad", "tx", "gov", "vote", fmt.Sprintf("%d", proposalNumber), "yes", fmt.Sprintf("--from=%s", from), "--gas", "300000", "--fees", "10000000uluna"} _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) require.NoError(n.t, err) n.LogActionF("successfully voted yes on proposal %d", proposalNumber) @@ -216,44 +235,77 @@ func extractProposalIDFromResponse(response string) (int, error) { return proposalID, nil } -func (n *NodeConfig) BankSend(amount string, sendAddress string, receiveAddress string) { - n.BankSendWithWallet(amount, sendAddress, receiveAddress, "val") +func (n *NodeConfig) BankSendError(amount string, sendAddress string, receiveAddress string, walletName string, gasLimit string, fees sdk.Coins, failError string) { + n.LogActionF("bank sending %s from address %s to %s", amount, sendAddress, receiveAddress) + cmd := []string{"terrad", "tx", "bank", "send", sendAddress, receiveAddress, amount, fmt.Sprintf("--from=%s", walletName)} + cmd = append(cmd, "--fees", fees.String(), "--gas", gasLimit) + _, _, err := n.containerManager.ExecTxCmdError(n.t, n.chainID, n.Name, cmd, failError, false) + require.NoError(n.t, err) + n.LogActionF("failed sent bank sent %s from address %s to %s", amount, sendAddress, receiveAddress) +} + +func (n *NodeConfig) BankSend(amount string, sendAddress string, receiveAddress string, feeDenoms []string, fees ...sdk.Coin) { + n.BankSendWithWallet(amount, sendAddress, receiveAddress, "val", feeDenoms, fees...) } -func (n *NodeConfig) BankSendWithWallet(amount string, sendAddress string, receiveAddress string, walletName string) { +func (n *NodeConfig) BankSendWithWallet(amount string, sendAddress string, receiveAddress string, walletName string, feeDenoms []string, fees ...sdk.Coin) { n.LogActionF("bank sending %s from address %s to %s", amount, sendAddress, receiveAddress) cmd := []string{"terrad", "tx", "bank", "send", sendAddress, receiveAddress, amount, fmt.Sprintf("--from=%s", walletName)} + gasPrices := feeDenomsToGasPrices(feeDenoms) + if len(fees) == 0 { + cmd = append(cmd, "--gas", "auto", "--gas-adjustment=1.2", fmt.Sprintf("--gas-prices=%s", gasPrices)) + } else { + feeCoins := sdk.NewCoins(fees...) + cmd = append(cmd, "--fees", feeCoins.String(), "--gas", "auto", "--gas-adjustment=1.2") + } _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) require.NoError(n.t, err) n.LogActionF("successfully sent bank sent %s from address %s to %s", amount, sendAddress, receiveAddress) } -func (n *NodeConfig) BankSendFeeGrantWithWallet(amount string, sendAddress string, receiveAddress string, feeGranter string, walletName string) { +func (n *NodeConfig) BankSendFeeGrantWithWallet(amount string, sendAddress string, receiveAddress string, feeGranter string, walletName string, feeDenoms []string, fees ...sdk.Coin) { n.LogActionF("bank sending %s from address %s to %s", amount, sendAddress, receiveAddress) cmd := []string{"terrad", "tx", "bank", "send", sendAddress, receiveAddress, amount, fmt.Sprintf("--fee-granter=%s", feeGranter), fmt.Sprintf("--from=%s", walletName)} + gasPrices := feeDenomsToGasPrices(feeDenoms) + if len(fees) == 0 { + cmd = append(cmd, "--gas", "auto", "--gas-adjustment=1.2", fmt.Sprintf("--gas-prices=%s", gasPrices)) + } else { + feeCoins := sdk.NewCoins(fees...) + cmd = append(cmd, "--fees", feeCoins.String(), "--gas", "auto", "--gas-adjustment=1.2") + } _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) require.NoError(n.t, err) n.LogActionF("successfully sent bank sent %s from address %s to %s", amount, sendAddress, receiveAddress) } -func (n *NodeConfig) BankMultiSend(amount string, split bool, sendAddress string, receiveAddresses ...string) { +func (n *NodeConfig) BankMultiSend(amount string, split bool, sendAddress string, feeDenoms []string, receiveAddresses []string) { n.LogActionF("bank multisending from %s to %s", sendAddress, strings.Join(receiveAddresses, ",")) cmd := []string{"terrad", "tx", "bank", "multi-send", sendAddress} cmd = append(cmd, receiveAddresses...) cmd = append(cmd, amount, "--from=val") + + gasPrices := feeDenomsToGasPrices(feeDenoms) + cmd = append(cmd, "--gas", "auto", "--gas-adjustment=1.2", fmt.Sprintf("--gas-prices=%s", gasPrices)) if split { cmd = append(cmd, "--split") } - _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) require.NoError(n.t, err) n.LogActionF("successfully multisent %s to %s", sendAddress, strings.Join(receiveAddresses, ",")) } -func (n *NodeConfig) GrantAddress(granter, gratee string, spendLimit string, walletName string) { +func (n *NodeConfig) GrantAddress(granter, gratee string, walletName string, feeDenom string, spendLimit ...string) { n.LogActionF("granting for address %s", gratee) - cmd := []string{"terrad", "tx", "feegrant", "grant", granter, gratee, fmt.Sprintf("--from=%s", walletName), fmt.Sprintf("--spend-limit=%s", spendLimit)} + cmd := []string{"terrad", "tx", "feegrant", "grant", granter, gratee, fmt.Sprintf("--from=%s", walletName), fmt.Sprintf("--spend-limit=%s", strings.Join(spendLimit, ",")), fmt.Sprintf("--fees=10%s", feeDenom)} + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) + require.NoError(n.t, err) + n.LogActionF("successfully granted for address %s", gratee) +} + +func (n *NodeConfig) RevokeGrant(granter, gratee string, walletName string, feeDenom string) { + n.LogActionF("revoking grant for address %s", gratee) + cmd := []string{"terrad", "tx", "feegrant", "revoke", granter, gratee, fmt.Sprintf("--from=%s", walletName), fmt.Sprintf("--fees=10%s", feeDenom)} _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainID, n.Name, cmd) require.NoError(n.t, err) n.LogActionF("successfully granted for address %s", gratee) @@ -315,3 +367,22 @@ func (n *NodeConfig) Status() (resultStatus, error) { //nolint } return result, nil } + +func feeDenomsToGasPrices(feeDenoms []string) string { + gasPrices := "" + for i, feeDenom := range feeDenoms { + switch feeDenom { + case initialization.TerraDenom: + gasPrices += fmt.Sprintf("%s%s", initialization.TerraGasPrice, initialization.TerraDenom) + case initialization.UsdDenom: + gasPrices += fmt.Sprintf("%s%s", initialization.UsdGasPrice, initialization.UsdDenom) + case initialization.EurDenom: + gasPrices += fmt.Sprintf("%s%s", initialization.EurGasPrice, initialization.EurDenom) + default: + } + if i != len(feeDenoms)-1 { + gasPrices += "," + } + } + return gasPrices +} diff --git a/tests/e2e/containers/containers.go b/tests/e2e/containers/containers.go index 8ffbbbd80..6cd0e854c 100644 --- a/tests/e2e/containers/containers.go +++ b/tests/e2e/containers/containers.go @@ -83,6 +83,23 @@ func (m *Manager) ExecTxCmd(t *testing.T, chainID string, containerName string, return m.ExecTxCmdWithSuccessString(t, chainID, containerName, command, "\"code\":0") } +// ExecTxCmdError Runs ExecCmd +func (m *Manager) ExecTxCmdError(t *testing.T, chainID string, containerName string, command []string, failStr string, checkTxHash bool) (bytes.Buffer, bytes.Buffer, error) { + allTxArgs := []string{fmt.Sprintf("--chain-id=%s", chainID), "--yes", "--keyring-backend=test", "--log_format=json"} + // parse to see if command has gas flags. If not, add default gas flags. + addGasFlags := true + for _, cmd := range command { + if strings.HasPrefix(cmd, "--gas") || strings.HasPrefix(cmd, "--fees") { + addGasFlags = false + } + } + if addGasFlags { + allTxArgs = append(allTxArgs, txDefaultGasArgs...) + } + txCommand := append(command, allTxArgs...) //nolint + return m.ExecCmd(t, containerName, txCommand, failStr, checkTxHash) +} + // ExecTxCmdWithSuccessString Runs ExecCmd, with flags for txs added. // namely adding flags `--chain-id={chain-id} --yes --keyring-backend=test "--log_format=json"`, // and searching for `successStr` @@ -104,6 +121,7 @@ func (m *Manager) ExecTxCmdWithSuccessString(t *testing.T, chainID string, conta // ExecHermesCmd executes command on the hermes relayer 1 container. func (m *Manager) ExecHermesCmd(t *testing.T, command []string, success string) (bytes.Buffer, bytes.Buffer, error) { + time.Sleep(time.Second * 30) return m.ExecCmd(t, hermesContainerName, command, success, false) } diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 26872dced..3a619954d 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -28,7 +28,7 @@ func (s *IntegrationTestSuite) TestIBCWasmHooks() { nodeA.InstantiateWasmContract( strconv.Itoa(chainA.LatestCodeID), `{"count": "0"}`, "", - initialization.ValidatorWalletName) + initialization.ValidatorWalletName, []string{}, sdk.NewCoin(initialization.TerraDenom, sdk.NewInt(2))) contracts, err := nodeA.QueryContractsFromID(chainA.LatestCodeID) s.NoError(err) @@ -104,6 +104,7 @@ func (s *IntegrationTestSuite) TestAddBurnTaxExemptionAddress() { s.Require().Contains(whitelistedAddresses, whitelistAddr2) } +// Each tx gas will cost 2 uluna (1 is for ante handler, 1 is for post handler) func (s *IntegrationTestSuite) TestFeeTax() { chain := s.configurer.GetChainConfig(0) node, err := chain.GetDefaultNode() @@ -119,14 +120,16 @@ func (s *IntegrationTestSuite) TestFeeTax() { s.Require().NoError(err) test1Addr := node.CreateWallet("test1") + test2Addr := node.CreateWallet("test2") // Test 1: banktypes.MsgSend // burn tax with bank send - node.BankSend(transferCoin1.String(), validatorAddr, test1Addr) - subAmount := transferAmount1.Add(initialization.TaxRate.MulInt(transferAmount1).TruncateInt()) - decremented := validatorBalance.Sub(sdk.NewCoin(initialization.TerraDenom, subAmount)) + node.BankSend(transferCoin1.String(), validatorAddr, test1Addr, []string{initialization.TerraDenom}) + + // Due to the fee estimate when using + decremented := validatorBalance.Sub(sdk.NewCoin(initialization.TerraDenom, subAmount.AddRaw(2))) newValidatorBalance, err := node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) s.Require().NoError(err) @@ -136,62 +139,38 @@ func (s *IntegrationTestSuite) TestFeeTax() { s.Require().Equal(balanceTest1.Amount, transferAmount1) s.Require().Equal(newValidatorBalance, decremented) - // Test 2: try bank send with grant - test2Addr := node.CreateWallet("test2") - transferAmount2 := sdkmath.NewInt(10000000) - transferCoin2 := sdk.NewCoin(initialization.TerraDenom, transferAmount2) - - node.BankSend(transferCoin2.String(), validatorAddr, test2Addr) - node.GrantAddress(test2Addr, test1Addr, transferCoin2.String(), "test2") - + // Test 2: banktypes.MsgMultiSend validatorBalance, err = node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) s.Require().NoError(err) - node.BankSendFeeGrantWithWallet(transferCoin2.String(), test1Addr, validatorAddr, test2Addr, "test1") - - newValidatorBalance, err = node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) - s.Require().NoError(err) - - balanceTest1, err = node.QuerySpecificBalance(test1Addr, initialization.TerraDenom) - s.Require().NoError(err) - - balanceTest2, err := node.QuerySpecificBalance(test2Addr, initialization.TerraDenom) - s.Require().NoError(err) - - s.Require().Equal(balanceTest1.Amount, transferAmount1.Sub(transferAmount2)) - s.Require().Equal(newValidatorBalance, validatorBalance.Add(transferCoin2)) - s.Require().Equal(balanceTest2.Amount, transferAmount2.Sub(initialization.TaxRate.MulInt(transferAmount2).TruncateInt())) - - // Test 3: banktypes.MsgMultiSend - validatorBalance, err = node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) - s.Require().NoError(err) - - node.BankMultiSend(transferCoin1.String(), false, validatorAddr, test1Addr, test2Addr) + totalTransferAmount := transferAmount1.Mul(sdk.NewInt(2)) + node.BankMultiSend(transferCoin1.String(), false, validatorAddr, []string{initialization.TerraDenom}, []string{test1Addr, test2Addr}) newValidatorBalance, err = node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) s.Require().NoError(err) - totalTransferAmount := transferAmount1.Mul(sdk.NewInt(2)) subAmount = totalTransferAmount.Add(initialization.TaxRate.MulInt(totalTransferAmount).TruncateInt()) - s.Require().Equal(newValidatorBalance, validatorBalance.Sub(sdk.NewCoin(initialization.TerraDenom, subAmount))) + s.Require().Equal(newValidatorBalance, validatorBalance.Sub(sdk.NewCoin(initialization.TerraDenom, subAmount.AddRaw(2)))) } +// Each tx gas will cost 2 uluna (1 is for ante handler, 1 is for post handler) func (s *IntegrationTestSuite) TestFeeTaxWasm() { chain := s.configurer.GetChainConfig(0) node, err := chain.GetDefaultNode() s.Require().NoError(err) - testAddr := node.CreateWallet("test") + testAddr := node.CreateWallet("test-wasm") transferAmount := sdkmath.NewInt(100000000) transferCoin := sdk.NewCoin(initialization.TerraDenom, transferAmount) - node.BankSend(fmt.Sprintf("%suluna", transferAmount.Mul(sdk.NewInt(4))), initialization.ValidatorWalletName, testAddr) + + node.BankSend(fmt.Sprintf("%suluna", transferAmount.Mul(sdk.NewInt(4))), initialization.ValidatorWalletName, testAddr, []string{initialization.TerraDenom}) node.StoreWasmCode("counter.wasm", initialization.ValidatorWalletName) chain.LatestCodeID = int(node.QueryLatestWasmCodeID()) // instantiate contract and transfer 100000000uluna node.InstantiateWasmContract( strconv.Itoa(chain.LatestCodeID), `{"count": "0"}`, transferCoin.String(), - "test") + "test-wasm", []string{initialization.TerraDenom}) contracts, err := node.QueryContractsFromID(chain.LatestCodeID) s.Require().NoError(err) @@ -199,17 +178,15 @@ func (s *IntegrationTestSuite) TestFeeTaxWasm() { balance1, err := node.QuerySpecificBalance(testAddr, initialization.TerraDenom) s.Require().NoError(err) - // 400000000 - 100000000 - 100000000 * TaxRate = 300000000 - 10000000 * TaxRate + // 400000000 - 100000000 - 100000000 * TaxRate - 2 (gas) = 300000000 - 10000000 * TaxRate - 2 (gas) taxAmount := initialization.TaxRate.MulInt(transferAmount).TruncateInt() - s.Require().Equal(balance1.Amount, transferAmount.Mul(sdk.NewInt(3)).Sub(taxAmount)) - - stabilityFee := sdk.NewDecWithPrec(2, 2).MulInt(transferAmount) + s.Require().Equal(balance1.Amount, transferAmount.Mul(sdk.NewInt(3)).Sub(taxAmount).SubRaw(2)) node.Instantiate2WasmContract( strconv.Itoa(chain.LatestCodeID), `{"count": "0"}`, "salt", transferCoin.String(), - fmt.Sprintf("%duluna", stabilityFee), "300000", "test") + "test-wasm", []string{initialization.TerraDenom}) contracts, err = node.QueryContractsFromID(chain.LatestCodeID) s.Require().NoError(err) @@ -217,16 +194,366 @@ func (s *IntegrationTestSuite) TestFeeTaxWasm() { balance2, err := node.QuerySpecificBalance(testAddr, initialization.TerraDenom) s.Require().NoError(err) - // balance1 - 100000000 - 100000000 * TaxRate + // balance1 - 100000000 - 100000000 * TaxRate - 2 (gas) taxAmount = initialization.TaxRate.MulInt(transferAmount).TruncateInt() - s.Require().Equal(balance2.Amount, balance1.Amount.Sub(transferAmount).Sub(taxAmount)) + s.Require().Equal(balance2.Amount, balance1.Amount.Sub(transferAmount).Sub(taxAmount).SubRaw(2)) contractAddr := contracts[0] - node.WasmExecute(contractAddr, `{"donate": {}}`, transferCoin.String(), fmt.Sprintf("%duluna", stabilityFee), "test") + node.WasmExecute(contractAddr, `{"donate": {}}`, transferCoin.String(), "test-wasm", []string{initialization.TerraDenom}) balance3, err := node.QuerySpecificBalance(testAddr, initialization.TerraDenom) s.Require().NoError(err) - // balance2 - 100000000 - 100000000 * TaxRate + // balance2 - 100000000 - 100000000 * TaxRate - 2 (gas) taxAmount = initialization.TaxRate.MulInt(transferAmount).TruncateInt() - s.Require().Equal(balance3.Amount, balance2.Amount.Sub(transferAmount).Sub(taxAmount)) + s.Require().Equal(balance3.Amount, balance2.Amount.Sub(transferAmount).Sub(taxAmount).SubRaw(2)) +} + +// Each tx gas will cost 2 token (1 is for ante handler, 1 is for post handler) +func (s *IntegrationTestSuite) TestFeeTaxGrant() { + chain := s.configurer.GetChainConfig(0) + node, err := chain.GetDefaultNode() + s.Require().NoError(err) + + transferAmount1 := sdkmath.NewInt(100000000) + transferCoin1 := sdk.NewCoin(initialization.TerraDenom, transferAmount1) + + validatorAddr := node.GetWallet(initialization.ValidatorWalletName) + s.Require().NotEqual(validatorAddr, "") + + test1Addr := node.CreateWallet("test1-grant") + test2Addr := node.CreateWallet("test2-grant") + + // Test 1: try bank send with grant + node.BankSend(transferCoin1.String(), validatorAddr, test1Addr, []string{initialization.TerraDenom}) + node.BankSend(transferCoin1.String(), validatorAddr, test1Addr, []string{initialization.TerraDenom}) + node.BankSend(transferCoin1.String(), validatorAddr, test2Addr, []string{initialization.TerraDenom}) + node.GrantAddress(test2Addr, test1Addr, "test2-grant", initialization.TerraDenom, transferCoin1.String()) + + validatorBalance, err := node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) + s.Require().NoError(err) + + node.BankSendFeeGrantWithWallet(transferCoin1.String(), test1Addr, validatorAddr, test2Addr, "test1-grant", []string{initialization.TerraDenom}) + + newValidatorBalance, err := node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) + s.Require().NoError(err) + + balanceTest1, err := node.QuerySpecificBalance(test1Addr, initialization.TerraDenom) + s.Require().NoError(err) + + balanceTest2, err := node.QuerySpecificBalance(test2Addr, initialization.TerraDenom) + s.Require().NoError(err) + + s.Require().Equal(balanceTest1, transferCoin1) + s.Require().Equal(newValidatorBalance, validatorBalance.Add(transferCoin1)) + // addr2 lost 2uluna to pay for grant msg's gas, 100000000 * TaxRate + 2uluna to pay for bank send msg's tx fees, + s.Require().Equal(balanceTest2.Amount, transferAmount1.Sub(initialization.TaxRate.MulInt(transferAmount1).TruncateInt()).SubRaw(12)) + + // Test 3: try bank send with no grant + transferAmount2 := sdkmath.NewInt(200000000) + transferUsdCoin2 := sdk.NewCoin(initialization.UsdDenom, transferAmount2) + transferTerraCoin2 := sdk.NewCoin(initialization.TerraDenom, transferAmount2) + + node.BankSend(transferTerraCoin2.String(), validatorAddr, test1Addr, []string{initialization.TerraDenom}) + node.BankSend(transferUsdCoin2.String(), validatorAddr, test1Addr, []string{initialization.UsdDenom}) + node.BankSend(transferUsdCoin2.String(), validatorAddr, test2Addr, []string{initialization.UsdDenom}) + + // Revoke previous grant and grant new ones + node.RevokeGrant(test2Addr, test1Addr, "test2-grant", initialization.UsdDenom) + feeAmountTerraDenom := sdkmath.NewInt(10) + feeCoinTerraDenom := sdk.NewCoin(initialization.TerraDenom, feeAmountTerraDenom) + node.GrantAddress(test2Addr, test1Addr, "test2-grant", initialization.UsdDenom, sdk.NewCoins(transferUsdCoin2, feeCoinTerraDenom).String()) + + validatorTerraBalance, err := node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) + s.Require().NoError(err) + balanceTest2TerraBalance, err := node.QuerySpecificBalance(test2Addr, initialization.TerraDenom) + s.Require().NoError(err) + + node.BankSendFeeGrantWithWallet(transferTerraCoin2.String(), test1Addr, validatorAddr, test2Addr, "test1-grant", []string{}, transferUsdCoin2, feeCoinTerraDenom) + + newValidatorTerraBalance, err := node.QuerySpecificBalance(validatorAddr, initialization.TerraDenom) + s.Require().NoError(err) + balanceTest1TerraBalance, err := node.QuerySpecificBalance(test1Addr, initialization.TerraDenom) + s.Require().NoError(err) + balanceTest1UsdBalance, err := node.QuerySpecificBalance(test1Addr, initialization.UsdDenom) + s.Require().NoError(err) + newBalanceTest2TerraBalance, err := node.QuerySpecificBalance(test2Addr, initialization.TerraDenom) + s.Require().NoError(err) + balanceTest2UsdBalance, err := node.QuerySpecificBalance(test2Addr, initialization.UsdDenom) + s.Require().NoError(err) + // The fee grant msg only support to pay by one denom, so only uusd balance will change + s.Require().Equal(newValidatorTerraBalance, validatorTerraBalance.Add(transferTerraCoin2)) + s.Require().Equal(balanceTest1TerraBalance, balanceTest1) + s.Require().Equal(balanceTest1UsdBalance, transferUsdCoin2) + // 1uluna will be used for ante handler + s.Require().Equal(newBalanceTest2TerraBalance.Amount, balanceTest2TerraBalance.Amount.SubRaw(1)) + s.Require().Equal( + balanceTest2UsdBalance.Amount, + transferAmount2.Sub( + initialization.TaxRate.MulInt(transferAmount2). // tax amount in the form of terra denom + Mul(initialization.UsdGasPrice.Quo(initialization.TerraGasPrice)). // convert terra denom to usd denom base on gas price + TruncateInt(), + ).SubRaw(21), // addr2 lost 10uusd to pay for revoke msg's gas, 10uusd to pay for grant msg's gas, 1uusd to pay for band send msg's gas + ) +} + +func (s *IntegrationTestSuite) TestFeeTaxNotSupport() { + if s.skipIBC { + s.T().Skip("Skipping IBC tests") + } + chainA := s.configurer.GetChainConfig(0) + chainB := s.configurer.GetChainConfig(1) + + nodeA, err := chainA.GetDefaultNode() + s.NoError(err) + nodeB, err := chainB.GetDefaultNode() + s.NoError(err) + + transferAmount1 := sdkmath.NewInt(30000000) + transferCoin1 := sdk.NewCoin(initialization.TerraDenom, transferAmount1) + + validatorAddrChainA := nodeA.GetWallet(initialization.ValidatorWalletName) + s.Require().NotEqual(validatorAddrChainA, "") + validatorAddrChainB := nodeB.GetWallet(initialization.ValidatorWalletName) + s.Require().NotEqual(validatorAddrChainB, "") + + testAddrChainA := nodeA.CreateWallet("test1-feetax-not-support") + test1AddrChainB := nodeB.CreateWallet("test1-feetax-not-support") + test2AddrChainB := nodeB.CreateWallet("test2-feetax-not-support") + + // Test 1: try bank send with ibc denom + nodeA.BankSend(transferCoin1.String(), validatorAddrChainA, testAddrChainA, []string{initialization.TerraDenom}) + nodeB.BankSend(transferCoin1.String(), validatorAddrChainB, test1AddrChainB, []string{initialization.TerraDenom}) + + transferAmount2 := sdkmath.NewInt(20000000) + transferCoin2 := sdk.NewCoin(initialization.TerraDenom, transferAmount2) + nodeA.SendIBCTransfer("test1-feetax-not-support", test1AddrChainB, transferCoin2.String(), "") + + // check the balance of the contract + s.Eventually( + func() bool { + balance, err := nodeB.QueryBalances(test1AddrChainB) + s.Require().NoError(err) + if len(balance) == 0 { + return false + } + return balance[0].Amount.Equal(transferAmount2) + }, + initialization.OneMin, + 10*time.Millisecond, + ) + terraIBCBalance, err := nodeB.QuerySpecificBalance(test1AddrChainB, initialization.TerraIBCDenom) + s.Require().NoError(err) + s.Require().Equal(terraIBCBalance.Amount, transferAmount2) + + terraBalance, err := nodeB.QuerySpecificBalance(test1AddrChainB, initialization.TerraDenom) + s.Require().NoError(err) + s.Require().Equal(terraBalance.Amount, transferAmount1) + + transferAmount3 := sdkmath.NewInt(10000000) + transferCoin3 := sdk.NewCoin(initialization.TerraIBCDenom, transferAmount3) + + nodeB.BankSend(transferCoin3.String(), test1AddrChainB, test2AddrChainB, []string{}, sdk.NewCoin(initialization.TerraDenom, sdkmath.NewInt(2))) + + newTerraIBCBalance, err := nodeB.QuerySpecificBalance(test1AddrChainB, initialization.TerraIBCDenom) + s.Require().NoError(err) + s.Require().Equal(newTerraIBCBalance.Amount, terraIBCBalance.Amount.Sub(transferAmount3)) + newTerraIBCBalance, err = nodeB.QuerySpecificBalance(test2AddrChainB, initialization.TerraIBCDenom) + s.Require().NoError(err) + s.Require().Equal(newTerraIBCBalance.Amount, transferAmount3) + + newTerraBalance, err := nodeB.QuerySpecificBalance(test1AddrChainB, initialization.TerraDenom) + s.Require().NoError(err) + // Tx will only cost 10uluna on chain B as gas + s.Require().Equal(newTerraBalance.Amount, terraBalance.Amount.Sub(sdkmath.NewInt(2))) +} + +func (s *IntegrationTestSuite) TestFeeTaxMultipleDenoms() { + chain := s.configurer.GetChainConfig(0) + node, err := chain.GetDefaultNode() + s.Require().NoError(err) + + transferAmount := sdkmath.NewInt(100000000) + transferCoin1 := sdk.NewCoin(initialization.TerraDenom, transferAmount) + transferCoin2 := sdk.NewCoin(initialization.UsdDenom, transferAmount) + + test1Addr := node.CreateWallet("test1-multiple-fees") + test2Addr := node.CreateWallet("test2-multiple-fees") + + validatorAddr := node.GetWallet(initialization.ValidatorWalletName) + s.Require().NotEqual(validatorAddr, "") + + node.BankSend(transferCoin1.String(), validatorAddr, test1Addr, []string{initialization.TerraDenom}) + node.BankSend(transferCoin1.String(), validatorAddr, test1Addr, []string{initialization.TerraDenom}) + + node.BankSend(transferCoin2.String(), validatorAddr, test1Addr, []string{initialization.TerraDenom}) + + taxByTerraDenom := initialization.TaxRate.MulInt(transferAmount.QuoRaw(2)).TruncateInt() + feeByTerraDenom := sdk.NewCoin(initialization.TerraDenom, taxByTerraDenom.AddRaw(1)) // 1 uluna to pay for ante handler gas + taxByUsdDenom := initialization.TaxRate.MulInt(transferAmount.QuoRaw(2)). + // convert terra denom to usd denom base on gas price + Mul(initialization.UsdGasPrice.Quo(initialization.TerraGasPrice)). + TruncateInt() + feeByUsdDenom := sdk.NewCoin(initialization.UsdDenom, taxByUsdDenom.AddRaw(1)) // 1 uusd to pay for post handler gas + + node.BankSend(transferCoin1.String(), test1Addr, test2Addr, []string{}, feeByTerraDenom, feeByUsdDenom) + + test1AddrTerraBalance, err := node.QuerySpecificBalance(test1Addr, initialization.TerraDenom) + s.Require().NoError(err) + test1AddrUsdBalance, err := node.QuerySpecificBalance(test1Addr, initialization.UsdDenom) + s.Require().NoError(err) + test2AddrTerraBalance, err := node.QuerySpecificBalance(test2Addr, initialization.TerraDenom) + s.Require().NoError(err) + + // Final denom will be paid by both uluna and uusd + s.Require().Equal(test2AddrTerraBalance, transferCoin1) + s.Require().Equal(test1AddrTerraBalance, transferCoin1.Sub(feeByTerraDenom)) + s.Require().Equal(test1AddrUsdBalance, transferCoin2.Sub(feeByUsdDenom)) +} + +func (s *IntegrationTestSuite) TestFeeTaxForwardWasm() { + chain := s.configurer.GetChainConfig(0) + node, err := chain.GetDefaultNode() + s.Require().NoError(err) + + transferAmount1 := sdkmath.NewInt(700000000) + transferCoin1 := sdk.NewCoin(initialization.TerraDenom, transferAmount1) + + test1Addr := node.CreateWallet("test1-forward-wasm") + test2Addr := node.CreateWallet("test2-forward-wasm") + + validatorAddr := node.GetWallet(initialization.ValidatorWalletName) + + node.BankSend(transferCoin1.String(), validatorAddr, test1Addr, []string{initialization.TerraDenom}) + + // Test 1: User ----(execute contract with funds)---> Contract ---(execute bank send msg)---> Another User + node.StoreWasmCode("forwarder.wasm", initialization.ValidatorWalletName) + + chain.LatestCodeID = int(node.QueryLatestWasmCodeID()) + node.InstantiateWasmContract( + strconv.Itoa(chain.LatestCodeID), + `{}`, "", + initialization.ValidatorWalletName, []string{}, sdk.NewCoin(initialization.TerraDenom, sdk.NewInt(2))) + + contracts, err := node.QueryContractsFromID(chain.LatestCodeID) + s.NoError(err) + s.Len(contracts, 1, "Wrong number of contracts for the counter") + contract1Addr := contracts[0] + + transferAmount2 := sdkmath.NewInt(100000000) + transferCoin2 := sdk.NewCoin(initialization.TerraDenom, transferAmount2) + node.WasmExecute( + contract1Addr, + fmt.Sprintf(`{"forward": {"recipient": "%s"}}`, test2Addr), + transferCoin2.String(), + "test1-forward-wasm", + []string{initialization.TerraDenom}, + ) + + test1AddrBalance, err := node.QuerySpecificBalance(test1Addr, initialization.TerraDenom) + s.Require().NoError(err) + test2AddrBalance, err := node.QuerySpecificBalance(test2Addr, initialization.TerraDenom) + s.Require().NoError(err) + + s.Require().Equal(test2AddrBalance, transferCoin2) + s.Require().Equal(test1AddrBalance.Amount, transferAmount1.Sub(transferAmount2). + // User 1 will paid 2 times on taxes due to the contract execute bank send msg + // 2uluna will be used for gas + Sub(initialization.TaxRate.MulInt(transferAmount2.MulRaw(2)).TruncateInt()).SubRaw(2)) + + // Test 2: Contract trigger another contract's execute msg + node.InstantiateWasmContract( + strconv.Itoa(chain.LatestCodeID), + `{}`, "", + initialization.ValidatorWalletName, []string{}, sdk.NewCoin(initialization.TerraDenom, sdk.NewInt(2))) + + contracts, err = node.QueryContractsFromID(chain.LatestCodeID) + s.NoError(err) + s.Len(contracts, 2, "Wrong number of contracts for the counter") + contract2Addr := contracts[1] + + node.WasmExecute( + contract1Addr, + fmt.Sprintf(`{"forward_to_contract": {"contract": "%s", "recipient": "%s"}}`, contract2Addr, test2Addr), + transferCoin2.String(), + "test1-forward-wasm", + []string{initialization.TerraDenom}, + ) + + newTest1AddrBalance, err := node.QuerySpecificBalance(test1Addr, initialization.TerraDenom) + s.Require().NoError(err) + newTest2AddrBalance, err := node.QuerySpecificBalance(test2Addr, initialization.TerraDenom) + s.Require().NoError(err) + + s.Require().Equal(newTest2AddrBalance, test2AddrBalance.Add(transferCoin2)) + s.Require().Equal(newTest1AddrBalance.Amount, test1AddrBalance.Amount.Sub(transferAmount2). + // User 1 will paid 3 times on taxes: execute contract1 msg, contract 1 execute contract 2 msg, contract 2 execute bank msg + // 2uluna will be used for gas + Sub(initialization.TaxRate.MulInt(transferAmount2.MulRaw(3)).TruncateInt()).SubRaw(2)) + + // Test 3: Error when forward tx + test1AddrBalance = newTest1AddrBalance + test2AddrBalance = newTest2AddrBalance + + node.WasmExecuteError( + contract1Addr, + fmt.Sprintf(`{"forward_to_cause_error": {"contract": "%s"}}`, contract2Addr), + transferCoin2.String(), + "test1-forward-wasm", + []string{initialization.TerraDenom}, + ) + + newTest1AddrBalance, err = node.QuerySpecificBalance(test1Addr, initialization.TerraDenom) + s.Require().NoError(err) + newTest2AddrBalance, err = node.QuerySpecificBalance(test2Addr, initialization.TerraDenom) + s.Require().NoError(err) + + s.Require().Equal(newTest2AddrBalance, test2AddrBalance) + // Transfer amount will we return + s.Require().Equal(newTest1AddrBalance.Amount, test1AddrBalance.Amount) +} + +func (s *IntegrationTestSuite) TestFeeTaxNotAcceptDenom() { + chain := s.configurer.GetChainConfig(0) + node, err := chain.GetDefaultNode() + s.Require().NoError(err) + + transferAmount1 := sdkmath.NewInt(500000000) + transferCoin1TerraDenom := sdk.NewCoin(initialization.TerraDenom, transferAmount1) + transferCoin1NonValueDenom := sdk.NewCoin(initialization.NonValueDenom, transferAmount1) + + test1Addr := node.CreateWallet("test1-not-accept-denom") + test2Addr := node.CreateWallet("test2-not-accept-denom") + + validatorAddr := node.GetWallet(initialization.ValidatorWalletName) + + node.BankSend(transferCoin1TerraDenom.String(), validatorAddr, test1Addr, []string{initialization.TerraDenom}) + + node.BankSend(transferCoin1NonValueDenom.String(), validatorAddr, test1Addr, []string{}, sdk.NewCoin(initialization.TerraDenom, sdkmath.NewInt(10))) + + // Test 1: Try to pay tx fee with non-value denom + transferAmount2 := sdkmath.NewInt(100000000) + transferCoin2 := sdk.NewCoin(initialization.TerraDenom, transferAmount2) + + gasLimit := transferAmount2.MulRaw(initialization.E10).String() + fees := sdk.NewCoins(sdk.NewCoin(initialization.NonValueDenom, transferAmount2)) + err = fmt.Errorf("can't find coin that matches") + // Tx will cause error cause it doesn't have the correct fees to pay for tx + node.BankSendError(transferCoin2.String(), test1Addr, test2Addr, "test1-not-accept-denom", gasLimit, fees, err.Error()) + + // Test 2: Try to trick the chain by paying with both uluna and non-value denom + + feeTerra := initialization.TaxRate.MulInt(transferAmount2).TruncateInt().AddRaw(2) + feeTerraCoin := sdk.NewCoin(initialization.TerraDenom, feeTerra) + fees = sdk.NewCoins(sdk.NewCoin(initialization.NonValueDenom, transferAmount2), feeTerraCoin) + + // At this time, the tx will ignore non-value denom and only deduct the uluna + node.BankSendWithWallet(transferCoin2.String(), test1Addr, test2Addr, "test1-not-accept-denom", []string{}, fees...) + + balanceTest1Terra, err := node.QuerySpecificBalance(test1Addr, initialization.TerraDenom) + s.Require().NoError(err) + balanceTest1NonValueDenom, err := node.QuerySpecificBalance(test1Addr, initialization.NonValueDenom) + s.Require().NoError(err) + + s.Require().Equal(balanceTest1Terra.Amount, transferAmount1.Sub(transferAmount2).Sub(feeTerra)) + s.Require().Equal(balanceTest1NonValueDenom.Amount, transferAmount1) } diff --git a/tests/e2e/initialization/config.go b/tests/e2e/initialization/config.go index 79a5bac58..9bd565699 100644 --- a/tests/e2e/initialization/config.go +++ b/tests/e2e/initialization/config.go @@ -12,6 +12,7 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" "github.com/cosmos/cosmos-sdk/x/genutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" @@ -22,6 +23,7 @@ import ( govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" "github.com/classic-terra/core/v3/tests/e2e/util" + tax2gastypes "github.com/classic-terra/core/v3/x/tax2gas/types" treasurytypes "github.com/classic-terra/core/v3/x/treasury/types" ) @@ -42,19 +44,27 @@ type NodeConfig struct { const ( // common TerraDenom = "uluna" - AtomDenom = "uatom" + UsdDenom = "uusd" + EurDenom = "ueur" + NonValueDenom = "nonvalue" TerraIBCDenom = "ibc/4627AD2524E3E0523047E35BB76CC90E37D9D57ACF14F0FCBCEB2480705F3CB8" - MinGasPrice = "0.000" + MinGasPrice = "0.00000000001" + E10 = 10000000000 IbcSendAmount = 3300000000 ValidatorWalletName = "val" // chainA - ChainAID = "terra-test-a" - TerraBalanceA = 20000000000000 - StakeBalanceA = 110000000000 - StakeAmountA = 100000000000 + ChainAID = "terra-test-a" + TerraBalanceA = 200000000000000 + UsdBalanceA = 300000000000000 + EurBalanceA = 400000000000000 + NonValueBalanceA = 10000000000000 + StakeBalanceA = 110000000000 + StakeAmountA = 100000000000 // chainB ChainBID = "terra-test-b" TerraBalanceB = 500000000000 + UsdBalanceB = 60000000000000 + EurBalanceB = 40000000000000 StakeBalanceB = 440000000000 StakeAmountB = 400000000000 GenesisFeeBalance = 100000000000 @@ -72,16 +82,21 @@ var ( StakeAmountIntB = sdk.NewInt(StakeAmountB) StakeAmountCoinB = sdk.NewCoin(TerraDenom, StakeAmountIntB) - InitBalanceStrA = fmt.Sprintf("%d%s", TerraBalanceA, TerraDenom) - InitBalanceStrB = fmt.Sprintf("%d%s", TerraBalanceB, TerraDenom) + InitBalanceStrA = fmt.Sprintf("%d%s,%d%s,%d%s,%d%s", TerraBalanceA, TerraDenom, UsdBalanceA, UsdDenom, EurBalanceA, EurDenom, NonValueBalanceA, NonValueDenom) + InitBalanceStrB = fmt.Sprintf("%d%s,%d%s,%d%s", TerraBalanceB, TerraDenom, UsdBalanceB, UsdDenom, EurBalanceB, EurDenom) // InitBalanceStrC = fmt.Sprintf("%d%s", TerraBalanceC, TerraDenom) LunaToken = sdk.NewInt64Coin(TerraDenom, IbcSendAmount) // 3,300luna tenTerra = sdk.Coins{sdk.NewInt64Coin(TerraDenom, 10_000_000)} - OneMin = time.Minute // nolint - TwoMin = 2 * time.Minute // nolint - FiveMin = 5 * time.Minute // nolint - TaxRate = sdk.NewDecWithPrec(2, 2) // 0.02 + TerraGasPrice = sdk.NewDecWithPrec(5, 11) // 0.5 * 10^-10 + UsdGasPrice = sdk.NewDecWithPrec(1, 10) // 1 * 10^-10 + EurGasPrice = sdk.NewDecWithPrec(2, 10) // 2 * 10^-10 + + OneMin = time.Minute // nolint + TwoMin = 2 * time.Minute // nolint + FiveMin = 5 * time.Minute // nolint + TaxRate = sdk.NewDecWithPrec(2, 2) // 0.02 + GasAdjustment = sdk.NewDecWithPrec(12, 1) ) func addAccount(path, moniker, amountStr string, accAddr sdk.AccAddress, forkHeight int) error { @@ -241,6 +256,11 @@ func initGenesis(chain *internalChain, forkHeight int) error { return err } + err = updateModuleGenesis(appGenState, distrtypes.ModuleName, &distrtypes.GenesisState{}, updateDistrGenesis) + if err != nil { + return err + } + err = updateModuleGenesis(appGenState, treasurytypes.ModuleName, &treasurytypes.GenesisState{}, updateTreasuryGenesis) if err != nil { return err @@ -256,6 +276,11 @@ func initGenesis(chain *internalChain, forkHeight int) error { return err } + err = updateModuleGenesis(appGenState, tax2gastypes.ModuleName, &tax2gastypes.GenesisState{}, updateTax2GasGenesis) + if err != nil { + return err + } + bz, err := json.MarshalIndent(appGenState, "", " ") if err != nil { return err @@ -282,7 +307,7 @@ func updateMintGenesis(mintGenState *minttypes.GenesisState) { } func updateBankGenesis(bankGenState *banktypes.GenesisState) { - denomsToRegister := []string{TerraDenom, AtomDenom} + denomsToRegister := []string{TerraDenom, UsdDenom, EurDenom} for _, denom := range denomsToRegister { setDenomMetadata(bankGenState, denom) } @@ -303,6 +328,11 @@ func updateCrisisGenesis(crisisGenState *crisistypes.GenesisState) { crisisGenState.ConstantFee.Denom = TerraDenom } +func updateDistrGenesis(distrGenState *distrtypes.GenesisState) { + distrGenState.Params.CommunityTax = sdk.NewDecWithPrec(2, 2) + distrGenState.Params.WithdrawAddrEnabled = true +} + func updateTreasuryGenesis(treasuryGenState *treasurytypes.GenesisState) { treasuryGenState.TaxRate = TaxRate treasuryGenState.Params.TaxPolicy = treasurytypes.PolicyConstraints{ @@ -352,6 +382,17 @@ func updateGenUtilGenesis(c *internalChain) func(*genutiltypes.GenesisState) { } } +func updateTax2GasGenesis(tax2gasGenState *tax2gastypes.GenesisState) { + tax2gasGenState.Params.GasPrices = sdk.NewDecCoins( + // Gas prices will be very small so that normal tx only care about taxes + sdk.NewDecCoinFromDec("uluna", TerraGasPrice), + sdk.NewDecCoinFromDec("uusd", UsdGasPrice), + sdk.NewDecCoinFromDec("ueur", EurGasPrice), + ) + tax2gasGenState.Params.BurnTaxRate = TaxRate + tax2gasGenState.Params.Enabled = true +} + func setDenomMetadata(genState *banktypes.GenesisState, denom string) { genState.DenomMetadata = append(genState.DenomMetadata, banktypes.Metadata{ Description: fmt.Sprintf("Registered denom %s for e2e testing", denom), diff --git a/tests/e2e/scripts/forwarder.wasm b/tests/e2e/scripts/forwarder.wasm new file mode 100644 index 0000000000000000000000000000000000000000..7a45a2e9d795dbed8a986922fbf24cef8f7d1283 GIT binary patch literal 167215 zcmeFa54>j8S>L<=y#LO5&pGqXFu(yOalfzhdPWC3dLtmFI(u%0KuK<*&26c21q3Rb zAp|B9jJ;f#L^P<0ViOe=?Nn2PR+QTtE7n-2w`rwq>cx9$FKyG?>1|qRwMrG0k4voz zvRtnV{`l6n-Fw?S^V@H^`L3*Jir#hGJC8=u<(qH4^~jC)-hAgBx86*7SGUt?6nNWR zcih64zkFMksbu86{T*+;`K~r?^7h+qx%*4M?DE53a^t`{vADS-I`jFa5GF`;sr;P-b_N_zgE7z2)`|pZd4n_6_g&#^}xXo_E}K zofe7#98B zt`#Lw`*K04e$ngkmYug-WzR2)vNz1{&VQ1BfRpVUTvR@g*T;^X$i@$3^|9~zyYt`S zFXbzPx8C;Fw;lQBA{*av*V~WYeAm%CL>krbws+ok%R6|Q_B{mIpYFZ=9k;$M+dq8k z9p89FS-xa+^PP9T?G`=WwRr1o?aSMC9=YwFcieI0wzuC1k&^XoN3vTcZ5nMHzT>WM z%x;-yBk9ODs-9cUyW=j3{H2?3xlI)D_B-Bo*Ns*wo!Kp9rA z;mdiMF)!RL9zx&8Vzn34+$@T5|qxrYx-7x3-WUpV&`ToD<FZ9)%xViqq4O_s;b_v&dEi>>-jVu;pxb&h`63ctv%a^;rkA+tHJ+9Q)jpuQ@DKnmO@1|B z%ExWN?^MD5X$y|4r8;~4^rEUbw7g?!DaxwSYBrqwEa=hm8bF>eAba)4Gj;Se>ok6O zkqH@li+xuVZ;56>bv2w^s4{O>nX8usGu5M0*}t7qfMWlJR;P}iNB`g_?@k@|7|*|9Vx# z)4ug;Ii3JFtNI52l1Qo6t5-$IJ?qtlkp?|3#nbNf>b!VbTCXm-LJ$e+-J}U=JrQ(_ z&~A@VjFIuJF}gtgdy9%);Ld8uVze64m@Xa6rhDLso%~+H|F7bCh2M+ekOS+p5(X*V zWXm9ClJc@_6imX?LSmBLVv@4jEe_4XL1DIrs>`OZ$pNdUp3&9XPuQm)()~{86E^YXMuJ0}OTv5D6xGtgPY5{hj#QtCh z$XG=ZRC|lp0MhW#@`uI!LAxMSd1q?8}T#-j%r<0N_P*@d}rwPmeq7mjGISgj=KiWM)6N*)O!e! z0VAR^uR54dFIgIbSWvs?VE#46AOlDnG4!2b%qvKl(G!=QCxoRz6^qg{w~**6S`gVU zOyC9s+|w^)sGKc{8gLQ8vc!zXb+^QqiUjc>;N6xfEaL?fb!cxXFt-$V+(|{qVlcm) z0D?7AufM3w2M)^)LAhFeS&JjwB@$5YU}CzAL`3f#T3!^sdV9-jrmxbM*G;Wp5Acz<*CJKB zrFM(cw$Y-hY_!zZ_qC)#Ryr!gT2hjuCo)?>SX5Xj68Ug<-J_?JAA?WEiVq9*Z)VE9 z5@yoc`gBs4(_Qt)V6ACctsohuL;duoeRCC$*=*E1P$DWMFDCW*U`67hUk%?lTM#27 zma3I%Sm*0A^v0-CA|m1NbQdVg*Qs|{0bD*ElJn|iu^ZS_d9WV^RcumAVo0pS6J2i1 znB^S2_Qm(Y!gP_hWrXDOScZK@q~G+t&Zi}v9@5!XMF;DW~5nMV>*2%b>xP zNWuNGWS7jkzUa6wdjNu!LD3iE_GNDYizp75-XI1@02l|M#rSf~tPHcRU;l=|duCp) z>hCSya7A&eP}r|e@;OJ5QRpTW!VXiiTHjmT+G&}3nwG2b@?y8pysWW~Iw-$pH5xSy zo91QF%xeOC@^`BOiEj{HyBpJC=(G`|H44G{bD#=-=}mWnwo%yfuw%Wfgh-P4aM|IN zUcqFEK1tUx@;f#9^pSDh3BJGNaQ&(Lo_gU;5ROI2pyOI8pU@$esJ;r*~JH|hdxBci$63&oTqx$XEb=Nii_YD)C3io*%)gc z7hI=!qQ2_d~f)efwIYZhiEPn`v*0wNV_Pr zw2Nf!q9BQJ&v-#<_3M@-s1H*POKm7it<=DtN^PgmqsZS9DG{JKK$LzBm_z3F5rfeR z^P1VkSLC8$G|t7+tZ))UsMITYCTTYX*7!>&6L?iaZUH;(ixY}W(2lD`(I8zB6g?__ zK9kXaBxoiUMpGY@fL&4ge1f%%w38GSPkX|W~1=+5* z{k+wg%L$WzdS0>;RJP zzpwWYn1F}N=V34}}5FJST;J&5G7sK3I40DUB7EuG3TbTGfiQpJflb;M&C{T5X z)p^ z7@>l_#qR0FbRR^hWTK8mWfz*DP}|LmjkOi3J6^H2WH1f7+*??kYfKB7d|mrz*~Zcu zW)!XknNh2u=wVU0XJoiwaj9E#W}h>THgZiL3EpuW#1^KuOc0tV~S%)B_pWun4kz?=>Fd9l^%W8~1RAc@>b(T{mU zn)z+D(i`mUw>6%F9GLlxlNOyW@^~yy(rjEm7>^62K9s8*qhm)Ie@E++zlqx{d2lo@ z%z}>bL9JKgz2%yCEfcGIR+fncZQe4|bcWT)aHYh!v*I5yrDd}D?G?$SU5eD141%kM z@uI22SoAiQ-$F{k6Acn^YCvV>l{5`1UnTK^N)-Vi?L|NkLIN?|A8y0dB#w=Ilp;H1 z2eLIY#TI}bs%1Zp@gg!J<6oW?uX>=AS8@O0*#eSyp@J&pPh0@<=?Gydcj7Tzj^R$E zJM-DK1{4zjQj?89J3Q^gUm~8iSz!ZvAvqOCQZ>rLd;zE8Ys0B{CDg$<^7qR1$>sWd zyU3%bVV_>u71T{zcSu-EkLN2z8J@&t-b&9eg!Vm46VV*CN(%n0q_+GXJ=_=kdBNxw zT2URB7pvjhyYmW)*)5og{ep^xPw@=J#6?(tl8NRv_hzlQa<&*2D7Bim=suPT1*wPUE2-s@ATYo!=t)`dKpb z!Jf5oR$VAoHqKjYV=3Jk@Gwr(rJ(#pDBqS=$5p=AQZ-LI>wh_Sm0P{pRlW$4nXxTu z3OK3AC(@p-CXR_|7P_&j`Ugi>l&hF#@XrK02fJ-xdd5U->}wu(Vte+5797={NlY)8 za{vZ1Mmt2Y&%|HfOQbl>cWuU(ElPq`tMjQ?(Rw07<~KcBAK1lE6@v{A}qteQkJ7 zuLKlXZl%P)l{7R^W!Fm-lr{Q*X-g@Np1yjHkZ91h+eUFPpj7o{Bs!B2gtKU=toN-~ zJK_l`Jm4uDMl>omk-yIDUP|DX9ow!H-{9DlBg`hAeVeJ+x2jk;RiRo#_+>@*m2MDq zVbGKGpVxy`gCSDwIJAt_BC!EQr`|VAr>@Cp9EZ_Z)}J(P!oqPc5;#Xi%cX>+C$1E_ zTZL>ihWlr4aa3ZX6kEBT#0q5vFS@Keu8b@pIF3M+~Hhc<*M& zTMDAWhJtEXRwn7IA@}Ljg{Cu{q!?{o&JM0le#oaAqL%$rX;N(Nt3+PGZ%Vy}XOfzR zG-gAOSqu{68n5AS=j%W#eV1U$9;yb1s!PIH6{7(PPVSH?mCZo_g`NcRjHi93tQ|as zw|WrF6&WfO-i_WGiU`C`X#j^J0K+^CiwmzF#G0L99)`u~8mFQT z*ENeL__c~BB$7qf5LiQW(G_Qf8Fvd?38p>jfQT|czb7zF3Mp4=fOC!Rgf>_aC2dUunoA8K#ypy8y;=!9Qe!!QWMRUFMXT!Nw?gAfeww{@ ziBg1dORin2!VOZn7o9ZpseVB>%iT?oWY5|v-Jh1_fdsXsMUnNepZpuGNFh&_}|Gv!c0zA+FGjm0=`|RLD;$*UWl>s zhVA{b*j*(`&4~k=MG3jau#lV1$-bgzIXxpFK_sLvP0uuHzzTC8o|W#|K&qlopG2eW zDhb?;xz(Ml%gjL zj2gj9mzkKvD}t9-{3@*Eyngz5o{2%~Ahs{2(~9*OQI0TRU7|rb{B1HS4&ha=J})GA z%v{xfh6`dB+e+S4+vfYy@2yQ`=7!1dXBan_X*N}(V3}<&c z6ezDXOeuonNn$^q7+B9$?9)~tgoep4%sjO46}c0HQO5K;3V~eEVsz)z!4iZj!bHHf z&_^SugH+@cDqx|-+5*BxnCvs_59^Bgzm#2RqmV}{&4q;e9@zr*1G@>pCY}`KvXc|U zGk(Hm);Ltacu!c!T5N`6B+|g9ac!@LdLV4JRk%y1&khD$b|Gm)=T4o9CZZC#Z$cIX zdG#2Eo=7yW3Xy1rRA99{NrkMD3i9_%C6Xc0d`_Z@l)J9CF~FR#DN`a z%Hk*s_0ks8Ypy6ON$UM$Iz`&v5;kTeLVVh#RL(_Itj>=AMV|KrJpC&WM}#@dP4XgC z3Yg-lYmyhzC^pH9L^M6H7$$jXe2S#NfC4D``jJWAm-PiS0RGYMIst)9zF3yIQhIHQ zB*@%AXG9vBsPW+1uyTLYtwcDGP$DpZ5|a{i#i-%o`a|v8zEk?P@8#^3Yso+{pD9f&br4&SLBeN1YB^sw z)F5gwdLmBe7P@(2c@GLb{P@97<=-;x&6}@iUKRrFPieoqh}e`zy!H-e2Nw@!lgIH# zV=*4gz6LtrX)==%N!8v5WA)zxEhP%1NouvH1JT1{T2T}vtSHI_Zt2i#y5df|s}>x3 z8Vg|}LksvKc=|v6VX(#I&$Ki%`7@h|CBG`mG+@jPTb7uowaihcG+~r{lz>0nvsT2q z7IRO_DSy9crGULVjEfyrMU0~UN?K(>NIZ3=fkm99fhJVqd@bmPG|)iA1l<8aNQ0$5(Qo5#lgv+g0RDF0F{bl6y9O^KSkK;2B zp1d-6a*oDRpG3A-8nH$*Z(k0V{LAd{%I7PL{B-~I^8wPXtY?Rbmdl#|`j&u}&~yF> zA&v(SJr~YI=Db()hy(BlKmV1VW#)|XxJU^^Nlh#ONaLQENu)$?(hew<^MYA%a$FyEUBWuNdQ;Mw>?^L|3HARHZ;Q7t`L;xN!sy2IY z)+d=0tyY#WgF<9V%TutYnM4v!SRjxA&0+JYKgzO)5?5t+@{a)vOR38Pbw9nq8IUvY2rIT$Nem z4utfQS-N=+akqif2;_m#T#>0CS>+eev%aWB_11|pO&gmj{NfQiUrE5dob8EMiykC+J)Ve5Zu)W!X1F7UtVf&kR;kKf**EG;7%{)h2n( zPH=r`_>7;`Plz}I_=`(hDH?3(CDP-Zr59A+(>sUgDs zCWoQm`x1xnVtSjdLq!HDtTm*t&!q5{AcZ$4cnU=vO|D`<)mM?DNddo{s8lOxdL-!G zNYJ8-6V-(`Ln?|YSJ}1V;T&d;z;fo`5i;PVF$G}-dNf|H-2+CM+g0Rhc~1(51StPn z?r}k;?afil2jsyH)Fn^@P;+UN+HL?0)_sO*8Lw=EeM zF|rn+nH+<*7d&fhfPxdCCd=TCp4v1OXvDDN=LAOiFbm}p@zQCDwk*msO;A&NlFA)p zkx~cRLdHy6P_j%2IPUe(l9;LUp{Y|Q0pn-c(j&1xu~cVd32$R`*=>`*$&|s+Ea`PZ z5R-n7_1+|k;PI>OA=8novK7BA-CLj38zGN;8{FG|Uam{`0Hp})BoMB~TLqFP2P&o` zBg6t719s@#rhrrJ3L2?ww>6@9qW%+Ztu9Pr9dLFy!>)Vl=eGo>g|-2oxHC;E@&+`1 z*?__%I-8Y;uLB3b36CAryc&x!0If=KD?=4ZxJU64$CXh&!9K_5_Gg(E0ZQudmbDgJ z)C#FuufvK_B~`uHk69Rt*{I$aA`R22|BOZGf9Y^7iMD*WUb^-Y#UU=S53AQ73`Yx# z<0XT+Z#`US#X$&f554)wx1C_HS9wCHYZ!Uk%})V76oaIv-8><2cv@YbJX%PG75E@x zLR{2Oe2c0eYg6ZkQ=Rh?aNTK396zOm8TmAAy!Vt6uz}rbCr&AWZFbrc51mp1A#vIgCm-D059XYwE%D(~N{GEqH-ZnJQUW1#+7d6A2;^{X zsKnC#G}wJ+a|yK|7vlKm%RHBOQbZm8a@bspD+Oj*^3dnjA|lZ?0h3K;d-#v2ok)Xj}Ob+IQ?+CTZ=vRED$^{lg8x6a0`uJvvaG1Wf6+1}d%H zllOY^j@Fq(WCz(xaqO6OPaqn!$H+<_IGCGC{R6hM*bB+)Wc^?-K8#Y9Tr117Q)Rkj zz!IulUMWj4WLOyRv=Or!E{N*y@Nup|QT=y@qf8y^XVE)Hz| zM^;cdWA!1E2rm)gNbZpcRs)%u8bLE%QX3^wTP3HcP@I%#dE{7xVouzn#7-^sj}kjY zATG2y(n^~7O{A`O*um_U-etP*Wj|kp19O@vU0H#igcd2@2F0h_`4kvkN@gOSUJOV) z8OQQUK5#jzr>bH9I&SZg(CwI(6QF8{7#Ml(qJU6vGu@$3%69r4SY%-=sNeIgGE|*l zsV9t}Y=)sMUGd7OTA~&&yXaRI7{AoGv(Jj7%v{;XUY8u!kG6(C%XWK<514_O?p$76 z&w4FyZ;py*TC7A?)z2O~kxd>cShLI~e=Wx&&`T?Lz1KUGsDE^}5EK3dHi>@yY|Mn8 zqS$SOh>k%*kLbVtv_Rl200!m@y`KE8ge!#qa`b_yNGEn+#ocEq82bAidSdLH(yasNPQw-M@BKghB7@OoXW>o ze{5t{`G{VK4q{GZMDSNLiywUV2?ZOk)x>SlTNWN=(*FBT{Nk?TlGw=@C7pS6n*wr3f$-auC!GRsG${td#_g?S zWuRw^`~vShgK_y%87l9-! z$?yfF`K(`bK>`fYbVhP2!gG2K2po9 zyu=*AGi5sW0j&=&S}S{OOlUyS!psQHmT2|B;Wh~&;c>cho=YUUK(1Dgm^=t+PTVoG z#IcB|%&F&n2;y}6!GEwU)LF+cChCNtwdY?f7$PLA%d4`7NY(2*`6zb8N8T}dkU0)l zmUv3^nmPKlv7MUvwt@_l#*~G8CPGbzdWqjP)lLk*ppN?mi8p9pDzN8qJ5?e{w4(i?&|MoXbAlwhRfa2k3)SHoXmm$oe1_!}~OmZD=s z&<#t?&UZfE5ut!PyMTTDz(`kIgTu94PCB}BZo9Y1r@q(`{Ssg&Ac@`!c7-l9tBg{q zP9khYW2HqBF{0C!TSIcWF(j8`C7YT^3PW;OpOEtc2SEKig1)|ju2t*xgVG=`ybG=R zKIYpIha%M}$F-aVe?06XMyMZ#i0HU6k4E$kA;hPa0rQf(SP1ilewU_;jSUK}!+shcVoZwUDxj{`equ+5*jjEKuYZV! ztVZC`#)6t<*icQC&9CU-dD~Pn;86CTY;7vc`kHGlGV+innZ|o->Ceydem&`AxgHhr5B+j&6b`Q`=j>wi`=O{BwcqNhjbih=1X9{ zGhG5#Fn14O-5T)N*j5IvDR`WkG-DbZY-UWbD?EI8A^H_x0kuWk(|(lAhV%gl%_yLR zXFQ|e;bSP6G;2b_b*!jCW7Z>lOr+)^ktYcXU45#x`c0>#JljbDN2HwWq&(u3=jJH^ zhb_5h1T{QA>+=i9i3>vNBDJ4y=1o1#o7j#M^QJ-lU}9*+b9!cIoze92!9tcPz5Ha` z%kI2M>yu2Mh{c-Eo03|x$W<4CCuEP9&|TzjNYhB6i=>1sjTE{_3f-OeVy$=(A<4Yy zLGcA0#Q1iH=UMjBNu;|F!QdiUb}?nLW>E*8&mjqZU(7iKsxxI$zfLn{YDs~3xfVep zgMLc}IHZFNFhH=}`v>QfrhZJC7!yTwGH2>*&SYk$vOv)z?rfJ99&<{U7M^fQmlmFM z3W%EbUo<(Vh2ze!1tmb=Lx_2(xf4^%>FMWA*jsI^=34QB=1$@F^!S+C0Hpy!qsT^2 zGcY|R9! zCX#a$6DRQ>dn}Cb2uqYN!EDK-$UB77a7a67unxpmCOO`u-F|&dnnPvG`(iegVWY<> zSD9wJlyyuu(Se648?!^Vq>1i;Eb6ZlHq7h@$EA@pDJx=9MqN;}^P-xU+kz;z`3wzs znGmKbf!Au$WiT!XB=(mg`VOY{NRAN~(Fkju%f@?b8kLSlX)S^*`_=>pP?1G)-bD)O zO?&~Gq-9L0tBJL^L_H@Cu#6^6iqV{=HZQqeDyG?(rZ2Q$hy@LXrmX>)mW7=()H382 z={wqa)sALfwWFO^kreYPWJ;lV72D;~yvpWjy>VhCb@s<&D)kV|v&CHM82p1&=+2fJ zS98fU>DZRjq#c{4Ny%K=Fj>MyC2=PMt}|JZkg~H`qsZWGo+^1}BYHZXrbf`Tjc>AB zdctpb(n4#Ha%D_W_LGUKXVN$OI};Sd1>R<9$dme-W*(RnAHk&HqvspU!f;4iQMrOG zCSp&cW7djKNEq^aQoGBbdZ{_}riHA0Is_MU7d1DZZ6LWk+xVD8LdeSSUihR3stTDJ z-U|#q=R09pDGvtyYG)fvr!F9j8RdTSF66^ObQwpyEDwO~GZLG(+zbjff36=fB}*BILX*)g^W z`I{TttW((7KF+V#sLVq3vof|X=q^nR5&rGcgh_iVOWV~1eZz?xE$!EZp$&plXqWly zSjWy5JtaH)!91z<`)D^7C+7MY>PB#f?-s5|R(5jck`Un8-kQadoYJ6O{vrXT9c`4t zTBJPONqNdCkIYlV=?{i|^`KA$DrHb=iq=#UR$h6$F}0Z)VSqHIw(QoK_D8K^H&RwbB_P(pAQW(HtC)NpEByeTxVe%3!DOUsoRg9MSVO2}+r~)WU=p8dvsYmZ z+h$*DL|tRDr!^7F!PXI+=^R_HdSqKz>>E}>`j8h&n(~d+?3(As^#-#W$>W7&Nga(C zblnJ`W+j9@rcy31L6gNM-fcD6*{u|KcD!Mwz;b<)MZ0OK0Ikm?L|{vnI;s-0EJFKb zpxG}D2RXoHy2w*}^LdquuVJFFrV}19wNc-gbv>xKwb3<|;OGTSG8}c&M@b6rFd1Lu zfv_!-oOA6YW6Z~xb&4m#!OJPEgeN%rNRm5xss(fh>le*~)<*5B$|KkKpA>mt(NhQD z3!)|noMeJq_wQ{s(RkYu$L{rIEf6$#r#2d+=lfVkV5bRURp@G=c&7r1Oo+AikL!mX z^)&EAoXA@q9fjL)xxD9uf*xp^kj02NC;aPoO(crMuDvm#Jcy5+u=o1Ge{jOpl=biN zd#U0rwVuxpqo8;~_JCpXDL7Gkk~!k&QEkIegct@1GkdWy18LjJ5yg$i0ZWB##abKf zQ(2|$$;IovJPIirawe~rW*pzEo*SL-edvVNnsjhaef-;bewaUAYdUd3aoz*@{oWAw zaOsXs-fDQ@dwdOEVawwQ-YWdW1d$I&MQ;nCeJEry*#SOzU{Sl!`bwz*uD_BcpyS@EE}g#PR^+O)SRobA!%?(OHK&Ap%te1@pQ#J0FR{!~SUwd*PZ8 zXDhHH*YQ>sHG|;0*rkIg*A`8>@ze_q?HmL-MC= zGXNo$a5L|kDw;}STbh=oIT&b{h(L8M%!a5HpW~Bh*UfTKFVM;^j=y9Pcj8BcP_qvw zpi;riFrlN^eWmjRhV_h%CN43gBDRiljE{GO6fiM0oF||v8e^jcqROc~Hb2+Gjz9U3 zDXcSvYhJ3Tjsa4Dj27pT`Z*O+blVL2WPw#!(9lC{(utasT`EL_slVy%)@>@AQK(w3 z6A2}qVWD#?X}34ueiDvSHGQT$+6>d4yyBih32f^->Fs1NjoB8XwqdFId8fcYZOU^_ znOj6+ZL!@+aW>t1l3xk$C-g2?CB&u0%KDKHScLO-Qlvk5B*((Hkl=4DjR}jc?+N5M z(MFCTAnwaWCSPajm2hhuPdu<-V*Tj_69(F%NCoB~3&88IQ2dzdsee_X=heKx+lZcO zH&vc*Wk`J|i+UOA;Oi&wZ5;m&=@IpS1;qLVFlK!c=Q>0@H&s)MGvp@sBlA?!#2fZA<0pH)Mh2) zsF#zZBiu?59|_Nr5RZc5nm~k(>SNYg>b%(J^vhiXC-h5BJT#UM^+&($glc@+HG(Xj zpQGc?l8l(yIn>9H^hldV(#wraI?}LB(b|heZ<})5DNHfil*gRHJfukhSe@f0g`q=t zP_xv5)I#>!(o{Zo_#^5cWZfn(du#u4Zu)gBTZlg>AjGw#EX%SgjG*r1fmxE_E&+~m78o~rT& zejhG14M5Sr5^Xp#7wGJcppM&kJA4iv9uXTL4zr_arTep1|~U?Z^5 zv7n#aWI-c+=N&MEA_Lo$V@^Tlwke>1ZamvbL3@&d6m9c_1&su6Quux%y(J6!DdY2_ zBqr7t+kON#Jz+NOTF@sxV-~dTYe^QgY{-YgY(AgIWI+35G+RACI_SR-RLcoMeixb?KbtE$b0ixW^PE%^HM6g&Qt^n?>O)=L zWsIF_r`R9 zf$P%&J5jX1T?hJRB*=v5j9D5v0>#sTzC1Mt`uYHN(IxqR259IXU{OV!da46`?JCg0 z#YkcPa+2p2`W$Dk4;0$O+h2q!Na8LvF|9R|0RxeC0YwuCf z);22Dp_IBvMNjRahxmchp_B?Vp=B&EP$(g9kZy>!1eI=6p(sb&>F0V_hM!wGLYkkO zro&TVXK7GC;|dX(_BNh>6|X_H>K*I=D4I41wQB+;_^t(Ik7^l6E*MLlu6_|IoHU$? zJtZ6sJpJO#Q5VXRgH2BF{iYB#_!>X0@WlMpC%e;;)!B};-Q4I%M^bqvrZy64@QVp7 zjtA5hY%X_U>L>k9Xc_0QsC`YwbZEMssNQye4?+zK-u2nS?}>-ZIo{Ro^V#U;_WjYh zjKerNJ&q-$pL?cLnxP^UO9<$RGYNw*q^c0TWHJr}h@ivkDBqR6zYHNR!%L^5hDY^U z3E#e=WDh@p$4R^tFz#kB$!@=Tp=`$FWWqO}sx;NX3MP1tm}pMZMTYUX5VLJi&an+~ zSfB9ZO_ZMB_%uJqLdiE5GZnL^utPqo4Ox>8rIHEgJ??pv;L$m4;Z1@QV7(hmH%sX3 zooYm&C^ulL7g{pqXEz@}K7H_&Nl3U5tWVfgy{UZEkZ1W;HQ>?+OA%^PeOiYK>rkxZ z0Kk-_=H)vC+ASoJHuY^IiNts>K@PnknEo2bMFvTxRhr3vFOj1PeY&`X$#h3~prI1F z%C|dam85dImZp+;F7m7x^K}3P>HZ_C+mEnitm@nw!x*J~aTW@ViJjY9486|4Qs11N zMaFAD2eotJ_ddnN7{*OffaQtP6{|+NgTbsyr0LKw$zB8P*UTnKHXdWCEM&kAHTR&$ z*Z1pX$*2(-GKe}3+b1>%SCf#AcS;cIFhYyPqqy04yL)O0XyTD<>-6<9;8oY6qWIJb z@bt-NL9}4Tbxf)jK-1B+rw0_2``qpr)RhKMi*cQrNL5E!Y&W1IscsJx!q}%KIu46j z%0|7gFTrhf)sV3eq{`GVz7~xYD zl(vT-l*lUeq8l4+wI*$4S7FnDHMfV*NK}17q7KD)*b^3el5KixH#2GV@^iXruGs#F>84^U-SnBV zlD|x{=A~H4?RB!vZG)eAD>-0)<*nq7TK(*?k`e9NF8n!58C~%?OZnxql=H2m5fK7m zD0`;oW8xO9tq>kstL!CfR>DM!l~So`(vJ3vNjr;`C^NJ)YM;}3wcc5TT^tLQg_bGD zXh7;P%ZsUu&S&&glbLs8P&74AQa$yu zrdV`SHBlHP&mKcngK^cXbsQO{76z8h9ocz4qPw5$rq4||n<=Bc9&r6<^ORG5HJ<{E zFVG9bMHXE!`qwy}`rCr8kf&j}V=MN&pPw7gTCrd3iFoSCr z7wRXR(p|)V%qdLbnmk%$!y9*83ufT(MHU&fCiuJ-8*rb`wA8~YU1yE$I90TZ_^eZ? z&5etK&$B4_5%mPj6E4lle5o}yyyg2K7&*;7L*U`mMf@}Q_S5EL1E)3oS5xEXxw_no z_fL2M-bkE5S|$s}EO8V?IQ(N%qxyB6ylgnPyWgiydNG0J7M7nKG{82$P9y;9^_@rn z*6BNu04I!xZY03RsC+Rlz)36VZ6p9|M6}w81YjXIuGWs_v{eyoS+kg1?TSY&sd3GY zQd!WgE{aFiqpDdf|9*njmZ}RPouxD~J~fX7c#({O2@N-q02&pA7Cfb2u77%n;jRcc zIjMJYKCe-Gl4|l6g)FoxWHT){KgOG5z&Y@rtN^b^^Y`QPsDS#I zMZz8K|!cFABXBw&M(K#Tsx(NAmvE|QBo_0pyfUb|3W-QpSV6RT|MI`TR zqB}gzoxlNIP{+30O@RZj@8-Y(#VeiXN-p3jB9|^q0Rx)@2O8N8_iYFqIFSMeqzI%+ z0Bb|wz%=QClPDo8&Ju^+#=rqpslb7!=Ya#A+AM-XF@;v4Cb(}NI3Q)9zyY!MMs+3j zcZ6drW+WcGGTWrc7l=SO(Fq)&rx7^t)caF#z&vn3NZb@S@as3`7b%jNxDDN|raNvp zM`6siHuSi}os1gn_x0&+a&egL3b}M$+Duogc{p|CEJoghe5A8h%FZhat}~@zwXx2p z7xG!3yfbygNo(W!pMU>}tS%-$K`XSQo&RplKfU^Pl0_Yfi*;rDDf+fUHJAhdL_5bgk6Uo1n|~Cht-GKKx(aIODw6@> zXuaO&bycU*%NbGF_^&bCVhNT_0Wx$>Q+{@8dD})+akw5`D-f`+Mx1J^cC`yd%~lNc zzacV@4?3{kS)%PZ#E0wrjj6Z}#ob)|M_A8WW8zLUw%S@6V>{Y7w!C_5IXP6ACw^Z1=bLy&^$$KQnnPs# z`AYeg^XRO?e0hiOjapqX|LF`mHv+YTj@~`jL60Jb>#`a2a*I)WR6Qx%K}X|=;M`9l zwp(gF_pt}l#4QA)bpKZ8G2AvFwZ=8%yv!kI9%qUVW`O&FNNqx_9&ATJ9*5xx#pii#M=vn0;{ zDz&wNFl&XM$nhh~kHdnss0Av818RmS> zv9S4$r=TXNgroQ5DKqLCDm3cmR46-~Y#=9l{e)?yyRH5ZHs;U3O2 zcW%q!q)`Sl*jbJq-S46fyu7mPy&9O|D=5n}*!pLg)`lO@laOpbA`vy&Ro%8~$HAJ^T{k zL1~|++)|5MA>zZ2f{2AKB0|8PUYs`YhZ+bakPyPE*BQHSLUi~4v${XpcKBYl?yJR@ zulo)BTXo+`@buWjx&QJED$x>W#P!)LEF@2>xe+}Z$y%bL*-rFXq-TW6m)$%&CXu%E ziixz%#@R251$S@D1KSyAzp7$H_H8iEHV&GCZIYb)A?3bk`$o2rlc$P@Eex=;B*T}7 z=09g##i+jAdi*&NNcoBp{poc0AAB24FP}4QzUMs@+b}!Xf;JJ=XB3eZB34J=qbkSS zLh`byBWr*yCS0d>>1?!d1AkFJD)_Z)6w~D|+CVv{@)X3fxjrl#?VLE%OlK}%KBSgE z1BzTNZl%ay8s-}Ki~468Vrq@Pv%%cF{LfQ9&=+>lIhCg1fx|7{wam~=B(|B(dOq=T zOlL!y{!!HYLV+eT!-#i{cWXPRidsCoE}acqHSibpCk4MQL}{7YJIK$(UCiJT0^X(R$*`*r zUKs?a#g`8O4g5v@kl@#e2#EDRf7be7UjF?mUj_n0pXe0JGg449J`xXOwdAM93cb(^?1nVMx_l2YhCv*zK*- zvz?Ne+n4uR$Hwwi6ggb?<@Tnnx{upCAd6HKcyfCyivLcuyo3W*mBz%`O%SnDS44Qu~(mROx7UFAQS;jX6hLM}vVH9+G=%ryOAm z9rqN6HhJsQ0=yMi)_&@WOcoaK_k zDGqobV=XQY8uIMx$0YlDU5@YoVfffwlJTSVi1j(u`uUK`BD#_+X%;?*jRs(Oq^kka zxu+q)b;C5WtI+^jNl6+2hTcj8sPs%S@-*eG0XCM8>qrKN>z*`#4nyrBy8|+a(e{ z=(Hstl=gi}D3ky#DaUs!VWkMiO85mW$=(xhO`CZb-tp}s0^87No80W zpTdOLSl#1~=Ibcvzb4dk8^hB9_gb(AEkug@M>PF2*}UWnE1$!PmwzU z>MW^!%N3c#-@dJ|SzwzVE*%SO+kDhoj`>@2d)9J*T$|!y%3l{#dgi^{h77PvemD2_ z@FT6f%tNev@}#BFSow4$xUWq1G7$@YDqviB{fa`yRcs7%ixG0b*=dqAZhyJ{9k&mb zJnu+NQdJtlsA|3-+o?_PFW3J?9ZecSzBp5;|~@*q_hNP>F>mRAK>M7YR;bN`6sZ>-Aq%sKi1WDzVTFm1x%v z6=icyVKUf^=P9(<2LittGU`=%G8uV-Ma$9TG03oN@XbTQx4jl;!gu-EjPKLNBRwX3 zpBj&(5FD89rG}<7^4WOjxRuK?I|q`82IcirYIl%Cv=aWF68cF*E8!zbXhgIUey+*! zlE+CRdda~}lK7ju1=;C3=kKKu(O6dm>m;$Gu$@-y_!tw5oh5}XaX5); zx7dlglGsTVp^eVi7`$-CJW%SH^61V2i+Tls5n6tpwy#dlV=ca@R(U==TV?BWopJOY zS2pGMXf^+DTRjpTmPmhx(4l^WsHt`YI%qwf6iBAvPI{6^-17wIK1^E$3 z#=Q&Dsyw9neO!VHMgF-c+mXu2L!`(+WGgqjka6<04hLLL@=Xy;ixh4`!9F|U{|1eG z8*Cxp{)6KG2CaM>B>DDI8Er0icA+q5ii=}_;TF>&7lk?q(3_bh^}<@3=J2IOU2n#n zzZ{J{ThhQBnoT_8#i+fs>+090eiwiJ6bQDG@sCTDDXxW!0O zDLOX;A;3|k2zsJYYy|UYBbdW<9D=9dlT;|L|F#f?dyudN<%14f8^Sv1peamWfuqW$ zIpJqmjFhgv*5MuGP}NCaOWjZ^%m~%n^0UndfFY|RVN_S*XX}0nZ89Uy)EG?#A8HKX zLvOw*U_*{9s#g!smJ%20AmN4sw^csaskRwIQ?&{z0-eQtb3FwbHZY$oax-5O5oP9^ zM?}q;FQT4;&!pFw*T{}|h4{=fh3(^Xw~DikB5CF!deF>6!_>7Se#_+3@)>g+hg?dQ zd|G-9F?1<)flVnBM!zv5B&3ST`#2~C;$pBTFb+Sfc%k&WTY>Jem&%-{!Ps`3=Y!D; z;sN>QlobzyfP&JwY&w91P!_Y<-6G$-~Iq=AfJZgLCd<=U;hqOL^j zsUkS*jE%urXUqffo+*z)v8}umXb$I}W&C=~Q@VlX1Oi^DDc^1mZjacs-NKT(7g*9v zVB3%SbfM(Nidji{6x67b9O{Q9DN!X;@cJ3j|HGRKgrf8L5>2Wm_ElLxao5{ zN>rfJ()scJiaNI^a_sJpKU(BIArMo(4c3LcV%i+Ry1h2|5GzHJa301lLKItcf_0x! z7=M4uh4C}jE2js7>(pw~q&NC3dKZUEVAJ?n1Q%~Py>t*F9-uUAM%!Fk1x~m5D0DlH zJS5rpy=7j`4}X{D(Z5PNN$8G`d@poIWsWG(RqP!44#}Y6;OC#w?&+R(Ol>^d9kXfU z+3u)2$Uz;KT#!nMU7&gW;6FS;@l_6D;*1``^Xo^Qz;(4e`viCO2BgV;bV1AWHhYvu z_cDRefh*t@>Tr!b?c=F(T92oF^Gc<7_A?Bw;Dc^Kcett9s^oZmbtcSXXIY=AZJI`Hv*y(Ro~HlGB8Q>i?ADST zhW;#vAyy3J7S}JBmA(B>Ios|C+no$R*0OFp?N}_!zi&RVcfoR z1c#<9?K|83C3V~PHNCf6axF#i^Qy8PW1~P)Fz#7VZ}s*zP=8Pd${b_sDbExS6@iJp z9_KMi202Tdy`XBjAeSC!^dd_c2O^3n*QO|qz?pCh<7;!JFs9$}hk8Zt{#=Uh>5N03 zJso?BZIK1F3RGaopDeDYp)8rJXqPEarT3AXFw z)LuW#pEzzDp!b&h!?ND3v2*VwYSXxv8W6hi$s)R;L%pQ=kSyv3a)oY4G*uExIu*2b zJ$-e|wopGh)~`SCzUWv#I);&VdGT6mKz3ZokB%T!H}!NSPy7889mWfPd2vnESBKRK zsHMk>g=>2(@$MeWIMJQ02R8SrucIuZTe1laq2xN%8uk79#e{l8_&T?L;BW(zAW_>u z0%(F{tpf?-rosIjk~|>c`dV2C}Ub|&RSCnZ!VhY2KIhlFl#n>rxw zEyOnb4YS0#WRQ6tnlQ*9`;_8=a^+Sg?|LY!6Y@eQh6 zN3tNk3+Wy;MQGL60b40;uHYJ}^*}Q~@|`hjK#_4k3g~H5Vkj81CPm;Lh>_R|T%mj; zxL^t*1YY&%EZ`+ShX!d}G;P$VBs=6v4jjeesp7OZSzj}GbTE;1IWYnpeQhU#EClaw zBx{aBWA-@0=D{m2QqbHD-N@Wc>7Ki3dO3Ihkkg-%`v*zC=@f^yowKXgF)tgdltLTG z=P5NJ%LDNEvCorV9r)q#a+g5dS@s|tLfxihC2DemGEb8zM?h$5K%dc4R+rm-lwGB= zF$KUDig86(A^P3qh6QTkC$dv0qV|S;e28XMHdY+2|~DTkT7TbAxKB5@I^Ox+M~PtCbGar=o*il+))c^7AyT-n@# zA{LF4OdisWS?|MiXihRge8KvgPBQtuVbO1dz3v#t{FktAB)x5=ko2qq`4}d;%Tk|u zJ!&L9r;e3@PWYc}prPUL)G1nSQ$l8+?4-C;&vp_dK{x1v{YvOyhPMVQYaxG-6C`r# zPFKw9$`e74#+u9dhb3U!8Cr1wW>3&}Wjei_s^n`$SNz5y&j?UBir0$P#A=S9U$u+} zAh*(35giiEiLLXQS0iTkDn zp?(I#H&WnXJDI{B*-8uy17I0EfhH0vASL?UGC=j+AjpfB@zw7w7+?Kf>mFicqA=e- z=E3OT^0*T^xQx;7;4--WVZ70C^^wOa`2NMsCm11MPUZW@cNOD??@ykQ?->}8`)0mB z*7ALj1K4PXAL6CQPjpg(An0tHMd;(%w8$g)q8tnHNt_%wjO)z;WoZg0puMMgm?v8GMcH9#Jt4oeJg|I_ zGbuc&BXMo1r;Sv%QTIueJOhU`jrJ_XvR*ivpjS>o{o| zdN~k>`xm;9^NAw(0ycrvp#M4R*xWvrll%l>eh*0_UCayo9c+;d8Q;L?n=Ff(Gbr2h zE3IJL^DB#*+Y{UKE6tYF)e&1CK(6O;X@L#-q!A(Tq~Tg0CGY_3a%exiiI}j97PwOS zWapf>Stu&$eUU1asJi{3@^mVz6ul1Ik(H(#R$)x=>GQYUeGM3tyDK>e4u7HFkgBTvDOp&0oQo$_%Mo|{MMyQ&;Ilb5 zOwfc-MB5XRY*fpEo`r1+y(2kiss>HhF)E;<(9xP%3N)yoJ;&7pbPS)_gTSX5WJ7qZ zjwVOw=&+d|F^Io07v#R-v8X@MNuhcw`}jOXcz#BfkDvtV$M_Z0$Mp_;hWa>AH#btc z4?w-glimROv5i0<-vqSoO4N;%>IVqmTuHkYf-X2;utn)fqX1O5DM4RP&QpYBZm13A zv>D5I2u?*Zs1GEIg3b=f6amR2L{YdAd}q7vmDJRv9ZE8FjiIMwS`DnGFj!>6lnCxL zWgt)*Ylx%+RLE9iTSF2fvW8CD8YRv+tqMV=`R*171H z)U^ZQ)Pm{@y^aP)r>CQwyc9g9xzZO@yCvzhZWFrOMOsu#+S5F2inP8!#R694FUu&y z+4g$MsZ5k^g8@my)8Epvfps6|O z{FW06dHHEL^z{x96X+N<6-((p>R!2rXO&wQYbm?{$5W{_7h_Yt8=Wuj$#*wRcjt&% z+cXBwSKD|&(@T`TB`gI!_UJ7oWHf}ofJ6T^v&DYmxPM>;w}M<4)d*@Sc#p((EZ?%2 zk;HfmfgtP-hAv{3sOzSwJwez_Q%^Kx3LP1L%U~PYE7Jf9w#~H1x#Oa_;-%1D+0vd` zIED6XUF0q86+wGy>1=4v>x(#~8rl>6#Xs$u0|}x+o)Q%rmO@lmiVEe0hV5)*dz~68 zOrDtVzb=IxEh_9YaH<(99C5{1ma~n!+)ER`h5qOFtCts+2&gFQ@A&Q$+U@Yu1($)F zPI7)c$_p?T6%11aeZoUU>>DcShmAU~L;?E{B~=jCB#z!pqHH0sL!8`LhBL${%TD<=;rLMnoogBMm(}7Xh4Y z)cqlUEhAO4WTI3ak7&u3O?y)O#n}j7d1uij+VQcurVr;%V`a+2V+>)Qbu>M1 zJI{&Gj&XN$*UAj1khH5>knz`8nLIAM1Y(nr+DL2-=mJ0fZAo;7#CpEh)$>~s9j8XRI5JJJfV4d+R8BK8Ufg|rr~Zr$RSy*c&n1dsB;5(W0$FXIn7>6D2jzkSAN0r>$&VoMm%cw|seRT{-XIfPeQnQHyoE z71eJmY8Nar=SHjvS>C38{mf}3?Y6}R^J0H&1?`FcHdCsXZ92b6(Eib%&KJ{SgBZ+m zMFOO__pmGL{HRq%UDOJ?OHt%MU0kx|jDKDs^M23IK8*_h#Mz{ue8nkvw9!=fqffzQ zL&4b1R^`L2EQ^wTN#nY#(T(+QD#%%0Dkv7^Nvi1hca+NMc}^vuq#!=IFBic7&j)_L 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 [TestFeeTaxWasm](../../tests/e2e/e2e_test.go#L158)| +| 2 | Not supported tx will not be deducted tax amount | User transfer or make some special transactions that not in the tax list | Tax shouldn't be deducted with correct amount| [TestFeeTaxNotSupport](../../tests/e2e/e2e_test.go#L306) | +| 3 | Special IBC tx will be bypass when gas limit is not exceeded | User make IBC transactions that happen both cases:
- Gas usage does not exceeded `maxTotalBypassMinFeeMsgGasUsage`
-Gas usage exceeded `maxTotalBypassMinFeeMsgGasUsage` | Bypass when gas limit not exceeded and deduct fee when exceed | 🛑 Not figure out the way to make update client in e2e, should be test in testnet | +| 4 | Forward transaction should deduct the amount to tx origin | User execute contract that will trigger an execute msg to another contract | - User should be the tx origin of the execute msg
- Tax should be deducted with correct amount | [TestFeeTaxForwardWasm](../../tests/e2e/e2e_test.go#L428) | +| 5 | Multiple forward works | Contracts will trigger another contracts multiple times | - User should be the tx origin of the execute msg
- Tax should be deducted with correct amount | [TestFeeTaxForwardWasm](../../tests/e2e/e2e_test.go#L428) | +| 6 | Error forward tx should return the tax and not consumed gas | User execute contract that will trigger an execute msg to another contract. The execute msg to another contract will be failed | Tax and not consumed gas should be revert to user | [TestFeeTaxForwardWasm](../../tests/e2e/e2e_test.go#L428) | +| 7 | Out of gas should return the tax and not consumed gas | User make some transactions with limited gas amount that will lead to cause `out of gas` error | Tax and not consumed gas should be revert to user | 🛑 Not figure out the way to make `out of gas` error occur, should be test in testnet | +| 8 | Grant msg should work | User grant multiple type of permissions to different transactions | Grant permission msg will only can deduct one denom in ante handler and one denom in post hanlder | [TestFeeTaxGrant](../../tests/e2e/e2e_test.go#L214) | +| 9 | Allow pay with multiple fees should work | User make transaction with multiple coins as fee | Fee can be paid by multiple denom, if one denom is not enough, then it will deduct other denom | [TestFeeTaxMultipleDenoms](../../tests/e2e/e2e_test.go#L380) | +| 10 | Try to pay with non value token denom should fail | User make transaction that use a different denom as fee | That denom should be reject and the tx should only accept denom listed in params | [TestFeeTaxNotAcceptDenom](../../tests/e2e/e2e_test.go#L531) | \ No newline at end of file diff --git a/x/tax2gas/ante/ante.go b/x/tax2gas/ante/ante.go new file mode 100644 index 000000000..1621943b8 --- /dev/null +++ b/x/tax2gas/ante/ante.go @@ -0,0 +1,267 @@ +package ante + +import ( + "fmt" + "math" + + tmstrings "github.com/cometbft/cometbft/libs/strings" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + tax2gasKeeper "github.com/classic-terra/core/v3/x/tax2gas/keeper" + "github.com/classic-terra/core/v3/x/tax2gas/types" + tax2gasutils "github.com/classic-terra/core/v3/x/tax2gas/utils" +) + +// FeeDecorator deducts fees from the first signer of the tx +// If the first signer does not have the funds to pay for the fees, return with InsufficientFunds error +// Call next AnteHandler if fees successfully deducted +// CONTRACT: Tx must implement FeeTx interface to use DeductFeeDecorator +type FeeDecorator struct { + accountKeeper ante.AccountKeeper + bankKeeper types.BankKeeper + feegrantKeeper types.FeegrantKeeper + treasuryKeeper types.TreasuryKeeper + tax2gasKeeper tax2gasKeeper.Keeper +} + +func NewFeeDecorator(ak ante.AccountKeeper, bk types.BankKeeper, fk types.FeegrantKeeper, tk types.TreasuryKeeper, taxKeeper tax2gasKeeper.Keeper) FeeDecorator { + return FeeDecorator{ + accountKeeper: ak, + bankKeeper: bk, + feegrantKeeper: fk, + treasuryKeeper: tk, + tax2gasKeeper: taxKeeper, + } +} + +func (fd FeeDecorator) 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 ( + priority int64 + err error + ) + + msgs := feeTx.GetMsgs() + if tax2gasutils.IsOracleTx(msgs) || !fd.tax2gasKeeper.IsEnabled(ctx) { + return next(ctx, tx, simulate) + } + + // Check if the gas price node set is larger than the current gas price + // it will be the new gas price + gasPrices := fd.GetFinalGasPrices(ctx) + // Compute taxes based on consumed gas + gasConsumed := ctx.GasMeter().GasConsumed() + gasConsumedFees, err := tax2gasutils.ComputeFeesOnGasConsumed(tx, gasPrices, sdk.NewInt(int64(gasConsumed))) + if err != nil { + return ctx, err + } + + // Compute taxes based on sent amount + burnTaxRate := fd.tax2gasKeeper.GetBurnTaxRate(ctx) + taxes := tax2gasutils.FilterMsgAndComputeTax(ctx, fd.treasuryKeeper, burnTaxRate, msgs...) + // Convert taxes to gas + taxGas, err := tax2gasutils.ComputeGas(gasPrices, taxes) + if err != nil { + return ctx, err + } + + // Bypass min fee requires: + // - the tx contains only message types that can bypass the minimum fee, + // see BypassMinFeeMsgTypes; + // - the total gas limit per message does not exceed MaxTotalBypassMinFeeMsgGasUsage, + // i.e., totalGas <= MaxTotalBypassMinFeeMsgGasUsage + // Otherwise, minimum fees and global fees are checked to prevent spam. + maxTotalBypassMinFeeMsgGasUsage := fd.tax2gasKeeper.GetMaxTotalBypassMinFeeMsgGasUsage(ctx) + doesNotExceedMaxGasUsage := feeTx.GetGas() <= maxTotalBypassMinFeeMsgGasUsage + allBypassMsgs := fd.ContainsOnlyBypassMinFeeMsgs(ctx, msgs) + allowedToBypassMinFee := allBypassMsgs && doesNotExceedMaxGasUsage + + if allowedToBypassMinFee { + return next(ctx, tx, simulate) + } + + if !simulate { + isOracleTx := tax2gasutils.IsOracleTx(msgs) + // the priority to be added in mempool is based on + // the tax gas that user need to pay + priority = int64(math.MaxInt64) + if !isOracleTx { + if taxGas.IsInt64() { + priority = taxGas.Int64() + } + } + } + + // Try to deduct the gasConsumed fees + paidDenom, err := fd.tryDeductFee(ctx, feeTx, gasConsumedFees, simulate) + if err != nil { + return ctx, err + } + + newCtx := ctx.WithPriority(priority). + WithValue(types.TaxGas, taxGas). + WithValue(types.FinalGasPrices, gasPrices) + if !taxGas.IsZero() { + newCtx.TaxGasMeter().ConsumeGas(taxGas, "ante handler taxGas") + } + newCtx = newCtx.WithValue(types.AnteConsumedGas, gasConsumed) + if paidDenom != "" { + newCtx = newCtx.WithValue(types.PaidDenom, paidDenom) + } + + return next(newCtx, tx, simulate) +} + +func (fd FeeDecorator) tryDeductFee(ctx sdk.Context, feeTx sdk.FeeTx, taxes sdk.Coins, simulate bool) (string, error) { + if addr := fd.accountKeeper.GetModuleAddress(authtypes.FeeCollectorName); addr == nil { + return "", fmt.Errorf("fee collector module account (%s) has not been set", authtypes.FeeCollectorName) + } + + feeCoins := feeTx.GetFee() + feePayer := feeTx.FeePayer() + feeGranter := feeTx.FeeGranter() + deductFeesFrom := feePayer + + foundCoins := sdk.Coins{} + if !taxes.IsZero() { + for _, coin := range feeCoins { + found, requiredFee := taxes.Find(coin.Denom) + if !found { + continue + } + if coin.Amount.GTE(requiredFee.Amount) { + foundCoins = foundCoins.Add(requiredFee) + } + } + } else { + return "", nil + } + + // if feegranter set deduct fee from feegranter account. + // this works with only when feegrant enabled. + if feeGranter != nil { + if fd.feegrantKeeper == nil { + return "", sdkerrors.ErrInvalidRequest.Wrap("fee grants are not enabled") + } else if !feeGranter.Equals(feePayer) { + allowance, err := fd.feegrantKeeper.GetAllowance(ctx, feeGranter, feePayer) + if err != nil { + return "", errorsmod.Wrapf(err, "fee-grant not found with granter %s and grantee %s", feeGranter, feePayer) + } + + granted := false + for _, foundCoin := range foundCoins { + _, err := allowance.Accept(ctx, sdk.NewCoins(foundCoin), feeTx.GetMsgs()) + if err == nil { + foundCoins = sdk.NewCoins(foundCoin) + granted = true + err = fd.feegrantKeeper.UseGrantedFees(ctx, feeGranter, feePayer, foundCoins, feeTx.GetMsgs()) + if err != nil { + return "", errorsmod.Wrapf(err, "%s does not allow to pay fees for %s", feeGranter, feePayer) + } + break + } + } + + if !granted { + return "", errorsmod.Wrapf(err, "%s does not allow to pay fees for %s", feeGranter, feePayer) + } + } + + deductFeesFrom = feeGranter + } + + deductFeesFromAcc := fd.accountKeeper.GetAccount(ctx, deductFeesFrom) + if deductFeesFromAcc == nil { + return "", sdkerrors.ErrUnknownAddress.Wrapf("fee payer address: %s does not exist", deductFeesFrom) + } + + // deduct the fees + if !foundCoins.IsZero() { + foundCoins, err := DeductFees(fd.bankKeeper, ctx, deductFeesFromAcc, foundCoins) + if err != nil { + return "", err + } + + events := sdk.Events{ + sdk.NewEvent( + sdk.EventTypeTx, + sdk.NewAttribute(sdk.AttributeKeyFee, foundCoins.String()), + sdk.NewAttribute(sdk.AttributeKeyFeePayer, deductFeesFrom.String()), + ), + } + ctx.EventManager().EmitEvents(events) + + // As there is only 1 element + return foundCoins.Denoms()[0], nil + } + if simulate { + return "", nil + } + return "", fmt.Errorf("can't find coin that matches. Expected %q, wanted %q", feeCoins, taxes) +} + +// DeductFees deducts fees from the given account. +func DeductFees(bankKeeper types.BankKeeper, ctx sdk.Context, acc authtypes.AccountI, fees sdk.Coins) (sdk.Coins, error) { + if !fees.IsValid() { + return nil, errorsmod.Wrapf(sdkerrors.ErrInsufficientFee, "invalid fee amount: %s", fees) + } + + for _, fee := range fees { + balance := bankKeeper.GetBalance(ctx, acc.GetAddress(), fee.Denom) + if balance.IsGTE(fee) { + err := bankKeeper.SendCoinsFromAccountToModule(ctx, acc.GetAddress(), authtypes.FeeCollectorName, sdk.NewCoins(fee)) + if err != nil { + return nil, errorsmod.Wrapf(err, "failed to send fee to fee collector: %s", fee) + } + return sdk.NewCoins(fee), nil + } + } + + return nil, sdkerrors.ErrInsufficientFunds +} + +func (fd FeeDecorator) ContainsOnlyBypassMinFeeMsgs(ctx sdk.Context, msgs []sdk.Msg) bool { + bypassMsgTypes := fd.tax2gasKeeper.GetBypassMinFeeMsgTypes(ctx) + + for _, msg := range msgs { + if tmstrings.StringInSlice(sdk.MsgTypeURL(msg), bypassMsgTypes) { + continue + } + return false + } + + return true +} + +func (fd FeeDecorator) GetFinalGasPrices(ctx sdk.Context) sdk.DecCoins { + tax2gasGasPrices := fd.tax2gasKeeper.GetGasPrices(ctx) + minGasPrices := ctx.MinGasPrices() + gasPrices := make(sdk.DecCoins, len(tax2gasGasPrices)) + + for i, gasPrice := range tax2gasGasPrices { + maxGasPrice := sdk.DecCoin{ + Denom: gasPrice.Denom, + Amount: sdk.MaxDec( + minGasPrices.AmountOf(gasPrice.Denom), + gasPrice.Amount, + ), + } + + gasPrices[i] = maxGasPrice + } + + return gasPrices +} diff --git a/x/tax2gas/ante/ante_test.go b/x/tax2gas/ante/ante_test.go new file mode 100644 index 000000000..ca2f73318 --- /dev/null +++ b/x/tax2gas/ante/ante_test.go @@ -0,0 +1,133 @@ +package ante_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 AnteTestSuite 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 *AnteTestSuite) SetupTest(isCheckTx bool) { + tempDir := suite.T().TempDir() + suite.app, suite.ctx = createTestApp(isCheckTx, tempDir) + suite.ctx = suite.ctx.WithBlockHeight(1) + + // Set up TxConfig. + encodingConfig := suite.SetupEncoding() + + suite.clientCtx = client.Context{}. + WithTxConfig(encodingConfig.TxConfig) +} + +func (suite *AnteTestSuite) 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 *AnteTestSuite) 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 TestAnteTestSuite(t *testing.T) { + suite.Run(t, new(AnteTestSuite)) +} diff --git a/x/tax2gas/ante/fee_test.go b/x/tax2gas/ante/fee_test.go new file mode 100644 index 000000000..d6320e3cc --- /dev/null +++ b/x/tax2gas/ante/fee_test.go @@ -0,0 +1,666 @@ +package ante_test + +import ( + "encoding/json" + "fmt" + "os" + "time" + + "cosmossdk.io/math" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + authz "github.com/cosmos/cosmos-sdk/x/authz" + "github.com/cosmos/cosmos-sdk/x/bank/testutil" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + core "github.com/classic-terra/core/v3/types" + markettypes "github.com/classic-terra/core/v3/x/market/types" + oracletypes "github.com/classic-terra/core/v3/x/oracle/types" + "github.com/classic-terra/core/v3/x/tax2gas/ante" + + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + ibcchanneltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v7/testing" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" +) + +var ( + sendCoin = sdk.NewInt64Coin(core.MicroLunaDenom, int64(1000000)) + sendCoins = sdk.NewCoins(sendCoin) +) + +func (s *AnteTestSuite) TestDeductFeeDecorator() { + s.SetupTest(true) // setup + s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() + + mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.Tax2gasKeeper) + antehandler := sdk.ChainAnteDecorators(mfd) + + // keys and addresses + priv1, _, addr1 := testdata.KeyTestPubAddr() + coins := sdk.NewCoins(sdk.NewCoin("atom", sdk.NewInt(300))) + testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, coins) + + privs, accNums, accSeqs := []cryptotypes.PrivKey{priv1}, []uint64{0}, []uint64{0} + tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) + s.Require().NoError(err) + + testCases := []struct { + name string + simulation bool + checkTx bool + mallate func() + expFail bool + expErrMsg string + }{ + { + name: "success: zero gas in simulation", + simulation: true, + checkTx: true, + mallate: func() { + // set zero gas + s.txBuilder.SetGasLimit(0) + }, + expFail: false, + }, + { + name: "Success: deduct sufficient fees", + simulation: false, + checkTx: true, + mallate: func() { + msg := testdata.NewTestMsg(addr1) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + // GasConsumed : 7328*28.325 = 207566 + err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(207566)))) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 207566)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(7328) + }, + expFail: false, + }, + { + name: "Fail: deduct insufficient fees", + simulation: false, + checkTx: true, + mallate: func() { + msg := testdata.NewTestMsg(addr1) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + // GasConsumed : 7328*28,325 = 207566 + err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(207565)))) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 207565)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(7328) + }, + expFail: true, + expErrMsg: "can't find coin that matches", + }, + { + name: "Success: Instantiate contract", + simulation: false, + checkTx: true, + mallate: func() { + msg := &wasmtypes.MsgInstantiateContract{ + Sender: addr1.String(), + Admin: addr1.String(), + CodeID: 0, + Msg: []byte{}, + Funds: sendCoins, + } + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + // Consumed gas at the point of ante is: 7220 but the gas limit is 100000 + // 100000*28.325 (gas fee) + 1000 (tax) = 2833500 + err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(2833500)))) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 2833500)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(100000) + }, + expFail: false, + }, + { + name: "Success: Instantiate2 contract", + simulation: false, + checkTx: true, + mallate: func() { + msg := &wasmtypes.MsgInstantiateContract2{ + Sender: addr1.String(), + Admin: addr1.String(), + CodeID: 0, + Msg: []byte{}, + Funds: sendCoins, + } + // Consumed gas at the point of ante is: 7220 but the gas limit is 100000 + // 100000*28.325 (gas fee) + 1000 (tax) = 2833500 + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(2833500)))) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 2833500)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(100000) + }, + expFail: false, + }, + { + name: "Fail: Instantiate2 contract insufficient fees", + simulation: false, + checkTx: true, + mallate: func() { + msg := &wasmtypes.MsgInstantiateContract2{ + Sender: addr1.String(), + Admin: addr1.String(), + CodeID: 0, + Msg: []byte{}, + Funds: sendCoins, + } + // Consumed gas at the point of ante is: 7220 + // 7220*28.325 (gas fee) = 207566 + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(2833499)))) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 207565)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(100000) + }, + expFail: true, + expErrMsg: "can't find coin that matches", + }, + { + name: "Success: Execute contract", + simulation: false, + checkTx: true, + mallate: func() { + msg := &wasmtypes.MsgExecuteContract{ + Sender: addr1.String(), + Contract: addr1.String(), + Msg: []byte{}, + Funds: sendCoins, + } + // Consumed gas at the point of ante is: 7220 but the gas limit is 100000 + // 100000*28.325 (gas fee) + 1000 (tax) = 2833500 + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(2833500)))) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 2833500)) + + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(100000) + }, + expFail: false, + }, + { + name: "Success: Bank send", + simulation: false, + checkTx: true, + mallate: func() { + msg := banktypes.NewMsgSend(addr1, addr1, sendCoins) + // Consumed gas at the point of ante is: 7220 but the gas limit is 100000 + // 100000*28.325 (gas fee) + 1000 (tax) = 2833500 + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(2833500)))) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 2833500)) + + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(100000) + }, + expFail: false, + }, + { + name: "Success: Bank multisend", + simulation: false, + checkTx: true, + mallate: func() { + msg := banktypes.NewMsgMultiSend( + []banktypes.Input{ + banktypes.NewInput(addr1, sendCoins), + banktypes.NewInput(addr1, sendCoins), + }, + []banktypes.Output{ + banktypes.NewOutput(addr1, sendCoins), + banktypes.NewOutput(addr1, sendCoins), + }) + // Consumed gas at the point of ante is: 7220 but the gas limit is 100000 + // 100000*28.325 (gas fee) + 2000 (tax) = 2834500 + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(2834500)))) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 2834500)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(100000) + }, + expFail: false, + }, + { + name: "Success: Market swapsend", + simulation: false, + checkTx: true, + mallate: func() { + msg := markettypes.NewMsgSwapSend(addr1, addr1, sendCoin, core.MicroKRWDenom) + // Consumed gas at the point of ante is: 7220 but the gas limit is 100000 + // 100000*28.325 (gas fee) + 1000 (tax) = 2833500 + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(2833500)))) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 2833500)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(100000) + }, + expFail: false, + }, + { + name: "Success: Authz exec", + simulation: false, + checkTx: true, + mallate: func() { + msg := authz.NewMsgExec(addr1, []sdk.Msg{banktypes.NewMsgSend(addr1, addr1, sendCoins)}) + // Consumed gas at the point of ante is: 7220 but the gas limit is 100000 + // 100000*28.325 (gas fee) + 1000 (tax) = 2833500 + s.Require().NoError(s.txBuilder.SetMsgs(&msg)) + err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(2833500)))) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 2833500)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(100000) + }, + expFail: false, + }, + { + name: "Fail: Authz exec", + simulation: false, + checkTx: true, + mallate: func() { + msg := authz.NewMsgExec(addr1, []sdk.Msg{banktypes.NewMsgSend(addr1, addr1, sendCoins)}) + // Consumed gas at the point of ante is: 7220 but the gas limit is 100000 + // 100000*28.325 (gas fee) + 1000 (tax) = 2833500 + s.Require().NoError(s.txBuilder.SetMsgs(&msg)) + err = testutil.FundAccount(s.app.BankKeeper, s.ctx, addr1, sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(2833500)))) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 2833500)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(100000) + }, + expFail: false, + }, + { + name: "Bypass: ibc MsgRecvPacket", + simulation: false, + checkTx: true, + mallate: func() { + msg := ibcchanneltypes.NewMsgRecvPacket( + ibcchanneltypes.Packet{}, + []byte(""), + ibcclienttypes.ZeroHeight(), + addr1.String(), + ) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 0)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(1_000_000) + }, + expFail: false, + }, + { + name: "Not Bypass: ibc MsgRecvPacket", + simulation: false, + checkTx: true, + mallate: func() { + msg := ibcchanneltypes.NewMsgRecvPacket( + ibcchanneltypes.Packet{}, + []byte(""), + ibcclienttypes.ZeroHeight(), + addr1.String(), + ) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 0)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(1_000_001) + }, + expFail: true, + expErrMsg: "can't find coin that matches", + }, + { + name: "Bypass: ibc MsgAcknowledgement", + simulation: false, + checkTx: true, + mallate: func() { + msg := ibcchanneltypes.NewMsgAcknowledgement( + ibcchanneltypes.Packet{}, + []byte(""), + []byte(""), + ibcclienttypes.ZeroHeight(), + addr1.String(), + ) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 0)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(1_000_000) + }, + expFail: false, + }, + { + name: "Bypass: ibc MsgUpdateClient", + simulation: false, + checkTx: true, + mallate: func() { + soloMachine := ibctesting.NewSolomachine(s.T(), s.app.AppCodec(), "solomachine", "", 2) + msg, err := ibcclienttypes.NewMsgUpdateClient( + soloMachine.ClientID, + soloMachine.CreateHeader(soloMachine.Diversifier), + string(addr1), + ) + s.Require().NoError(err) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 0)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(1_000_000) + }, + expFail: false, + }, + { + name: "Bypass: ibc MsgTimeout", + simulation: false, + checkTx: true, + mallate: func() { + msg := ibcchanneltypes.NewMsgTimeout( + ibcchanneltypes.Packet{}, + 1, + []byte(""), + ibcclienttypes.ZeroHeight(), + addr1.String(), + ) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 0)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(1_000_000) + }, + expFail: false, + }, + { + name: "Bypass: ibc MsgTimeoutOnClose", + simulation: false, + checkTx: true, + mallate: func() { + msg := ibcchanneltypes.NewMsgTimeoutOnClose( + ibcchanneltypes.Packet{}, + 1, + []byte(""), + []byte(""), + ibcclienttypes.ZeroHeight(), + addr1.String(), + ) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 0)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(1_000_000) + }, + expFail: false, + }, + { + name: "Other msgs must pay gas fee", + simulation: false, + checkTx: true, + mallate: func() { + msg := stakingtypes.NewMsgDelegate( + addr1, + sdk.ValAddress(addr1), + sdk.NewCoin(core.MicroLunaDenom, math.NewInt(100000)), + ) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 0)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(1_000_000) + }, + expFail: true, + expErrMsg: "can't find coin that matches", + }, + { + name: "Oracle zero fee", + simulation: false, + checkTx: true, + mallate: func() { + val, err := stakingtypes.NewValidator(sdk.ValAddress(addr1), priv1.PubKey(), stakingtypes.Description{}) + s.Require().NoError(err) + + msg := oracletypes.NewMsgAggregateExchangeRatePrevote( + oracletypes.GetAggregateVoteHash("salt", "exchange rates", val.GetOperator()), + addr1, + val.GetOperator(), + ) + s.Require().NoError(s.txBuilder.SetMsgs(msg)) + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, 0)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(1_000_000) + }, + expFail: false, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + tc.mallate() + s.ctx = s.app.BaseApp.NewContext(tc.checkTx, tmproto.Header{}) + + _, err = antehandler(s.ctx, tx, tc.simulation) + + if tc.expFail { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.expErrMsg) + } else { + s.Require().NoError(err) + } + }) + } +} + +func (s *AnteTestSuite) TestTaxExemption() { + // keys and addresses + var privs []cryptotypes.PrivKey + var addrs []sdk.AccAddress + + // 0, 1: exemption + // 2, 3: normal + for i := 0; i < 4; i++ { + priv, _, addr := testdata.KeyTestPubAddr() + privs = append(privs, priv) + addrs = append(addrs, addr) + } + + // set send amount + sendAmt := int64(1000000) + sendCoin := sdk.NewInt64Coin(core.MicroLunaDenom, sendAmt) + feeAmt := int64(1000) + + cases := []struct { + name string + msgSigner cryptotypes.PrivKey + msgCreator func() []sdk.Msg + minFeeAmount int64 + gasLimit uint64 + }{ + { + name: "MsgSend(exemption -> exemption)", + msgSigner: privs[0], + msgCreator: func() []sdk.Msg { + var msgs []sdk.Msg + + msg1 := banktypes.NewMsgSend(addrs[0], addrs[1], sdk.NewCoins(sendCoin)) + msgs = append(msgs, msg1) + + return msgs + }, + // 263241*28.325 = 7456302 - only gas fee + minFeeAmount: 7456302, + gasLimit: 263241, + }, { + name: "MsgSend(normal -> normal)", + msgSigner: privs[2], + msgCreator: func() []sdk.Msg { + var msgs []sdk.Msg + + msg1 := banktypes.NewMsgSend(addrs[2], addrs[3], sdk.NewCoins(sendCoin)) + msgs = append(msgs, msg1) + + return msgs + }, + // tax this one hence burn amount is fee amount + // gasLimit * 28.325 = 8497500 + minFeeAmount: 8497500 + feeAmt, + }, { + name: "MsgExec(MsgSend(normal -> normal))", + msgSigner: privs[2], + msgCreator: func() []sdk.Msg { + var msgs []sdk.Msg + + msg1 := authz.NewMsgExec(addrs[1], []sdk.Msg{banktypes.NewMsgSend(addrs[2], addrs[3], sdk.NewCoins(sendCoin))}) + msgs = append(msgs, &msg1) + + return msgs + }, + // tax this one hence burn amount is fee amount + // gasLimit * 28.325 = 8497500 + minFeeAmount: 8497500 + feeAmt, + }, { + name: "MsgSend(exemption -> normal), MsgSend(exemption -> exemption)", + msgSigner: privs[0], + msgCreator: func() []sdk.Msg { + var msgs []sdk.Msg + + msg1 := banktypes.NewMsgSend(addrs[0], addrs[2], sdk.NewCoins(sendCoin)) + msgs = append(msgs, msg1) + msg2 := banktypes.NewMsgSend(addrs[0], addrs[1], sdk.NewCoins(sendCoin)) + msgs = append(msgs, msg2) + + return msgs + }, + // tax this one hence burn amount is fee amount + // gasLimit * 28.325 = 8497500 + minFeeAmount: 8497500 + feeAmt, + }, { + name: "MsgSend(exemption -> exemption), MsgMultiSend(exemption -> normal, exemption -> exemption)", + msgSigner: privs[0], + msgCreator: func() []sdk.Msg { + var msgs []sdk.Msg + + msg1 := banktypes.NewMsgSend(addrs[0], addrs[1], sdk.NewCoins(sendCoin)) + msgs = append(msgs, msg1) + msg2 := banktypes.NewMsgMultiSend( + []banktypes.Input{ + { + Address: addrs[0].String(), + Coins: sdk.NewCoins(sendCoin), + }, + { + Address: addrs[0].String(), + Coins: sdk.NewCoins(sendCoin), + }, + }, + []banktypes.Output{ + { + Address: addrs[2].String(), + Coins: sdk.NewCoins(sendCoin), + }, + { + Address: addrs[1].String(), + Coins: sdk.NewCoins(sendCoin), + }, + }, + ) + msgs = append(msgs, msg2) + + return msgs + }, + // gasLimit * 28.325 = 8497500 + minFeeAmount: 8497500 + feeAmt*2, + }, { + name: "MsgExecuteContract(exemption), MsgExecuteContract(normal)", + msgSigner: privs[3], + msgCreator: func() []sdk.Msg { + sendAmount := int64(1000000) + sendCoins := sdk.NewCoins(sdk.NewInt64Coin(core.MicroSDRDenom, sendAmount)) + // get wasm code for wasm contract create and instantiate + wasmCode, err := os.ReadFile("./testdata/hackatom.wasm") + s.Require().NoError(err) + per := wasmkeeper.NewDefaultPermissionKeeper(s.app.WasmKeeper) + // set wasm default params + s.app.WasmKeeper.SetParams(s.ctx, wasmtypes.DefaultParams()) + // wasm create + CodeID, _, err := per.Create(s.ctx, addrs[0], wasmCode, nil) + s.Require().NoError(err) + // params for contract init + r := wasmkeeper.HackatomExampleInitMsg{Verifier: addrs[0], Beneficiary: addrs[0]} + bz, err := json.Marshal(r) + s.Require().NoError(err) + // change block time for contract instantiate + s.ctx = s.ctx.WithBlockTime(time.Date(2020, time.April, 22, 12, 0, 0, 0, time.UTC)) + // instantiate contract then set the contract address to tax exemption + addr, _, err := per.Instantiate(s.ctx, CodeID, addrs[0], nil, bz, "my label", nil) + s.Require().NoError(err) + s.app.TreasuryKeeper.AddBurnTaxExemptionAddress(s.ctx, addr.String()) + // instantiate contract then not set to tax exemption + addr1, _, err := per.Instantiate(s.ctx, CodeID, addrs[0], nil, bz, "my label", nil) + s.Require().NoError(err) + + var msgs []sdk.Msg + // msg and signatures + msg1 := &wasmtypes.MsgExecuteContract{ + Sender: addrs[0].String(), + Contract: addr.String(), + Msg: []byte{}, + Funds: sendCoins, + } + msgs = append(msgs, msg1) + + msg2 := &wasmtypes.MsgExecuteContract{ + Sender: addrs[3].String(), + Contract: addr1.String(), + Msg: []byte{}, + Funds: sendCoins, + } + msgs = append(msgs, msg2) + return msgs + }, + // gasLimit*28.325 = 33990000 + minFeeAmount: 33990000 + feeAmt, + gasLimit: 1200000, + }, + } + + // there should be no coin in burn module + for _, c := range cases { + s.SetupTest(true) // setup + require := s.Require() + tk := s.app.TreasuryKeeper + burnSplitRate := sdk.NewDecWithPrec(5, 1) + + // Set burn split rate to 50% + tk.SetBurnSplitRate(s.ctx, burnSplitRate) + + fmt.Printf("CASE = %s \n", c.name) + s.txBuilder = s.clientCtx.TxConfig.NewTxBuilder() + + tk.AddBurnTaxExemptionAddress(s.ctx, addrs[0].String()) + tk.AddBurnTaxExemptionAddress(s.ctx, addrs[1].String()) + + mfd := ante.NewFeeDecorator(s.app.AccountKeeper, s.app.BankKeeper, s.app.FeeGrantKeeper, s.app.TreasuryKeeper, s.app.Tax2gasKeeper) + antehandler := sdk.ChainAnteDecorators(mfd) + + for i := 0; i < 4; i++ { + coins := sdk.NewCoins(sdk.NewCoin(core.MicroLunaDenom, sdk.NewInt(100000000000))) + testutil.FundAccount(s.app.BankKeeper, s.ctx, addrs[i], coins) + } + + // msg and signatures + feeAmount := sdk.NewCoins(sdk.NewInt64Coin(core.MicroLunaDenom, c.minFeeAmount)) + gasLimit := uint64(300000) + if c.gasLimit != 0 { + gasLimit = c.gasLimit + } + require.NoError(s.txBuilder.SetMsgs(c.msgCreator()...)) + s.txBuilder.SetFeeAmount(feeAmount) + s.txBuilder.SetGasLimit(gasLimit) + + privs, accNums, accSeqs := []cryptotypes.PrivKey{c.msgSigner}, []uint64{0}, []uint64{0} + tx, err := s.CreateTestTx(privs, accNums, accSeqs, s.ctx.ChainID()) + require.NoError(err) + + _, err = antehandler(s.ctx, tx, false) + require.NoError(err) + } +} diff --git a/custom/auth/ante/testdata/hackatom.wasm b/x/tax2gas/ante/testdata/hackatom.wasm similarity index 100% rename from custom/auth/ante/testdata/hackatom.wasm rename to x/tax2gas/ante/testdata/hackatom.wasm diff --git a/x/tax2gas/client/cli/query.go b/x/tax2gas/client/cli/query.go new file mode 100644 index 000000000..f3e697795 --- /dev/null +++ b/x/tax2gas/client/cli/query.go @@ -0,0 +1,89 @@ +package cli + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +// GetQueryCmd returns the cli query commands for this module +func GetQueryCmd() *cobra.Command { + tax2gasQueryCmd := &cobra.Command{ + Use: types.ModuleName, + Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName), + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + tax2gasQueryCmd.AddCommand( + GetCmdQueryParams(), + GetCmdBurnTaxRate(), + ) + + return tax2gasQueryCmd +} + +// GetCmdQueryParams implements a command to return the current parameters. +func GetCmdQueryParams() *cobra.Command { + cmd := &cobra.Command{ + Use: "params", + Short: "Query the current tax2gas module parameters", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + + params := &types.QueryParamsRequest{} + + res, err := queryClient.Params(context.Background(), params) + if err != nil { + return err + } + + return clientCtx.PrintProto(&res.Params) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +// GetCmdBurnTaxRate implements a command to return the current burn tax rate. +func GetCmdBurnTaxRate() *cobra.Command { + cmd := &cobra.Command{ + Use: "burn-tax-rate", + Short: "Query the current burn tax rate", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := types.NewQueryClient(clientCtx) + + burnTaxRate := &types.QueryBurnTaxRateRequest{} + + res, err := queryClient.BurnTaxRate(context.Background(), burnTaxRate) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/x/tax2gas/client/cli/tx.go b/x/tax2gas/client/cli/tx.go new file mode 100644 index 000000000..53254c471 --- /dev/null +++ b/x/tax2gas/client/cli/tx.go @@ -0,0 +1,21 @@ +package cli + +import ( + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +// NewTxCmd returns a root CLI command handler for certain modules transaction commands. +func NewTxCmd() *cobra.Command { + txCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "tax2gas subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + return txCmd +} diff --git a/x/tax2gas/exported/alias.go b/x/tax2gas/exported/alias.go new file mode 100644 index 000000000..7f10c1602 --- /dev/null +++ b/x/tax2gas/exported/alias.go @@ -0,0 +1,12 @@ +// DONTCOVER +package exported + +import ( + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +var NewQueryClient = types.NewQueryClient + +type ( + QueryBurnTaxRateRequest = types.QueryBurnTaxRateRequest +) diff --git a/x/tax2gas/genesis.go b/x/tax2gas/genesis.go new file mode 100644 index 000000000..0ef46e9f7 --- /dev/null +++ b/x/tax2gas/genesis.go @@ -0,0 +1,14 @@ +package module + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/classic-terra/core/v3/x/tax2gas/keeper" + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +// InitGenesis initializes default parameters +// and the keeper's address to pubkey map +func InitGenesis(ctx sdk.Context, keeper keeper.Keeper, data *types.GenesisState) { + keeper.SetParams(ctx, data.Params) +} diff --git a/x/tax2gas/keeper/keeper.go b/x/tax2gas/keeper/keeper.go new file mode 100644 index 000000000..45db46e59 --- /dev/null +++ b/x/tax2gas/keeper/keeper.go @@ -0,0 +1,83 @@ +package keeper + +import ( + "fmt" + + "github.com/cometbft/cometbft/libs/log" + + "github.com/cosmos/cosmos-sdk/codec" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +type Keeper struct { + storeKey storetypes.StoreKey + cdc codec.BinaryCodec + + // the address capable of executing a MsgUpdateParams message. Typically, this + // should be the x/gov module account. + authority string +} + +func NewKeeper( + cdc codec.BinaryCodec, + storeKey storetypes.StoreKey, + authority string, +) Keeper { + if _, err := sdk.AccAddressFromBech32(authority); err != nil { + panic(fmt.Errorf("invalid bank authority address: %w", err)) + } + + return Keeper{cdc: cdc, storeKey: storeKey, authority: authority} +} + +// InitGenesis initializes the tax2gas module's state from a provided genesis +// state. +func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) { + if err := genState.Validate(); err != nil { + panic(err) + } + + k.SetParams(ctx, genState.Params) +} + +// ExportGenesis returns the tax2gas module's exported genesis. +func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { + return &types.GenesisState{ + Params: k.GetParams(ctx), + } +} + +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + +// GetAuthority returns the x/tax2gas module's authority. +func (k Keeper) GetAuthority() string { + return k.authority +} + +func (k Keeper) IsEnabled(ctx sdk.Context) bool { + return k.GetParams(ctx).Enabled +} + +func (k Keeper) GetGasPrices(ctx sdk.Context) sdk.DecCoins { + return k.GetParams(ctx).GasPrices.Sort() +} + +func (k Keeper) GetBurnTaxRate(ctx sdk.Context) sdk.Dec { + return k.GetParams(ctx).BurnTaxRate +} + +// GetBypassMinFeeMsgTypes gets the tax2gas module's BypassMinFeeMsgTypes. +func (k Keeper) GetBypassMinFeeMsgTypes(ctx sdk.Context) []string { + return k.GetParams(ctx).BypassMinFeeMsgTypes +} + +// GetBypassMinFeeMsgTypes gets the tax2gas module's BypassMinFeeMsgTypes. +func (k Keeper) GetMaxTotalBypassMinFeeMsgGasUsage(ctx sdk.Context) uint64 { + return k.GetParams(ctx).MaxTotalBypassMinFeeMsgGasUsage +} diff --git a/x/tax2gas/keeper/keeper_test.go b/x/tax2gas/keeper/keeper_test.go new file mode 100644 index 000000000..d0694d89e --- /dev/null +++ b/x/tax2gas/keeper/keeper_test.go @@ -0,0 +1,122 @@ +package keeper_test + +import ( + "testing" + + "github.com/classic-terra/core/v3/x/tax2gas/keeper" + "github.com/classic-terra/core/v3/x/tax2gas/types" + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/stretchr/testify/suite" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + tmtime "github.com/cometbft/cometbft/types/time" + "github.com/cosmos/cosmos-sdk/baseapp" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" +) + +type KeeperTestSuite struct { + suite.Suite + + ctx sdk.Context + keeper keeper.Keeper + + queryClient types.QueryClient + msgServer types.MsgServer + + encCfg moduletestutil.TestEncodingConfig +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} + +func (suite *KeeperTestSuite) SetupTest() { + key := sdk.NewKVStoreKey(types.StoreKey) + testCtx := testutil.DefaultContextWithDB(suite.T(), key, sdk.NewTransientStoreKey("transient_test")) + ctx := testCtx.Ctx.WithBlockHeader(tmproto.Header{Time: tmtime.Now()}) + encCfg := moduletestutil.MakeTestEncodingConfig() + + // gomock initializations + + suite.ctx = ctx + suite.keeper = keeper.NewKeeper( + encCfg.Codec, + key, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) + suite.keeper.SetParams(suite.ctx, types.DefaultParams()) + + types.RegisterInterfaces(encCfg.InterfaceRegistry) + + queryHelper := baseapp.NewQueryServerTestHelper(ctx, encCfg.InterfaceRegistry) + types.RegisterQueryServer(queryHelper, suite.keeper) + queryClient := types.NewQueryClient(queryHelper) + + suite.queryClient = queryClient + suite.msgServer = keeper.NewMsgServerImpl(suite.keeper) + suite.encCfg = encCfg +} + +func (suite *KeeperTestSuite) TestGetAuthority() { + NewKeeperWithAuthority := func(authority string) keeper.Keeper { + return keeper.NewKeeper( + moduletestutil.MakeTestEncodingConfig().Codec, + sdk.NewKVStoreKey(types.StoreKey), + authority, + ) + } + + tests := map[string]string{ + "some random account": "cosmos139f7kncmglres2nf3h4hc4tade85ekfr8sulz5", + "gov module account": authtypes.NewModuleAddress(govtypes.ModuleName).String(), + "another module account": authtypes.NewModuleAddress(minttypes.ModuleName).String(), + } + + for name, expected := range tests { + suite.T().Run(name, func(t *testing.T) { + kpr := NewKeeperWithAuthority(expected) + actual := kpr.GetAuthority() + suite.Require().Equal(expected, actual) + }) + } +} + +func (suite *KeeperTestSuite) TestSetParams() { + ctx, tax2gasKeeper := suite.ctx, suite.keeper + require := suite.Require() + + tax2gasKeeper.SetParams(ctx, types.DefaultParams()) + tests := []struct { + name string + params types.Params + expFail bool + }{ + { + name: "empty gas prices", + params: types.Params{ + Enabled: true, + }, + expFail: true, + }, + { + name: "default params", + params: types.DefaultParams(), + expFail: false, + }, + } + + for _, tc := range tests { + suite.T().Run(tc.name, func(t *testing.T) { + err := tax2gasKeeper.SetParams(ctx, tc.params) + if tc.expFail { + require.Error(err) + } else { + require.NoError(err) + } + }) + } +} diff --git a/x/tax2gas/keeper/msg_server.go b/x/tax2gas/keeper/msg_server.go new file mode 100644 index 000000000..2bfff7a50 --- /dev/null +++ b/x/tax2gas/keeper/msg_server.go @@ -0,0 +1,36 @@ +package keeper + +import ( + "context" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +type msgServer struct { + Keeper +} + +var _ types.MsgServer = msgServer{} + +// NewMsgServerImpl returns an implementation of the tax2gas MsgServer interface +// for the provided Keeper. +func NewMsgServerImpl(keeper Keeper) types.MsgServer { + return &msgServer{Keeper: keeper} +} + +func (k msgServer) UpdateParams(goCtx context.Context, req *types.MsgUpdateParams) (*types.MsgUpdateParamsResponse, error) { + if k.GetAuthority() != req.Authority { + return nil, errorsmod.Wrapf(govtypes.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.GetAuthority(), req.Authority) + } + + ctx := sdk.UnwrapSDKContext(goCtx) + if err := k.SetParams(ctx, req.Params); err != nil { + return nil, err + } + + return &types.MsgUpdateParamsResponse{}, nil +} diff --git a/x/tax2gas/keeper/msg_server_test.go b/x/tax2gas/keeper/msg_server_test.go new file mode 100644 index 000000000..6867de094 --- /dev/null +++ b/x/tax2gas/keeper/msg_server_test.go @@ -0,0 +1,60 @@ +package keeper_test + +import ( + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +func (suite *KeeperTestSuite) TestMsgUpdateParams() { + // default params + params := types.DefaultParams() + + testCases := []struct { + name string + input *types.MsgUpdateParams + expErr bool + expErrMsg string + }{ + { + name: "invalid authority", + input: &types.MsgUpdateParams{ + Authority: "invalid", + Params: params, + }, + expErr: true, + expErrMsg: "invalid authority", + }, + { + name: "empty gas prices", + input: &types.MsgUpdateParams{ + Authority: suite.keeper.GetAuthority(), + Params: types.Params{ + Enabled: true, + }, + }, + expErr: true, + expErrMsg: "must provide at least 1 gas prices", + }, + { + name: "all good", + input: &types.MsgUpdateParams{ + Authority: suite.keeper.GetAuthority(), + Params: params, + }, + expErr: false, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + _, err := suite.msgServer.UpdateParams(suite.ctx, tc.input) + + if tc.expErr { + suite.Require().Error(err) + suite.Require().Contains(err.Error(), tc.expErrMsg) + } else { + suite.Require().NoError(err) + } + }) + } +} diff --git a/x/tax2gas/keeper/params.go b/x/tax2gas/keeper/params.go new file mode 100644 index 000000000..d36ab16e1 --- /dev/null +++ b/x/tax2gas/keeper/params.go @@ -0,0 +1,34 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +// SetParams sets the tax2gas module's parameters. +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) error { + if err := params.Validate(); err != nil { + return err + } + store := ctx.KVStore(k.storeKey) + bz, err := k.cdc.Marshal(¶ms) + if err != nil { + return err + } + store.Set(types.ParamsKey, bz) + + return nil +} + +// GetParams gets the tax2gas module's parameters. +func (k Keeper) GetParams(clientCtx sdk.Context) (params types.Params) { + store := clientCtx.KVStore(k.storeKey) + bz := store.Get(types.ParamsKey) + if bz == nil { + return params + } + + k.cdc.MustUnmarshal(bz, ¶ms) + return params +} diff --git a/x/tax2gas/keeper/querier.go b/x/tax2gas/keeper/querier.go new file mode 100644 index 000000000..b475751c2 --- /dev/null +++ b/x/tax2gas/keeper/querier.go @@ -0,0 +1,23 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +var _ types.QueryServer = Keeper{} + +// Params queries params of tax2gas module +func (k Keeper) Params(c context.Context, _ *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + return &types.QueryParamsResponse{Params: k.GetParams(ctx)}, nil +} + +// BurnTaxRate queries burn tax rate of tax2gas module +func (k Keeper) BurnTaxRate(c context.Context, _ *types.QueryBurnTaxRateRequest) (*types.QueryBurnTaxRateResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + return &types.QueryBurnTaxRateResponse{BurnTaxRate: k.GetBurnTaxRate(ctx)}, nil +} diff --git a/x/tax2gas/keeper/querier_test.go b/x/tax2gas/keeper/querier_test.go new file mode 100644 index 000000000..d4d3d04b1 --- /dev/null +++ b/x/tax2gas/keeper/querier_test.go @@ -0,0 +1,14 @@ +package keeper_test + +import ( + "context" + + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +func (suite *KeeperTestSuite) TestQueryParams() { + res, err := suite.queryClient.Params(context.Background(), &types.QueryParamsRequest{}) + suite.Require().NoError(err) + suite.Require().NotNil(res) + suite.Require().Equal(suite.keeper.GetParams(suite.ctx), res.GetParams()) +} diff --git a/x/tax2gas/module.go b/x/tax2gas/module.go new file mode 100644 index 000000000..e34c9764b --- /dev/null +++ b/x/tax2gas/module.go @@ -0,0 +1,122 @@ +package module + +import ( + "context" + "encoding/json" + "fmt" + + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + + "github.com/classic-terra/core/v3/x/tax2gas/client/cli" + "github.com/classic-terra/core/v3/x/tax2gas/keeper" + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +type AppModuleBasic struct { + cdc codec.Codec +} + +func (AppModuleBasic) Name() string { return types.ModuleName } + +func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + types.RegisterLegacyAminoCodec(cdc) +} + +func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(types.DefaultGenesis()) +} + +// ValidateGenesis performs genesis state validation for the tax2gas module. +func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error { + var genState types.GenesisState + if err := cdc.UnmarshalJSON(bz, &genState); err != nil { + return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) + } + return genState.Validate() +} + +// --------------------------------------- +// Interfaces. +func (b AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { + if err := types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)); err != nil { + panic(err) + } +} + +func (b AppModuleBasic) GetTxCmd() *cobra.Command { + return cli.NewTxCmd() +} + +func (b AppModuleBasic) GetQueryCmd() *cobra.Command { + return cli.GetQueryCmd() +} + +// RegisterInterfaces registers interfaces and implementations of the tax2gas module. +func (AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) { + types.RegisterInterfaces(registry) +} + +type AppModule struct { + AppModuleBasic + + k keeper.Keeper +} + +func (am AppModule) RegisterServices(cfg module.Configurator) { + types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.k)) + // queryproto.RegisterQueryServer(cfg.QueryServer(), grpc.Querier{Q: module.NewQuerier(am.k)}) + types.RegisterQueryServer(cfg.QueryServer(), am.k) +} + +func NewAppModule(cdc codec.Codec, tax2gasKeeper keeper.Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{cdc}, + k: tax2gasKeeper, + } +} + +func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) { +} + +// QuerierRoute returns the tax2gas module's querier route name. +func (AppModule) QuerierRoute() string { return types.RouterKey } + +// InitGenesis performs genesis initialization for the tax2gas module. +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, gs json.RawMessage) []abci.ValidatorUpdate { + var genesisState types.GenesisState + + cdc.MustUnmarshalJSON(gs, &genesisState) + InitGenesis(ctx, am.k, &genesisState) + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the exported genesis state as raw bytes for the tax2gas. +// module. +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + genState := am.k.ExportGenesis(ctx) + return cdc.MustMarshalJSON(genState) +} + +// BeginBlock performs TODO. +func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// EndBlock performs TODO. +func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +// ConsensusVersion implements AppModule/ConsensusVersion. +func (AppModule) ConsensusVersion() uint64 { return 1 } diff --git a/custom/auth/ante/fee_burntax.go b/x/tax2gas/post/burntax.go similarity index 94% rename from custom/auth/ante/fee_burntax.go rename to x/tax2gas/post/burntax.go index e69f2448b..c59381112 100644 --- a/custom/auth/ante/fee_burntax.go +++ b/x/tax2gas/post/burntax.go @@ -1,4 +1,4 @@ -package ante +package post import ( errorsmod "cosmossdk.io/errors" @@ -12,7 +12,7 @@ import ( ) // BurnTaxSplit splits -func (fd FeeDecorator) BurnTaxSplit(ctx sdk.Context, taxes sdk.Coins) (err error) { +func (fd Tax2gasPostDecorator) BurnTaxSplit(ctx sdk.Context, taxes sdk.Coins) (err error) { burnSplitRate := fd.treasuryKeeper.GetBurnSplitRate(ctx) oracleSplitRate := fd.treasuryKeeper.GetOracleSplitRate(ctx) communityTax := fd.distrKeeper.GetCommunityTax(ctx) @@ -90,5 +90,7 @@ func (fd FeeDecorator) BurnTaxSplit(ctx sdk.Context, taxes sdk.Coins) (err error } } + // Record tax proceeds + fd.treasuryKeeper.RecordEpochTaxProceeds(ctx, taxes) return nil } diff --git a/x/tax2gas/post/post.go b/x/tax2gas/post/post.go new file mode 100644 index 000000000..8435ecfe7 --- /dev/null +++ b/x/tax2gas/post/post.go @@ -0,0 +1,173 @@ +package post + +import ( + sdkmath "cosmossdk.io/math" + + errorsmod "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth/ante" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + + tax2gasKeeper "github.com/classic-terra/core/v3/x/tax2gas/keeper" + "github.com/classic-terra/core/v3/x/tax2gas/types" + tax2gasutils "github.com/classic-terra/core/v3/x/tax2gas/utils" +) + +type Tax2gasPostDecorator struct { + accountKeeper ante.AccountKeeper + bankKeeper types.BankKeeper + feegrantKeeper types.FeegrantKeeper + treasuryKeeper types.TreasuryKeeper + distrKeeper types.DistrKeeper + tax2gasKeeper tax2gasKeeper.Keeper +} + +func NewTax2GasPostDecorator(accountKeeper ante.AccountKeeper, bankKeeper types.BankKeeper, feegrantKeeper types.FeegrantKeeper, treasuryKeeper types.TreasuryKeeper, distrKeeper types.DistrKeeper, tax2gasKeeper tax2gasKeeper.Keeper) Tax2gasPostDecorator { + return Tax2gasPostDecorator{ + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + feegrantKeeper: feegrantKeeper, + treasuryKeeper: treasuryKeeper, + distrKeeper: distrKeeper, + tax2gasKeeper: tax2gasKeeper, + } +} + +func (tgd Tax2gasPostDecorator) PostHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, success bool, next sdk.PostHandler) (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") + } + msgs := feeTx.GetMsgs() + if tax2gasutils.IsOracleTx(msgs) || !tgd.tax2gasKeeper.IsEnabled(ctx) { + return next(ctx, tx, simulate, success) + } + + feeCoins := feeTx.GetFee() + anteConsumedGas, ok := ctx.Value(types.AnteConsumedGas).(uint64) + if !simulate && !ok { + // If cannot found the anteConsumedGas, that's mean the tx is bypass + // Skip this tx as it's bypass + return next(ctx, tx, simulate, success) + } + + // Get paid denom identified in ante handler + paidDenom, ok := ctx.Value(types.PaidDenom).(string) + if !simulate && !ok { + // If cannot found the paidDenom, that's mean this is the init genesis tx + // Skip this tx as it's init genesis tx + return next(ctx, tx, simulate, success) + } + + gasPrices, ok := ctx.Value(types.FinalGasPrices).(sdk.DecCoins) + if !ok { + gasPrices = tgd.tax2gasKeeper.GetGasPrices(ctx) + } + + found, paidDenomGasPrice := tax2gasutils.GetGasPriceByDenom(gasPrices, paidDenom) + if !simulate && !found { + return ctx, types.ErrDenomNotFound + } + paidAmount := paidDenomGasPrice.Mul(sdk.NewDec(int64(anteConsumedGas))) + + if !simulate { + // Deduct feeCoins with paid amount + feeCoins = feeCoins.Sub(sdk.NewCoin(paidDenom, paidAmount.Ceil().RoundInt())) + } + + taxGas := ctx.TaxGasMeter().GasConsumed() + + // 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 + // we need to add the --fee flag manually + totalGasConsumed := ctx.GasMeter().GasConsumed() + + if taxGas.IsUint64() { + taxGasUint64 := taxGas.Uint64() + // Check if gas not overflow + if totalGasConsumed+taxGasUint64 >= totalGasConsumed && totalGasConsumed+taxGasUint64 >= taxGasUint64 { + if simulate { + ctx.GasMeter().ConsumeGas(taxGasUint64, "consume tax gas") + } + } + } + + // Deduct the gas consumed amount spent on ante handler + totalGasRemaining := sdkmath.NewInt(int64(totalGasConsumed - anteConsumedGas)).Add(taxGas) + + feePayer := feeTx.FeePayer() + feeGranter := feeTx.FeeGranter() + + // if feegranter set deduct fee from feegranter account. + // this works with only when feegrant enabled. + if feeGranter != nil { + allowance, err := tgd.feegrantKeeper.GetAllowance(ctx, feeGranter, feePayer) + if err != nil { + return ctx, errorsmod.Wrapf(err, "fee-grant not found with granter %s and grantee %s", feeGranter, feePayer) + } + + gasRemainingFees, err := tax2gasutils.ComputeFeesOnGasConsumed(tx, gasPrices, totalGasRemaining) + if err != nil { + return ctx, err + } + + // For this tx, we only accept to pay by one denom + for _, feeRequired := range gasRemainingFees { + _, err := allowance.Accept(ctx, sdk.NewCoins(feeRequired), feeTx.GetMsgs()) + if err == nil { + err = tgd.feegrantKeeper.UseGrantedFees(ctx, feeGranter, feePayer, sdk.NewCoins(feeRequired), feeTx.GetMsgs()) + if err != nil { + return ctx, errorsmod.Wrapf(err, "%s does not allow to pay fees for %s", feeGranter, feePayer) + } + feeGranter := tgd.accountKeeper.GetAccount(ctx, feeGranter) + err = tgd.bankKeeper.SendCoinsFromAccountToModule(ctx, feeGranter.GetAddress(), authtypes.FeeCollectorName, sdk.NewCoins(feeRequired)) + if err != nil { + return ctx, errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds, err.Error()) + } + + // Calculate tax fee and BurnTaxSplit + _, gasPrice := tax2gasutils.GetGasPriceByDenom(gasPrices, feeRequired.Denom) + taxFee := gasPrice.MulInt(taxGas).Ceil().RoundInt() + + err := tgd.BurnTaxSplit(ctx, sdk.NewCoins(sdk.NewCoin(feeRequired.Denom, taxFee))) + if err != nil { + return ctx, err + } + return next(ctx, tx, simulate, success) + } + } + return ctx, errorsmod.Wrapf(err, "%s does not allow to pay fees for %s", feeGranter, feePayer) + } + + // 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() { + 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) + + if !simulate && taxes.IsZero() { + payableFees = feeCoins + } + err := tgd.bankKeeper.SendCoinsFromAccountToModule(ctx, feePayerAccount.GetAddress(), authtypes.FeeCollectorName, payableFees) + if err != nil { + return ctx, errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds, err.Error()) + } + + err = tgd.BurnTaxSplit(ctx, taxes) + if err != nil { + return ctx, err + } + + return next(ctx, tx, simulate, success) +} diff --git a/x/tax2gas/types/codec.go b/x/tax2gas/types/codec.go new file mode 100644 index 000000000..cffb8f985 --- /dev/null +++ b/x/tax2gas/types/codec.go @@ -0,0 +1,39 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/msgservice" + authzcodec "github.com/cosmos/cosmos-sdk/x/authz/codec" +) + +// RegisterLegacyAminoCodec registers the necessary x/tax2gas interfaces and concrete types +// on the provided LegacyAmino codec. These types are used for Amino JSON serialization. +func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + cdc.RegisterConcrete(&MsgUpdateParams{}, "terra/tax2gas/MsgUpdateParams", nil) +} + +func RegisterInterfaces(registry types.InterfaceRegistry) { + registry.RegisterImplementations( + (*sdk.Msg)(nil), + &MsgUpdateParams{}, + ) + + msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) +} + +var ( + amino = codec.NewLegacyAmino() + ModuleCdc = codec.NewAminoCodec(amino) +) + +func init() { + RegisterLegacyAminoCodec(amino) + // Register all Amino interfaces and concrete types on the authz Amino codec so that this can later be + // used to properly serialize MsgGrant and MsgExec instances + sdk.RegisterLegacyAminoCodec(amino) + RegisterLegacyAminoCodec(authzcodec.Amino) + + amino.Seal() +} diff --git a/x/tax2gas/types/errors.go b/x/tax2gas/types/errors.go new file mode 100644 index 000000000..501df004d --- /dev/null +++ b/x/tax2gas/types/errors.go @@ -0,0 +1,12 @@ +package types + +import ( + errorsmod "cosmossdk.io/errors" +) + +// Tax2Gas errors +var ( + ErrParsing = errorsmod.Register(ModuleName, 1, "Parsing errors") + ErrCoinNotFound = errorsmod.Register(ModuleName, 2, "Coin not found") + ErrDenomNotFound = errorsmod.Register(ModuleName, 3, "Denom not found") +) diff --git a/x/tax2gas/types/events.go b/x/tax2gas/types/events.go new file mode 100644 index 000000000..885a885a7 --- /dev/null +++ b/x/tax2gas/types/events.go @@ -0,0 +1,5 @@ +package types + +const ( + AttributeValueCategory = ModuleName +) diff --git a/x/tax2gas/types/expected_keeper.go b/x/tax2gas/types/expected_keeper.go new file mode 100644 index 000000000..2b6e805c6 --- /dev/null +++ b/x/tax2gas/types/expected_keeper.go @@ -0,0 +1,42 @@ +package types + +import ( + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/cosmos/cosmos-sdk/x/feegrant" +) + +// TreasuryKeeper for tax charging & recording +type TreasuryKeeper interface { + RecordEpochTaxProceeds(ctx sdk.Context, delta sdk.Coins) + GetTaxRate(ctx sdk.Context) (taxRate sdk.Dec) + GetTaxCap(ctx sdk.Context, denom string) (taxCap math.Int) + GetBurnSplitRate(ctx sdk.Context) sdk.Dec + HasBurnTaxExemptionAddress(ctx sdk.Context, addresses ...string) bool + HasBurnTaxExemptionContract(ctx sdk.Context, address string) bool + GetMinInitialDepositRatio(ctx sdk.Context) sdk.Dec + GetOracleSplitRate(ctx sdk.Context) sdk.Dec +} + +// BankKeeper defines the contract needed for supply related APIs (noalias) +type BankKeeper interface { + GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin + IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error + SendCoins(ctx sdk.Context, from, to sdk.AccAddress, amt sdk.Coins) error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + SendCoinsFromModuleToModule(ctx sdk.Context, senderModule string, recipientModule string, amt sdk.Coins) error + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error +} + +type FeegrantKeeper interface { + GetAllowance(ctx sdk.Context, granter, grantee sdk.AccAddress) (feegrant.FeeAllowanceI, error) + UseGrantedFees(ctx sdk.Context, granter, grantee sdk.AccAddress, fee sdk.Coins, msgs []sdk.Msg) error +} + +type DistrKeeper interface { + FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error + GetFeePool(ctx sdk.Context) distributiontypes.FeePool + GetCommunityTax(ctx sdk.Context) math.LegacyDec + SetFeePool(ctx sdk.Context, feePool distributiontypes.FeePool) +} diff --git a/x/tax2gas/types/genesis.go b/x/tax2gas/types/genesis.go new file mode 100644 index 000000000..964a1a3ea --- /dev/null +++ b/x/tax2gas/types/genesis.go @@ -0,0 +1,17 @@ +package types + +// DefaultGenesis returns the default tax2gas genesis state. +func DefaultGenesis() *GenesisState { + return &GenesisState{ + Params: DefaultParams(), + } +} + +// Validate performs basic genesis state validation returning an error upon any +// failure. +func (gs GenesisState) Validate() error { + if err := gs.Params.Validate(); err != nil { + return err + } + return nil +} diff --git a/x/tax2gas/types/genesis.pb.go b/x/tax2gas/types/genesis.pb.go new file mode 100644 index 000000000..0dedecbb5 --- /dev/null +++ b/x/tax2gas/types/genesis.pb.go @@ -0,0 +1,710 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: terra/tax2gas/v1beta1/genesis.proto + +package types + +import ( + fmt "fmt" + github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types" + types "github.com/cosmos/cosmos-sdk/types" + _ "github.com/cosmos/cosmos-sdk/types/tx/amino" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type Params struct { + GasPrices github_com_cosmos_cosmos_sdk_types.DecCoins `protobuf:"bytes,1,rep,name=gas_prices,json=gasPrices,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.DecCoins" json:"gas_prices" yaml:"gas_prices"` + BurnTaxRate github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,2,opt,name=burn_tax_rate,json=burnTaxRate,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"burn_tax_rate"` + Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"` + // bypass_min_fee_msg_types defines a list of message type urls + // that are free of fee charge. + BypassMinFeeMsgTypes []string `protobuf:"bytes,4,rep,name=bypass_min_fee_msg_types,json=bypassMinFeeMsgTypes,proto3" json:"bypass_min_fee_msg_types,omitempty" yaml:"bypass_min_fee_msg_types"` + // max_total_bypass_min_fee_msg_gas_usage defines the total maximum gas usage + // allowed for a transaction containing only messages of types in bypass_min_fee_msg_types + // to bypass fee charge. + MaxTotalBypassMinFeeMsgGasUsage uint64 `protobuf:"varint,5,opt,name=max_total_bypass_min_fee_msg_gas_usage,json=maxTotalBypassMinFeeMsgGasUsage,proto3" json:"max_total_bypass_min_fee_msg_gas_usage,omitempty"` +} + +func (m *Params) Reset() { *m = Params{} } +func (m *Params) String() string { return proto.CompactTextString(m) } +func (*Params) ProtoMessage() {} +func (*Params) Descriptor() ([]byte, []int) { + return fileDescriptor_589c4ef0e5113034, []int{0} +} +func (m *Params) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Params) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Params.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Params) XXX_Merge(src proto.Message) { + xxx_messageInfo_Params.Merge(m, src) +} +func (m *Params) XXX_Size() int { + return m.Size() +} +func (m *Params) XXX_DiscardUnknown() { + xxx_messageInfo_Params.DiscardUnknown(m) +} + +var xxx_messageInfo_Params proto.InternalMessageInfo + +func (m *Params) GetGasPrices() github_com_cosmos_cosmos_sdk_types.DecCoins { + if m != nil { + return m.GasPrices + } + return nil +} + +func (m *Params) GetEnabled() bool { + if m != nil { + return m.Enabled + } + return false +} + +func (m *Params) GetBypassMinFeeMsgTypes() []string { + if m != nil { + return m.BypassMinFeeMsgTypes + } + return nil +} + +func (m *Params) GetMaxTotalBypassMinFeeMsgGasUsage() uint64 { + if m != nil { + return m.MaxTotalBypassMinFeeMsgGasUsage + } + return 0 +} + +// GenesisState defines the tax2gas module's genesis state. +type GenesisState struct { + // params is the container of tax2gas parameters. + Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` +} + +func (m *GenesisState) Reset() { *m = GenesisState{} } +func (m *GenesisState) String() string { return proto.CompactTextString(m) } +func (*GenesisState) ProtoMessage() {} +func (*GenesisState) Descriptor() ([]byte, []int) { + return fileDescriptor_589c4ef0e5113034, []int{1} +} +func (m *GenesisState) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GenesisState) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GenesisState.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GenesisState) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenesisState.Merge(m, src) +} +func (m *GenesisState) XXX_Size() int { + return m.Size() +} +func (m *GenesisState) XXX_DiscardUnknown() { + xxx_messageInfo_GenesisState.DiscardUnknown(m) +} + +var xxx_messageInfo_GenesisState proto.InternalMessageInfo + +func (m *GenesisState) GetParams() Params { + if m != nil { + return m.Params + } + return Params{} +} + +func init() { + proto.RegisterType((*Params)(nil), "terra.tax2gas.v1beta1.Params") + proto.RegisterType((*GenesisState)(nil), "terra.tax2gas.v1beta1.GenesisState") +} + +func init() { + proto.RegisterFile("terra/tax2gas/v1beta1/genesis.proto", fileDescriptor_589c4ef0e5113034) +} + +var fileDescriptor_589c4ef0e5113034 = []byte{ + // 512 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0xbf, 0x6f, 0xd3, 0x40, + 0x14, 0xc7, 0x73, 0xa4, 0x04, 0xe2, 0xc0, 0x50, 0xab, 0x20, 0xab, 0x02, 0xdb, 0x32, 0x52, 0x65, + 0x15, 0x6a, 0xd3, 0x74, 0x2b, 0x9b, 0x41, 0x54, 0x80, 0x22, 0x2a, 0x13, 0x16, 0x16, 0xeb, 0xd9, + 0x3d, 0x8c, 0x45, 0xce, 0x67, 0xf9, 0x5d, 0x2a, 0x67, 0x64, 0xed, 0xc4, 0xc8, 0xc8, 0x88, 0x98, + 0xfa, 0x67, 0x74, 0xec, 0x88, 0x18, 0x02, 0x4a, 0x86, 0x4a, 0x8c, 0xdd, 0xd8, 0xd0, 0xf9, 0x9c, + 0x52, 0xa4, 0x56, 0x62, 0xf1, 0x8f, 0x7b, 0x9f, 0xef, 0xf7, 0x9e, 0xde, 0xfb, 0x6a, 0xf7, 0x04, + 0x2d, 0x4b, 0xf0, 0x05, 0x54, 0xfd, 0x14, 0xd0, 0xdf, 0xdf, 0x8c, 0xa9, 0x80, 0x4d, 0x3f, 0xa5, + 0x39, 0xc5, 0x0c, 0xbd, 0xa2, 0xe4, 0x82, 0xeb, 0xb7, 0x6a, 0xc8, 0x6b, 0x20, 0xaf, 0x81, 0x56, + 0x57, 0x52, 0x9e, 0xf2, 0x9a, 0xf0, 0xe5, 0x97, 0x82, 0x57, 0xcd, 0x84, 0x23, 0xe3, 0xe8, 0xc7, + 0x80, 0xf4, 0xcc, 0x2f, 0xe1, 0x59, 0xde, 0xd4, 0x97, 0x81, 0x65, 0x39, 0xf7, 0xeb, 0xa7, 0x3a, + 0x72, 0x7e, 0xb7, 0xb5, 0xce, 0x2e, 0x94, 0xc0, 0x50, 0x3f, 0x20, 0x9a, 0x96, 0x02, 0x46, 0x45, + 0x99, 0x25, 0x14, 0x0d, 0x62, 0xb7, 0xdd, 0x5e, 0xff, 0x8e, 0xa7, 0x3c, 0x3d, 0xe9, 0xb9, 0xb8, + 0xde, 0x7b, 0x42, 0x93, 0xc7, 0x3c, 0xcb, 0x83, 0xc1, 0xd1, 0xd4, 0x6a, 0x9d, 0x4e, 0xad, 0xe5, + 0x09, 0xb0, 0xd1, 0xb6, 0xf3, 0x57, 0xed, 0x7c, 0xfd, 0x61, 0xdd, 0x4f, 0x33, 0xf1, 0x6e, 0x1c, + 0x7b, 0x09, 0x67, 0x7e, 0xd3, 0x98, 0x7a, 0x6d, 0xe0, 0xde, 0x7b, 0x5f, 0x4c, 0x0a, 0x8a, 0x0b, + 0x23, 0xfc, 0x72, 0x72, 0xb8, 0x4e, 0xc2, 0x6e, 0x0a, 0xb8, 0x5b, 0xeb, 0xf5, 0x50, 0xbb, 0x19, + 0x8f, 0xcb, 0x3c, 0x12, 0x50, 0x45, 0x25, 0x08, 0x6a, 0x5c, 0xb1, 0x89, 0xdb, 0x0d, 0x3c, 0x79, + 0xe1, 0xf7, 0xa9, 0xb5, 0xf6, 0x7f, 0xde, 0x61, 0x4f, 0x9a, 0x0c, 0xa1, 0x0a, 0x41, 0x50, 0xdd, + 0xd0, 0xae, 0xd1, 0x1c, 0xe2, 0x11, 0xdd, 0x33, 0xda, 0x36, 0x71, 0xaf, 0x87, 0x8b, 0x5f, 0xfd, + 0x03, 0xd1, 0x8c, 0x78, 0x52, 0x00, 0x62, 0xc4, 0xb2, 0x3c, 0x7a, 0x4b, 0x69, 0xc4, 0x30, 0x8d, + 0x6a, 0x1f, 0x63, 0xc9, 0x6e, 0xbb, 0xdd, 0xe0, 0xd9, 0xaf, 0xa9, 0xe5, 0x5c, 0xc6, 0x3c, 0xe0, + 0x2c, 0x13, 0x94, 0x15, 0x62, 0x72, 0x3a, 0xb5, 0x2c, 0x35, 0x8c, 0xcb, 0x58, 0x27, 0x5c, 0x51, + 0xa5, 0x41, 0x96, 0x3f, 0xa5, 0x74, 0x80, 0xe9, 0x50, 0x1e, 0xeb, 0x2f, 0xb5, 0x35, 0x06, 0x55, + 0x24, 0xb8, 0x80, 0x51, 0x74, 0x81, 0x58, 0x0e, 0x77, 0x8c, 0x90, 0x52, 0xe3, 0xaa, 0x4d, 0xdc, + 0xa5, 0xd0, 0x62, 0x50, 0x0d, 0x25, 0x1c, 0xfc, 0xeb, 0xb6, 0x03, 0xf8, 0x5a, 0x62, 0xdb, 0xd6, + 0xa7, 0xcf, 0x16, 0x39, 0x38, 0x39, 0x5c, 0xbf, 0xad, 0x82, 0x56, 0x9d, 0x45, 0x4d, 0x2d, 0xdc, + 0x79, 0xa1, 0xdd, 0xd8, 0x51, 0x61, 0x7b, 0x25, 0xe4, 0x7c, 0x1e, 0x69, 0x9d, 0xa2, 0xae, 0x18, + 0xc4, 0x26, 0x6e, 0xaf, 0x7f, 0xd7, 0xbb, 0x30, 0x7c, 0x9e, 0x92, 0x07, 0x4b, 0x72, 0x17, 0x61, + 0x23, 0x09, 0x9e, 0x1f, 0xcd, 0x4c, 0x72, 0x3c, 0x33, 0xc9, 0xcf, 0x99, 0x49, 0x3e, 0xce, 0xcd, + 0xd6, 0xf1, 0xdc, 0x6c, 0x7d, 0x9b, 0x9b, 0xad, 0x37, 0x0f, 0xcf, 0xef, 0x6a, 0x04, 0x88, 0x59, + 0xb2, 0xa1, 0x3a, 0x4a, 0x78, 0x49, 0xfd, 0xfd, 0xad, 0x73, 0x9d, 0xd5, 0x13, 0x8a, 0x3b, 0x75, + 0x36, 0xb7, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0x0f, 0xa0, 0xf6, 0x80, 0x22, 0x03, 0x00, 0x00, +} + +func (m *Params) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Params) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.MaxTotalBypassMinFeeMsgGasUsage != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.MaxTotalBypassMinFeeMsgGasUsage)) + i-- + dAtA[i] = 0x28 + } + if len(m.BypassMinFeeMsgTypes) > 0 { + for iNdEx := len(m.BypassMinFeeMsgTypes) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.BypassMinFeeMsgTypes[iNdEx]) + copy(dAtA[i:], m.BypassMinFeeMsgTypes[iNdEx]) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.BypassMinFeeMsgTypes[iNdEx]))) + i-- + dAtA[i] = 0x22 + } + } + if m.Enabled { + i-- + if m.Enabled { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x18 + } + { + size := m.BurnTaxRate.Size() + i -= size + if _, err := m.BurnTaxRate.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + if len(m.GasPrices) > 0 { + for iNdEx := len(m.GasPrices) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.GasPrices[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *GenesisState) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GenesisState) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int { + offset -= sovGenesis(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *Params) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.GasPrices) > 0 { + for _, e := range m.GasPrices { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + l = m.BurnTaxRate.Size() + n += 1 + l + sovGenesis(uint64(l)) + if m.Enabled { + n += 2 + } + if len(m.BypassMinFeeMsgTypes) > 0 { + for _, s := range m.BypassMinFeeMsgTypes { + l = len(s) + n += 1 + l + sovGenesis(uint64(l)) + } + } + if m.MaxTotalBypassMinFeeMsgGasUsage != 0 { + n += 1 + sovGenesis(uint64(m.MaxTotalBypassMinFeeMsgGasUsage)) + } + return n +} + +func (m *GenesisState) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.Params.Size() + n += 1 + l + sovGenesis(uint64(l)) + return n +} + +func sovGenesis(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozGenesis(x uint64) (n int) { + return sovGenesis(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *Params) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Params: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Params: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field GasPrices", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.GasPrices = append(m.GasPrices, types.DecCoin{}) + if err := m.GasPrices[len(m.GasPrices)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BurnTaxRate", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.BurnTaxRate.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Enabled", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Enabled = bool(v != 0) + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BypassMinFeeMsgTypes", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.BypassMinFeeMsgTypes = append(m.BypassMinFeeMsgTypes, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field MaxTotalBypassMinFeeMsgGasUsage", wireType) + } + m.MaxTotalBypassMinFeeMsgGasUsage = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.MaxTotalBypassMinFeeMsgGasUsage |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GenesisState) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GenesisState: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Params", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Params.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipGenesis(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthGenesis + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupGenesis + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthGenesis + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthGenesis = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowGenesis = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupGenesis = fmt.Errorf("proto: unexpected end of group") +) diff --git a/x/tax2gas/types/keys.go b/x/tax2gas/types/keys.go new file mode 100644 index 000000000..ef679c3a2 --- /dev/null +++ b/x/tax2gas/types/keys.go @@ -0,0 +1,22 @@ +package types + +const ( + ModuleName = "tax2gas" + + StoreKey = ModuleName + + RouterKey = ModuleName + + AnteConsumedGas = "anteConsumedGas" + + TaxGas = "taxGas" + + FinalGasPrices = "finalGasPrices" + + PaidDenom = "paidDenom" +) + +// Key defines the store key for tax2gas. +var ( + ParamsKey = []byte{0x1} +) diff --git a/x/tax2gas/types/msgs.go b/x/tax2gas/types/msgs.go new file mode 100644 index 000000000..b0b3e5b48 --- /dev/null +++ b/x/tax2gas/types/msgs.go @@ -0,0 +1,26 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + TypeMsgUpdateParams = "update_params" +) + +var _ sdk.Msg = &MsgUpdateParams{} + +func (msg MsgUpdateParams) Route() string { return ModuleName } +func (msg MsgUpdateParams) Type() string { return TypeMsgUpdateParams } +func (msg MsgUpdateParams) ValidateBasic() error { + return msg.Params.Validate() +} + +func (msg MsgUpdateParams) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&msg)) +} + +func (msg MsgUpdateParams) GetSigners() []sdk.AccAddress { + authority, _ := sdk.AccAddressFromBech32(msg.Authority) + return []sdk.AccAddress{authority} +} diff --git a/x/tax2gas/types/params.go b/x/tax2gas/types/params.go new file mode 100644 index 000000000..f152d000a --- /dev/null +++ b/x/tax2gas/types/params.go @@ -0,0 +1,79 @@ +package types + +import ( + fmt "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + ibcclienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + ibcchanneltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" +) + +var ( + // DefaultMinGasPrices is set at runtime to the staking token with zero amount i.e. "0uatom" + // see DefaultZeroGlobalFee method in gaia/x/globalfee/ante/fee.go. + DefaultGasPrices = sdk.NewDecCoins( + sdk.NewDecCoinFromDec("uluna", sdk.NewDecWithPrec(28325, 3)), + sdk.NewDecCoinFromDec("usdr", sdk.NewDecWithPrec(52469, 5)), + sdk.NewDecCoinFromDec("uusd", sdk.NewDecWithPrec(75, 2)), + sdk.NewDecCoinFromDec("ukrw", sdk.NewDecWithPrec(850, 0)), + sdk.NewDecCoinFromDec("umnt", sdk.NewDecWithPrec(2142855, 3)), + sdk.NewDecCoinFromDec("ueur", sdk.NewDecWithPrec(625, 3)), + sdk.NewDecCoinFromDec("ucny", sdk.NewDecWithPrec(49, 1)), + sdk.NewDecCoinFromDec("ujpy", sdk.NewDecWithPrec(8185, 2)), + sdk.NewDecCoinFromDec("ugbp", sdk.NewDecWithPrec(55, 2)), + sdk.NewDecCoinFromDec("uinr", sdk.NewDecWithPrec(544, 1)), + sdk.NewDecCoinFromDec("ucad", sdk.NewDecWithPrec(95, 2)), + sdk.NewDecCoinFromDec("uchf", sdk.NewDecWithPrec(7, 1)), + sdk.NewDecCoinFromDec("uaud", sdk.NewDecWithPrec(95, 2)), + sdk.NewDecCoinFromDec("usgd", sdk.NewDec(1)), + sdk.NewDecCoinFromDec("uthb", sdk.NewDecWithPrec(231, 1)), + sdk.NewDecCoinFromDec("usek", sdk.NewDecWithPrec(625, 2)), + sdk.NewDecCoinFromDec("unok", sdk.NewDecWithPrec(625, 2)), + sdk.NewDecCoinFromDec("udkk", sdk.NewDecWithPrec(45, 1)), + sdk.NewDecCoinFromDec("uidr", sdk.NewDecWithPrec(10900, 0)), + sdk.NewDecCoinFromDec("uphp", sdk.NewDecWithPrec(38, 0)), + sdk.NewDecCoinFromDec("uhkd", sdk.NewDecWithPrec(585, 2)), + sdk.NewDecCoinFromDec("umyr", sdk.NewDecWithPrec(3, 0)), + sdk.NewDecCoinFromDec("utwd", sdk.NewDecWithPrec(20, 0)), + ) + DefaultBypassMinFeeMsgTypes = []string{ + sdk.MsgTypeURL(&ibcclienttypes.MsgUpdateClient{}), + sdk.MsgTypeURL(&ibcchanneltypes.MsgRecvPacket{}), + sdk.MsgTypeURL(&ibcchanneltypes.MsgAcknowledgement{}), + sdk.MsgTypeURL(&ibcchanneltypes.MsgTimeout{}), + sdk.MsgTypeURL(&ibcchanneltypes.MsgTimeoutOnClose{}), + } + + // maxTotalBypassMinFeeMsgGasUsage is the allowed maximum gas usage + // for all the bypass msgs in a transactions. + // A transaction that contains only bypass message types and the gas usage does not + // exceed maxTotalBypassMinFeeMsgGasUsage can be accepted with a zero fee. + // For details, see gaiafeeante.NewFeeDecorator() + DefaultmaxTotalBypassMinFeeMsgGasUsage uint64 = 1_000_000 +) + +func NewParams() Params { + return Params{} +} + +// DefaultParams are the default tax2gas module parameters. +func DefaultParams() Params { + return Params{ + GasPrices: DefaultGasPrices, + BurnTaxRate: sdk.NewDecWithPrec(5, 3), + Enabled: true, + BypassMinFeeMsgTypes: DefaultBypassMinFeeMsgTypes, + MaxTotalBypassMinFeeMsgGasUsage: DefaultmaxTotalBypassMinFeeMsgGasUsage, + } +} + +// Validate validates params. +func (p Params) Validate() error { + if p.Enabled { + if len(p.GasPrices) == 0 { + return fmt.Errorf("must provide at least 1 gas prices") + } + } + return nil +} diff --git a/x/tax2gas/types/query.pb.go b/x/tax2gas/types/query.pb.go new file mode 100644 index 000000000..714b6a802 --- /dev/null +++ b/x/tax2gas/types/query.pb.go @@ -0,0 +1,860 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: terra/tax2gas/v1beta1/query.proto + +package types + +import ( + context "context" + fmt "fmt" + github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types" + _ "github.com/cosmos/gogoproto/gogoproto" + grpc1 "github.com/cosmos/gogoproto/grpc" + proto "github.com/cosmos/gogoproto/proto" + _ "google.golang.org/genproto/googleapis/api/annotations" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// =============================== Params +type QueryParamsRequest struct { +} + +func (m *QueryParamsRequest) Reset() { *m = QueryParamsRequest{} } +func (m *QueryParamsRequest) String() string { return proto.CompactTextString(m) } +func (*QueryParamsRequest) ProtoMessage() {} +func (*QueryParamsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_bfce3f3a760d419d, []int{0} +} +func (m *QueryParamsRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryParamsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryParamsRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryParamsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryParamsRequest.Merge(m, src) +} +func (m *QueryParamsRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryParamsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryParamsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryParamsRequest proto.InternalMessageInfo + +type QueryParamsResponse struct { + Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` +} + +func (m *QueryParamsResponse) Reset() { *m = QueryParamsResponse{} } +func (m *QueryParamsResponse) String() string { return proto.CompactTextString(m) } +func (*QueryParamsResponse) ProtoMessage() {} +func (*QueryParamsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_bfce3f3a760d419d, []int{1} +} +func (m *QueryParamsResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryParamsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryParamsResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryParamsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryParamsResponse.Merge(m, src) +} +func (m *QueryParamsResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryParamsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryParamsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryParamsResponse proto.InternalMessageInfo + +func (m *QueryParamsResponse) GetParams() Params { + if m != nil { + return m.Params + } + return Params{} +} + +type QueryBurnTaxRateRequest struct { +} + +func (m *QueryBurnTaxRateRequest) Reset() { *m = QueryBurnTaxRateRequest{} } +func (m *QueryBurnTaxRateRequest) String() string { return proto.CompactTextString(m) } +func (*QueryBurnTaxRateRequest) ProtoMessage() {} +func (*QueryBurnTaxRateRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_bfce3f3a760d419d, []int{2} +} +func (m *QueryBurnTaxRateRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryBurnTaxRateRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryBurnTaxRateRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryBurnTaxRateRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryBurnTaxRateRequest.Merge(m, src) +} +func (m *QueryBurnTaxRateRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryBurnTaxRateRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryBurnTaxRateRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryBurnTaxRateRequest proto.InternalMessageInfo + +type QueryBurnTaxRateResponse struct { + BurnTaxRate github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,1,opt,name=burn_tax_rate,json=burnTaxRate,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"burn_tax_rate"` +} + +func (m *QueryBurnTaxRateResponse) Reset() { *m = QueryBurnTaxRateResponse{} } +func (m *QueryBurnTaxRateResponse) String() string { return proto.CompactTextString(m) } +func (*QueryBurnTaxRateResponse) ProtoMessage() {} +func (*QueryBurnTaxRateResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_bfce3f3a760d419d, []int{3} +} +func (m *QueryBurnTaxRateResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryBurnTaxRateResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryBurnTaxRateResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryBurnTaxRateResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryBurnTaxRateResponse.Merge(m, src) +} +func (m *QueryBurnTaxRateResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryBurnTaxRateResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryBurnTaxRateResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryBurnTaxRateResponse proto.InternalMessageInfo + +func init() { + proto.RegisterType((*QueryParamsRequest)(nil), "terra.tax2gas.v1beta1.QueryParamsRequest") + proto.RegisterType((*QueryParamsResponse)(nil), "terra.tax2gas.v1beta1.QueryParamsResponse") + proto.RegisterType((*QueryBurnTaxRateRequest)(nil), "terra.tax2gas.v1beta1.QueryBurnTaxRateRequest") + proto.RegisterType((*QueryBurnTaxRateResponse)(nil), "terra.tax2gas.v1beta1.QueryBurnTaxRateResponse") +} + +func init() { proto.RegisterFile("terra/tax2gas/v1beta1/query.proto", fileDescriptor_bfce3f3a760d419d) } + +var fileDescriptor_bfce3f3a760d419d = []byte{ + // 400 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0x41, 0x6f, 0xda, 0x30, + 0x1c, 0xc5, 0x13, 0xb4, 0x21, 0xcd, 0x68, 0x17, 0x0f, 0x04, 0x8b, 0x20, 0x6c, 0x99, 0x34, 0x6d, + 0x93, 0xb0, 0x07, 0x1c, 0x77, 0x8b, 0x76, 0xda, 0x69, 0x8b, 0x76, 0xda, 0x05, 0x39, 0x99, 0x95, + 0x45, 0x03, 0x3b, 0xd8, 0x0e, 0x0a, 0x97, 0x1e, 0xfa, 0x09, 0xaa, 0xf6, 0xda, 0x0f, 0xc4, 0x11, + 0xa9, 0x97, 0xaa, 0x07, 0x54, 0x41, 0x3f, 0x48, 0x85, 0xe3, 0xb6, 0x50, 0x68, 0xc5, 0x29, 0x91, + 0xfd, 0xfb, 0xbf, 0xf7, 0xfe, 0x2f, 0x01, 0xef, 0x15, 0x15, 0x82, 0x60, 0x45, 0xf2, 0x5e, 0x4c, + 0x24, 0x9e, 0x74, 0x43, 0xaa, 0x48, 0x17, 0x8f, 0x33, 0x2a, 0xa6, 0x28, 0x15, 0x5c, 0x71, 0x58, + 0xd3, 0x08, 0x32, 0x08, 0x32, 0x88, 0x53, 0x8d, 0x79, 0xcc, 0x35, 0x81, 0xd7, 0x6f, 0x05, 0xec, + 0x34, 0x63, 0xce, 0xe3, 0x21, 0xc5, 0x24, 0x4d, 0x30, 0x61, 0x8c, 0x2b, 0xa2, 0x12, 0xce, 0xa4, + 0xb9, 0xfd, 0xb0, 0xdf, 0x2d, 0xa6, 0x8c, 0xca, 0xc4, 0x40, 0x5e, 0x15, 0xc0, 0x5f, 0x6b, 0xfb, + 0x9f, 0x44, 0x90, 0x91, 0x0c, 0xe8, 0x38, 0xa3, 0x52, 0x79, 0x01, 0x78, 0xb3, 0x75, 0x2a, 0x53, + 0xce, 0x24, 0x85, 0xdf, 0x40, 0x39, 0xd5, 0x27, 0x0d, 0xfb, 0x9d, 0xfd, 0xa9, 0xd2, 0x6b, 0xa1, + 0xbd, 0x69, 0x51, 0x31, 0xe6, 0xbf, 0x98, 0x2d, 0xda, 0x56, 0x60, 0x46, 0xbc, 0xb7, 0xa0, 0xae, + 0x35, 0xfd, 0x4c, 0xb0, 0xdf, 0x24, 0x0f, 0x88, 0xa2, 0x77, 0x76, 0x0c, 0x34, 0x76, 0xaf, 0x8c, + 0x67, 0x00, 0x5e, 0x87, 0x99, 0x60, 0x03, 0x45, 0xf2, 0x81, 0x20, 0x8a, 0x6a, 0xeb, 0x57, 0x3e, + 0x5a, 0x6b, 0x5f, 0x2d, 0xda, 0x1f, 0xe3, 0x44, 0xfd, 0xcb, 0x42, 0x14, 0xf1, 0x11, 0x8e, 0xb8, + 0x1c, 0x71, 0x69, 0x1e, 0x1d, 0xf9, 0xf7, 0x3f, 0x56, 0xd3, 0x94, 0x4a, 0xf4, 0x9d, 0x46, 0x41, + 0x25, 0x7c, 0xd0, 0xee, 0x9d, 0x97, 0xc0, 0x4b, 0x6d, 0x08, 0x8f, 0x40, 0xb9, 0x08, 0x0b, 0x3f, + 0x3f, 0xb1, 0xcb, 0x6e, 0x3b, 0xce, 0x97, 0x43, 0xd0, 0x22, 0xbe, 0xd7, 0x3a, 0xbe, 0xb8, 0x39, + 0x2b, 0xd5, 0x61, 0x0d, 0x6f, 0x7f, 0x0d, 0xe3, 0x7a, 0x6a, 0x83, 0xca, 0xc6, 0xd6, 0x10, 0x3d, + 0x27, 0xbd, 0xdb, 0x9c, 0x83, 0x0f, 0xe6, 0x4d, 0x1e, 0x4f, 0xe7, 0x69, 0x42, 0xe7, 0x51, 0x9e, + 0x0d, 0xd6, 0xff, 0x31, 0x5b, 0xba, 0xf6, 0x7c, 0xe9, 0xda, 0xd7, 0x4b, 0xd7, 0x3e, 0x59, 0xb9, + 0xd6, 0x7c, 0xe5, 0x5a, 0x97, 0x2b, 0xd7, 0xfa, 0xf3, 0x75, 0xb3, 0xed, 0x21, 0x91, 0x32, 0x89, + 0x3a, 0x85, 0x4e, 0xc4, 0x05, 0xc5, 0x93, 0x3e, 0xce, 0xef, 0x15, 0x75, 0xf7, 0x61, 0x59, 0xff, + 0x66, 0xfd, 0xdb, 0x00, 0x00, 0x00, 0xff, 0xff, 0x2f, 0xec, 0x37, 0x77, 0xfb, 0x02, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// QueryClient is the client API for Query service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type QueryClient interface { + Params(ctx context.Context, in *QueryParamsRequest, opts ...grpc.CallOption) (*QueryParamsResponse, error) + BurnTaxRate(ctx context.Context, in *QueryBurnTaxRateRequest, opts ...grpc.CallOption) (*QueryBurnTaxRateResponse, error) +} + +type queryClient struct { + cc grpc1.ClientConn +} + +func NewQueryClient(cc grpc1.ClientConn) QueryClient { + return &queryClient{cc} +} + +func (c *queryClient) Params(ctx context.Context, in *QueryParamsRequest, opts ...grpc.CallOption) (*QueryParamsResponse, error) { + out := new(QueryParamsResponse) + err := c.cc.Invoke(ctx, "/terra.tax2gas.v1beta1.Query/Params", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *queryClient) BurnTaxRate(ctx context.Context, in *QueryBurnTaxRateRequest, opts ...grpc.CallOption) (*QueryBurnTaxRateResponse, error) { + out := new(QueryBurnTaxRateResponse) + err := c.cc.Invoke(ctx, "/terra.tax2gas.v1beta1.Query/BurnTaxRate", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// QueryServer is the server API for Query service. +type QueryServer interface { + Params(context.Context, *QueryParamsRequest) (*QueryParamsResponse, error) + BurnTaxRate(context.Context, *QueryBurnTaxRateRequest) (*QueryBurnTaxRateResponse, error) +} + +// UnimplementedQueryServer can be embedded to have forward compatible implementations. +type UnimplementedQueryServer struct { +} + +func (*UnimplementedQueryServer) Params(ctx context.Context, req *QueryParamsRequest) (*QueryParamsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Params not implemented") +} +func (*UnimplementedQueryServer) BurnTaxRate(ctx context.Context, req *QueryBurnTaxRateRequest) (*QueryBurnTaxRateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method BurnTaxRate not implemented") +} + +func RegisterQueryServer(s grpc1.Server, srv QueryServer) { + s.RegisterService(&_Query_serviceDesc, srv) +} + +func _Query_Params_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryParamsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).Params(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terra.tax2gas.v1beta1.Query/Params", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).Params(ctx, req.(*QueryParamsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Query_BurnTaxRate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryBurnTaxRateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).BurnTaxRate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terra.tax2gas.v1beta1.Query/BurnTaxRate", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).BurnTaxRate(ctx, req.(*QueryBurnTaxRateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Query_serviceDesc = grpc.ServiceDesc{ + ServiceName: "terra.tax2gas.v1beta1.Query", + HandlerType: (*QueryServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Params", + Handler: _Query_Params_Handler, + }, + { + MethodName: "BurnTaxRate", + Handler: _Query_BurnTaxRate_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "terra/tax2gas/v1beta1/query.proto", +} + +func (m *QueryParamsRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryParamsRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryParamsRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func (m *QueryParamsResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryParamsResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryParamsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func (m *QueryBurnTaxRateRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryBurnTaxRateRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryBurnTaxRateRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func (m *QueryBurnTaxRateResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryBurnTaxRateResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryBurnTaxRateResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size := m.BurnTaxRate.Size() + i -= size + if _, err := m.BurnTaxRate.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func encodeVarintQuery(dAtA []byte, offset int, v uint64) int { + offset -= sovQuery(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *QueryParamsRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *QueryParamsResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.Params.Size() + n += 1 + l + sovQuery(uint64(l)) + return n +} + +func (m *QueryBurnTaxRateRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *QueryBurnTaxRateResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.BurnTaxRate.Size() + n += 1 + l + sovQuery(uint64(l)) + return n +} + +func sovQuery(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozQuery(x uint64) (n int) { + return sovQuery(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *QueryParamsRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryParamsRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryParamsRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryParamsResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryParamsResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryParamsResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Params", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Params.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryBurnTaxRateRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryBurnTaxRateRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryBurnTaxRateRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryBurnTaxRateResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryBurnTaxRateResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryBurnTaxRateResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BurnTaxRate", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.BurnTaxRate.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipQuery(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowQuery + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowQuery + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowQuery + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthQuery + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupQuery + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthQuery + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthQuery = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowQuery = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupQuery = fmt.Errorf("proto: unexpected end of group") +) diff --git a/x/tax2gas/types/query.pb.gw.go b/x/tax2gas/types/query.pb.gw.go new file mode 100644 index 000000000..67a40a180 --- /dev/null +++ b/x/tax2gas/types/query.pb.gw.go @@ -0,0 +1,218 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: terra/tax2gas/v1beta1/query.proto + +/* +Package types is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package types + +import ( + "context" + "io" + "net/http" + + "github.com/golang/protobuf/descriptor" + "github.com/golang/protobuf/proto" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/grpc-ecosystem/grpc-gateway/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// Suppress "imported and not used" errors +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray +var _ = descriptor.ForMessage +var _ = metadata.Join + +func request_Query_Params_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryParamsRequest + var metadata runtime.ServerMetadata + + msg, err := client.Params(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Query_Params_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryParamsRequest + var metadata runtime.ServerMetadata + + msg, err := server.Params(ctx, &protoReq) + return msg, metadata, err + +} + +func request_Query_BurnTaxRate_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryBurnTaxRateRequest + var metadata runtime.ServerMetadata + + msg, err := client.BurnTaxRate(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Query_BurnTaxRate_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryBurnTaxRateRequest + var metadata runtime.ServerMetadata + + msg, err := server.BurnTaxRate(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterQueryHandlerServer registers the http handlers for service Query to "mux". +// UnaryRPC :call QueryServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterQueryHandlerFromEndpoint instead. +func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, server QueryServer) error { + + mux.Handle("GET", pattern_Query_Params_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Query_Params_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_Params_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_Query_BurnTaxRate_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Query_BurnTaxRate_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_BurnTaxRate_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +// RegisterQueryHandlerFromEndpoint is same as RegisterQueryHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterQueryHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.Dial(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterQueryHandler(ctx, mux, conn) +} + +// RegisterQueryHandler registers the http handlers for service Query to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterQueryHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterQueryHandlerClient(ctx, mux, NewQueryClient(conn)) +} + +// RegisterQueryHandlerClient registers the http handlers for service Query +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "QueryClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "QueryClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "QueryClient" to call the correct interceptors. +func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, client QueryClient) error { + + mux.Handle("GET", pattern_Query_Params_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Query_Params_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_Params_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_Query_BurnTaxRate_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Query_BurnTaxRate_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_BurnTaxRate_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_Query_Params_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"terra", "tax2gas", "Params"}, "", runtime.AssumeColonVerbOpt(false))) + + pattern_Query_BurnTaxRate_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"terra", "tax2gas", "BurnTaxRate"}, "", runtime.AssumeColonVerbOpt(false))) +) + +var ( + forward_Query_Params_0 = runtime.ForwardResponseMessage + + forward_Query_BurnTaxRate_0 = runtime.ForwardResponseMessage +) diff --git a/x/tax2gas/types/tx.pb.go b/x/tax2gas/types/tx.pb.go new file mode 100644 index 000000000..14e51e963 --- /dev/null +++ b/x/tax2gas/types/tx.pb.go @@ -0,0 +1,599 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: terra/tax2gas/v1beta1/tx.proto + +package types + +import ( + context "context" + fmt "fmt" + _ "github.com/cosmos/cosmos-proto" + _ "github.com/cosmos/cosmos-sdk/types/msgservice" + _ "github.com/cosmos/cosmos-sdk/types/tx/amino" + _ "github.com/cosmos/gogoproto/gogoproto" + grpc1 "github.com/cosmos/gogoproto/grpc" + proto "github.com/cosmos/gogoproto/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// MsgUpdateParams is the Msg/UpdateParams request type. +// +// Since: cosmos-sdk 0.47 +type MsgUpdateParams struct { + // authority is the address that controls the module (defaults to x/gov unless overwritten). + Authority string `protobuf:"bytes,1,opt,name=authority,proto3" json:"authority,omitempty"` + // params defines the x/tax2gas parameters to update. + // + // NOTE: All parameters must be supplied. + Params Params `protobuf:"bytes,2,opt,name=params,proto3" json:"params"` +} + +func (m *MsgUpdateParams) Reset() { *m = MsgUpdateParams{} } +func (m *MsgUpdateParams) String() string { return proto.CompactTextString(m) } +func (*MsgUpdateParams) ProtoMessage() {} +func (*MsgUpdateParams) Descriptor() ([]byte, []int) { + return fileDescriptor_a20cba19b3d258de, []int{0} +} +func (m *MsgUpdateParams) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgUpdateParams) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgUpdateParams.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgUpdateParams) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgUpdateParams.Merge(m, src) +} +func (m *MsgUpdateParams) XXX_Size() int { + return m.Size() +} +func (m *MsgUpdateParams) XXX_DiscardUnknown() { + xxx_messageInfo_MsgUpdateParams.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgUpdateParams proto.InternalMessageInfo + +func (m *MsgUpdateParams) GetAuthority() string { + if m != nil { + return m.Authority + } + return "" +} + +func (m *MsgUpdateParams) GetParams() Params { + if m != nil { + return m.Params + } + return Params{} +} + +// MsgUpdateParamsResponse defines the response structure for executing a +// MsgUpdateParams message. +// +// Since: cosmos-sdk 0.47 +type MsgUpdateParamsResponse struct { +} + +func (m *MsgUpdateParamsResponse) Reset() { *m = MsgUpdateParamsResponse{} } +func (m *MsgUpdateParamsResponse) String() string { return proto.CompactTextString(m) } +func (*MsgUpdateParamsResponse) ProtoMessage() {} +func (*MsgUpdateParamsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_a20cba19b3d258de, []int{1} +} +func (m *MsgUpdateParamsResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgUpdateParamsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgUpdateParamsResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgUpdateParamsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgUpdateParamsResponse.Merge(m, src) +} +func (m *MsgUpdateParamsResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgUpdateParamsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgUpdateParamsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgUpdateParamsResponse proto.InternalMessageInfo + +func init() { + proto.RegisterType((*MsgUpdateParams)(nil), "terra.tax2gas.v1beta1.MsgUpdateParams") + proto.RegisterType((*MsgUpdateParamsResponse)(nil), "terra.tax2gas.v1beta1.MsgUpdateParamsResponse") +} + +func init() { proto.RegisterFile("terra/tax2gas/v1beta1/tx.proto", fileDescriptor_a20cba19b3d258de) } + +var fileDescriptor_a20cba19b3d258de = []byte{ + // 365 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x2b, 0x49, 0x2d, 0x2a, + 0x4a, 0xd4, 0x2f, 0x49, 0xac, 0x30, 0x4a, 0x4f, 0x2c, 0xd6, 0x2f, 0x33, 0x4c, 0x4a, 0x2d, 0x49, + 0x34, 0xd4, 0x2f, 0xa9, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x05, 0xcb, 0xeb, 0x41, + 0xe5, 0xf5, 0xa0, 0xf2, 0x52, 0x22, 0xe9, 0xf9, 0xe9, 0xf9, 0x60, 0x15, 0xfa, 0x20, 0x16, 0x44, + 0xb1, 0x94, 0x78, 0x72, 0x7e, 0x71, 0x6e, 0x7e, 0xb1, 0x7e, 0x6e, 0x71, 0xba, 0x7e, 0x99, 0x21, + 0x88, 0x82, 0x4a, 0x08, 0x26, 0xe6, 0x66, 0xe6, 0xe5, 0xeb, 0x83, 0x49, 0xa8, 0x90, 0x24, 0x44, + 0x6d, 0x3c, 0xc4, 0x10, 0x08, 0x07, 0x2a, 0xa5, 0x8c, 0xdd, 0x4d, 0xe9, 0xa9, 0x79, 0xa9, 0xc5, + 0x99, 0x50, 0x45, 0x4a, 0xfb, 0x19, 0xb9, 0xf8, 0x7d, 0x8b, 0xd3, 0x43, 0x0b, 0x52, 0x12, 0x4b, + 0x52, 0x03, 0x12, 0x8b, 0x12, 0x73, 0x8b, 0x85, 0xcc, 0xb8, 0x38, 0x13, 0x4b, 0x4b, 0x32, 0xf2, + 0x8b, 0x32, 0x4b, 0x2a, 0x25, 0x18, 0x15, 0x18, 0x35, 0x38, 0x9d, 0x24, 0x2e, 0x6d, 0xd1, 0x15, + 0x81, 0x9a, 0xee, 0x98, 0x92, 0x52, 0x94, 0x5a, 0x5c, 0x1c, 0x5c, 0x52, 0x94, 0x99, 0x97, 0x1e, + 0x84, 0x50, 0x2a, 0xe4, 0xc0, 0xc5, 0x56, 0x00, 0x36, 0x41, 0x82, 0x49, 0x81, 0x51, 0x83, 0xdb, + 0x48, 0x56, 0x0f, 0xab, 0xaf, 0xf5, 0x20, 0xd6, 0x38, 0x71, 0x9e, 0xb8, 0x27, 0xcf, 0xb0, 0xe2, + 0xf9, 0x06, 0x2d, 0xc6, 0x20, 0xa8, 0x3e, 0x2b, 0xa3, 0xa6, 0xe7, 0x1b, 0xb4, 0x10, 0x26, 0x76, + 0x3d, 0xdf, 0xa0, 0x25, 0x0f, 0xf1, 0x45, 0x05, 0xdc, 0x1f, 0x68, 0xae, 0x55, 0x92, 0xe4, 0x12, + 0x47, 0x13, 0x0a, 0x4a, 0x2d, 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x35, 0x2a, 0xe1, 0x62, 0xf6, 0x2d, + 0x4e, 0x17, 0x4a, 0xe3, 0xe2, 0x41, 0xf1, 0x9f, 0x1a, 0x0e, 0x77, 0xa1, 0x19, 0x23, 0xa5, 0x47, + 0x9c, 0x3a, 0x98, 0x75, 0x52, 0xac, 0x0d, 0x20, 0xcf, 0x38, 0x79, 0x9d, 0x78, 0x24, 0xc7, 0x78, + 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, 0x8c, 0x13, 0x1e, 0xcb, 0x31, 0x5c, 0x78, 0x2c, 0xc7, + 0x70, 0xe3, 0xb1, 0x1c, 0x43, 0x94, 0x41, 0x7a, 0x66, 0x49, 0x46, 0x69, 0x92, 0x5e, 0x72, 0x7e, + 0xae, 0x7e, 0x72, 0x4e, 0x62, 0x71, 0x71, 0x66, 0xb2, 0x2e, 0xc4, 0x7b, 0xc9, 0xf9, 0x45, 0xa9, + 0xfa, 0x65, 0xc6, 0x48, 0xde, 0x2c, 0xa9, 0x2c, 0x48, 0x2d, 0x4e, 0x62, 0x03, 0xc7, 0x92, 0x31, + 0x20, 0x00, 0x00, 0xff, 0xff, 0xd2, 0x9a, 0x8b, 0x48, 0x60, 0x02, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// MsgClient is the client API for Msg service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type MsgClient interface { + UpdateParams(ctx context.Context, in *MsgUpdateParams, opts ...grpc.CallOption) (*MsgUpdateParamsResponse, error) +} + +type msgClient struct { + cc grpc1.ClientConn +} + +func NewMsgClient(cc grpc1.ClientConn) MsgClient { + return &msgClient{cc} +} + +func (c *msgClient) UpdateParams(ctx context.Context, in *MsgUpdateParams, opts ...grpc.CallOption) (*MsgUpdateParamsResponse, error) { + out := new(MsgUpdateParamsResponse) + err := c.cc.Invoke(ctx, "/terra.tax2gas.v1beta1.Msg/UpdateParams", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// MsgServer is the server API for Msg service. +type MsgServer interface { + UpdateParams(context.Context, *MsgUpdateParams) (*MsgUpdateParamsResponse, error) +} + +// UnimplementedMsgServer can be embedded to have forward compatible implementations. +type UnimplementedMsgServer struct { +} + +func (*UnimplementedMsgServer) UpdateParams(ctx context.Context, req *MsgUpdateParams) (*MsgUpdateParamsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateParams not implemented") +} + +func RegisterMsgServer(s grpc1.Server, srv MsgServer) { + s.RegisterService(&_Msg_serviceDesc, srv) +} + +func _Msg_UpdateParams_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgUpdateParams) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).UpdateParams(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terra.tax2gas.v1beta1.Msg/UpdateParams", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).UpdateParams(ctx, req.(*MsgUpdateParams)) + } + return interceptor(ctx, in, info, handler) +} + +var _Msg_serviceDesc = grpc.ServiceDesc{ + ServiceName: "terra.tax2gas.v1beta1.Msg", + HandlerType: (*MsgServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "UpdateParams", + Handler: _Msg_UpdateParams_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "terra/tax2gas/v1beta1/tx.proto", +} + +func (m *MsgUpdateParams) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgUpdateParams) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgUpdateParams) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + if len(m.Authority) > 0 { + i -= len(m.Authority) + copy(dAtA[i:], m.Authority) + i = encodeVarintTx(dAtA, i, uint64(len(m.Authority))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MsgUpdateParamsResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgUpdateParamsResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgUpdateParamsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func encodeVarintTx(dAtA []byte, offset int, v uint64) int { + offset -= sovTx(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *MsgUpdateParams) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Authority) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = m.Params.Size() + n += 1 + l + sovTx(uint64(l)) + return n +} + +func (m *MsgUpdateParamsResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func sovTx(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozTx(x uint64) (n int) { + return sovTx(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *MsgUpdateParams) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgUpdateParams: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgUpdateParams: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Authority", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Authority = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Params", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Params.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgUpdateParamsResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgUpdateParamsResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgUpdateParamsResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipTx(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthTx + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupTx + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthTx + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthTx = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowTx = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupTx = fmt.Errorf("proto: unexpected end of group") +) diff --git a/x/tax2gas/utils/fee_tax.go b/x/tax2gas/utils/fee_tax.go new file mode 100644 index 000000000..7a5272f62 --- /dev/null +++ b/x/tax2gas/utils/fee_tax.go @@ -0,0 +1,147 @@ +package utils + +import ( + "regexp" + "strings" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authz "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + marketexported "github.com/classic-terra/core/v3/x/market/exported" + "github.com/classic-terra/core/v3/x/tax2gas/types" +) + +var IBCRegexp = regexp.MustCompile("^ibc/[a-fA-F0-9]{64}$") + +func isIBCDenom(denom string) bool { + return IBCRegexp.MatchString(strings.ToLower(denom)) +} + +// FilterMsgAndComputeTax computes the stability tax on messages. +func FilterMsgAndComputeTax(ctx sdk.Context, tk types.TreasuryKeeper, burnTaxRate sdk.Dec, msgs ...sdk.Msg) sdk.Coins { + taxes := sdk.Coins{} + + for _, msg := range msgs { + switch msg := msg.(type) { + case *banktypes.MsgSend: + if !tk.HasBurnTaxExemptionAddress(ctx, msg.FromAddress, msg.ToAddress) { + taxes = taxes.Add(ComputeTax(burnTaxRate, msg.Amount)...) + } + + case *banktypes.MsgMultiSend: + tainted := 0 + + for _, input := range msg.Inputs { + if tk.HasBurnTaxExemptionAddress(ctx, input.Address) { + tainted++ + } + } + + for _, output := range msg.Outputs { + if tk.HasBurnTaxExemptionAddress(ctx, output.Address) { + tainted++ + } + } + + if tainted != len(msg.Inputs)+len(msg.Outputs) { + for _, input := range msg.Inputs { + taxes = taxes.Add(ComputeTax(burnTaxRate, input.Coins)...) + } + } + + case *marketexported.MsgSwapSend: + taxes = taxes.Add(ComputeTax(burnTaxRate, sdk.NewCoins(msg.OfferCoin))...) + + case *wasmtypes.MsgInstantiateContract: + taxes = taxes.Add(ComputeTax(burnTaxRate, msg.Funds)...) + + case *wasmtypes.MsgInstantiateContract2: + taxes = taxes.Add(ComputeTax(burnTaxRate, msg.Funds)...) + + case *wasmtypes.MsgExecuteContract: + if !tk.HasBurnTaxExemptionContract(ctx, msg.Contract) { + taxes = taxes.Add(ComputeTax(burnTaxRate, msg.Funds)...) + } + + case *authz.MsgExec: + messages, err := msg.GetMessages() + if err == nil { + taxes = taxes.Add(FilterMsgAndComputeTax(ctx, tk, burnTaxRate, messages...)...) + } + } + } + + return taxes +} + +// computes the stability tax according to tax-rate and tax-cap +func ComputeTax(burnTaxRate sdk.Dec, principal sdk.Coins) sdk.Coins { + taxes := sdk.Coins{} + + for _, coin := range principal { + if coin.Denom == sdk.DefaultBondDenom { + continue + } + + if isIBCDenom(coin.Denom) { + continue + } + + tax := sdk.NewDecFromInt(coin.Amount).Mul(burnTaxRate).TruncateInt() + if tax.Equal(sdk.ZeroInt()) { + continue + } + + taxes = taxes.Add(sdk.NewCoin(coin.Denom, tax)) + } + + return taxes +} + +func ComputeGas(gasPrices sdk.DecCoins, taxes sdk.Coins) (sdkmath.Int, error) { + taxes = taxes.Sort() + tax2gas := sdkmath.ZeroInt() + // Convert to gas + i, j := 0, 0 + for i < len(gasPrices) && j < len(taxes) { + switch { + case gasPrices[i].Denom == taxes[j].Denom: + tax2gas = tax2gas.Add(sdk.NewDec(taxes[j].Amount.Int64()).Quo((gasPrices[i].Amount)).Ceil().RoundInt()) + i++ + j++ + case gasPrices[i].Denom < taxes[j].Denom: + i++ + default: + j++ + } + } + + return tax2gas, nil +} + +func ComputeFeesOnGasConsumed(tx sdk.Tx, gasPrices sdk.DecCoins, gas sdkmath.Int) (sdk.Coins, error) { + feeTx, ok := tx.(sdk.FeeTx) + if !ok { + return nil, errorsmod.Wrap(sdkerrors.ErrTxDecode, "Tx must be a FeeTx") + } + + isOracleTx := IsOracleTx(feeTx.GetMsgs()) + + gasFees := make(sdk.Coins, len(gasPrices)) + if !isOracleTx && len(gasPrices) != 0 { + // Determine the required fees by multiplying each required minimum gas + // price by the gas limit, where fee = ceil(minGasPrice * gasLimit). + glDec := sdk.NewDecFromInt(gas) + for i, gp := range gasPrices { + fee := gp.Amount.Mul(glDec) + gasFees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt()) + } + } + + return gasFees, nil +} diff --git a/x/tax2gas/utils/utils.go b/x/tax2gas/utils/utils.go new file mode 100644 index 000000000..f234d9163 --- /dev/null +++ b/x/tax2gas/utils/utils.go @@ -0,0 +1,102 @@ +package utils + +import ( + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + + oracleexported "github.com/classic-terra/core/v3/x/oracle/exported" +) + +func IsOracleTx(msgs []sdk.Msg) bool { + for _, msg := range msgs { + switch msg.(type) { + case *oracleexported.MsgAggregateExchangeRatePrevote: + continue + case *oracleexported.MsgAggregateExchangeRateVote: + continue + default: + return false + } + } + + return true +} + +// Find returns true and Dec amount if the denom exists in gasPrices. Otherwise it returns false +// and a zero dec. Uses binary search. +// CONTRACT: gasPrices must be valid (sorted). +func GetGasPriceByDenom(gasPrices sdk.DecCoins, denom string) (bool, sdk.Dec) { + switch len(gasPrices) { + case 0: + return false, sdk.ZeroDec() + + case 1: + gasPrice := gasPrices[0] + if gasPrice.Denom == denom { + return true, gasPrice.Amount + } + return false, sdk.ZeroDec() + + default: + midIdx := len(gasPrices) / 2 // 2:1, 3:1, 4:2 + gasPrice := gasPrices[midIdx] + switch { + case denom < gasPrice.Denom: + return GetGasPriceByDenom(gasPrices[:midIdx], denom) + case denom == gasPrice.Denom: + return true, gasPrice.Amount + default: + return GetGasPriceByDenom(gasPrices[midIdx+1:], denom) + } + } +} + +func CalculateTaxesAndPayableFee(gasPrices sdk.DecCoins, feeCoins sdk.Coins, taxGas sdkmath.Int, totalGasRemaining sdkmath.Int) (taxes, payableFees sdk.Coins, gasRemaining sdkmath.Int) { + taxGasRemaining := taxGas + taxes = sdk.NewCoins() + payableFees = sdk.NewCoins() + gasRemaining = totalGasRemaining + for _, feeCoin := range feeCoins { + found, gasPrice := GetGasPriceByDenom(gasPrices, feeCoin.Denom) + if !found { + continue + } + taxFeeRequired := sdk.NewCoin(feeCoin.Denom, gasPrice.MulInt(taxGasRemaining).Ceil().RoundInt()) + totalFeeRequired := sdk.NewCoin(feeCoin.Denom, gasPrice.MulInt(gasRemaining).Ceil().RoundInt()) + + switch { + case taxGasRemaining.IsPositive(): + switch { + case feeCoin.IsGTE(totalFeeRequired): + taxes = taxes.Add(taxFeeRequired) + payableFees = payableFees.Add(totalFeeRequired) + gasRemaining = sdkmath.ZeroInt() + return taxes, payableFees, gasRemaining + case feeCoin.IsGTE(taxFeeRequired): + taxes = taxes.Add(taxFeeRequired) + taxGasRemaining = sdkmath.ZeroInt() + payableFees = payableFees.Add(feeCoin) + totalFeeRemaining := sdk.NewDecCoinFromCoin(totalFeeRequired.Sub(feeCoin)) + gasRemaining = totalFeeRemaining.Amount.Quo(gasPrice).Ceil().RoundInt() + default: + taxes = taxes.Add(feeCoin) + payableFees = payableFees.Add(feeCoin) + taxFeeRemaining := sdk.NewDecCoinFromCoin(taxFeeRequired.Sub(feeCoin)) + taxGasRemaining = taxFeeRemaining.Amount.Quo(gasPrice).Ceil().RoundInt() + gasRemaining = gasRemaining.Sub(taxGas.Sub(taxGasRemaining)) + } + case gasRemaining.IsPositive(): + if feeCoin.IsGTE(totalFeeRequired) { + payableFees = payableFees.Add(totalFeeRequired) + gasRemaining = sdkmath.ZeroInt() + return taxes, payableFees, gasRemaining + } + payableFees = payableFees.Add(feeCoin) + totalFeeRemaining := sdk.NewDecCoinFromCoin(totalFeeRequired.Sub(feeCoin)) + gasRemaining = totalFeeRemaining.Amount.Quo(gasPrice).Ceil().RoundInt() + default: + return taxes, payableFees, gasRemaining + } + } + return taxes, payableFees, gasRemaining +}