diff --git a/docs/api.md b/docs/api.md index 61630dcef..3e3a93bfe 100644 --- a/docs/api.md +++ b/docs/api.md @@ -6,6 +6,7 @@ ## Table of Contents - [coreum/asset/ft/v1/authz.proto](#coreum/asset/ft/v1/authz.proto) + - [BurnAuthorization](#coreum.asset.ft.v1.BurnAuthorization) - [MintAuthorization](#coreum.asset.ft.v1.MintAuthorization) - [coreum/asset/ft/v1/event.proto](#coreum/asset/ft/v1/event.proto) @@ -329,6 +330,22 @@ + + +### BurnAuthorization +BurnAuthorization allows the grantee to burn up to burn_limit coin from +the granter's account. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `burn_limit` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | repeated | | + + + + + + ### MintAuthorization @@ -338,7 +355,7 @@ the granter's account. | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | -| `mint_limit` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | | +| `mint_limit` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | repeated | | diff --git a/integration-tests/modules/assetft_test.go b/integration-tests/modules/assetft_test.go index a5c198f8a..b7f7df903 100644 --- a/integration-tests/modules/assetft_test.go +++ b/integration-tests/modules/assetft_test.go @@ -2619,7 +2619,7 @@ func TestAuthzMintAuthorizationLimit(t *testing.T) { grantMintMsg, err := authztypes.NewMsgGrant( granter, grantee, - assetfttypes.NewMintAuthorization(sdk.NewCoin(denom, sdk.NewInt(1000))), + assetfttypes.NewMintAuthorization(sdk.NewCoins(sdk.NewCoin(denom, sdk.NewInt(1000)))), lo.ToPtr(time.Now().Add(time.Minute)), ) require.NoError(t, err) @@ -2673,7 +2673,7 @@ func TestAuthzMintAuthorizationLimit(t *testing.T) { requireT.Equal(1, len(gransRes.Grants)) updatedGrant := assetfttypes.MintAuthorization{} chain.ClientContext.Codec().MustUnmarshal(gransRes.Grants[0].Authorization.Value, &updatedGrant) - requireT.EqualValues("499", updatedGrant.MintLimit.Amount.String()) + requireT.EqualValues("499", updatedGrant.MintLimit.AmountOf(denom).String()) // try to mint exceeding limit msgMint = &assetfttypes.MsgMint{ @@ -2754,7 +2754,7 @@ func TestAuthzMintAuthorizationLimit_GrantFromNonIssuer(t *testing.T) { }, }) - // mint and grant authorization + // issue and grant authorization issueMsg := &assetfttypes.MsgIssue{ Issuer: issuer.String(), Symbol: "symbol", @@ -2765,6 +2765,7 @@ func TestAuthzMintAuthorizationLimit_GrantFromNonIssuer(t *testing.T) { assetfttypes.Feature_minting, }, } + _, err := client.BroadcastTx( ctx, chain.ClientContext.WithFromAddress(issuer), @@ -2777,7 +2778,9 @@ func TestAuthzMintAuthorizationLimit_GrantFromNonIssuer(t *testing.T) { grantMintMsg, err := authztypes.NewMsgGrant( granter, grantee, - assetfttypes.NewMintAuthorization(sdk.NewCoin(denom, sdk.NewInt(1000))), + assetfttypes.NewMintAuthorization(sdk.NewCoins( + sdk.NewCoin(denom, sdk.NewInt(1000)), + )), lo.ToPtr(time.Now().Add(time.Minute)), ) require.NoError(t, err) @@ -2821,6 +2824,419 @@ func TestAuthzMintAuthorizationLimit_GrantFromNonIssuer(t *testing.T) { requireT.ErrorIs(err, cosmoserrors.ErrUnauthorized) } +// TestAuthzMintAuthorizationLimit_MultipleCoins tests the authz MintLimitAuthorization msg works as expected +// if there are multiple coins in the grant. +func TestAuthzMintAuthorizationLimit_MultipleCoins(t *testing.T) { + t.Parallel() + + ctx, chain := integrationtests.NewCoreumTestingContext(t) + + requireT := require.New(t) + + authzClient := authztypes.NewQueryClient(chain.ClientContext) + + issuer := chain.GenAccount() + grantee := chain.GenAccount() + + chain.FundAccountWithOptions(ctx, t, issuer, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &assetfttypes.MsgIssue{}, + &assetfttypes.MsgIssue{}, + &authztypes.MsgGrant{}, + }, + Amount: chain.QueryAssetFTParams(ctx, t).IssueFee.Amount.Mul(sdk.NewInt(2)), + }) + + // issue and grant authorization + issueMsg1 := &assetfttypes.MsgIssue{ + Issuer: issuer.String(), + Symbol: "symbolminting", + Subunit: "subunitminting", + Precision: 1, + InitialAmount: sdkmath.NewInt(0), + Features: []assetfttypes.Feature{ + assetfttypes.Feature_minting, + }, + } + + issueMsg2 := &assetfttypes.MsgIssue{ + Issuer: issuer.String(), + Symbol: "symbol", + Subunit: "subunit", + Precision: 1, + InitialAmount: sdkmath.NewInt(0), + Features: []assetfttypes.Feature{}, + } + + _, err := client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(issueMsg1, issueMsg2)), + issueMsg1, issueMsg2, + ) + requireT.NoError(err) + + denom1 := assetfttypes.BuildDenom(issueMsg1.Subunit, issuer) + denom2 := assetfttypes.BuildDenom(issueMsg2.Subunit, issuer) + grantMintMsg, err := authztypes.NewMsgGrant( + issuer, + grantee, + assetfttypes.NewMintAuthorization(sdk.NewCoins( + sdk.NewCoin(denom1, sdk.NewInt(1000)), + sdk.NewCoin(denom2, sdk.NewInt(1000)), + )), + lo.ToPtr(time.Now().Add(time.Minute)), + ) + require.NoError(t, err) + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(grantMintMsg)), + grantMintMsg, + ) + requireT.NoError(err) + + // assert granted + gransRes, err := authzClient.Grants(ctx, &authztypes.QueryGrantsRequest{ + Granter: issuer.String(), + Grantee: grantee.String(), + }) + requireT.NoError(err) + requireT.Equal(1, len(gransRes.Grants)) + + // try to mint using the authz + msgMint := &assetfttypes.MsgMint{ + Sender: issuer.String(), + Coin: sdk.NewCoin(denom1, sdkmath.NewInt(501)), + } + + execMsg := authztypes.NewMsgExec(grantee, []sdk.Msg{msgMint}) + chain.FundAccountWithOptions(ctx, t, grantee, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &execMsg, + }, + }) + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(grantee), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(&execMsg)), + &execMsg, + ) + requireT.NoError(err) + + gransRes, err = authzClient.Grants(ctx, &authztypes.QueryGrantsRequest{ + Granter: issuer.String(), + Grantee: grantee.String(), + }) + requireT.NoError(err) + requireT.Equal(1, len(gransRes.Grants)) + updatedGrant := assetfttypes.BurnAuthorization{} + chain.ClientContext.Codec().MustUnmarshal(gransRes.Grants[0].Authorization.Value, &updatedGrant) + requireT.EqualValues("499", updatedGrant.BurnLimit.AmountOf(denom1).String()) + requireT.EqualValues("1000", updatedGrant.BurnLimit.AmountOf(denom2).String()) +} + +// TestAuthzBurnAuthorizationLimit tests the authz BurnLimitAuthorization msg works as expected. +func TestAuthzBurnAuthorizationLimit(t *testing.T) { + t.Parallel() + + ctx, chain := integrationtests.NewCoreumTestingContext(t) + + requireT := require.New(t) + + bankClient := banktypes.NewQueryClient(chain.ClientContext) + authzClient := authztypes.NewQueryClient(chain.ClientContext) + + granter := chain.GenAccount() + grantee := chain.GenAccount() + + chain.FundAccountWithOptions(ctx, t, granter, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &assetfttypes.MsgIssue{}, + &authztypes.MsgGrant{}, + &authztypes.MsgGrant{}, + }, + Amount: chain.QueryAssetFTParams(ctx, t).IssueFee.Amount, + }) + + // grant authorization + issueMsg := &assetfttypes.MsgIssue{ + Issuer: granter.String(), + Symbol: "symbol", + Subunit: "subunit", + Precision: 1, + InitialAmount: sdkmath.NewInt(10000), + Features: []assetfttypes.Feature{}, + } + denom := assetfttypes.BuildDenom(issueMsg.Subunit, granter) + grantBurnMsg, err := authztypes.NewMsgGrant( + granter, + grantee, + assetfttypes.NewBurnAuthorization(sdk.NewCoins(sdk.NewCoin(denom, sdk.NewInt(1000)))), + lo.ToPtr(time.Now().Add(time.Minute)), + ) + require.NoError(t, err) + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(granter), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(grantBurnMsg, issueMsg)), + grantBurnMsg, issueMsg, + ) + requireT.NoError(err) + + // assert granted + gransRes, err := authzClient.Grants(ctx, &authztypes.QueryGrantsRequest{ + Granter: granter.String(), + Grantee: grantee.String(), + }) + requireT.NoError(err) + requireT.Equal(1, len(gransRes.Grants)) + + // try to burn using the authz + msgBurn := &assetfttypes.MsgBurn{ + Sender: granter.String(), + Coin: sdk.NewCoin(denom, sdkmath.NewInt(501)), + } + + execMsg := authztypes.NewMsgExec(grantee, []sdk.Msg{msgBurn}) + chain.FundAccountWithOptions(ctx, t, grantee, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &execMsg, + }, + }) + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(grantee), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(&execMsg)), + &execMsg, + ) + requireT.NoError(err) + + supply, err := bankClient.SupplyOf(ctx, &banktypes.QuerySupplyOfRequest{Denom: denom}) + requireT.NoError(err) + requireT.EqualValues("9499", supply.Amount.Amount.String()) + + gransRes, err = authzClient.Grants(ctx, &authztypes.QueryGrantsRequest{ + Granter: granter.String(), + Grantee: grantee.String(), + }) + requireT.NoError(err) + requireT.Equal(1, len(gransRes.Grants)) + updatedGrant := assetfttypes.BurnAuthorization{} + chain.ClientContext.Codec().MustUnmarshal(gransRes.Grants[0].Authorization.Value, &updatedGrant) + requireT.EqualValues("499", updatedGrant.BurnLimit.AmountOf(denom).String()) + + // try to burn exceeding limit + msgBurn = &assetfttypes.MsgBurn{ + Sender: granter.String(), + Coin: sdk.NewCoin(denom, sdkmath.NewInt(500)), + } + + execMsg = authztypes.NewMsgExec(grantee, []sdk.Msg{msgBurn}) + chain.FundAccountWithOptions(ctx, t, grantee, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &execMsg, + }, + }) + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(grantee), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(&execMsg)), + &execMsg, + ) + requireT.Error(err) + requireT.ErrorIs(err, cosmoserrors.ErrUnauthorized) + + // burning the entire limit should remove the grant + msgBurn = &assetfttypes.MsgBurn{ + Sender: granter.String(), + Coin: sdk.NewCoin(denom, sdkmath.NewInt(499)), + } + + execMsg = authztypes.NewMsgExec(grantee, []sdk.Msg{msgBurn}) + chain.FundAccountWithOptions(ctx, t, grantee, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &execMsg, + }, + }) + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(grantee), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(&execMsg)), + &execMsg, + ) + requireT.NoError(err) + gransRes, err = authzClient.Grants(ctx, &authztypes.QueryGrantsRequest{ + Granter: granter.String(), + Grantee: grantee.String(), + }) + requireT.NoError(err) + requireT.Equal(0, len(gransRes.Grants)) + + supply, err = bankClient.SupplyOf(ctx, &banktypes.QuerySupplyOfRequest{Denom: denom}) + requireT.NoError(err) + requireT.EqualValues("9000", supply.Amount.Amount.String()) +} + +// TestAuthzBurnAuthorizationLimit_GrantFromNonIssuer tests the authz BurnLimitAuthorization msg works as expected if +// the granter is non-issuer address. +func TestAuthzBurnAuthorizationLimit_GrantFromNonIssuer(t *testing.T) { + t.Parallel() + + ctx, chain := integrationtests.NewCoreumTestingContext(t) + + requireT := require.New(t) + + authzClient := authztypes.NewQueryClient(chain.ClientContext) + + issuer := chain.GenAccount() + granter := chain.GenAccount() + grantee := chain.GenAccount() + + chain.FundAccountWithOptions(ctx, t, issuer, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &assetfttypes.MsgIssue{}, + &assetfttypes.MsgIssue{}, + &banktypes.MsgSend{}, + &banktypes.MsgSend{}, + }, + Amount: chain.QueryAssetFTParams(ctx, t).IssueFee.Amount.Mul(sdk.NewInt(2)), + }) + + chain.FundAccountWithOptions(ctx, t, granter, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &authztypes.MsgGrant{}, + &authztypes.MsgGrant{}, + }, + }) + + // issue and grant authorization + issueWithBurningFeatureMsg := &assetfttypes.MsgIssue{ + Issuer: issuer.String(), + Symbol: "symbolburning", + Subunit: "subunitburning", + Precision: 1, + InitialAmount: sdkmath.NewInt(10000), + Features: []assetfttypes.Feature{ + assetfttypes.Feature_burning, + }, + } + + issueWithoutBurningFeatureMsg := &assetfttypes.MsgIssue{ + Issuer: issuer.String(), + Symbol: "symbol", + Subunit: "subunit", + Precision: 1, + InitialAmount: sdkmath.NewInt(10000), + Features: []assetfttypes.Feature{}, + } + _, err := client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(issueWithBurningFeatureMsg, issueWithoutBurningFeatureMsg)), + issueWithBurningFeatureMsg, issueWithoutBurningFeatureMsg, + ) + requireT.NoError(err) + + denomBurning := assetfttypes.BuildDenom(issueWithBurningFeatureMsg.Subunit, issuer) + denomNoBurning := assetfttypes.BuildDenom(issueWithoutBurningFeatureMsg.Subunit, issuer) + + // send coins to granter + sendMsg := &banktypes.MsgSend{ + FromAddress: issuer.String(), + ToAddress: granter.String(), + Amount: sdk.NewCoins( + sdk.NewCoin(denomBurning, sdkmath.NewInt(1000)), + sdk.NewCoin(denomNoBurning, sdkmath.NewInt(1000)), + ), + } + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(sendMsg)), + sendMsg, + ) + requireT.NoError(err) + + // grant authz to burn + grantMsg, err := authztypes.NewMsgGrant( + granter, + grantee, + assetfttypes.NewBurnAuthorization(sdk.NewCoins( + sdk.NewCoin(denomBurning, sdk.NewInt(1000)), + sdk.NewCoin(denomNoBurning, sdk.NewInt(1000)), + )), + lo.ToPtr(time.Now().Add(time.Minute)), + ) + require.NoError(t, err) + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(granter), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(grantMsg)), + grantMsg, + ) + requireT.NoError(err) + + // assert granted + gransRes, err := authzClient.Grants(ctx, &authztypes.QueryGrantsRequest{ + Granter: granter.String(), + Grantee: grantee.String(), + }) + requireT.NoError(err) + requireT.Equal(1, len(gransRes.Grants)) + + // try to burn using the authz when burning is enabled + msgBurn := &assetfttypes.MsgBurn{ + Sender: granter.String(), + Coin: sdk.NewCoin(denomBurning, sdkmath.NewInt(501)), + } + + execMsg := authztypes.NewMsgExec(grantee, []sdk.Msg{msgBurn}) + chain.FundAccountWithOptions(ctx, t, grantee, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &execMsg, + }, + }) + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(grantee), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(&execMsg)), + &execMsg, + ) + requireT.NoError(err) + + // try to burn using the authz when burning is not enabled + msgBurn = &assetfttypes.MsgBurn{ + Sender: granter.String(), + Coin: sdk.NewCoin(denomNoBurning, sdkmath.NewInt(501)), + } + + execMsg = authztypes.NewMsgExec(grantee, []sdk.Msg{msgBurn}) + chain.FundAccountWithOptions(ctx, t, grantee, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &execMsg, + }, + }) + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(grantee), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(&execMsg)), + &execMsg, + ) + requireT.Error(err) + requireT.ErrorIs(err, assetfttypes.ErrFeatureDisabled) +} + // TestAssetFTBurnRate_OnMinting verifies both burn rate and send commission rate are not applied on received minted tokens. func TestAssetFT_RatesAreNotApplied_OnMinting(t *testing.T) { assertT := assert.New(t) diff --git a/proto/coreum/asset/ft/v1/authz.proto b/proto/coreum/asset/ft/v1/authz.proto index 154b60953..a4ff36661 100644 --- a/proto/coreum/asset/ft/v1/authz.proto +++ b/proto/coreum/asset/ft/v1/authz.proto @@ -14,8 +14,22 @@ message MintAuthorization { option (cosmos_proto.implements_interface) = "cosmos.authz.v1beta1.Authorization"; option (amino.name) = "cosmos-sdk/MintAuthorization"; - cosmos.base.v1beta1.Coin mint_limit = 1 [ + repeated cosmos.base.v1beta1.Coin mint_limit = 1 [ (gogoproto.nullable) = false, - (amino.dont_omitempty) = false + (amino.dont_omitempty) = false, + (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" + ]; +} + +// BurnAuthorization allows the grantee to burn up to burn_limit coin from +// the granter's account. +message BurnAuthorization { + option (cosmos_proto.implements_interface) = "cosmos.authz.v1beta1.Authorization"; + option (amino.name) = "cosmos-sdk/BurnAuthorization"; + + repeated cosmos.base.v1beta1.Coin burn_limit = 1 [ + (gogoproto.nullable) = false, + (amino.dont_omitempty) = false, + (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" ]; } diff --git a/x/asset/ft/client/cli/tx.go b/x/asset/ft/client/cli/tx.go index 2c9909365..1842a5388 100644 --- a/x/asset/ft/client/cli/tx.go +++ b/x/asset/ft/client/cli/tx.go @@ -5,6 +5,7 @@ import ( "sort" "strconv" "strings" + "time" sdkerrors "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" @@ -13,6 +14,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/authz" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -26,6 +28,9 @@ const ( BurnRateFlag = "burn-rate" SendCommissionRateFlag = "send-commission-rate" IBCEnabledFlag = "ibc-enabled" + MintLimitFlag = "mint-limit" + BurnLimitFlag = "burn-limit" + ExpirationFlag = "expiration" ) // GetTxCmd returns the transaction commands for this module. @@ -48,6 +53,7 @@ func GetTxCmd() *cobra.Command { CmdTxGloballyUnfreeze(), CmdTxSetWhitelistedLimit(), CmdTxUpgradeV1(), + CmdGrantAuthorization(), ) return cmd @@ -496,3 +502,93 @@ $ %s tx %s upgrade-v1 ABC-%s --%s=true --from [sender] return cmd } + +// CmdGrantAuthorization returns a CLI command handler for creating a MsgGrant transaction. +func CmdGrantAuthorization() *cobra.Command { + cmd := &cobra.Command{ + Use: "grant [grantee] [message_type=\"mint\"|\"burn\"] --from --burn-limit 10ucore --mint-limit 10ucore", + Short: "Grant authorization to an address", + Long: fmt.Sprintf(`Grant authorization to an address. +Examples: +$ %s tx grant mint --mint-limit 100ucore --expiration 1667979596 + +$ %s tx grant burn --burn-limit 100ucore --expiration 1667979596 +`, version.AppName, version.AppName), + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + grantee, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + var authorization authz.Authorization + switch args[1] { + case "mint": + limit, err := cmd.Flags().GetString(MintLimitFlag) + if err != nil { + return err + } + + limitCoins, err := sdk.ParseCoinsNormalized(limit) + if err != nil { + return err + } + + if !limitCoins.IsAllPositive() { + return fmt.Errorf("mint-limit should be greater than zero") + } + authorization = types.NewMintAuthorization(limitCoins) + case "burn": + limit, err := cmd.Flags().GetString(BurnLimitFlag) + if err != nil { + return err + } + + limitCoins, err := sdk.ParseCoinsNormalized(limit) + if err != nil { + return err + } + + if !limitCoins.IsAllPositive() { + return fmt.Errorf("burn-limit should be greater than zero") + } + authorization = types.NewBurnAuthorization(limitCoins) + default: + return errors.Errorf("invalid authorization types, %s", args[1]) + } + + expire, err := getExpireTime(cmd) + if err != nil { + return err + } + + grantMsg, err := authz.NewMsgGrant(clientCtx.GetFromAddress(), grantee, authorization, expire) + if err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), grantMsg) + }, + } + flags.AddTxFlagsToCmd(cmd) + cmd.Flags().Int64(ExpirationFlag, 0, "Expire time as Unix timestamp. Set zero (0) for no expiry.") + cmd.Flags().String(BurnLimitFlag, "", "The Amount that is allowed to be burnt.") + cmd.Flags().String(MintLimitFlag, "", "The Amount that is allowed to be minted.") + return cmd +} + +func getExpireTime(cmd *cobra.Command) (*time.Time, error) { + exp, err := cmd.Flags().GetInt64(ExpirationFlag) + if err != nil { + return nil, err + } + if exp == 0 { + return nil, nil //nolint:nilnil //the intent of this function is to simplify return nil time. + } + e := time.Unix(exp, 0) + return &e, nil +} diff --git a/x/asset/ft/types/authz.pb.go b/x/asset/ft/types/authz.pb.go index c0241fb71..bdd3095c1 100644 --- a/x/asset/ft/types/authz.pb.go +++ b/x/asset/ft/types/authz.pb.go @@ -6,6 +6,7 @@ package types import ( fmt "fmt" _ "github.com/cosmos/cosmos-proto" + 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" @@ -29,7 +30,7 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // MintAuthorization allows the grantee to mint up to mint_limit coin from // the granter's account. type MintAuthorization struct { - MintLimit types.Coin `protobuf:"bytes,1,opt,name=mint_limit,json=mintLimit,proto3" json:"mint_limit"` + MintLimit github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,1,rep,name=mint_limit,json=mintLimit,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"mint_limit"` } func (m *MintAuthorization) Reset() { *m = MintAuthorization{} } @@ -65,21 +66,68 @@ func (m *MintAuthorization) XXX_DiscardUnknown() { var xxx_messageInfo_MintAuthorization proto.InternalMessageInfo -func (m *MintAuthorization) GetMintLimit() types.Coin { +func (m *MintAuthorization) GetMintLimit() github_com_cosmos_cosmos_sdk_types.Coins { if m != nil { return m.MintLimit } - return types.Coin{} + return nil +} + +// BurnAuthorization allows the grantee to burn up to burn_limit coin from +// the granter's account. +type BurnAuthorization struct { + BurnLimit github_com_cosmos_cosmos_sdk_types.Coins `protobuf:"bytes,1,rep,name=burn_limit,json=burnLimit,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.Coins" json:"burn_limit"` +} + +func (m *BurnAuthorization) Reset() { *m = BurnAuthorization{} } +func (m *BurnAuthorization) String() string { return proto.CompactTextString(m) } +func (*BurnAuthorization) ProtoMessage() {} +func (*BurnAuthorization) Descriptor() ([]byte, []int) { + return fileDescriptor_8e6a458149a08610, []int{1} +} +func (m *BurnAuthorization) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *BurnAuthorization) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_BurnAuthorization.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 *BurnAuthorization) XXX_Merge(src proto.Message) { + xxx_messageInfo_BurnAuthorization.Merge(m, src) +} +func (m *BurnAuthorization) XXX_Size() int { + return m.Size() +} +func (m *BurnAuthorization) XXX_DiscardUnknown() { + xxx_messageInfo_BurnAuthorization.DiscardUnknown(m) +} + +var xxx_messageInfo_BurnAuthorization proto.InternalMessageInfo + +func (m *BurnAuthorization) GetBurnLimit() github_com_cosmos_cosmos_sdk_types.Coins { + if m != nil { + return m.BurnLimit + } + return nil } func init() { proto.RegisterType((*MintAuthorization)(nil), "coreum.asset.ft.v1.MintAuthorization") + proto.RegisterType((*BurnAuthorization)(nil), "coreum.asset.ft.v1.BurnAuthorization") } func init() { proto.RegisterFile("coreum/asset/ft/v1/authz.proto", fileDescriptor_8e6a458149a08610) } var fileDescriptor_8e6a458149a08610 = []byte{ - // 305 bytes of a gzipped FileDescriptorProto + // 344 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4b, 0xce, 0x2f, 0x4a, 0x2d, 0xcd, 0xd5, 0x4f, 0x2c, 0x2e, 0x4e, 0x2d, 0xd1, 0x4f, 0x2b, 0xd1, 0x2f, 0x33, 0xd4, 0x4f, 0x2c, 0x2d, 0xc9, 0xa8, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x82, 0xc8, 0xeb, 0x81, @@ -87,19 +135,21 @@ var fileDescriptor_8e6a458149a08610 = []byte{ 0x24, 0x44, 0x99, 0x94, 0x48, 0x7a, 0x7e, 0x7a, 0x3e, 0x98, 0xa9, 0x0f, 0x62, 0x41, 0x45, 0x25, 0x93, 0xf3, 0x8b, 0x73, 0xf3, 0x8b, 0xe3, 0x21, 0x12, 0x10, 0x0e, 0x54, 0x4a, 0x0e, 0xc2, 0xd3, 0x4f, 0x4a, 0x2c, 0x4e, 0xd5, 0x2f, 0x33, 0x4c, 0x4a, 0x2d, 0x49, 0x34, 0xd4, 0x4f, 0xce, 0xcf, - 0xcc, 0x83, 0xc8, 0x2b, 0x2d, 0x64, 0xe4, 0x12, 0xf4, 0xcd, 0xcc, 0x2b, 0x71, 0x2c, 0x2d, 0xc9, - 0xc8, 0x2f, 0xca, 0xac, 0x4a, 0x2c, 0xc9, 0xcc, 0xcf, 0x13, 0x72, 0xe6, 0xe2, 0xca, 0xcd, 0xcc, - 0x2b, 0x89, 0xcf, 0xc9, 0xcc, 0xcd, 0x2c, 0x91, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x36, 0x92, 0xd4, - 0x83, 0x1a, 0x0c, 0x32, 0x4a, 0x0f, 0x6a, 0x94, 0x9e, 0x73, 0x7e, 0x66, 0x9e, 0x13, 0xe7, 0x89, - 0x7b, 0xf2, 0x0c, 0x2b, 0x9e, 0x6f, 0xd0, 0x62, 0x08, 0xe2, 0x04, 0xe9, 0xf3, 0x01, 0x69, 0xb3, - 0x72, 0x3f, 0xb5, 0x45, 0x57, 0x09, 0xaa, 0x07, 0xe2, 0x55, 0x98, 0x26, 0x14, 0xcb, 0xba, 0x9e, - 0x6f, 0xd0, 0x92, 0x81, 0x28, 0xd3, 0x2d, 0x4e, 0xc9, 0xd6, 0xc7, 0x70, 0x8d, 0x53, 0xc0, 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, 0x99, 0xa5, 0x67, 0x96, 0x64, 0x94, 0x26, - 0xe9, 0x25, 0xe7, 0xe7, 0xea, 0x3b, 0x83, 0x03, 0xd0, 0x2d, 0xbf, 0x34, 0x2f, 0x05, 0xac, 0x4d, - 0x1f, 0x1a, 0xe2, 0x65, 0xc6, 0xfa, 0x15, 0x88, 0x60, 0x2f, 0xa9, 0x2c, 0x48, 0x2d, 0x4e, 0x62, - 0x03, 0x7b, 0xde, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x21, 0x39, 0x17, 0xc1, 0x96, 0x01, 0x00, - 0x00, + 0xcc, 0x83, 0xc8, 0x2b, 0x9d, 0x65, 0xe4, 0x12, 0xf4, 0xcd, 0xcc, 0x2b, 0x71, 0x2c, 0x2d, 0xc9, + 0xc8, 0x2f, 0xca, 0xac, 0x4a, 0x2c, 0xc9, 0xcc, 0xcf, 0x13, 0xca, 0xe7, 0xe2, 0xca, 0xcd, 0xcc, + 0x2b, 0x89, 0xcf, 0xc9, 0xcc, 0xcd, 0x2c, 0x91, 0x60, 0x54, 0x60, 0xd6, 0xe0, 0x36, 0x92, 0xd4, + 0x83, 0x1a, 0x0c, 0x32, 0x4a, 0x0f, 0x6a, 0x94, 0x9e, 0x73, 0x7e, 0x66, 0x9e, 0x93, 0xe9, 0x89, + 0x7b, 0xf2, 0x0c, 0xab, 0xee, 0xcb, 0x6b, 0xa4, 0x67, 0x96, 0x64, 0x94, 0x26, 0xe9, 0x25, 0xe7, + 0xe7, 0x42, 0x5d, 0x01, 0xa5, 0x74, 0x8b, 0x53, 0xb2, 0xf5, 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0xc1, + 0x1a, 0x8a, 0x57, 0x3c, 0xdf, 0xa0, 0xc5, 0x10, 0xc4, 0x09, 0xb2, 0xc3, 0x07, 0x64, 0x85, 0x95, + 0xfb, 0xa9, 0x2d, 0xba, 0x4a, 0x50, 0xf3, 0x21, 0xc1, 0x02, 0xb3, 0x00, 0xc5, 0x61, 0x5d, 0xcf, + 0x37, 0x68, 0xc9, 0x20, 0x19, 0x89, 0xe1, 0x72, 0xb0, 0x7f, 0x9c, 0x4a, 0x8b, 0xf2, 0x30, 0xfc, + 0x93, 0x54, 0x5a, 0x94, 0x47, 0x6b, 0xff, 0x80, 0xec, 0xa0, 0xc8, 0x3f, 0x18, 0x2e, 0x77, 0x0a, + 0x38, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, 0x18, 0x27, 0x3c, 0x96, + 0x63, 0xb8, 0xf0, 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x28, 0x33, 0x24, 0xc7, 0x39, 0x83, + 0x13, 0x8f, 0x5b, 0x7e, 0x69, 0x5e, 0x0a, 0x58, 0x9b, 0x3e, 0x34, 0xb5, 0x95, 0x19, 0xeb, 0x57, + 0x20, 0x92, 0x1c, 0xd8, 0xc1, 0x49, 0x6c, 0xe0, 0x88, 0x37, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, + 0x21, 0xfb, 0x13, 0xa8, 0x92, 0x02, 0x00, 0x00, } func (m *MintAuthorization) Marshal() (dAtA []byte, err error) { @@ -122,16 +172,57 @@ func (m *MintAuthorization) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - { - size, err := m.MintLimit.MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err + if len(m.MintLimit) > 0 { + for iNdEx := len(m.MintLimit) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.MintLimit[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintAuthz(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *BurnAuthorization) 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 *BurnAuthorization) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *BurnAuthorization) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.BurnLimit) > 0 { + for iNdEx := len(m.BurnLimit) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.BurnLimit[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintAuthz(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa } - i -= size - i = encodeVarintAuthz(dAtA, i, uint64(size)) } - i-- - dAtA[i] = 0xa return len(dAtA) - i, nil } @@ -152,8 +243,27 @@ func (m *MintAuthorization) Size() (n int) { } var l int _ = l - l = m.MintLimit.Size() - n += 1 + l + sovAuthz(uint64(l)) + if len(m.MintLimit) > 0 { + for _, e := range m.MintLimit { + l = e.Size() + n += 1 + l + sovAuthz(uint64(l)) + } + } + return n +} + +func (m *BurnAuthorization) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.BurnLimit) > 0 { + for _, e := range m.BurnLimit { + l = e.Size() + n += 1 + l + sovAuthz(uint64(l)) + } + } return n } @@ -221,7 +331,92 @@ func (m *MintAuthorization) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - if err := m.MintLimit.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + m.MintLimit = append(m.MintLimit, types.Coin{}) + if err := m.MintLimit[len(m.MintLimit)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipAuthz(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthAuthz + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *BurnAuthorization) 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 ErrIntOverflowAuthz + } + 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: BurnAuthorization: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: BurnAuthorization: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BurnLimit", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAuthz + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthAuthz + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthAuthz + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.BurnLimit = append(m.BurnLimit, types.Coin{}) + if err := m.BurnLimit[len(m.BurnLimit)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex diff --git a/x/asset/ft/types/burn_authorization.go b/x/asset/ft/types/burn_authorization.go new file mode 100644 index 000000000..96b438a0f --- /dev/null +++ b/x/asset/ft/types/burn_authorization.go @@ -0,0 +1,50 @@ +//nolint:dupl // this code is identical to the mint part, but they should not be merged. +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/authz" +) + +var _ authz.Authorization = &BurnAuthorization{} + +// NewBurnAuthorization returns a new BurnAuthorization object. +func NewBurnAuthorization(burnLimit sdk.Coins) *BurnAuthorization { + return &BurnAuthorization{ + BurnLimit: burnLimit, + } +} + +// MsgTypeURL implements Authorization.MsgTypeURL. +func (a BurnAuthorization) MsgTypeURL() string { + return sdk.MsgTypeURL(&MsgBurn{}) +} + +// Accept implements Authorization.Accept. +func (a BurnAuthorization) Accept(ctx sdk.Context, msg sdk.Msg) (authz.AcceptResponse, error) { + mBurn, ok := msg.(*MsgBurn) + if !ok { + return authz.AcceptResponse{}, sdkerrors.ErrInvalidType.Wrap("type mismatch") + } + + limitLeft, isNegative := a.BurnLimit.SafeSub(mBurn.Coin) + if isNegative { + return authz.AcceptResponse{}, sdkerrors.ErrUnauthorized.Wrapf("requested amount is more than burn limit") + } + + return authz.AcceptResponse{ + Accept: true, + Delete: limitLeft.IsZero(), + Updated: &BurnAuthorization{BurnLimit: limitLeft}, + }, nil +} + +// ValidateBasic implements Authorization.ValidateBasic. +func (a BurnAuthorization) ValidateBasic() error { + if !a.BurnLimit.IsAllPositive() { + return sdkerrors.ErrInvalidCoins.Wrapf("burn limit must be positive") + } + + return nil +} diff --git a/x/asset/ft/types/codec.go b/x/asset/ft/types/codec.go index 600000a0d..efc1e3901 100644 --- a/x/asset/ft/types/codec.go +++ b/x/asset/ft/types/codec.go @@ -27,6 +27,7 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) { registry.RegisterImplementations( (*authz.Authorization)(nil), &MintAuthorization{}, + &BurnAuthorization{}, ) msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) } diff --git a/x/asset/ft/types/mint_authorization.go b/x/asset/ft/types/mint_authorization.go index 7cd9c6b78..aa0d4b7d7 100644 --- a/x/asset/ft/types/mint_authorization.go +++ b/x/asset/ft/types/mint_authorization.go @@ -1,3 +1,4 @@ +//nolint:dupl // this code is identical to the burn part, but they should not be merged. package types import ( @@ -9,7 +10,7 @@ import ( var _ authz.Authorization = &MintAuthorization{} // NewMintAuthorization returns a new MintAuthorization object. -func NewMintAuthorization(mintLimit sdk.Coin) *MintAuthorization { +func NewMintAuthorization(mintLimit sdk.Coins) *MintAuthorization { return &MintAuthorization{ MintLimit: mintLimit, } @@ -27,8 +28,8 @@ func (a MintAuthorization) Accept(ctx sdk.Context, msg sdk.Msg) (authz.AcceptRes return authz.AcceptResponse{}, sdkerrors.ErrInvalidType.Wrap("type mismatch") } - limitLeft, err := a.MintLimit.SafeSub(mMint.Coin) - if err != nil { + limitLeft, isNegative := a.MintLimit.SafeSub(mMint.Coin) + if isNegative { return authz.AcceptResponse{}, sdkerrors.ErrUnauthorized.Wrapf("requested amount is more than mint limit") } @@ -41,8 +42,8 @@ func (a MintAuthorization) Accept(ctx sdk.Context, msg sdk.Msg) (authz.AcceptRes // ValidateBasic implements Authorization.ValidateBasic. func (a MintAuthorization) ValidateBasic() error { - if !a.MintLimit.IsPositive() { - return sdkerrors.ErrInvalidCoins.Wrapf("spend limit must be positive") + if !a.MintLimit.IsAllPositive() { + return sdkerrors.ErrInvalidCoins.Wrapf("mint limit must be positive") } return nil }