diff --git a/CHANGELOG.md b/CHANGELOG.md index 44fcf198b728..da4abf38ce45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -139,6 +139,7 @@ that allows for arbitrary vesting periods. * `ValidateSigCountDecorator`: Validate the number of signatures in tx based on app-parameters. * `IncrementSequenceDecorator`: Increments the account sequence for each signer to prevent replay attacks. * (cli) [\#5223](https://github.com/cosmos/cosmos-sdk/issues/5223) Cosmos Ledger App v2.0.0 is now supported. The changes are backwards compatible and App v1.5.x is still supported. +* (modules) [\#5249](https://github.com/cosmos/cosmos-sdk/pull/5249) Funds are now allowed to be directly sent to the community pool (via the distribution module account). ### Improvements diff --git a/simapp/app.go b/simapp/app.go index 84a1197713c4..1f10c7674bd9 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -72,6 +72,11 @@ var ( staking.NotBondedPoolName: {supply.Burner, supply.Staking}, gov.ModuleName: {supply.Burner}, } + + // module accounts that are allowed to receive tokens + allowedReceivingModAcc = map[string]bool{ + distr.ModuleName: true, + } ) // MakeCodec - custom tx codec @@ -167,7 +172,7 @@ func NewSimApp( ) app.BankKeeper = bank.NewBaseKeeper( app.AccountKeeper, app.subspaces[bank.ModuleName], bank.DefaultCodespace, - app.ModuleAccountAddrs(), + app.BlacklistedAccAddrs(), ) app.SupplyKeeper = supply.NewKeeper( app.cdc, keys[supply.StoreKey], app.AccountKeeper, app.BankKeeper, maccPerms, @@ -322,6 +327,16 @@ func (app *SimApp) ModuleAccountAddrs() map[string]bool { return modAccAddrs } +// BlacklistedAccAddrs returns all the app's module account addresses black listed for receiving tokens. +func (app *SimApp) BlacklistedAccAddrs() map[string]bool { + blacklistedAddrs := make(map[string]bool) + for acc := range maccPerms { + blacklistedAddrs[supply.NewModuleAddress(acc).String()] = !allowedReceivingModAcc[acc] + } + + return blacklistedAddrs +} + // Codec returns SimApp's codec. // // NOTE: This is solely to be used for testing purposes as it may be desirable diff --git a/simapp/app_test.go b/simapp/app_test.go index 4cddce7b6786..2be73cdfbf0e 100644 --- a/simapp/app_test.go +++ b/simapp/app_test.go @@ -42,7 +42,7 @@ func TestBlackListedAddrs(t *testing.T) { app := NewSimApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, 0) for acc := range maccPerms { - require.True(t, app.BankKeeper.BlacklistedAddr(app.SupplyKeeper.GetModuleAddress(acc))) + require.Equal(t, !allowedReceivingModAcc[acc], app.BankKeeper.BlacklistedAddr(app.SupplyKeeper.GetModuleAddress(acc))) } } diff --git a/simapp/test_helpers.go b/simapp/test_helpers.go index 7d91a610893a..c7874bc0b7c9 100644 --- a/simapp/test_helpers.go +++ b/simapp/test_helpers.go @@ -105,7 +105,7 @@ func CheckBalance(t *testing.T, app *SimApp, addr sdk.AccAddress, exp sdk.Coins) ctxCheck := app.BaseApp.NewContext(true, abci.Header{}) res := app.AccountKeeper.GetAccount(ctxCheck, addr) - require.Equal(t, exp, res.GetCoins()) + require.True(t, exp.IsEqual(res.GetCoins())) } // SignCheckDeliver checks a generated signed transaction and simulates a diff --git a/x/bank/app_test.go b/x/bank/app_test.go index 35a42d8d601b..9ec1bdfaa3f3 100644 --- a/x/bank/app_test.go +++ b/x/bank/app_test.go @@ -3,6 +3,8 @@ package bank_test import ( "testing" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/supply" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" @@ -118,36 +120,70 @@ func TestSendNotEnoughBalance(t *testing.T) { require.Equal(t, res2.GetSequence(), origSeq+1) } -// A module account cannot be the recipient of bank sends +// A module account cannot be the recipient of bank sends unless it has been marked as such func TestSendToModuleAcc(t *testing.T) { - acc := &auth.BaseAccount{ - Address: addr1, - Coins: coins, + tests := []struct { + name string + fromBalance sdk.Coins + msg types.MsgSend + expSimPass bool + expPass bool + expFromBalance sdk.Coins + expToBalance sdk.Coins + }{ + { + name: "Normal module account cannot be the recipient of bank sends", + fromBalance: coins, + msg: types.NewMsgSend(addr1, moduleAccAddr, coins), + expSimPass: false, + expPass: false, + expFromBalance: coins, + expToBalance: sdk.NewCoins(), + }, + { + name: "Allowed module account can be the recipient of bank sends", + fromBalance: coins, + msg: types.NewMsgSend(addr1, supply.NewModuleAddress(distribution.ModuleName), coins), + expPass: true, + expSimPass: true, + expFromBalance: sdk.NewCoins(), + expToBalance: coins, + }, } - genAccs := []authexported.GenesisAccount{acc} - app := simapp.SetupWithGenesisAccounts(genAccs) + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + acc := &auth.BaseAccount{ + Address: test.msg.FromAddress, + Coins: test.fromBalance, + } - ctxCheck := app.BaseApp.NewContext(true, abci.Header{}) + genAccs := []authexported.GenesisAccount{acc} + app := simapp.SetupWithGenesisAccounts(genAccs) - res1 := app.AccountKeeper.GetAccount(ctxCheck, addr1) - require.NotNil(t, res1) - require.Equal(t, acc, res1.(*auth.BaseAccount)) + ctxCheck := app.BaseApp.NewContext(true, abci.Header{}) - origAccNum := res1.GetAccountNumber() - origSeq := res1.GetSequence() + res1 := app.AccountKeeper.GetAccount(ctxCheck, test.msg.FromAddress) + require.NotNil(t, res1) + require.Equal(t, acc, res1.(*auth.BaseAccount)) - header := abci.Header{Height: app.LastBlockHeight() + 1} - simapp.SignCheckDeliver(t, app.Codec(), app.BaseApp, header, []sdk.Msg{sendMsg2}, []uint64{origAccNum}, []uint64{origSeq}, false, false, priv1) + origAccNum := res1.GetAccountNumber() + origSeq := res1.GetSequence() - simapp.CheckBalance(t, app, addr1, coins) - simapp.CheckBalance(t, app, moduleAccAddr, sdk.Coins(nil)) + header := abci.Header{Height: app.LastBlockHeight() + 1} + simapp.SignCheckDeliver(t, app.Codec(), app.BaseApp, header, []sdk.Msg{test.msg}, []uint64{origAccNum}, []uint64{origSeq}, test.expSimPass, test.expPass, priv1) - res2 := app.AccountKeeper.GetAccount(app.NewContext(true, abci.Header{}), addr1) - require.NotNil(t, res2) + simapp.CheckBalance(t, app, test.msg.FromAddress, test.expFromBalance) + simapp.CheckBalance(t, app, test.msg.ToAddress, test.expToBalance) - require.Equal(t, res2.GetAccountNumber(), origAccNum) - require.Equal(t, res2.GetSequence(), origSeq+1) + res2 := app.AccountKeeper.GetAccount(app.NewContext(true, abci.Header{}), addr1) + require.NotNil(t, res2) + + require.Equal(t, res2.GetAccountNumber(), origAccNum) + require.Equal(t, res2.GetSequence(), origSeq+1) + }) + } } func TestMsgMultiSendWithAccounts(t *testing.T) { diff --git a/x/bank/internal/keeper/keeper_test.go b/x/bank/internal/keeper/keeper_test.go index e136da5c1cbb..ba6c3f56c440 100644 --- a/x/bank/internal/keeper/keeper_test.go +++ b/x/bank/internal/keeper/keeper_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/cosmos/cosmos-sdk/x/supply" "github.com/tendermint/tendermint/libs/common" "github.com/stretchr/testify/require" @@ -97,7 +98,8 @@ func TestKeeper(t *testing.T) { // Test retrieving black listed accounts for acc := range simapp.GetMaccPerms() { - require.True(t, app.BankKeeper.BlacklistedAddr(app.SupplyKeeper.GetModuleAddress(acc))) + addr := supply.NewModuleAddress(acc) + require.Equal(t, app.BlacklistedAccAddrs()[addr.String()], app.BankKeeper.BlacklistedAddr(addr)) } } diff --git a/x/distribution/client/cli/tx.go b/x/distribution/client/cli/tx.go index 22140eccd21e..b73244d61c08 100644 --- a/x/distribution/client/cli/tx.go +++ b/x/distribution/client/cli/tx.go @@ -47,6 +47,7 @@ func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command { GetCmdWithdrawRewards(cdc), GetCmdSetWithdrawAddr(cdc), GetCmdWithdrawAllRewards(cdc, storeKey), + GetCmdFundCommunityPool(cdc), )...) return distTxCmd @@ -259,3 +260,35 @@ Where proposal.json contains: return cmd } + +// command to fund the community pool +func GetCmdFundCommunityPool(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "fund-community-pool [amount]", + Args: cobra.ExactArgs(1), + Short: "funds the community pool with the specified amount", + Long: strings.TrimSpace( + fmt.Sprintf(`Funds the community pool with the specified amount + +Example: +$ %s tx fund-community-pool 100uatom --from mykey +`, + version.ClientName, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + depositorAddr := cliCtx.GetFromAddress() + amount, err := sdk.ParseCoins(args[0]) + if err != nil { + return err + } + + msg := types.NewMsgDepositIntoCommunityPool(amount, depositorAddr) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } +} diff --git a/x/distribution/client/rest/tx.go b/x/distribution/client/rest/tx.go index 6dbb6f26ea6d..6044e150ed11 100644 --- a/x/distribution/client/rest/tx.go +++ b/x/distribution/client/rest/tx.go @@ -39,6 +39,12 @@ func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router, queryRoute strin withdrawValidatorRewardsHandlerFn(cliCtx), ).Methods("POST") + // Fund the community pool + r.HandleFunc( + "/distribution/community_pool", + fundCommunityPoolHandlerFn(cliCtx), + ).Methods("POST") + } type ( @@ -50,6 +56,11 @@ type ( BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` WithdrawAddress sdk.AccAddress `json:"withdraw_address" yaml:"withdraw_address"` } + + fundCommunityPoolReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Amount sdk.Coins `json:"amount" yaml:"amount"` + } ) // Withdraw delegator rewards @@ -177,6 +188,30 @@ func withdrawValidatorRewardsHandlerFn(cliCtx context.CLIContext) http.HandlerFu } } +// Fund the community pool +func fundCommunityPoolHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req fundCommunityPoolReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + fromAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + msg := types.NewMsgDepositIntoCommunityPool(req.Amount, fromAddr) + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + // Auxiliary func checkDelegatorAddressVar(w http.ResponseWriter, r *http.Request) (sdk.AccAddress, bool) { diff --git a/x/distribution/handler.go b/x/distribution/handler.go index ca047cec3672..d2561241160d 100644 --- a/x/distribution/handler.go +++ b/x/distribution/handler.go @@ -23,6 +23,9 @@ func NewHandler(k keeper.Keeper) sdk.Handler { case types.MsgWithdrawValidatorCommission: return handleMsgWithdrawValidatorCommission(ctx, msg, k) + case types.MsgDepositIntoCommunityPool: + return handleMsgDepositIntoCommunityPool(ctx, msg, k) + default: errMsg := fmt.Sprintf("unrecognized distribution message type: %T", msg) return sdk.ErrUnknownRequest(errMsg).Result() @@ -83,6 +86,22 @@ func handleMsgWithdrawValidatorCommission(ctx sdk.Context, msg types.MsgWithdraw return sdk.Result{Events: ctx.EventManager().Events()} } +func handleMsgDepositIntoCommunityPool(ctx sdk.Context, msg types.MsgDepositIntoCommunityPool, k keeper.Keeper) sdk.Result { + if err := k.DepositCommunityPoolFunds(ctx, msg.Amount, msg.Depositor); err != nil { + return sdk.ResultFromError(err) + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()), + ), + ) + + return sdk.Result{Events: ctx.EventManager().Events()} +} + func NewCommunityPoolSpendProposalHandler(k Keeper) govtypes.Handler { return func(ctx sdk.Context, content govtypes.Content) sdk.Error { switch c := content.(type) { diff --git a/x/distribution/keeper/keeper.go b/x/distribution/keeper/keeper.go index 0f9b56199063..a36834fb7c75 100644 --- a/x/distribution/keeper/keeper.go +++ b/x/distribution/keeper/keeper.go @@ -149,3 +149,16 @@ func (k Keeper) GetTotalRewards(ctx sdk.Context) (totalRewards sdk.DecCoins) { ) return totalRewards } + +// DepositCommunityPoolFunds allows to transfer the specified amount from the sender into the community pool +func (k Keeper) DepositCommunityPoolFunds(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error { + if err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, amount); err != nil { + return err + } + + feePool := k.GetFeePool(ctx) + feePool.CommunityPool = feePool.CommunityPool.Add(sdk.NewDecCoins(amount)) + k.SetFeePool(ctx, feePool) + + return nil +} diff --git a/x/distribution/keeper/keeper_test.go b/x/distribution/keeper/keeper_test.go index 49d4e3c18302..a493e4fea6dd 100644 --- a/x/distribution/keeper/keeper_test.go +++ b/x/distribution/keeper/keeper_test.go @@ -3,6 +3,8 @@ package keeper import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" sdk "github.com/cosmos/cosmos-sdk/types" @@ -89,3 +91,20 @@ func TestGetTotalRewards(t *testing.T) { require.Equal(t, expectedRewards, totalRewards) } + +func TestDepositCommunityPoolFunds(t *testing.T) { + // nolint dogsled + ctx, _, bk, keeper, _, _, _ := CreateTestInputAdvanced(t, false, 1000, sdk.NewDecWithPrec(2, 2)) + + amount := sdk.NewCoins(sdk.NewInt64Coin("stake", 100)) + _ = bk.SetCoins(ctx, delAddr1, amount) + + initPool := keeper.GetFeePool(ctx) + assert.Empty(t, initPool.CommunityPool) + + err := keeper.DepositCommunityPoolFunds(ctx, amount, delAddr1) + assert.Nil(t, err) + + assert.Equal(t, initPool.CommunityPool.Add(sdk.NewDecCoins(amount)), keeper.GetFeePool(ctx).CommunityPool) + assert.Empty(t, bk.GetCoins(ctx, delAddr1)) +} diff --git a/x/distribution/types/msg.go b/x/distribution/types/msg.go index a5eb8422be97..8bc8b1a0dd52 100644 --- a/x/distribution/types/msg.go +++ b/x/distribution/types/msg.go @@ -116,3 +116,41 @@ func (msg MsgWithdrawValidatorCommission) ValidateBasic() sdk.Error { } return nil } + +// msg struct for delegation withdraw from a single validator +type MsgDepositIntoCommunityPool struct { + Amount sdk.Coins `json:"amount" yaml:"amount"` + Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` +} + +func NewMsgDepositIntoCommunityPool(amount sdk.Coins, depositor sdk.AccAddress) MsgDepositIntoCommunityPool { + return MsgDepositIntoCommunityPool{ + Amount: amount, + Depositor: depositor, + } +} + +func (msg MsgDepositIntoCommunityPool) Route() string { return ModuleName } +func (msg MsgDepositIntoCommunityPool) Type() string { return "deposit_into_community_pool" } + +// Return address that must sign over msg.GetSignBytes() +func (msg MsgDepositIntoCommunityPool) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Depositor} +} + +// get the bytes for the message signer to sign on +func (msg MsgDepositIntoCommunityPool) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(msg) + return sdk.MustSortJSON(bz) +} + +// quick validity check +func (msg MsgDepositIntoCommunityPool) ValidateBasic() sdk.Error { + if !msg.Amount.IsValid() { + return sdk.ErrInvalidCoins(msg.Amount.String()) + } + if msg.Depositor.Empty() { + return sdk.ErrInvalidAddress(msg.Depositor.String()) + } + return nil +} diff --git a/x/distribution/types/msg_test.go b/x/distribution/types/msg_test.go index 4fe6125a2cb8..c78931191527 100644 --- a/x/distribution/types/msg_test.go +++ b/x/distribution/types/msg_test.go @@ -72,3 +72,24 @@ func TestMsgWithdrawValidatorCommission(t *testing.T) { } } } + +// test ValidateBasic for MsgDepositIntoCommunityPool +func TestMsgDepositIntoCommunityPool(t *testing.T) { + tests := []struct { + amount sdk.Coins + depositor sdk.AccAddress + expectPass bool + }{ + {sdk.NewCoins(sdk.NewInt64Coin("uatom", 10000)), sdk.AccAddress{}, false}, + {sdk.Coins{sdk.NewInt64Coin("uatom", 10), sdk.NewInt64Coin("uatom", 10)}, delAddr1, false}, + {sdk.NewCoins(sdk.NewInt64Coin("uatom", 1000)), delAddr1, true}, + } + for i, tc := range tests { + msg := NewMsgDepositIntoCommunityPool(tc.amount, tc.depositor) + if tc.expectPass { + require.Nil(t, msg.ValidateBasic(), "test index: %v", i) + } else { + require.NotNil(t, msg.ValidateBasic(), "test index: %v", i) + } + } +}