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 #17810

Merged
merged 9 commits into from
Oct 18, 2023
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* (client) [#17513](https://github.com/cosmos/cosmos-sdk/pull/17513) Allow overwritting `client.toml`. Use `client.CreateClientConfig` in place of `client.ReadFromClientConfig` and provide a custom template and a custom config.
* (x/bank) [#17569](https://github.com/cosmos/cosmos-sdk/pull/17569) Introduce a new message type, `MsgBurn `, to burn coins.
* (server) [#17094](https://github.com/cosmos/cosmos-sdk/pull/17094) Add duration `shutdown-grace` for resource clean up (closing database handles) before exit.
* (x/auth/vesting) [#17810](https://github.com/cosmos/cosmos-sdk/pull/17810) Add the ability to specify a start time for continuous vesting accounts.

### Improvements

Expand Down
243 changes: 152 additions & 91 deletions api/cosmos/vesting/v1beta1/tx.pulsar.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions proto/cosmos/vesting/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ 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).
cmwaters marked this conversation as resolved.
Show resolved Hide resolved
//
// Since 0.51.x
int64 start_time = 6;
}

// MsgCreateVestingAccountResponse defines the Msg/CreateVestingAccount response type.
Expand Down
10 changes: 8 additions & 2 deletions x/auth/vesting/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import (

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

// GetTxCmd returns vesting module's transaction commands.
Expand Down Expand Up @@ -79,13 +80,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.NewMsgCreateVestingAccountWithStartTime(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.")
flags.AddTxFlagsToCmd(cmd)

return cmd
Expand Down
14 changes: 11 additions & 3 deletions x/auth/vesting/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ func (s msgServer) CreateVestingAccount(ctx context.Context, msg *types.MsgCreat
}

if msg.EndTime <= 0 {
tac0turtle marked this conversation as resolved.
Show resolved Hide resolved
return nil, errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "invalid end time")
return nil, sdkerrors.ErrInvalidRequest.Wrap("invalid end time")
}

if msg.EndTime <= msg.StartTime {
return nil, sdkerrors.ErrInvalidRequest.Wrap("invalid start and end time (must be start < end)")
}

if err := s.BankKeeper.IsSendEnabledCoins(ctx, msg.Amount...); err != nil {
Expand All @@ -70,8 +74,12 @@ func (s msgServer) CreateVestingAccount(ctx context.Context, msg *types.MsgCreat
if msg.Delayed {
vestingAccount = types.NewDelayedVestingAccountRaw(baseVestingAccount)
} else {
sdkctx := sdk.UnwrapSDKContext(ctx)
vestingAccount = types.NewContinuousVestingAccountRaw(baseVestingAccount, sdkctx.HeaderInfo().Time.Unix())
start := msg.StartTime
Copy link
Member

@julienrbrt julienrbrt Oct 5, 2023

Choose a reason for hiding this comment

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

Actually, we should add a check that it does not start in the past and that start is a valid time ( >= 0)

Missing the <= 0 check for instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whats the effect of starting at the past? That instantly a portion will be vested right?

Copy link
Member

@julienrbrt julienrbrt Oct 11, 2023

Choose a reason for hiding this comment

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

Probably, but that is a weird use case. Avoiding starting in the past can avoid people making fat-finger mistakes.

if msg.StartTime == 0 {
sdkctx := sdk.UnwrapSDKContext(ctx)
start = sdkctx.HeaderInfo().Time.Unix()
}
vestingAccount = types.NewContinuousVestingAccountRaw(baseVestingAccount, start)
}

s.AccountKeeper.SetAccount(ctx, vestingAccount)
Expand Down
12 changes: 12 additions & 0 deletions x/auth/vesting/types/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ func NewMsgCreateVestingAccount(fromAddr, toAddr sdk.AccAddress, amount sdk.Coin
}
}

// NewMsgCreateVestingAccount returns a reference to a new MsgCreateVestingAccount.
func NewMsgCreateVestingAccountWithStartTime(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,
}
}

// NewMsgCreatePermanentLockedAccount returns a reference to a new MsgCreatePermanentLockedAccount.
func NewMsgCreatePermanentLockedAccount(fromAddr, toAddr sdk.AccAddress, amount sdk.Coins) *MsgCreatePermanentLockedAccount {
return &MsgCreatePermanentLockedAccount{
Expand Down
120 changes: 81 additions & 39 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.

62 changes: 47 additions & 15 deletions x/auth/vesting/types/vesting_account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,70 +67,102 @@ 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, err := types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix())
cva, err := types.NewContinuousVestingAccount(bacc, origCoins, startTime.Unix(), endTime.Unix())
require.NoError(t, err)

// 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))
t50 := time.Duration(0.5 * float64(endTime.Sub(startTime)))
vestedCoins = cva.GetVestedCoins(startTime.Add(t50))
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins)

// require 75% of coins vested
t75 := time.Duration(0.75 * float64(endTime.Sub(startTime)))
vestedCoins = cva.GetVestedCoins(startTime.Add(t75))
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, err := types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix())
cva, err := types.NewContinuousVestingAccount(bacc, origCoins, startTime.Unix(), endTime.Unix())
require.NoError(t, err)

// 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.Equal(t, emptyCoins, 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
t50 := time.Duration(0.5 * float64(endTime.Sub(startTime)))
vestingCoins = cva.GetVestingCoins(startTime.Add(t50))
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
t75 := time.Duration(0.75 * float64(endTime.Sub(startTime)))
vestingCoins = cva.GetVestingCoins(startTime.Add(t75))
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, err := types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix())
cva, err := types.NewContinuousVestingAccount(bacc, origCoins, startTime.Unix(), endTime.Unix())
require.NoError(t, err)

// 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)
cmwaters marked this conversation as resolved.
Show resolved Hide resolved
}

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