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!: Add start time for continuous vesting accounts #342

Merged
merged 8 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions proto/cosmos/vesting/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ message MsgCreateVestingAccount {
// end of vesting as unix time (in seconds).
int64 end_time = 4;
bool delayed = 5;
// start of vesting as unix time (in seconds).
int64 start_time = 6;
}

// MsgCreateVestingAccountResponse defines the Msg/CreateVestingAccount response type.
Expand Down
2 changes: 1 addition & 1 deletion x/auth/spec/07_client.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ simd tx vesting create-periodic-vesting-account cosmos1.. periods.json

#### create-vesting-account

The `create-vesting-account` command creates a new vesting account funded with an allocation of tokens. The account can either be a delayed or continuous vesting account, which is determined by the '--delayed' flag. All vesting accouts created will have their start time set by the committed block's time. The end_time must be provided as a UNIX epoch timestamp.
The `create-vesting-account` command creates a new vesting account funded with an allocation of tokens. The account can either be a delayed or continuous vesting account, which is determined by the '--delayed' flag. All vesting accounts created will have their start time set by the committed block's time unless specified explicitly using the `--start-time`flag. The `end_time` must be provided as a UNIX epoch timestamp.

```bash
simd tx vesting create-vesting-account [to_address] [amount] [end_time] [flags]
Expand Down
11 changes: 8 additions & 3 deletions x/auth/vesting/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import (

// Transaction command flags
const (
FlagDelayed = "delayed"
FlagDelayed = "delayed"
FlagStartTime = "start-time"
)

// GetTxCmd returns vesting module's transaction commands.
Expand Down Expand Up @@ -72,14 +73,18 @@ timestamp.`,
}

delayed, _ := cmd.Flags().GetBool(FlagDelayed)
startTime, err := cmd.Flags().GetInt64(FlagStartTime)
if err != nil {
return err
}

msg := types.NewMsgCreateVestingAccount(clientCtx.GetFromAddress(), toAddr, amount, endTime, delayed)

msg := types.NewMsgCreateVestingAccount(clientCtx.GetFromAddress(), toAddr, amount, startTime, endTime, delayed)
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

cmd.Flags().Bool(FlagDelayed, false, "Create a delayed vesting account if true")
cmd.Flags().Int64(FlagStartTime, 0, "Optional start time (as a UNIX epoch timestamp) for continuous vesting accounts. If 0 (default), the block's time of the block this tx is committed to will be used.")
Copy link
Member Author

Choose a reason for hiding this comment

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

Comment for reviewer:

It's a bit odd that end_time is passed as a mandatory arg but start-time as an optional flag:
create-vesting-account [to_address] [amount] [end_time] --start-time ...

flags.AddTxFlagsToCmd(cmd)

return cmd
Expand Down
15 changes: 15 additions & 0 deletions x/auth/vesting/client/testutil/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ func (s *IntegrationTestSuite) TestNewMsgCreateVestingAccountCmd() {
expectedCode: 0,
respType: &sdk.TxResponse{},
},
"create a continuous vesting account with start time": {
args: []string{
sdk.AccAddress("addr2_______________").String(),
sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String(),
"4070908800",
fmt.Sprintf("--%s=%d", cli.FlagStartTime, 4070808800),
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
},
expectErr: false,
expectedCode: 0,
respType: &sdk.TxResponse{},
},
Comment on lines +66 to +80
Copy link
Member

Choose a reason for hiding this comment

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

added a quick test here, but we're not checking that the state really functioned as intented

"create a delayed vesting account": {
args: []string{
sdk.AccAddress("addr3_______________").String(),
Expand Down
14 changes: 13 additions & 1 deletion x/auth/vesting/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ func (s msgServer) CreateVestingAccount(goCtx context.Context, msg *types.MsgCre
return nil, err
}

if msg.EndTime <= 0 {
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "invalid end time")
}

if msg.EndTime <= msg.StartTime {
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "invalid start and end time (must be start < end)")
}
evan-forbes marked this conversation as resolved.
Show resolved Hide resolved

if bk.BlockedAddr(to) {
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", msg.ToAddress)
}
Expand All @@ -60,7 +68,11 @@ func (s msgServer) CreateVestingAccount(goCtx context.Context, msg *types.MsgCre
if msg.Delayed {
vestingAccount = types.NewDelayedVestingAccountRaw(baseVestingAccount)
} else {
vestingAccount = types.NewContinuousVestingAccountRaw(baseVestingAccount, ctx.BlockTime().Unix())
start := ctx.BlockTime().Unix()
if msg.StartTime != 0 {
start = msg.StartTime
}
vestingAccount = types.NewContinuousVestingAccountRaw(baseVestingAccount, start)
}

ak.SetAccount(ctx, vestingAccount)
Expand Down
3 changes: 2 additions & 1 deletion x/auth/vesting/types/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ var _ sdk.Msg = &MsgCreatePeriodicVestingAccount{}
// NewMsgCreateVestingAccount returns a reference to a new MsgCreateVestingAccount.
//
//nolint:interfacer
func NewMsgCreateVestingAccount(fromAddr, toAddr sdk.AccAddress, amount sdk.Coins, endTime int64, delayed bool) *MsgCreateVestingAccount {
func NewMsgCreateVestingAccount(fromAddr, toAddr sdk.AccAddress, amount sdk.Coins, startTime, endTime int64, delayed bool) *MsgCreateVestingAccount {
return &MsgCreateVestingAccount{
FromAddress: fromAddr.String(),
ToAddress: toAddr.String(),
Amount: amount,
StartTime: startTime,
EndTime: endTime,
Delayed: delayed,
}
Expand Down
119 changes: 79 additions & 40 deletions x/auth/vesting/types/tx.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 43 additions & 15 deletions x/auth/vesting/types/vesting_account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,67 +38,95 @@ func (s *VestingAccountTestSuite) SetupTest() {

func TestGetVestedCoinsContVestingAcc(t *testing.T) {
now := tmtime.Now()
endTime := now.Add(24 * time.Hour)
startTime := now.Add(24 * time.Hour)
endTime := startTime.Add(24 * time.Hour)

bacc, origCoins := initBaseAccount()
cva := types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix())
cva := types.NewContinuousVestingAccount(bacc, origCoins, startTime.Unix(), endTime.Unix())

// require no coins vested in the very beginning of the vesting schedule
// require no coins vested _before_ the start time of the vesting schedule
vestedCoins := cva.GetVestedCoins(now)
require.Nil(t, vestedCoins)

// require no coins vested _before_ the very beginning of the vesting schedule
vestedCoins = cva.GetVestedCoins(startTime.Add(-1))
require.Nil(t, vestedCoins)

// require all coins vested at the end of the vesting schedule
vestedCoins = cva.GetVestedCoins(endTime)
require.Equal(t, origCoins, vestedCoins)

// require 50% of coins vested
vestedCoins = cva.GetVestedCoins(now.Add(12 * time.Hour))
vestedCoins = cva.GetVestedCoins(startTime.Add(12 * time.Hour))
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins)

// require 75% of coins vested
vestedCoins = cva.GetVestedCoins(startTime.Add(18 * time.Hour))
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 750), sdk.NewInt64Coin(stakeDenom, 75)}, vestedCoins)

// require 100% of coins vested
vestedCoins = cva.GetVestedCoins(now.Add(48 * time.Hour))
vestedCoins = cva.GetVestedCoins(endTime)
require.Equal(t, origCoins, vestedCoins)
}

func TestGetVestingCoinsContVestingAcc(t *testing.T) {
now := tmtime.Now()
endTime := now.Add(24 * time.Hour)
startTime := now.Add(24 * time.Hour)
endTime := startTime.Add(24 * time.Hour)

bacc, origCoins := initBaseAccount()
cva := types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix())
cva := types.NewContinuousVestingAccount(bacc, origCoins, startTime.Unix(), endTime.Unix())

// require all coins vesting in the beginning of the vesting schedule
// require all coins vesting before the start time of the vesting schedule
vestingCoins := cva.GetVestingCoins(now)
require.Equal(t, origCoins, vestingCoins)

// require all coins vesting right before the start time of the vesting schedule
vestingCoins = cva.GetVestingCoins(startTime.Add(-1))
require.Equal(t, origCoins, vestingCoins)

// require no coins vesting at the end of the vesting schedule
vestingCoins = cva.GetVestingCoins(endTime)
require.Nil(t, vestingCoins)

// require 50% of coins vesting
vestingCoins = cva.GetVestingCoins(now.Add(12 * time.Hour))
// require 50% of coins vesting in the middle between start and end time
vestingCoins = cva.GetVestingCoins(startTime.Add(12 * time.Hour))
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestingCoins)

// require 25% of coins vesting after 3/4 of the time between start and end time has passed
vestingCoins = cva.GetVestingCoins(startTime.Add(18 * time.Hour))
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}, vestingCoins)
}

func TestSpendableCoinsContVestingAcc(t *testing.T) {
now := tmtime.Now()
endTime := now.Add(24 * time.Hour)
startTime := now.Add(24 * time.Hour)
endTime := startTime.Add(24 * time.Hour)

bacc, origCoins := initBaseAccount()
cva := types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix())
cva := types.NewContinuousVestingAccount(bacc, origCoins, startTime.Unix(), endTime.Unix())

// require that all original coins are locked at the end of the vesting
// require that all original coins are locked before the beginning of the vesting
// schedule
lockedCoins := cva.LockedCoins(now)
require.Equal(t, origCoins, lockedCoins)

// require that there exist no locked coins in the beginning of the
// require that all original coins are locked at the beginning of the vesting
// schedule
lockedCoins = cva.LockedCoins(startTime)
require.Equal(t, origCoins, lockedCoins)

// require that there exist no locked coins in the end of the vesting schedule
lockedCoins = cva.LockedCoins(endTime)
require.Equal(t, sdk.NewCoins(), lockedCoins)

// require that all vested coins (50%) are spendable
lockedCoins = cva.LockedCoins(now.Add(12 * time.Hour))
lockedCoins = cva.LockedCoins(startTime.Add(12 * time.Hour))
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, lockedCoins)

// require 25% of coins vesting after 3/4 of the time between start and end time has passed
lockedCoins = cva.LockedCoins(startTime.Add(18 * time.Hour))
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}, lockedCoins)
}

func TestTrackDelegationContVestingAcc(t *testing.T) {
Expand Down