diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f6a2c7c7f09..900898acdd84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features * Add functionality to update denom metadata via gov proposal [#270](https://github.com/provenance-io/cosmos-sdk/pull/270) +* (x/authz) [#12648](https://github.com/cosmos/cosmos-sdk/pull/12648) Add an allow list, an optional list of addresses allowed to receive bank assests via authz MsgSend grant. ### Improvements diff --git a/proto/cosmos/bank/v1beta1/authz.proto b/proto/cosmos/bank/v1beta1/authz.proto index 4f58b15e4970..36f695f4c937 100644 --- a/proto/cosmos/bank/v1beta1/authz.proto +++ b/proto/cosmos/bank/v1beta1/authz.proto @@ -16,4 +16,10 @@ message SendAuthorization { repeated cosmos.base.v1beta1.Coin spend_limit = 1 [(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"]; + + // allow_list specifies an optional list of addresses to whom the grantee can send tokens on behalf of the + // granter. If omitted, any recipient is allowed. + // + // Since: cosmos-sdk 0.47 + repeated string allow_list = 2; } diff --git a/x/authz/client/cli/tx.go b/x/authz/client/cli/tx.go index 0cc066e38fdf..daf1c5ef0347 100644 --- a/x/authz/client/cli/tx.go +++ b/x/authz/client/cli/tx.go @@ -27,6 +27,7 @@ const ( FlagAllowedValidators = "allowed-validators" FlagDenyValidators = "deny-validators" FlagAllowedAuthorizations = "allowed-authorizations" + FlagAllowList = "allow-list" delegate = "delegate" redelegate = "redelegate" unbond = "unbond" @@ -110,7 +111,18 @@ Examples: return fmt.Errorf("spend-limit should be greater than zero") } - authorization = bank.NewSendAuthorization(spendLimit) + allowList, err := cmd.Flags().GetStringSlice(FlagAllowList) + if err != nil { + return err + } + + allowed, err := bech32toAccAddresses(allowList) + if err != nil { + return err + } + + authorization = bank.NewSendAuthorization(spendLimit, allowed) + case "generic": msgType, err := cmd.Flags().GetString(FlagMsgType) if err != nil { @@ -157,12 +169,12 @@ Examples: delegateLimit = &spendLimit } - allowed, err := bech32toValidatorAddresses(allowValidators) + allowed, err := bech32toValAddresses(allowValidators) if err != nil { return err } - denied, err := bech32toValidatorAddresses(denyValidators) + denied, err := bech32toValAddresses(denyValidators) if err != nil { return err } @@ -201,6 +213,7 @@ Examples: cmd.Flags().String(FlagSpendLimit, "", "SpendLimit for Send Authorization, an array of Coins allowed spend") cmd.Flags().StringSlice(FlagAllowedValidators, []string{}, "Allowed validators addresses separated by ,") cmd.Flags().StringSlice(FlagDenyValidators, []string{}, "Deny validators addresses separated by ,") + cmd.Flags().StringSlice(FlagAllowList, []string{}, "Allowed addresses grantee is allowed to send funds separated by ,") cmd.Flags().Int64(FlagExpiration, 0, "Expire time as Unix timestamp. Set zero (0) for no expiry. Default is 0.") cmd.Flags().Int32(FlagAllowedAuthorizations, 0, "Allowed authorizations for a Count Authorization") return cmd @@ -291,7 +304,8 @@ Example: return cmd } -func bech32toValidatorAddresses(validators []string) ([]sdk.ValAddress, error) { +// bech32toValAddresses returns []ValAddress from a list of Bech32 string addresses. +func bech32toValAddresses(validators []string) ([]sdk.ValAddress, error) { vals := make([]sdk.ValAddress, len(validators)) for i, validator := range validators { addr, err := sdk.ValAddressFromBech32(validator) @@ -302,3 +316,16 @@ func bech32toValidatorAddresses(validators []string) ([]sdk.ValAddress, error) { } return vals, nil } + +// bech32toAccAddresses returns []AccAddress from a list of Bech32 string addresses. +func bech32toAccAddresses(accAddrs []string) ([]sdk.AccAddress, error) { + addrs := make([]sdk.AccAddress, len(accAddrs)) + for i, addr := range accAddrs { + accAddr, err := sdk.AccAddressFromBech32(addr) + if err != nil { + return nil, err + } + addrs[i] = accAddr + } + return addrs, nil +} diff --git a/x/authz/client/testutil/grpc.go b/x/authz/client/testutil/grpc.go index 20a0f7c903fc..f29f858cb22a 100644 --- a/x/authz/client/testutil/grpc.go +++ b/x/authz/client/testutil/grpc.go @@ -195,7 +195,7 @@ func (s *IntegrationTestSuite) TestQueryGranterGrantsGRPC() { fmt.Sprintf("%s/cosmos/authz/v1beta1/grants/granter/%s", val.APIAddress, val.Address.String()), false, "", - 7, + 8, }, } for _, tc := range testCases { diff --git a/x/authz/client/testutil/query.go b/x/authz/client/testutil/query.go index 45921a054de6..f4326ad4173f 100644 --- a/x/authz/client/testutil/query.go +++ b/x/authz/client/testutil/query.go @@ -161,7 +161,18 @@ func (s *IntegrationTestSuite) TestQueryAuthorization() { fmt.Sprintf("--%s=json", tmcli.OutputFlag), }, false, - `{"@type":"/cosmos.bank.v1beta1.SendAuthorization","spend_limit":[{"denom":"stake","amount":"100"}]}`, + `{"@type":"/cosmos.bank.v1beta1.SendAuthorization","spend_limit":[{"denom":"stake","amount":"100"}],"allow_list":[]}`, + }, + { + "Valid txn with allowed list (json)", + []string{ + val.Address.String(), + s.grantee[3].String(), + typeMsgSend, + fmt.Sprintf("--%s=json", tmcli.OutputFlag), + }, + false, + fmt.Sprintf(`{"@type":"/cosmos.bank.v1beta1.SendAuthorization","spend_limit":[{"denom":"stake","amount":"88"}],"allow_list":["%s"]}`, s.grantee[4]), }, } for _, tc := range testCases { @@ -221,7 +232,7 @@ func (s *IntegrationTestSuite) TestQueryGranterGrants() { }, false, "", - 7, + 8, }, { "valid case with pagination", diff --git a/x/authz/client/testutil/tx.go b/x/authz/client/testutil/tx.go index 9ca24b00758b..99818c6c8f17 100644 --- a/x/authz/client/testutil/tx.go +++ b/x/authz/client/testutil/tx.go @@ -45,7 +45,7 @@ func (s *IntegrationTestSuite) SetupSuite() { s.Require().NoError(err) val := s.network.Validators[0] - s.grantee = make([]sdk.AccAddress, 3) + s.grantee = make([]sdk.AccAddress, 6) // Send some funds to the new account. // Create new account in the keyring. @@ -86,7 +86,7 @@ func (s *IntegrationTestSuite) SetupSuite() { s.grantee[2] = s.createAccount("grantee3") // grant send authorization to grantee3 - out, err = CreateGrant(val, []string{ + _, err = CreateGrant(val, []string{ s.grantee[2].String(), "send", fmt.Sprintf("--%s=100stake", cli.FlagSpendLimit), @@ -98,6 +98,30 @@ func (s *IntegrationTestSuite) SetupSuite() { }) s.Require().NoError(err) + // Create new accounts in the keyring. + s.grantee[3] = s.createAccount("grantee4") + s.msgSendExec(s.grantee[3]) + + s.grantee[4] = s.createAccount("grantee5") + s.grantee[5] = s.createAccount("grantee6") + + // grant send authorization with allow list to grantee4 + out, err = CreateGrant( + val, + []string{ + s.grantee[3].String(), + "send", + fmt.Sprintf("--%s=100stake", cli.FlagSpendLimit), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%d", cli.FlagExpiration, time.Now().Add(time.Minute*time.Duration(120)).Unix()), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=%s", cli.FlagAllowList, s.grantee[4]), + }, + ) + s.Require().NoError(err) + err = s.network.WaitForNextBlock() s.Require().NoError(err) @@ -404,6 +428,40 @@ func (s *IntegrationTestSuite) TestCLITxGrantAuthorization() { false, "", }, + { + "Valid tx send authorization with allow list", + []string{ + grantee.String(), + "send", + fmt.Sprintf("--%s=100stake", cli.FlagSpendLimit), + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%d", cli.FlagExpiration, twoHours), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=%s", cli.FlagAllowList, s.grantee[1]), + }, + 0, + false, + "", + }, + { + "Invalid tx send authorization with duplicate allow list", + []string{ + grantee.String(), + "send", + fmt.Sprintf("--%s=100stake", cli.FlagSpendLimit), + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%d", cli.FlagExpiration, twoHours), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=%s", cli.FlagAllowList, fmt.Sprintf("%s,%s", s.grantee[1], s.grantee[1])), + }, + 0, + true, + "duplicate entry", + }, { "Valid tx generic authorization", []string{ @@ -875,6 +933,86 @@ func (s *IntegrationTestSuite) TestNewExecGrantAuthorized() { } } +func (s *IntegrationTestSuite) TestExecSendAuthzWithAllowList() { + val := s.network.Validators[0] + grantee := s.grantee[3] + allowedAddr := s.grantee[4] + notAllowedAddr := s.grantee[5] + twoHours := time.Now().Add(time.Minute * time.Duration(120)).Unix() + + _, err := CreateGrant( + val, + []string{ + grantee.String(), + "send", + fmt.Sprintf("--%s=100stake", cli.FlagSpendLimit), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%d", cli.FlagExpiration, twoHours), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=%s", cli.FlagAllowList, allowedAddr), + }, + ) + s.Require().NoError(err) + + tokens := sdk.NewCoins( + sdk.NewCoin("stake", sdk.NewInt(12)), + ) + + validGeneratedTx, err := banktestutil.MsgSendExec( + val.ClientCtx, + val.Address, + allowedAddr, + tokens, + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=true", flags.FlagGenerateOnly), + ) + s.Require().NoError(err) + execMsg := testutil.WriteToNewTempFile(s.T(), validGeneratedTx.String()) + + invalidGeneratedTx, err := banktestutil.MsgSendExec( + val.ClientCtx, + val.Address, + notAllowedAddr, + tokens, + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=true", flags.FlagGenerateOnly), + ) + s.Require().NoError(err) + execMsg1 := testutil.WriteToNewTempFile(s.T(), invalidGeneratedTx.String()) + + // test sending to allowed address + args := []string{ + execMsg.Name(), + fmt.Sprintf("--%s=%s", flags.FlagFrom, grantee.String()), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + } + var response sdk.TxResponse + cmd := cli.NewCmdExecAuthorization() + out, err := clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args) + s.Require().NoError(err) + s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), &response), out.String()) + + // test sending to not allowed address + args = []string{ + execMsg1.Name(), + fmt.Sprintf("--%s=%s", flags.FlagFrom, grantee.String()), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + } + out, err = clitestutil.ExecTestCLICmd(val.ClientCtx, cmd, args) + s.Require().NoError(err) + s.Contains(out.String(), fmt.Sprintf("cannot send to %s address", notAllowedAddr)) +} + func (s *IntegrationTestSuite) TestExecDelegateAuthorization() { val := s.network.Validators[0] grantee := s.grantee[0] diff --git a/x/authz/keeper/keeper_test.go b/x/authz/keeper/keeper_test.go index fdf8d5d08135..f54808837259 100644 --- a/x/authz/keeper/keeper_test.go +++ b/x/authz/keeper/keeper_test.go @@ -109,9 +109,10 @@ func (s *TestSuite) TestKeeperIter() { granteeAddr := addrs[1] granter2Addr := addrs[2] e := ctx.BlockTime().AddDate(1, 0, 0) + sendAuthz := banktypes.NewSendAuthorization(coins100, nil) - s.app.AuthzKeeper.SaveGrant(ctx, granteeAddr, granterAddr, banktypes.NewSendAuthorization(coins100), &e) - s.app.AuthzKeeper.SaveGrant(ctx, granteeAddr, granter2Addr, banktypes.NewSendAuthorization(coins100), &e) + s.app.AuthzKeeper.SaveGrant(ctx, granteeAddr, granterAddr, sendAuthz, &e) + s.app.AuthzKeeper.SaveGrant(ctx, granteeAddr, granter2Addr, sendAuthz, &e) app.AuthzKeeper.IterateGrants(ctx, func(granter, grantee sdk.AccAddress, grant authz.Grant) bool { s.Require().Equal(granteeAddr, grantee) @@ -128,7 +129,7 @@ func (s *TestSuite) TestDispatchAction() { granterAddr := addrs[0] granteeAddr := addrs[1] recipientAddr := addrs[2] - a := banktypes.NewSendAuthorization(coins100) + a := banktypes.NewSendAuthorization(coins100, nil) require.NoError(testutil.FundAccount(app.BankKeeper, s.ctx, granterAddr, coins1000)) @@ -374,7 +375,7 @@ func (s *TestSuite) TestGetAuthorization() { genAuthMulti := authz.NewGenericAuthorization(sdk.MsgTypeURL(&banktypes.MsgMultiSend{})) genAuthSend := authz.NewGenericAuthorization(sdk.MsgTypeURL(&banktypes.MsgSend{})) - sendAuth := banktypes.NewSendAuthorization(coins10) + sendAuth := banktypes.NewSendAuthorization(coins10, nil) start := s.ctx.BlockHeader().Time expired := start.Add(time.Duration(1) * time.Second) diff --git a/x/authz/migrations/v046/store_test.go b/x/authz/migrations/v046/store_test.go index 1142c115cb0d..064bbd6ca323 100644 --- a/x/authz/migrations/v046/store_test.go +++ b/x/authz/migrations/v046/store_test.go @@ -32,6 +32,7 @@ func TestMigration(t *testing.T) { blockTime := ctx.BlockTime() oneDay := blockTime.AddDate(0, 0, 1) oneYear := blockTime.AddDate(1, 0, 0) + sendAuthz := banktypes.NewSendAuthorization(coins100, nil) grants := []struct { granter sdk.AccAddress @@ -44,7 +45,7 @@ func TestMigration(t *testing.T) { grantee1, sendMsgType, func() authz.Grant { - any, err := codectypes.NewAnyWithValue(banktypes.NewSendAuthorization(coins100)) + any, err := codectypes.NewAnyWithValue(sendAuthz) require.NoError(t, err) return authz.Grant{ Authorization: any, @@ -57,7 +58,7 @@ func TestMigration(t *testing.T) { grantee2, sendMsgType, func() authz.Grant { - any, err := codectypes.NewAnyWithValue(banktypes.NewSendAuthorization(coins100)) + any, err := codectypes.NewAnyWithValue(sendAuthz) require.NoError(t, err) return authz.Grant{ Authorization: any, diff --git a/x/authz/module/abci_test.go b/x/authz/module/abci_test.go index 9b58c5eadbc7..95fd99bd2811 100644 --- a/x/authz/module/abci_test.go +++ b/x/authz/module/abci_test.go @@ -26,9 +26,10 @@ func TestExpiredGrantsQueue(t *testing.T) { expiration := ctx.BlockTime().AddDate(0, 1, 0) expiration2 := expiration.AddDate(1, 0, 0) smallCoins := sdk.NewCoins(sdk.NewInt64Coin("stake", 10)) + sendAuthz := banktypes.NewSendAuthorization(smallCoins, nil) save := func(grantee sdk.AccAddress, exp *time.Time) { - err := app.AuthzKeeper.SaveGrant(ctx, grantee, granter, banktypes.NewSendAuthorization(smallCoins), exp) + err := app.AuthzKeeper.SaveGrant(ctx, grantee, granter, sendAuthz, exp) require.NoError(t, err, "Grant from %s", grantee.String()) } save(grantee1, &expiration) diff --git a/x/authz/msgs_test.go b/x/authz/msgs_test.go index 0318225df226..9b642a3a82f2 100644 --- a/x/authz/msgs_test.go +++ b/x/authz/msgs_test.go @@ -167,7 +167,8 @@ func TestAminoJSON(t *testing.T) { require.NoError(t, err) grant, err := authz.NewGrant(blockTime, authz.NewGenericAuthorization(typeURL), &expiresAt) require.NoError(t, err) - sendGrant, err := authz.NewGrant(blockTime, banktypes.NewSendAuthorization(sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(1000)))), &expiresAt) + sendAuthz := banktypes.NewSendAuthorization(sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(1000))), nil) + sendGrant, err := authz.NewGrant(blockTime, sendAuthz, &expiresAt) require.NoError(t, err) valAddr, err := sdk.ValAddressFromBech32("cosmosvaloper1xcy3els9ua75kdm783c3qu0rfa2eples6eavqq") require.NoError(t, err) diff --git a/x/authz/simulation/decoder_test.go b/x/authz/simulation/decoder_test.go index c169e84137f6..9aa3b2913926 100644 --- a/x/authz/simulation/decoder_test.go +++ b/x/authz/simulation/decoder_test.go @@ -22,7 +22,8 @@ func TestDecodeStore(t *testing.T) { now := time.Now().UTC() e := now.Add(1) - grant, _ := authz.NewGrant(now, banktypes.NewSendAuthorization(sdk.NewCoins(sdk.NewInt64Coin("foo", 123))), &e) + sendAuthz := banktypes.NewSendAuthorization(sdk.NewCoins(sdk.NewInt64Coin("foo", 123)), nil) + grant, _ := authz.NewGrant(now, sendAuthz, &e) grantBz, err := cdc.Marshal(&grant) require.NoError(t, err) kvPairs := kv.Pairs{ diff --git a/x/authz/simulation/genesis.go b/x/authz/simulation/genesis.go index 8c4609b7bb5c..e1443683bb18 100644 --- a/x/authz/simulation/genesis.go +++ b/x/authz/simulation/genesis.go @@ -37,7 +37,8 @@ func genGrant(r *rand.Rand, accounts []simtypes.Account, genT time.Time) []authz func generateRandomGrant(r *rand.Rand) *codectypes.Any { authorizations := make([]*codectypes.Any, 2) - authorizations[0] = newAnyAuthorization(banktypes.NewSendAuthorization(sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(1000))))) + sendAuthz := banktypes.NewSendAuthorization(sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(1000))), nil) + authorizations[0] = newAnyAuthorization(sendAuthz) authorizations[1] = newAnyAuthorization(authz.NewGenericAuthorization(sdk.MsgTypeURL(&v1.MsgSubmitProposal{}))) return authorizations[r.Intn(len(authorizations))] @@ -57,7 +58,9 @@ func RandomizedGenState(simState *module.SimulationState) { var grants []authz.GrantAuthorization simState.AppParams.GetOrGenerate( simState.Cdc, "authz", &grants, simState.Rand, - func(r *rand.Rand) { grants = genGrant(r, simState.Accounts, simState.GenTimestamp) }, + func(r *rand.Rand) { + grants = genGrant(r, simState.Accounts, simState.GenTimestamp) + }, ) authzGrantsGenesis := authz.NewGenesisState(grants) diff --git a/x/authz/simulation/operations.go b/x/authz/simulation/operations.go index 2818ada5d67c..3eda31484219 100644 --- a/x/authz/simulation/operations.go +++ b/x/authz/simulation/operations.go @@ -112,7 +112,9 @@ func SimulateMsgGrant(ak authz.AccountKeeper, bk authz.BankKeeper, _ keeper.Keep if !t1.Before(ctx.BlockTime()) { expiration = &t1 } - msg, err := authz.NewMsgGrant(granter.Address, grantee.Address, generateRandomAuthorization(r, spendLimit), expiration) + randomAuthz := generateRandomAuthorization(r, spendLimit) + + msg, err := authz.NewMsgGrant(granter.Address, grantee.Address, randomAuthz, expiration) if err != nil { return simtypes.NoOpMsg(authz.ModuleName, TypeMsgGrant, err.Error()), nil, err } @@ -143,7 +145,8 @@ func SimulateMsgGrant(ak authz.AccountKeeper, bk authz.BankKeeper, _ keeper.Keep func generateRandomAuthorization(r *rand.Rand, spendLimit sdk.Coins) authz.Authorization { authorizations := make([]authz.Authorization, 2) - authorizations[0] = banktype.NewSendAuthorization(spendLimit) + sendAuthz := banktype.NewSendAuthorization(spendLimit, nil) + authorizations[0] = sendAuthz authorizations[1] = authz.NewGenericAuthorization(sdk.MsgTypeURL(&banktype.MsgSend{})) return authorizations[r.Intn(len(authorizations))] diff --git a/x/authz/simulation/operations_test.go b/x/authz/simulation/operations_test.go index fab35b4d63c9..32f866256a61 100644 --- a/x/authz/simulation/operations_test.go +++ b/x/authz/simulation/operations_test.go @@ -135,7 +135,7 @@ func (suite *SimTestSuite) TestSimulateRevoke() { granter := accounts[0] grantee := accounts[1] - a := banktypes.NewSendAuthorization(initCoins) + a := banktypes.NewSendAuthorization(initCoins, nil) expire := time.Now().Add(30 * time.Hour) err := suite.app.AuthzKeeper.SaveGrant(suite.ctx, grantee.Address, granter.Address, a, &expire) @@ -170,7 +170,7 @@ func (suite *SimTestSuite) TestSimulateExec() { granter := accounts[0] grantee := accounts[1] - a := banktypes.NewSendAuthorization(initCoins) + a := banktypes.NewSendAuthorization(initCoins, nil) expire := suite.ctx.BlockTime().Add(1 * time.Hour) err := suite.app.AuthzKeeper.SaveGrant(suite.ctx, grantee.Address, granter.Address, a, &expire) diff --git a/x/bank/types/authz.pb.go b/x/bank/types/authz.pb.go index 904a7f9ce793..8a2f6ae3a8f3 100644 --- a/x/bank/types/authz.pb.go +++ b/x/bank/types/authz.pb.go @@ -32,6 +32,11 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // Since: cosmos-sdk 0.43 type SendAuthorization struct { SpendLimit github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,1,rep,name=spend_limit,json=spendLimit,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"spend_limit"` + // allow_list specifies an optional list of addresses to whom the grantee can send tokens on behalf of the + // granter. If omitted, any recipient is allowed. + // + // Since: cosmos-sdk 0.47 + AllowList []string `protobuf:"bytes,2,rep,name=allow_list,json=allowList,proto3" json:"allow_list,omitempty"` } func (m *SendAuthorization) Reset() { *m = SendAuthorization{} } @@ -74,6 +79,13 @@ func (m *SendAuthorization) GetSpendLimit() github_com_cosmos_cosmos_sdk_types.C return nil } +func (m *SendAuthorization) GetAllowList() []string { + if m != nil { + return m.AllowList + } + return nil +} + func init() { proto.RegisterType((*SendAuthorization)(nil), "cosmos.bank.v1beta1.SendAuthorization") } @@ -81,24 +93,25 @@ func init() { func init() { proto.RegisterFile("cosmos/bank/v1beta1/authz.proto", fileDescriptor_a4d2a37888ea779f) } var fileDescriptor_a4d2a37888ea779f = []byte{ - // 257 bytes of a gzipped FileDescriptorProto + // 285 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4f, 0xce, 0x2f, 0xce, 0xcd, 0x2f, 0xd6, 0x4f, 0x4a, 0xcc, 0xcb, 0xd6, 0x2f, 0x33, 0x4c, 0x4a, 0x2d, 0x49, 0x34, 0xd4, 0x4f, 0x2c, 0x2d, 0xc9, 0xa8, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x86, 0x28, 0xd0, 0x03, 0x29, 0xd0, 0x83, 0x2a, 0x90, 0x12, 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0xcb, 0xeb, 0x83, 0x58, 0x10, 0xa5, 0x52, 0x92, 0x10, 0xa5, 0xf1, 0x10, 0x09, 0xa8, 0x3e, 0x88, 0x94, 0x1c, 0xdc, 0x9a, - 0xe2, 0x54, 0xb8, 0x35, 0xc9, 0xf9, 0x99, 0x79, 0x10, 0x79, 0xa5, 0x29, 0x8c, 0x5c, 0x82, 0xc1, + 0xe2, 0x54, 0xb8, 0x35, 0xc9, 0xf9, 0x99, 0x79, 0x10, 0x79, 0xa5, 0xcd, 0x8c, 0x5c, 0x82, 0xc1, 0xa9, 0x79, 0x29, 0x8e, 0xa5, 0x25, 0x19, 0xf9, 0x45, 0x99, 0x55, 0x89, 0x25, 0x99, 0xf9, 0x79, 0x42, 0x39, 0x5c, 0xdc, 0xc5, 0x05, 0xa9, 0x79, 0x29, 0xf1, 0x39, 0x99, 0xb9, 0x99, 0x25, 0x12, 0x8c, 0x0a, 0xcc, 0x1a, 0xdc, 0x46, 0x92, 0x7a, 0x70, 0x17, 0x15, 0xa7, 0xc2, 0x5c, 0xa4, 0xe7, 0x9c, 0x9f, 0x99, 0xe7, 0x64, 0x70, 0xe2, 0x9e, 0x3c, 0xc3, 0xaa, 0xfb, 0xf2, 0x1a, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9, 0x50, 0x67, 0x40, 0x29, 0xdd, 0xe2, 0x94, 0x6c, 0xfd, 0x92, 0xca, 0x82, 0xd4, 0x62, 0xb0, 0x86, 0xe2, 0x20, 0x2e, 0xb0, 0xf9, 0x3e, 0x20, 0xe3, - 0xad, 0x04, 0x4f, 0x6d, 0xd1, 0xe5, 0x45, 0x71, 0x80, 0x93, 0xf3, 0x89, 0x47, 0x72, 0x8c, 0x17, - 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3, 0x85, 0xc7, 0x72, 0x0c, - 0x37, 0x1e, 0xcb, 0x31, 0x44, 0x69, 0xe2, 0xb5, 0xa2, 0x02, 0x12, 0x9e, 0x60, 0x9b, 0x92, 0xd8, - 0xc0, 0x5e, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x8c, 0x86, 0xc3, 0x2e, 0x6b, 0x01, 0x00, - 0x00, + 0x85, 0x64, 0xb9, 0xb8, 0x12, 0x73, 0x72, 0xf2, 0xcb, 0xe3, 0x73, 0x32, 0x8b, 0x4b, 0x24, 0x98, + 0x14, 0x98, 0x35, 0x38, 0x83, 0x38, 0xc1, 0x22, 0x3e, 0x99, 0xc5, 0x25, 0x56, 0x82, 0xa7, 0xb6, + 0xe8, 0xf2, 0xa2, 0xb8, 0xcf, 0xc9, 0xf9, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x1f, + 0x3c, 0x92, 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0xe1, 0xc2, 0x63, 0x39, 0x86, 0x1b, 0x8f, 0xe5, 0x18, + 0xa2, 0x34, 0xf1, 0xba, 0xa0, 0x02, 0x12, 0xdc, 0x60, 0x87, 0x24, 0xb1, 0x81, 0x43, 0xc0, 0x18, + 0x10, 0x00, 0x00, 0xff, 0xff, 0xbd, 0x94, 0xb9, 0xfa, 0x8a, 0x01, 0x00, 0x00, } func (m *SendAuthorization) Marshal() (dAtA []byte, err error) { @@ -121,6 +134,15 @@ func (m *SendAuthorization) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.AllowList) > 0 { + for iNdEx := len(m.AllowList) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.AllowList[iNdEx]) + copy(dAtA[i:], m.AllowList[iNdEx]) + i = encodeVarintAuthz(dAtA, i, uint64(len(m.AllowList[iNdEx]))) + i-- + dAtA[i] = 0x12 + } + } if len(m.SpendLimit) > 0 { for iNdEx := len(m.SpendLimit) - 1; iNdEx >= 0; iNdEx-- { { @@ -161,6 +183,12 @@ func (m *SendAuthorization) Size() (n int) { n += 1 + l + sovAuthz(uint64(l)) } } + if len(m.AllowList) > 0 { + for _, s := range m.AllowList { + l = len(s) + n += 1 + l + sovAuthz(uint64(l)) + } + } return n } @@ -233,6 +261,38 @@ func (m *SendAuthorization) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AllowList", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuthz + } + 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 ErrInvalidLengthAuthz + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthAuthz + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AllowList = append(m.AllowList, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipAuthz(dAtA[iNdEx:]) diff --git a/x/bank/types/errors.go b/x/bank/types/errors.go index 8446d957b678..13af3dc4c7aa 100644 --- a/x/bank/types/errors.go +++ b/x/bank/types/errors.go @@ -12,4 +12,6 @@ var ( ErrSendDisabled = sdkerrors.Register(ModuleName, 5, "send transactions are disabled") ErrDenomMetadataNotFound = sdkerrors.Register(ModuleName, 6, "client denom metadata not found") ErrInvalidKey = sdkerrors.Register(ModuleName, 7, "invalid key") + ErrDuplicateEntry = sdkerrors.Register(ModuleName, 8, "duplicate entry") + ErrMultipleSenders = sdkerrors.Register(ModuleName, 9, "multiple senders not allowed") ) diff --git a/x/bank/types/send_authorization.go b/x/bank/types/send_authorization.go index 7eb90b594918..91b37def1c55 100644 --- a/x/bank/types/send_authorization.go +++ b/x/bank/types/send_authorization.go @@ -6,13 +6,21 @@ import ( "github.com/cosmos/cosmos-sdk/x/authz" ) +// TODO: Revisit this once we have propoer gas fee framework. +// Tracking issues https://github.com/cosmos/cosmos-sdk/issues/9054, https://github.com/cosmos/cosmos-sdk/discussions/9072 +const gasCostPerIteration = uint64(10) + var _ authz.Authorization = &SendAuthorization{} // NewSendAuthorization creates a new SendAuthorization object. -func NewSendAuthorization(spendLimit sdk.Coins) *SendAuthorization { - return &SendAuthorization{ - SpendLimit: spendLimit, - } +func NewSendAuthorization(spendLimit sdk.Coins, allowed []sdk.AccAddress) *SendAuthorization { + allowedAddrs := toBech32Addresses(allowed) + + a := SendAuthorization{} + a.AllowList = allowedAddrs + a.SpendLimit = spendLimit + + return &a } // MsgTypeURL implements Authorization.MsgTypeURL. @@ -26,6 +34,7 @@ func (a SendAuthorization) Accept(ctx sdk.Context, msg sdk.Msg) (authz.AcceptRes if !ok { return authz.AcceptResponse{}, sdkerrors.ErrInvalidType.Wrap("type mismatch") } + toAddr := mSend.ToAddress limitLeft, isNegative := a.SpendLimit.SafeSub(mSend.Amount...) if isNegative { return authz.AcceptResponse{}, sdkerrors.ErrInsufficientFunds.Wrapf("requested amount is more than spend limit") @@ -34,7 +43,22 @@ func (a SendAuthorization) Accept(ctx sdk.Context, msg sdk.Msg) (authz.AcceptRes return authz.AcceptResponse{Accept: true, Delete: true}, nil } - return authz.AcceptResponse{Accept: true, Delete: false, Updated: &SendAuthorization{SpendLimit: limitLeft}}, nil + isAddrExists := false + allowedList := a.GetAllowList() + + for _, addr := range allowedList { + ctx.GasMeter().ConsumeGas(gasCostPerIteration, "send authorization") + if addr == toAddr { + isAddrExists = true + break + } + } + + if len(allowedList) > 0 && !isAddrExists { + return authz.AcceptResponse{}, sdkerrors.ErrUnauthorized.Wrapf("cannot send to %s address", toAddr) + } + + return authz.AcceptResponse{Accept: true, Delete: false, Updated: &SendAuthorization{SpendLimit: limitLeft, AllowList: allowedList}}, nil } // ValidateBasic implements Authorization.ValidateBasic. @@ -45,5 +69,25 @@ func (a SendAuthorization) ValidateBasic() error { if !a.SpendLimit.IsAllPositive() { return sdkerrors.ErrInvalidCoins.Wrapf("spend limit must be positive") } + + found := make(map[string]bool, 0) + for i := 0; i < len(a.AllowList); i++ { + if found[a.AllowList[i]] { + return ErrDuplicateEntry + } + found[a.AllowList[i]] = true + } return nil } + +func toBech32Addresses(allowed []sdk.AccAddress) []string { + if len(allowed) == 0 { + return nil + } + + allowedAddrs := make([]string, len(allowed)) + for i, addr := range allowed { + allowedAddrs[i] = addr.String() + } + return allowedAddrs +} diff --git a/x/bank/types/send_authorization_test.go b/x/bank/types/send_authorization_test.go index 5e058317f1e9..0a2c6caa4edf 100644 --- a/x/bank/types/send_authorization_test.go +++ b/x/bank/types/send_authorization_test.go @@ -1,6 +1,7 @@ package types_test import ( + fmt "fmt" "testing" "github.com/stretchr/testify/require" @@ -12,16 +13,19 @@ import ( ) var ( - coins1000 = sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(1000))) - coins500 = sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(500))) - fromAddr = sdk.AccAddress("_____from _____") - toAddr = sdk.AccAddress("_______to________") + coins1000 = sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(1000))) + coins500 = sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(500))) + fromAddr = sdk.AccAddress("_____from _____") + toAddr = sdk.AccAddress("_______to________") + unknownAddr = sdk.AccAddress("_____unknown_____") ) func TestSendAuthorization(t *testing.T) { app := simapp.Setup(t, false) ctx := app.BaseApp.NewContext(false, tmproto.Header{}) - authorization := types.NewSendAuthorization(coins1000) + allowList := make([]sdk.AccAddress, 1) + allowList[0] = toAddr + authorization := types.NewSendAuthorization(coins1000, nil) t.Log("verify authorization returns valid method name") require.Equal(t, authorization.MsgTypeURL(), "/cosmos.bank.v1beta1.MsgSend") @@ -36,7 +40,7 @@ func TestSendAuthorization(t *testing.T) { require.True(t, resp.Delete) require.Nil(t, resp.Updated) - authorization = types.NewSendAuthorization(coins1000) + authorization = types.NewSendAuthorization(coins1000, nil) require.Equal(t, authorization.MsgTypeURL(), "/cosmos.bank.v1beta1.MsgSend") require.NoError(t, authorization.ValidateBasic()) send = types.NewMsgSend(fromAddr, toAddr, coins500) @@ -47,7 +51,7 @@ func TestSendAuthorization(t *testing.T) { require.NoError(t, err) require.False(t, resp.Delete) require.NotNil(t, resp.Updated) - sendAuth := types.NewSendAuthorization(coins500) + sendAuth := types.NewSendAuthorization(coins500, nil) require.Equal(t, sendAuth.String(), resp.Updated.String()) t.Log("expect updated authorization nil after spending remaining amount") @@ -55,4 +59,17 @@ func TestSendAuthorization(t *testing.T) { require.NoError(t, err) require.True(t, resp.Delete) require.Nil(t, resp.Updated) + + t.Log("allow list and no address") + authzWithAllowList := types.NewSendAuthorization(coins1000, allowList) + require.Equal(t, authzWithAllowList.MsgTypeURL(), "/cosmos.bank.v1beta1.MsgSend") + require.NoError(t, authorization.ValidateBasic()) + send = types.NewMsgSend(fromAddr, unknownAddr, coins500) + require.NoError(t, authzWithAllowList.ValidateBasic()) + resp, err = authzWithAllowList.Accept(ctx, send) + require.False(t, resp.Accept) + require.False(t, resp.Delete) + require.Nil(t, resp.Updated) + require.Error(t, err) + require.Contains(t, err.Error(), fmt.Sprintf("cannot send to %s address", unknownAddr)) }