diff --git a/app/app.go b/app/app.go index 1e76d2974..eca8c5457 100644 --- a/app/app.go +++ b/app/app.go @@ -90,6 +90,8 @@ import ( upgradekeeper "github.com/cosmos/cosmos-sdk/x/upgrade/keeper" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + "github.com/iov-one/starnamed/x/burner" + burnertypes "github.com/iov-one/starnamed/x/burner/types" "github.com/iov-one/starnamed/x/configuration" "github.com/iov-one/starnamed/x/offchain" "github.com/iov-one/starnamed/x/starname" @@ -208,11 +210,17 @@ var ( govtypes.ModuleName: {authtypes.Burner}, ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, wasm.ModuleName: {authtypes.Burner}, + burnertypes.ModuleName: {authtypes.Burner}, } + //NOTE: this was included from wasmd repo but the allowedReceivingModAcc variable was not used, + /*allowedReceivingModAcc = map[string]bool{ + //distrtypes.ModuleName: true, + }*/ + // module accounts that are allowed to receive tokens - allowedReceivingModAcc = map[string]bool{ - distrtypes.ModuleName: true, + allowedReceivingModules = map[string]bool{ + burnertypes.ModuleName: true, } ) @@ -481,6 +489,7 @@ func NewWasmApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest b transferModule, configuration.NewAppModule(app.configKeeper), starname.NewAppModule(app.starnameKeeper), + burner.NewAppModule(app.bankKeeper, app.accountKeeper), ) // During begin block slashing happens after distr.BeginBlocker so that @@ -491,7 +500,7 @@ func NewWasmApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest b upgradetypes.ModuleName, minttypes.ModuleName, distrtypes.ModuleName, slashingtypes.ModuleName, evidencetypes.ModuleName, stakingtypes.ModuleName, ibchost.ModuleName, ) - app.mm.SetOrderEndBlockers(crisistypes.ModuleName, govtypes.ModuleName, stakingtypes.ModuleName, starname.ModuleName) + app.mm.SetOrderEndBlockers(crisistypes.ModuleName, govtypes.ModuleName, stakingtypes.ModuleName, burnertypes.ModuleName, starname.ModuleName) // NOTE: The genutils module must occur after staking so that pools are // properly initialized with tokens from genesis accounts. @@ -611,7 +620,8 @@ func (app *WasmApp) LoadHeight(height int64) error { func (app *WasmApp) ModuleAccountAddrs() map[string]bool { modAccAddrs := make(map[string]bool) for acc := range maccPerms { - modAccAddrs[authtypes.NewModuleAddress(acc).String()] = true + moduleCanReceive, modulePresentInArray := allowedReceivingModules[acc] + modAccAddrs[authtypes.NewModuleAddress(acc).String()] = !(modulePresentInArray && moduleCanReceive) } return modAccAddrs diff --git a/app/app_test.go b/app/app_test.go index f92d3e4f4..5a6e1143b 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -5,12 +5,13 @@ import ( "os" "testing" - "github.com/iov-one/starnamed/x/wasm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" db "github.com/tendermint/tm-db" + + "github.com/iov-one/starnamed/x/wasm" ) var emptyWasmOpts []wasm.Option = nil @@ -45,7 +46,13 @@ func TestBlockedAddrs(t *testing.T) { for acc := range maccPerms { t.Run(acc, func(t *testing.T) { - require.True(t, gapp.bankKeeper.BlockedAddr(gapp.accountKeeper.GetModuleAddress(acc)), + var expected bool + if allowedReceivingModules[acc] { + expected = false + } else { + expected = true + } + require.Equal(t, expected, gapp.bankKeeper.BlockedAddr(gapp.accountKeeper.GetModuleAddress(acc)), "ensure that blocked addresses are properly set in bank keeper", ) }) diff --git a/scripts/integration/setup.sh b/scripts/integration/setup.sh index 010ef3585..bb911f183 100755 --- a/scripts/integration/setup.sh +++ b/scripts/integration/setup.sh @@ -9,6 +9,7 @@ FEE=${DENOM_FEE:-tiov} CHAIN_ID=${CHAIN:-testing} MONIKER=${MONIKER:-node001} +rm -f "$HOME"/.${BINARY}/config/genesis.json ${BINARY} init --chain-id "$CHAIN_ID" "$MONIKER" 2>&1 | jq .chain_id sed --in-place 's/timeout_commit = "5s"/timeout_commit = "1s"/' "$HOME"/.${BINARY}/config/config.toml sed --in-place 's/enable = false/enable = true/' "$HOME"/.${BINARY}/config/app.toml # enable api diff --git a/scripts/integration/test/CLI.test.js b/scripts/integration/test/CLI.test.js index dcc342026..38b8f9a8e 100644 --- a/scripts/integration/test/CLI.test.js +++ b/scripts/integration/test/CLI.test.js @@ -1,5 +1,5 @@ import { Base64 } from "js-base64"; -import { gasPrices, cli, denomFee, denomStake, getBalance, memo, msig1, msig1SignTx, signAndBroadcastTx, signer, w1, w2, writeTmpJson, makeTx } from "./common"; +import { burner, cli, denomFee, denomStake, gasPrices, getBalance, memo, msig1, msig1SignTx, signAndBroadcastTx, signer, w1, w2, writeTmpJson, makeTx } from "./common"; import compareObjects from "./compareObjects"; import forge from "node-forge"; @@ -602,6 +602,7 @@ describe( "Tests the CLI.", () => { expect( resolved1.account.metadata_uri ).toEqual( undefined ); } ); + it( `Should throw an error while querying the yield for less than 100k blocks`, async () => { try { cli(["query", "starname", "yield"]); @@ -609,4 +610,22 @@ describe( "Tests the CLI.", () => { expect(e.message).toContain("not enough data") } } ); + + + it( `Should burn tokens.`, async () => { + const signer = w1; + const amount = 1e6; + const supply0 = { balances: cli( [ "query", "bank", "total" ] ).supply }; + const balance0 = cli( [ "query", "bank", "balances", signer ] ); + const burned = cli( [ "tx", "send", signer, burner, `${amount}${denomFee}`, "--yes", "--broadcast-mode", "block", "--gas-prices", gasPrices, "--memo", memo() ] ); + const supply = { balances: cli( [ "query", "bank", "total" ] ).supply }; + const balance = cli( [ "query", "bank", "balances", signer ] ); + const blackhole = cli( [ "query", "bank", "balances", burner ] ); + + expect( burned.txhash ).toBeDefined(); + if ( !burned.logs ) throw new Error( registered.raw_log ); + expect( +getBalance( supply ) ).toEqual( +getBalance( supply0 ) - amount ); + expect( +getBalance( balance ) ).toBeLessThan( +getBalance( balance0 ) - amount); // less than to account for fees + expect( +getBalance( blackhole ) ).toBe( 0 ); + } ); } ); diff --git a/scripts/integration/test/common.js b/scripts/integration/test/common.js index 52f708688..e23da5244 100644 --- a/scripts/integration/test/common.js +++ b/scripts/integration/test/common.js @@ -22,6 +22,7 @@ export const w1 = "star19jj4wc3lxd54hkzl42m7ze73rzy3dd3wry2f3q"; // w1 export const w2 = "star1l4mvu36chkj9lczjhy9anshptdfm497fune6la"; // w2 export const w3 = "star1aj9qqrftdqussgpnq6lqj08gwy6ysppf53c8e9"; // w3 export const msig1 = "star1d3lhm5vtta78cm7c7ytzqh7z5pcgktmautntqv"; // msig1 +export const burner = "star1v7uw4xhrcv0vk7qp8jf9lu3hm5d8uu5ywlkzeg"; // burner const dirSdk = process.env.COSMOS_SDK_DIR || String( spawnSync( "go", [ "list", "-f", `"{{ .Dir }}"`, "-m", "github.com/cosmos/cosmos-sdk" ] ).stdout ).trim().slice( 1, -1 ); diff --git a/x/burner/abci.go b/x/burner/abci.go new file mode 100644 index 000000000..c10867d44 --- /dev/null +++ b/x/burner/abci.go @@ -0,0 +1,21 @@ +package burner + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/iov-one/starnamed/x/burner/types" +) + +//TODO: we could add a test for this function + +//EndBlocker burns all the coins owned by the burner module +func EndBlocker(ctx sdk.Context, supplyKeeper types.SupplyKeeper, accountKeeper types.AccountKeeper) { + moduleAcc := accountKeeper.GetModuleAccount(ctx, types.ModuleName) + if balance := supplyKeeper.GetAllBalances(ctx, moduleAcc.GetAddress()); !balance.IsZero() { + if err := supplyKeeper.BurnCoins(ctx, types.ModuleName, balance); err != nil { + panic(fmt.Sprintf("Error while burning tokens of the burner module account: %s", err.Error())) + } + } +} diff --git a/x/burner/doc.go b/x/burner/doc.go new file mode 100644 index 000000000..8ecda3995 --- /dev/null +++ b/x/burner/doc.go @@ -0,0 +1,3 @@ +// Package burner contains the burner module, that burns all tokens sent +// to its address (star1v7uw4xhrcv0vk7qp8jf9lu3hm5d8uu5ywlkzeg) +package burner diff --git a/x/burner/module.go b/x/burner/module.go new file mode 100644 index 000000000..ace025f3d --- /dev/null +++ b/x/burner/module.go @@ -0,0 +1,133 @@ +package burner + +import ( + "encoding/json" + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/iov-one/starnamed/x/burner/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// AppModuleBasic defines the basic application module used by the burner module. +type AppModuleBasic struct { + cdc codec.Marshaler +} + +// RegisterLegacyAminoCodec registers the amino codec. +func (b AppModuleBasic) RegisterLegacyAminoCodec(*codec.LegacyAmino) { +} + +// RegisterGRPCGatewayRoutes registers the query handler client. +func (b AppModuleBasic) RegisterGRPCGatewayRoutes(client.Context, *runtime.ServeMux) { +} + +// Name returns the burner module's name. +func (AppModuleBasic) Name() string { return types.ModuleName } + +// DefaultGenesis returns default genesis state as raw bytes for the burner module. +func (AppModuleBasic) DefaultGenesis(codec.JSONMarshaler) json.RawMessage { + return nil +} + +// ValidateGenesis performs genesis state validation for the burner module. +func (b AppModuleBasic) ValidateGenesis(_ codec.JSONMarshaler, _ client.TxEncodingConfig, genesisData json.RawMessage) error { + if len(genesisData) > 0 { + return fmt.Errorf("invalid genesis data for module burner: should be empty") + } + return nil +} + +// RegisterRESTRoutes registers the REST routes for this module. +func (AppModuleBasic) RegisterRESTRoutes(client.Context, *mux.Router) { +} + +// GetQueryCmd returns no root query command for this module. +func (AppModuleBasic) GetQueryCmd() *cobra.Command { + return nil +} + +// GetTxCmd returns the root tx command for this module. +func (AppModuleBasic) GetTxCmd() *cobra.Command { + return nil +} + +// RegisterInterfaces implements InterfaceModule +func (b AppModuleBasic) RegisterInterfaces(cdctypes.InterfaceRegistry) { +} + +// AppModule implements an application module for the burner module. +type AppModule struct { + AppModuleBasic + supplyKeeper types.SupplyKeeper + accountKeeper types.AccountKeeper +} + +// NewAppModule creates a new AppModule object. +func NewAppModule(supplyKeeper types.SupplyKeeper, accountKeeper types.AccountKeeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + supplyKeeper: supplyKeeper, + accountKeeper: accountKeeper, + } +} + +// Name returns the burner module's name. +func (am AppModule) Name() string { return am.AppModuleBasic.Name() } + +// RegisterServices allows a module to register services +func (am AppModule) RegisterServices(module.Configurator) { +} + +// LegacyQuerierHandler provides an sdk.Querier object that uses the legacy amino codec. +func (AppModule) LegacyQuerierHandler(*codec.LegacyAmino) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) { + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "%s", path[0]) + } +} + +// RegisterInvariants registers the burner module invariants. +func (AppModule) RegisterInvariants(sdk.InvariantRegistry) {} + +// Route returns the message routing key for the burner module. +func (am AppModule) Route() sdk.Route { + return sdk.NewRoute(types.ModuleName, func(sdk.Context, sdk.Msg) (*sdk.Result, error) { + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "unknown request") + }) +} + +// QuerierRoute returns the burner module's querier route name. +func (AppModule) QuerierRoute() string { return types.ModuleName } + +// BeginBlock returns the begin blocker for the burner module. +func (AppModule) BeginBlock(sdk.Context, abci.RequestBeginBlock) {} + +// EndBlock returns the end blocker for the burner module. It returns no validator updates. +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + EndBlocker(ctx, am.supplyKeeper, am.accountKeeper) + return []abci.ValidatorUpdate{} +} + +// InitGenesis performs genesis initialization for the burner module. It returns no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, _ codec.JSONMarshaler, _ json.RawMessage) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the exported genesis state as raw bytes for the burner module. +func (am AppModule) ExportGenesis(sdk.Context, codec.JSONMarshaler) json.RawMessage { + return am.DefaultGenesis(nil) +} diff --git a/x/burner/types/expected_keepers.go b/x/burner/types/expected_keepers.go new file mode 100644 index 000000000..b711355f7 --- /dev/null +++ b/x/burner/types/expected_keepers.go @@ -0,0 +1,16 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +type SupplyKeeper interface { + GetAllBalances(sdk.Context, sdk.AccAddress) sdk.Coins + BurnCoins(sdk.Context, string, sdk.Coins) error +} + +type AccountKeeper interface { + GetModuleAddress(string) sdk.AccAddress + GetModuleAccount(sdk.Context, string) types.ModuleAccountI +} diff --git a/x/burner/types/keys.go b/x/burner/types/keys.go new file mode 100644 index 000000000..b12dc9ed9 --- /dev/null +++ b/x/burner/types/keys.go @@ -0,0 +1,8 @@ +package types + +// Module names +const ( + // ModuleName is the name of the module + // the corresponding bech32 address is star1v7uw4xhrcv0vk7qp8jf9lu3hm5d8uu5ywlkzeg + ModuleName = "burner" +)