Skip to content

Commit

Permalink
feat: allow new amounts to be added to periodic vesting accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
JimLarson committed Nov 4, 2021
1 parent 32a6495 commit 0eaeea9
Show file tree
Hide file tree
Showing 10 changed files with 400 additions and 47 deletions.
1 change: 1 addition & 0 deletions docs/core/proto-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -8206,6 +8206,7 @@ account.
| `to_address` | [string](#string) | | |
| `start_time` | [int64](#int64) | | |
| `vesting_periods` | [Period](#cosmos.vesting.v1beta1.Period) | repeated | |
| `merge` | [bool](#bool) | | |



Expand Down
1 change: 1 addition & 0 deletions proto/cosmos/vesting/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ message MsgCreatePeriodicVestingAccount {
string to_address = 2 [(gogoproto.moretags) = "yaml:\"to_address\""];
int64 start_time = 3 [(gogoproto.moretags) = "yaml:\"start_time\""];
repeated Period vesting_periods = 4 [(gogoproto.nullable) = false];
bool merge = 5;
}

// MsgCreatePeriodicVestingAccountResponse defines the Msg/CreatePeriodicVestingAccount
Expand Down
6 changes: 5 additions & 1 deletion x/auth/vesting/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
// Transaction command flags
const (
FlagDelayed = "delayed"
FlagMerge = "merge"
)

// GetTxCmd returns vesting module's transaction commands.
Expand Down Expand Up @@ -157,7 +158,9 @@ func NewMsgCreatePeriodicVestingAccountCmd() *cobra.Command {
periods = append(periods, period)
}

msg := types.NewMsgCreatePeriodicVestingAccount(clientCtx.GetFromAddress(), toAddr, vestingData.StartTime, periods)
merge, _ := cmd.Flags().GetBool(FlagMerge)

msg := types.NewMsgCreatePeriodicVestingAccount(clientCtx.GetFromAddress(), toAddr, vestingData.StartTime, periods, merge)
if err := msg.ValidateBasic(); err != nil {
return err
}
Expand All @@ -166,6 +169,7 @@ func NewMsgCreatePeriodicVestingAccountCmd() *cobra.Command {
},
}

cmd.Flags().Bool(FlagMerge, false, "Merge new amount and schedule with existing periodic vesting account, if any")
flags.AddTxFlagsToCmd(cmd)

return cmd
Expand Down
14 changes: 14 additions & 0 deletions x/auth/vesting/client/testutil/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,20 @@ func (s *IntegrationTestSuite) TestNewMsgCreatePeriodicVestingAccountCmd() {
},
expectErr: true,
},
"merge": {
args: []string{
sdk.AccAddress("addr9_______________").String(),
"testdata/periods1.json",
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address),
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()),
"--merge",
},
expectErr: false,
expectedCode: 0,
respType: &sdk.TxResponse{},
},
}

for name, tc := range testCases {
Expand Down
79 changes: 77 additions & 2 deletions x/auth/vesting/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package vesting_test

import (
"testing"
"time"

"github.com/stretchr/testify/suite"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
Expand Down Expand Up @@ -111,7 +112,7 @@ func (suite *HandlerTestSuite) TestMsgCreatePeriodicVestingAccount() {
}{
{
name: "create periodic vesting account",
msg: types.NewMsgCreatePeriodicVestingAccount(addr1, addr3, 0, period),
msg: types.NewMsgCreatePeriodicVestingAccount(addr1, addr3, 0, period, false),
expectErr: false,
},
{
Expand All @@ -136,7 +137,7 @@ func (suite *HandlerTestSuite) TestMsgCreatePeriodicVestingAccount() {
},
{
name: "account exists",
msg: types.NewMsgCreatePeriodicVestingAccount(addr1, addr1, 0, period),
msg: types.NewMsgCreatePeriodicVestingAccount(addr1, addr1, 0, period, false),
expectErr: true,
},
}
Expand Down Expand Up @@ -171,6 +172,80 @@ func (suite *HandlerTestSuite) TestMsgCreatePeriodicVestingAccount() {
}
}

func (suite *HandlerTestSuite) TestMsgCreatePeriodicVestingAccount_Merge() {
tst := func(amt int64) sdk.Coin {
return sdk.NewInt64Coin("test", amt)
}
ctx := suite.app.BaseApp.NewContext(false, tmproto.Header{Height: suite.app.LastBlockHeight() + 1})

addr1 := sdk.AccAddress([]byte("addr1_______________"))
addr2 := sdk.AccAddress([]byte("addr2_______________"))
addr3 := sdk.AccAddress([]byte("addr3_______________"))
addr4 := sdk.AccAddress([]byte("addr4_______________"))

// Create the funding account
acc1 := suite.app.AccountKeeper.NewAccountWithAddress(ctx, addr1)
suite.app.AccountKeeper.SetAccount(ctx, acc1)
suite.Require().NoError(simapp.FundAccount(suite.app.BankKeeper, ctx, addr1, sdk.NewCoins(tst(1000))))

// Create a normal account - cannot merge into it
acc2 := suite.app.AccountKeeper.NewAccountWithAddress(ctx, addr2)
suite.app.AccountKeeper.SetAccount(ctx, acc2)
periods := []types.Period{
{Length: 1000, Amount: sdk.NewCoins(tst(60))},
{Length: 1000, Amount: sdk.NewCoins(tst(40))},
}
res, err := suite.handler(ctx, types.NewMsgCreatePeriodicVestingAccount(addr1, addr2, 0, periods, true))
suite.Require().Nil(res, "want nil result when merging with non-periodic vesting account")
suite.Require().Error(err, "want failure when merging with non-periodic vesting account")
funderBalance := suite.app.BankKeeper.GetBalance(ctx, addr1, "test")
suite.Require().Equal(funderBalance, tst(1000))

// Create a PVA normally
res, err = suite.handler(ctx, types.NewMsgCreatePeriodicVestingAccount(addr1, addr3, 0, periods, false))
suite.Require().NotNil(res)
suite.Require().NoError(err)
acc3 := suite.app.AccountKeeper.GetAccount(ctx, addr3)
suite.Require().NotNil(acc3)
suite.Require().IsType(&types.PeriodicVestingAccount{}, acc3)
funderBalance = suite.app.BankKeeper.GetBalance(ctx, addr1, "test")
suite.Require().Equal(funderBalance, tst(900))
balance := suite.app.BankKeeper.GetBalance(ctx, addr3, "test")
suite.Require().Equal(balance, tst(100))

// Add new funding to it
res, err = suite.handler(ctx, types.NewMsgCreatePeriodicVestingAccount(addr1, addr3, 2000, periods, true))
suite.Require().NotNil(res)
suite.Require().NoError(err)
acc3 = suite.app.AccountKeeper.GetAccount(ctx, addr3)
suite.Require().NotNil(acc3)
suite.Require().IsType(&types.PeriodicVestingAccount{}, acc3)
funderBalance = suite.app.BankKeeper.GetBalance(ctx, addr1, "test")
suite.Require().Equal(funderBalance, tst(800))
balance = suite.app.BankKeeper.GetBalance(ctx, addr3, "test")
suite.Require().Equal(balance, tst(200))
pva := acc3.(*types.PeriodicVestingAccount)
suite.Require().True(pva.GetVestingCoins(time.Unix(0, 0)).IsEqual(sdk.NewCoins(tst(200))))
suite.Require().True(pva.GetVestingCoins(time.Unix(1005, 0)).IsEqual(sdk.NewCoins(tst(140))))
suite.Require().True(pva.GetVestingCoins(time.Unix(2005, 0)).IsEqual(sdk.NewCoins(tst(100))))
suite.Require().True(pva.GetVestingCoins(time.Unix(3005, 0)).IsEqual(sdk.NewCoins(tst(40))))
suite.Require().True(pva.GetVestingCoins(time.Unix(4005, 0)).IsEqual(sdk.NewCoins(tst(0))))

// Can create a new periodic vesting account using merge flag too
acc4 := suite.app.AccountKeeper.GetAccount(ctx, addr4)
suite.Require().Nil(acc4)
res, err = suite.handler(ctx, types.NewMsgCreatePeriodicVestingAccount(addr1, addr4, 0, periods, true))
suite.Require().NotNil(res)
suite.Require().NoError(err)
acc4 = suite.app.AccountKeeper.GetAccount(ctx, addr4)
suite.Require().NotNil(acc4)
suite.Require().IsType(&types.PeriodicVestingAccount{}, acc4)
funderBalance = suite.app.BankKeeper.GetBalance(ctx, addr1, "test")
suite.Require().Equal(funderBalance, tst(700))
balance = suite.app.BankKeeper.GetBalance(ctx, addr4, "test")
suite.Require().Equal(balance, tst(100))
}

func TestHandlerTestSuite(t *testing.T) {
suite.Run(t, new(HandlerTestSuite))
}
110 changes: 103 additions & 7 deletions x/auth/vesting/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,86 @@ func (s msgServer) CreateVestingAccount(goCtx context.Context, msg *types.MsgCre
return &types.MsgCreateVestingAccountResponse{}, nil
}

func min64(i, j int64) int64 {
if i < j {
return i
}
return j
}

// mergePeriods returns the merge of two vesting period schedules.
// The merge is defined as the union of the vesting events, with simultaneous
// events combined into a single event.
// Returns new start time, new end time, and merged vesting events, relative to
// the new start time.
func mergePeriods(startP, startQ int64, p, q []types.Period) (int64, int64, []types.Period) {
timeP := startP // time of last merged p event, next p event is relative to this time
timeQ := startQ // time of last merged q event, next q event is relative to this time
iP := 0 // p indexes before this have been merged
iQ := 0 // q indexes before this have been merged
lenP := len(p)
lenQ := len(q)
startTime := min64(startP, startQ) // we pick the earlier time
time := startTime // time of last merged event, or the start time
merged := []types.Period{}

// emit adds a merged period and updates the last event time
emit := func(nextTime int64, amount sdk.Coins) {
period := types.Period{
Length: nextTime - time,
Amount: amount,
}
merged = append(merged, period)
time = nextTime
}

// consumeP emits the next period from p, updating indexes
consumeP := func(nextP int64) {
emit(nextP, p[iP].Amount)
timeP = nextP
iP++
}

// consumeQ emits the next period from q, updating indexes
consumeQ := func(nextQ int64) {
emit(nextQ, q[iQ].Amount)
timeQ = nextQ
iQ++
}

// consumeBoth emits a merge of the next periods from p and q, updating indexes
consumeBoth := func(nextTime int64) {
emit(nextTime, p[iP].Amount.Add(q[iQ].Amount...))
timeP = nextTime
timeQ = nextTime
iP++
iQ++
}

for iP < lenP && iQ < lenQ {
nextP := timeP + p[iP].Length // next p event in absolute time
nextQ := timeQ + q[iQ].Length // next q event in absolute time
if nextP < nextQ {
consumeP(nextP)
} else if nextP > nextQ {
consumeQ(nextQ)
} else {
consumeBoth(nextP)
}
}
for iP < lenP {
// Ragged end - consume remaining p
nextP := timeP + p[iP].Length
consumeP(nextP)
}
for iQ < lenQ {
// Ragged end - consume remaining q
nextQ := timeQ + q[iQ].Length
consumeQ(nextQ)
}
return startTime, time, merged
}

func (s msgServer) CreatePeriodicVestingAccount(goCtx context.Context, msg *types.MsgCreatePeriodicVestingAccount) (*types.MsgCreatePeriodicVestingAccountResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

Expand All @@ -118,19 +198,35 @@ func (s msgServer) CreatePeriodicVestingAccount(goCtx context.Context, msg *type
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", msg.ToAddress)
}

if acc := ak.GetAccount(ctx, to); acc != nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "account %s already exists", msg.ToAddress)
}

var totalCoins sdk.Coins

for _, period := range msg.VestingPeriods {
totalCoins = totalCoins.Add(period.Amount...)
}
totalCoins = totalCoins.Sort()

baseAccount := ak.NewAccountWithAddress(ctx, to)
acc := ak.GetAccount(ctx, to)

acc := types.NewPeriodicVestingAccount(baseAccount.(*authtypes.BaseAccount), totalCoins.Sort(), msg.StartTime, msg.VestingPeriods)
if acc != nil {
pva, ok := acc.(*types.PeriodicVestingAccount)
if !msg.Merge {
if ok {
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "account %s already exists; consider using --merge", msg.ToAddress)
}
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "account %s already exists", msg.ToAddress)
}
if !ok {
return nil, sdkerrors.Wrapf(sdkerrors.ErrNotSupported, "account %s must be a periodic vestic account", msg.ToAddress)
}
newStart, newEnd, newPeriods := mergePeriods(pva.StartTime, msg.GetStartTime(),
pva.GetVestingPeriods(), msg.GetVestingPeriods())
pva.StartTime = newStart
pva.EndTime = newEnd
pva.VestingPeriods = newPeriods
pva.OriginalVesting = pva.OriginalVesting.Add(totalCoins...)
} else {
baseAccount := ak.NewAccountWithAddress(ctx, to)
acc = types.NewPeriodicVestingAccount(baseAccount.(*authtypes.BaseAccount), totalCoins, msg.StartTime, msg.VestingPeriods)
}

ak.SetAccount(ctx, acc)

Expand Down
Loading

0 comments on commit 0eaeea9

Please sign in to comment.