Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: clawback vesting accounts can return grants to the funder #342

Merged
merged 9 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/core/proto-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,8 @@
- [MsgCreatePeriodicVestingAccountResponse](#cosmos.vesting.v1beta1.MsgCreatePeriodicVestingAccountResponse)
- [MsgCreateVestingAccount](#cosmos.vesting.v1beta1.MsgCreateVestingAccount)
- [MsgCreateVestingAccountResponse](#cosmos.vesting.v1beta1.MsgCreateVestingAccountResponse)
- [MsgReturnGrants](#cosmos.vesting.v1beta1.MsgReturnGrants)
- [MsgReturnGrantsResponse](#cosmos.vesting.v1beta1.MsgReturnGrantsResponse)

- [Msg](#cosmos.vesting.v1beta1.Msg)

Expand Down Expand Up @@ -8731,6 +8733,32 @@ MsgCreateVestingAccountResponse defines the MsgCreateVestingAccount response typ




<a name="cosmos.vesting.v1beta1.MsgReturnGrants"></a>

### MsgReturnGrants
MsgReturnGrants defines a message for a grantee to return the unvested portion of a
vesting grant to the funder. Currently only applies to ClawbackVesting accounts.
Comment on lines +8740 to +8741
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update for current algorithm.

Suggested change
MsgReturnGrants defines a message for a grantee to return the unvested portion of a
vesting grant to the funder. Currently only applies to ClawbackVesting accounts.
MsgReturnGrants defines a message for a grantee to return all granted assets,
including delegated, undelegated and unbonding, vested and unvested,
are transferred to the original funder of the account. Might not be complete if
some vested assets have been transferred out of the account. Currently only applies to
ClawbackVesting accounts.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a generated file, but I made the requested change upstream in proto/cosmos/vesting/v1beta1/tx.proto.



| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `address` | [string](#string) | | address is the address of the grantee account returning the grant. |






<a name="cosmos.vesting.v1beta1.MsgReturnGrantsResponse"></a>

### MsgReturnGrantsResponse
MsgReturnGrantsResponse defines the ReturnGrants response type.





<!-- end messages -->

<!-- end enums -->
Expand All @@ -8749,6 +8777,7 @@ Msg defines the bank Msg service.
| `CreatePeriodicVestingAccount` | [MsgCreatePeriodicVestingAccount](#cosmos.vesting.v1beta1.MsgCreatePeriodicVestingAccount) | [MsgCreatePeriodicVestingAccountResponse](#cosmos.vesting.v1beta1.MsgCreatePeriodicVestingAccountResponse) | CreatePeriodicVestingAccount defines a method that enables creating a periodic vesting account. | |
| `CreateClawbackVestingAccount` | [MsgCreateClawbackVestingAccount](#cosmos.vesting.v1beta1.MsgCreateClawbackVestingAccount) | [MsgCreateClawbackVestingAccountResponse](#cosmos.vesting.v1beta1.MsgCreateClawbackVestingAccountResponse) | CreateClawbackVestingAccount defines a method that enables creating a vesting account that is subject to clawback. | |
| `Clawback` | [MsgClawback](#cosmos.vesting.v1beta1.MsgClawback) | [MsgClawbackResponse](#cosmos.vesting.v1beta1.MsgClawbackResponse) | Clawback removes the unvested tokens from a ClawbackVestingAccount. | |
| `ReturnGrants` | [MsgReturnGrants](#cosmos.vesting.v1beta1.MsgReturnGrants) | [MsgReturnGrantsResponse](#cosmos.vesting.v1beta1.MsgReturnGrantsResponse) | ReturnGrants returns vesting grants to the funder. | |

<!-- end services -->

Expand Down
32 changes: 22 additions & 10 deletions proto/cosmos/vesting/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ service Msg {
rpc CreateClawbackVestingAccount(MsgCreateClawbackVestingAccount) returns (MsgCreateClawbackVestingAccountResponse);
// Clawback removes the unvested tokens from a ClawbackVestingAccount.
rpc Clawback(MsgClawback) returns (MsgClawbackResponse);
// ReturnGrants returns vesting grants to the funder.
rpc ReturnGrants(MsgReturnGrants) returns (MsgReturnGrantsResponse);
}

// MsgCreateVestingAccount defines a message that enables creating a vesting
Expand All @@ -28,17 +30,17 @@ message MsgCreateVestingAccount {
option (gogoproto.equal) = true;

// Address of the account providing the funds, which must also sign the request.
string from_address = 1 [(gogoproto.moretags) = "yaml:\"from_address\""];
string from_address = 1 [(gogoproto.moretags) = "yaml:\"from_address\""];
// Address of the vesting account to create.
string to_address = 2 [(gogoproto.moretags) = "yaml:\"to_address\""];
string to_address = 2 [(gogoproto.moretags) = "yaml:\"to_address\""];
// Amount to transfer to the new account.
repeated cosmos.base.v1beta1.Coin amount = 3
[(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"];
// End time of the vesting duration.
int64 end_time = 4 [(gogoproto.moretags) = "yaml:\"end_time\""];
// If true, creates a DelayedVestingAccount,
// otherwise creates a ContinuousVestingAccount.
bool delayed = 5;
bool delayed = 5;
}

// MsgCreateVestingAccountResponse defines the MsgCreateVestingAccount response type.
Expand All @@ -50,11 +52,11 @@ message MsgCreatePeriodicVestingAccount {
option (gogoproto.equal) = false;

// Address of the account providing the funds, which must also sign the request.
string from_address = 1 [(gogoproto.moretags) = "yaml:\"from_address\""];
string from_address = 1 [(gogoproto.moretags) = "yaml:\"from_address\""];
// Address of the account to receive the funds.
string to_address = 2 [(gogoproto.moretags) = "yaml:\"to_address\""];
string to_address = 2 [(gogoproto.moretags) = "yaml:\"to_address\""];
// Start time of the vesting. Periods start relative to this time.
int64 start_time = 3 [(gogoproto.moretags) = "yaml:\"start_time\""];
int64 start_time = 3 [(gogoproto.moretags) = "yaml:\"start_time\""];
// Vesting events as a sequence of durations and amounts, starting relative to start_time.
repeated Period vesting_periods = 4 [(gogoproto.nullable) = false];
// If true, merge this new grant into an existing PeriodicVestingAccount,
Expand All @@ -74,12 +76,12 @@ message MsgCreateClawbackVestingAccount {
// Address of the account providing the funds, which must also sign the request.
string from_address = 1 [(gogoproto.moretags) = "yaml:\"from_address\""];
// Address of the account to receive the funds.
string to_address = 2 [(gogoproto.moretags) = "yaml:\"to_address\""];
string to_address = 2 [(gogoproto.moretags) = "yaml:\"to_address\""];
// Start time of the vesting. Periods start relative to this time.
int64 start_time = 3 [(gogoproto.moretags) = "yaml:\"start_time\""];
// Unlocking events as a sequence of durations and amounts, starting relative to start_time.
int64 start_time = 3 [(gogoproto.moretags) = "yaml:\"start_time\""];
// Unlocking events as a sequence of durations and amounts, starting relative to start_time.
repeated Period lockup_periods = 4 [(gogoproto.nullable) = false];
// Vesting events as a sequence of durations and amounts, starting relative to start_time.
// Vesting events as a sequence of durations and amounts, starting relative to start_time.
repeated Period vesting_periods = 5 [(gogoproto.nullable) = false];
// If true, merge this new grant into an existing ClawbackVestingAccount,
// or create it if it does not exist. If false, creates a new account.
Expand All @@ -103,3 +105,13 @@ message MsgClawback {

// MsgClawbackResponse defines the MsgClawback response type.
message MsgClawbackResponse {}

// MsgReturnGrants defines a message for a grantee to return the unvested portion of a
// vesting grant to the funder. Currently only applies to ClawbackVesting accounts.
message MsgReturnGrants {
// address is the address of the grantee account returning the grant.
string address = 1;
}

// MsgReturnGrantsResponse defines the ReturnGrants response type.
message MsgReturnGrantsResponse {}
31 changes: 31 additions & 0 deletions x/auth/vesting/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func GetTxCmd() *cobra.Command {
NewMsgCreatePeriodicVestingAccountCmd(),
NewMsgCreateClawbackVestingAccountCmd(),
NewMsgClawbackCmd(),
NewMsgReturnGrantsCmd(),
)

return txCmd
Expand Down Expand Up @@ -311,3 +312,33 @@ func NewMsgClawbackCmd() *cobra.Command {
flags.AddTxFlagsToCmd(cmd)
return cmd
}

// NewMsgReturnGrantsCmd returns a CLI command handler for creating a
// MsgReturnGrantsCmd transaction.
func NewMsgReturnGrantsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "return-grants",
Short: "Transfer grants out of a vesting account.",
Long: `Must be authorized by the vesting account itself.
michaelfig marked this conversation as resolved.
Show resolved Hide resolved
All granted assets, including delegated and undelegating, vested and unvested,
are transferred to the original funder of the account. Might not be complete if
some vested assets have been transferred out of the account.
Currently only supported for ClawbackVestingAccount.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}

msg := types.NewMsgReturnGrants(clientCtx.GetFromAddress())
if err := msg.ValidateBasic(); err != nil {
return err
}

return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
flags.AddTxFlagsToCmd(cmd)
return cmd
}
64 changes: 64 additions & 0 deletions x/auth/vesting/client/testutil/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"github.com/stretchr/testify/suite"

"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
"github.com/cosmos/cosmos-sdk/testutil/network"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -387,6 +390,67 @@ func (s *IntegrationTestSuite) TestNewMsgCreateClawbackVestingAccountCmd() {
}
}

func (s *IntegrationTestSuite) TestNewMsgReturnGrantsCmd() {
val := s.network.Validators[0]

consPrivKey := ed25519.GenPrivKey()
consPubKeyBz, err := s.cfg.Codec.MarshalInterfaceJSON(consPrivKey.PubKey())
s.Require().NoError(err)
s.Require().NotNil(consPubKeyBz)

info, _, err := val.ClientCtx.Keyring.NewMnemonic("NewClawback", keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
s.Require().NoError(err)

addr := sdk.AccAddress(info.GetPubKey().Address())

_, err = clitestutil.ExecTestCLICmd(val.ClientCtx, cli.NewMsgCreateClawbackVestingAccountCmd(), []string{
addr.String(),
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address),
fmt.Sprintf("--%s=%s", cli.FlagLockup, "testdata/periods1.json"),
fmt.Sprintf("--%s=%s", cli.FlagVesting, "testdata/periods1.json"),
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()),
})
s.Require().NoError(err)

for _, tc := range []struct {
name string
args []string
expectErr bool
expectedCode uint32
respType proto.Message
}{
{
name: "basic",
args: []string{
fmt.Sprintf("--%s=%s", flags.FlagFrom, addr),
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()),
},
expectErr: false,
expectedCode: 0,
respType: &sdk.TxResponse{},
},
} {
s.Run(tc.name, func() {
clientCtx := val.ClientCtx

bw, err := clitestutil.ExecTestCLICmd(clientCtx, cli.NewMsgReturnGrantsCmd(), tc.args)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
s.Require().NoError(clientCtx.Codec.UnmarshalJSON(bw.Bytes(), tc.respType), bw.String())

txResp := tc.respType.(*sdk.TxResponse)
s.Require().Equal(tc.expectedCode, txResp.Code)
}
})
}
}

func (s *IntegrationTestSuite) TestNewMsgClawbackCmd() {
val := s.network.Validators[0]
addr := sdk.AccAddress("addr30______________")
Expand Down
15 changes: 15 additions & 0 deletions x/auth/vesting/exported/exported.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,25 @@ type VestingAccount interface {
GetDelegatedVesting() sdk.Coins
}

// Additional vesting account behaviors are abstracted by interfaces to avoid
// cyclic package dependencies. Specifically, the account-wrapping mechanism
// to support liens in Agoric/agoric-sdk.

// AddGrantAction encapsulates the data needed to add a grant to an account.
type AddGrantAction interface {
// AddToAccount adds the grant to the specified account.
// The rawAccount should bypass any account wrappers.
AddToAccount(ctx sdk.Context, rawAccount VestingAccount) error
}

// ReturnGrantAction encapsulates the data needed to return grants from an account.
type ReturnGrantAction interface {
// TakeGrants removes the original vesting amount from the account
// and clears the original vesting amount and schedule.
// The rawAccount should bypass any account wrappers.
TakeGrants(ctx sdk.Context, rawAccount VestingAccount) error
}

// ClabackAction encapsulates the data needed to perform clawback.
type ClawbackAction interface {
// TakeFromAccount removes unvested tokens from the specified account.
Expand Down Expand Up @@ -87,4 +99,7 @@ type ClawbackVestingAccountI interface {

// PostReward preforms post-reward processing described by action.
PostReward(ctx sdk.Context, reward sdk.Coins, action RewardAction) error

// ReturnGrants returns all grants to the funder.
ReturnGrants(ctx sdk.Context, action ReturnGrantAction) error
}
32 changes: 32 additions & 0 deletions x/auth/vesting/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,35 @@ func (s msgServer) Clawback(goCtx context.Context, msg *types.MsgClawback) (*typ

return &types.MsgClawbackResponse{}, nil
}

// ReturnGrants removes the unvested amount from a vesting account,
// returning it to the funder. Currently only supported for ClawbackVestingAccount.
func (s msgServer) ReturnGrants(goCtx context.Context, msg *types.MsgReturnGrants) (*types.MsgReturnGrantsResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

ak := s.AccountKeeper
bk := s.BankKeeper
sk := s.StakingKeeper

addr, err := sdk.AccAddressFromBech32(msg.GetAddress())
if err != nil {
return nil, err
}

acc := ak.GetAccount(ctx, addr)
if acc == nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrNotFound, "account %s does not exist", msg.Address)
}
va, ok := acc.(exported.ClawbackVestingAccountI)
if !ok {
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "account does not support return-grants: %s", msg.Address)
}

returnGrantsAction := types.NewReturnGrantAction(ak, bk, sk)
err = va.ReturnGrants(ctx, returnGrantsAction)
if err != nil {
return nil, err
}

return &types.MsgReturnGrantsResponse{}, nil
}
45 changes: 45 additions & 0 deletions x/auth/vesting/types/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const (

// TypeMsgClawback defines the type value for a MsgClawback.
TypeMsgClawback = "msg_clawback"

// TypeMsgClawback defines the type value for a MsgClawback.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TypeMsgClawback defines the type value for a MsgClawback.
// TypeMsgReturnGrants defines the type value for a MsgReturnGrants.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

TypeMsgReturnGrants = "msg_return_grants"
)

var (
Expand Down Expand Up @@ -309,3 +312,45 @@ func (msg MsgClawback) ValidateBasic() error {

return nil
}

// NewMsgReturnGrants returns a reference to a new MsgReturnGrants.
//
//nolint:interfacer
func NewMsgReturnGrants(addr sdk.AccAddress) *MsgReturnGrants {
return &MsgReturnGrants{
Address: addr.String(),
}
}

// Route returns the message route for a MsgReturnGrants.
func (msg MsgReturnGrants) Route() string { return RouterKey }

// Type returns the message type for a MsgReturnGrants.
func (msg MsgReturnGrants) Type() string { return TypeMsgClawback }

// GetSigners returns the expected signers for a MsgReturnGrants.
func (msg MsgReturnGrants) GetSigners() []sdk.AccAddress {
addr, err := sdk.AccAddressFromBech32(msg.Address)
if err != nil {
panic(err)
}
return []sdk.AccAddress{addr}
}

// GetSignBytes returns the bytes all expected signers must sign over for a
// MsgReturnGrants.
func (msg MsgReturnGrants) GetSignBytes() []byte {
return sdk.MustSortJSON(amino.MustMarshalJSON(&msg))
}

// ValidateBasic Implements Msg.
func (msg MsgReturnGrants) ValidateBasic() error {
addr, err := sdk.AccAddressFromBech32(msg.GetAddress())
if err != nil {
return err
}
if err := sdk.VerifyAddressFormat(addr); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid account address: %s", err)
}
return nil
}
Loading
Loading