From c530ac3d9bf89620e9e80c4eace172519ae3e31d Mon Sep 17 00:00:00 2001 From: "Masih H. Derkani" Date: Mon, 18 May 2026 13:59:43 +0100 Subject: [PATCH] Optimise vested amounts computation using `math/big` Continuous-vesting proration is amount * elapsed / duration, which is an integer computation. Going through sdk.Dec allocates a scaled big.Int per coin only to truncate the extra precision back away. Use math/big.Int directly and skip the round trip. While at it: * rename the loop-local "x" in PeriodicVestingAccount.GetVestedCoins to "elapsed" so it matches the continuous-vesting variable naming * correct the Validate() error string on continuous and periodic vesting accounts: the predicate fails when start-time >= end-time, so "cannot be before end-time" was reversed * cover two non-aligned timestamps in TestGetVestedCoinsContVestingAcc --- giga/deps/xbank/keeper/send.go | 30 +- giga/deps/xevm/keeper/balance.go | 18 +- precompiles/bank/bank_test.go | 6 +- sei-cosmos/x/auth/vesting/types/msgs.go | 41 +- .../x/auth/vesting/types/vesting_account.go | 46 +- .../vesting/types/vesting_account_test.go | 842 +++++++----------- sei-cosmos/x/bank/keeper/send.go | 52 +- sei-cosmos/x/bank/keeper/send_test.go | 20 +- x/evm/keeper/abci.go | 3 + x/evm/keeper/balance.go | 20 +- 10 files changed, 463 insertions(+), 615 deletions(-) diff --git a/giga/deps/xbank/keeper/send.go b/giga/deps/xbank/keeper/send.go index a6879e9d45..fe0585103f 100644 --- a/giga/deps/xbank/keeper/send.go +++ b/giga/deps/xbank/keeper/send.go @@ -204,9 +204,12 @@ func (k BaseSendKeeper) sendCoinsWithoutAccCreation(ctx sdk.Context, fromAddr sd return nil } -// SubUnlockedCoins removes the unlocked amt coins of the given account. An error is -// returned if the resulting balance is negative or the initial amount is invalid. -// A coin_spent event is emitted after. +// SubUnlockedCoins removes the unlocked amt coins of the given account. An error +// is returned if the resulting balance is negative or the initial amount is +// invalid. A coin_spent event is emitted after. +// +// When checkNeg is false the spendable-balance check is skipped and SubUnsafe +// is used; callers must ensure the operation is sound. func (k BaseSendKeeper) SubUnlockedCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins, checkNeg bool) error { if !amt.IsValid() { return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, amt.String()) @@ -216,12 +219,11 @@ func (k BaseSendKeeper) SubUnlockedCoins(ctx sdk.Context, addr sdk.AccAddress, a for _, coin := range amt { balance := k.GetBalance(ctx, addr, coin.Denom) - if checkNeg { - locked := sdk.NewCoin(coin.Denom, lockedCoins.AmountOf(coin.Denom)) - spendable := balance.Sub(locked) - _, hasNeg := sdk.Coins{spendable}.SafeSub(sdk.Coins{coin}) - if hasNeg { + if checkNeg { + spendableAmt := sdk.MaxInt(balance.Amount.Sub(lockedCoins.AmountOf(coin.Denom)), sdk.ZeroInt()) + if spendableAmt.LT(coin.Amount) { + spendable := sdk.NewCoin(coin.Denom, spendableAmt) return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "%s is smaller than %s", spendable, coin) } } @@ -233,16 +235,12 @@ func (k BaseSendKeeper) SubUnlockedCoins(ctx sdk.Context, addr sdk.AccAddress, a newBalance = balance.SubUnsafe(coin) } - err := k.setBalance(ctx, addr, newBalance, checkNeg) - if err != nil { + if err := k.setBalance(ctx, addr, newBalance, checkNeg); err != nil { return err } } - // emit coin spent event - ctx.EventManager().EmitEvent( - types.NewCoinSpentEvent(addr, amt), - ) + ctx.EventManager().EmitEvent(types.NewCoinSpentEvent(addr, amt)) return nil } @@ -378,6 +376,10 @@ func (k BaseSendKeeper) SubWei(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "%swei is smaller than %swei", currentAggregatedBalance, amt) } useiBalance, weiBalance := SplitUseiWeiAmount(postAggregatedbalance) + lockedUsei := k.LockedCoins(ctx, addr).AmountOf(sdk.MustGetBaseDenom()) + if useiBalance.LT(lockedUsei) { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "%suei is smaller than locked %suei", useiBalance, lockedUsei) + } if err := k.setBalance(ctx, addr, sdk.NewCoin(sdk.MustGetBaseDenom(), useiBalance), true); err != nil { return err } diff --git a/giga/deps/xevm/keeper/balance.go b/giga/deps/xevm/keeper/balance.go index 4fdcedd223..882c9debe2 100644 --- a/giga/deps/xevm/keeper/balance.go +++ b/giga/deps/xevm/keeper/balance.go @@ -7,11 +7,19 @@ import ( sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" ) +// GetBalance returns addr's EVM-denominated balance in wei: spendable usei +// (scaled to wei) plus the wei remainder. +// +// Spendable usei is computed as (total − locked) rather than via +// BankKeeper.SpendableCoins, which iterates; LockedCoins does not. func (k *Keeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress) *big.Int { + bk := k.BankKeeper() denom := k.GetBaseDenom(ctx) - allUsei := k.BankKeeper().GetBalance(ctx, addr, denom).Amount - lockedUsei := k.BankKeeper().LockedCoins(ctx, addr).AmountOf(denom) // LockedCoins doesn't use iterators - usei := allUsei.Sub(lockedUsei) - wei := k.BankKeeper().GetWeiBalance(ctx, addr) - return usei.Mul(state.SdkUseiToSweiMultiplier).Add(wei).BigInt() + + total := bk.GetBalance(ctx, addr, denom).Amount + locked := bk.LockedCoins(ctx, addr).AmountOf(denom) + spendable := sdk.MaxInt(total.Sub(locked), sdk.ZeroInt()) + + wei := bk.GetWeiBalance(ctx, addr) + return spendable.Mul(state.SdkUseiToSweiMultiplier).Add(wei).BigInt() } diff --git a/precompiles/bank/bank_test.go b/precompiles/bank/bank_test.go index a1cae321e8..bdd79c4f17 100644 --- a/precompiles/bank/bank_test.go +++ b/precompiles/bank/bank_test.go @@ -193,9 +193,9 @@ func TestRun(t *testing.T) { sdk.NewAttribute(banktypes.AttributeKeySender, senderAddr.String()), ), // gas refund to the sender - banktypes.NewCoinReceivedEvent(senderAddr, sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(132401)))), + banktypes.NewCoinReceivedEvent(senderAddr, sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(131092)))), // tip is paid to the validator - banktypes.NewCoinReceivedEvent(sdk.MustAccAddressFromBech32("sei1v4mx6hmrda5kucnpwdjsqqqqqqqqqqqqlve8dv"), sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(67599)))), + banktypes.NewCoinReceivedEvent(sdk.MustAccAddressFromBech32("sei1v4mx6hmrda5kucnpwdjsqqqqqqqqqqqqlve8dv"), sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(68908)))), } require.EqualValues(t, expectedEvts.ToABCIEvents(), evts) @@ -244,7 +244,7 @@ func TestRun(t *testing.T) { Denom: "ufoo", }, bank.CoinBalance(parsedBalances[0])) require.Equal(t, bank.CoinBalance{ - Amount: big.NewInt(9932390), + Amount: big.NewInt(9931081), Denom: "usei", }, bank.CoinBalance(parsedBalances[1])) diff --git a/sei-cosmos/x/auth/vesting/types/msgs.go b/sei-cosmos/x/auth/vesting/types/msgs.go index 7082d2fa05..c958cc5a60 100644 --- a/sei-cosmos/x/auth/vesting/types/msgs.go +++ b/sei-cosmos/x/auth/vesting/types/msgs.go @@ -8,6 +8,10 @@ import ( // TypeMsgCreateVestingAccount defines the type value for a MsgCreateVestingAccount. const TypeMsgCreateVestingAccount = "msg_create_vesting_account" +// maxVestingCoinAmountBitLen bounds the magnitude of a single coin amount in +// MsgCreateVestingAccount. 2^200 is well above any realistic token supply. +const maxVestingCoinAmountBitLen = 200 + var _ sdk.Msg = &MsgCreateVestingAccount{} // NewMsgCreateVestingAccount returns a reference to a new MsgCreateVestingAccount. @@ -30,27 +34,15 @@ func (msg MsgCreateVestingAccount) Type() string { return TypeMsgCreateVestingAc // ValidateBasic Implements Msg. func (msg MsgCreateVestingAccount) ValidateBasic() error { - from, err := sdk.AccAddressFromBech32(msg.FromAddress) - if err != nil { + + if err := validateAddr(msg.FromAddress, "sender"); err != nil { return err } - to, err := sdk.AccAddressFromBech32(msg.ToAddress) - if err != nil { + if err := validateAddr(msg.ToAddress, "recipient"); err != nil { return err } - if err := sdk.VerifyAddressFormat(from); err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid sender address: %s", err) - } - - if err := sdk.VerifyAddressFormat(to); err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid recipient address: %s", err) - } - - if !msg.Amount.IsValid() { - return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String()) - } - if !msg.Amount.IsAllPositive() { + if !msg.Amount.IsValid() || !msg.Amount.IsAllPositive() { return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String()) } @@ -58,6 +50,23 @@ func (msg MsgCreateVestingAccount) ValidateBasic() error { return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "invalid end time") } + for _, c := range msg.Amount { + if c.Amount.BigInt().BitLen() > maxVestingCoinAmountBitLen { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "%s amount is out of range", c.Denom) + } + } + + return nil +} + +func validateAddr(bech32, label string) error { + addr, err := sdk.AccAddressFromBech32(bech32) + if err != nil { + return err + } + if err := sdk.VerifyAddressFormat(addr); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid %s address: %s", label, err) + } return nil } diff --git a/sei-cosmos/x/auth/vesting/types/vesting_account.go b/sei-cosmos/x/auth/vesting/types/vesting_account.go index ef6c01b6d8..ef93871db7 100644 --- a/sei-cosmos/x/auth/vesting/types/vesting_account.go +++ b/sei-cosmos/x/auth/vesting/types/vesting_account.go @@ -3,6 +3,7 @@ package types import ( "errors" "fmt" + "math/big" "time" yaml "gopkg.in/yaml.v2" @@ -226,25 +227,32 @@ func NewContinuousVestingAccount(baseAcc *authtypes.BaseAccount, originalVesting // GetVestedCoins returns the total number of vested coins. If no coins are vested, // nil is returned. func (cva ContinuousVestingAccount) GetVestedCoins(blockTime time.Time) sdk.Coins { - var vestedCoins sdk.Coins - - // We must handle the case where the start time for a vesting account has - // been set into the future or when the start of the chain is not exactly - // known. - if blockTime.Unix() <= cva.StartTime { - return vestedCoins - } else if blockTime.Unix() >= cva.EndTime { + // Handle the edges: start time set in the future (or genesis time not yet + // known), and the vesting period already fully elapsed. + now := blockTime.Unix() + if now <= cva.StartTime { + return nil + } + if now >= cva.EndTime { return cva.OriginalVesting } - // calculate the vesting scalar - x := blockTime.Unix() - cva.StartTime - y := cva.EndTime - cva.StartTime - s := sdk.NewDec(x).Quo(sdk.NewDec(y)) + // vestedFraction = elapsed / duration, in (0, 1). + elapsed := now - cva.StartTime + duration := cva.EndTime - cva.StartTime + vestedFraction := sdk.NewDec(elapsed).Quo(sdk.NewDec(duration)) + + // Pull out the underlying big.Int once. It carries vestedFraction * 10^Precision, + // so multiplying a raw coin amount by it and then interpreting the product + // at the same precision yields amount * vestedFraction — equivalent to + // ovc.Amount.ToDec().Mul(vestedFraction).RoundInt(), without a per-coin Dec. + fractionScaled := vestedFraction.BigInt() - for _, ovc := range cva.OriginalVesting { - vestedAmt := ovc.Amount.ToDec().Mul(s).RoundInt() - vestedCoins = append(vestedCoins, sdk.NewCoin(ovc.Denom, vestedAmt)) + vestedCoins := make(sdk.Coins, len(cva.OriginalVesting)) + for i, ovc := range cva.OriginalVesting { + prod := new(big.Int).Mul(ovc.Amount.BigInt(), fractionScaled) + vestedAmt := sdk.NewDecFromBigIntWithPrec(prod, sdk.Precision).RoundInt() + vestedCoins[i] = sdk.NewCoin(ovc.Denom, vestedAmt) } return vestedCoins @@ -278,7 +286,7 @@ func (cva ContinuousVestingAccount) GetStartTime() int64 { // Validate checks for errors on the account fields func (cva ContinuousVestingAccount) Validate() error { if cva.GetStartTime() >= cva.GetEndTime() { - return errors.New("vesting start-time cannot be before end-time") + return errors.New("vesting start-time must be before end-time") } return cva.BaseVestingAccount.Validate() @@ -363,8 +371,8 @@ func (pva PeriodicVestingAccount) GetVestedCoins(blockTime time.Time) sdk.Coins // for each period, if the period is over, add those coins as vested and check the next period. for _, period := range pva.VestingPeriods { - x := blockTime.Unix() - currentPeriodStartTime - if x < period.Length { + elapsed := blockTime.Unix() - currentPeriodStartTime + if elapsed < period.Length { break } @@ -410,7 +418,7 @@ func (pva PeriodicVestingAccount) GetVestingPeriods() Periods { // Validate checks for errors on the account fields func (pva PeriodicVestingAccount) Validate() error { if pva.GetStartTime() >= pva.GetEndTime() { - return errors.New("vesting start-time cannot be before end-time") + return errors.New("vesting start-time must be before end-time") } endTime := pva.StartTime originalVesting := sdk.NewCoins() diff --git a/sei-cosmos/x/auth/vesting/types/vesting_account_test.go b/sei-cosmos/x/auth/vesting/types/vesting_account_test.go index a706b7d50a..202ee61f9a 100644 --- a/sei-cosmos/x/auth/vesting/types/vesting_account_test.go +++ b/sei-cosmos/x/auth/vesting/types/vesting_account_test.go @@ -14,649 +14,459 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/types" ) -var ( +const ( stakeDenom = "usei" feeDenom = "fee" ) func TestGetVestedCoinsContVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - - bacc, origCoins := initBaseAccount() + now, endTime, bacc, origCoins := vestingFixture() cva := types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) - // require no coins vested in the very beginning of the vesting schedule - vestedCoins := cva.GetVestedCoins(now) - 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)) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins) - - // require 100% of coins vested - vestedCoins = cva.GetVestedCoins(now.Add(48 * time.Hour)) - require.Equal(t, origCoins, vestedCoins) + require.Nil(t, cva.GetVestedCoins(now)) + require.Equal(t, origCoins, cva.GetVestedCoins(endTime)) + require.Equal(t, cs(fee(500), stake(50)), cva.GetVestedCoins(now.Add(12*time.Hour))) + require.Equal(t, cs(fee(292), stake(29)), cva.GetVestedCoins(now.Add(7*time.Hour))) + require.Equal(t, cs(fee(708), stake(71)), cva.GetVestedCoins(now.Add(17*time.Hour))) + require.Equal(t, origCoins, cva.GetVestedCoins(now.Add(48*time.Hour))) } func TestGetVestingCoinsContVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - - bacc, origCoins := initBaseAccount() + now, endTime, bacc, origCoins := vestingFixture() cva := types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) - // require all coins vesting in the beginning of the vesting schedule - vestingCoins := cva.GetVestingCoins(now) - 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.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestingCoins) + require.Equal(t, origCoins, cva.GetVestingCoins(now)) + require.Nil(t, cva.GetVestingCoins(endTime)) + require.Equal(t, cs(fee(500), stake(50)), cva.GetVestingCoins(now.Add(12*time.Hour))) } func TestSpendableCoinsContVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - - bacc, origCoins := initBaseAccount() + now, endTime, bacc, origCoins := vestingFixture() cva := types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) - // require that all original coins are locked at the end 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 - 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)) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, lockedCoins) + require.Equal(t, origCoins, cva.LockedCoins(now)) + require.Equal(t, sdk.NewCoins(), cva.LockedCoins(endTime)) + require.Equal(t, cs(fee(500), stake(50)), cva.LockedCoins(now.Add(12*time.Hour))) } func TestTrackDelegationContVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - - bacc, origCoins := initBaseAccount() + now, endTime, bacc, origCoins := vestingFixture() + newCVA := func() *types.ContinuousVestingAccount { + return types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) + } - // require the ability to delegate all vesting coins - cva := types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) + // At t=0: nothing vested, so all delegation counts as vesting. + cva := newCVA() cva.TrackDelegation(now, origCoins, origCoins) require.Equal(t, origCoins, cva.DelegatedVesting) require.Nil(t, cva.DelegatedFree) - // require the ability to delegate all vested coins - cva = types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) + // At t=end: fully vested, so all delegation counts as free. + cva = newCVA() cva.TrackDelegation(endTime, origCoins, origCoins) require.Nil(t, cva.DelegatedVesting) require.Equal(t, origCoins, cva.DelegatedFree) - // require the ability to delegate all vesting coins (50%) and all vested coins (50%) - cva = types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) - cva.TrackDelegation(now.Add(12*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, cva.DelegatedVesting) + // At t=12h (50% vested): first 50 stake is vesting; second 50 is free. + cva = newCVA() + cva.TrackDelegation(now.Add(12*time.Hour), origCoins, cs(stake(50))) + require.Equal(t, cs(stake(50)), cva.DelegatedVesting) require.Nil(t, cva.DelegatedFree) + cva.TrackDelegation(now.Add(12*time.Hour), origCoins, cs(stake(50))) + require.Equal(t, cs(stake(50)), cva.DelegatedVesting) + require.Equal(t, cs(stake(50)), cva.DelegatedFree) - cva.TrackDelegation(now.Add(12*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, cva.DelegatedVesting) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, cva.DelegatedFree) - - // require no modifications when delegation amount is zero or not enough funds - cva = types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) + // Over-delegation panics and leaves state untouched. + cva = newCVA() require.Panics(t, func() { - cva.TrackDelegation(endTime, origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 1000000)}) + cva.TrackDelegation(endTime, origCoins, cs(stake(1000000))) }) require.Nil(t, cva.DelegatedVesting) require.Nil(t, cva.DelegatedFree) } func TestTrackUndelegationContVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - - bacc, origCoins := initBaseAccount() + now, endTime, bacc, origCoins := vestingFixture() + newCVA := func() *types.ContinuousVestingAccount { + return types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) + } - // require the ability to undelegate all vesting coins - cva := types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) + // Undelegate everything delegated while fully vesting. + cva := newCVA() cva.TrackDelegation(now, origCoins, origCoins) cva.TrackUndelegation(origCoins) require.Nil(t, cva.DelegatedFree) require.Nil(t, cva.DelegatedVesting) - // require the ability to undelegate all vested coins - cva = types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) - + // Undelegate everything delegated when fully vested. + cva = newCVA() cva.TrackDelegation(endTime, origCoins, origCoins) cva.TrackUndelegation(origCoins) require.Nil(t, cva.DelegatedFree) require.Nil(t, cva.DelegatedVesting) - // require no modifications when the undelegation amount is zero - cva = types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) - + // Zero-amount undelegation panics; state untouched. + cva = newCVA() require.Panics(t, func() { - cva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 0)}) + cva.TrackUndelegation(cs(stake(0))) }) require.Nil(t, cva.DelegatedFree) require.Nil(t, cva.DelegatedVesting) - // vest 50% and delegate to two validators - cva = types.NewContinuousVestingAccount(bacc, origCoins, now.Unix(), endTime.Unix(), nil) - cva.TrackDelegation(now.Add(12*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) - cva.TrackDelegation(now.Add(12*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + // At t=12h: delegate 50 to two validators (one ends up vesting, one free), + // then undelegate from each with the first having been slashed 50%. + cva = newCVA() + cva.TrackDelegation(now.Add(12*time.Hour), origCoins, cs(stake(50))) + cva.TrackDelegation(now.Add(12*time.Hour), origCoins, cs(stake(50))) - // undelegate from one validator that got slashed 50% - cva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, cva.DelegatedFree) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, cva.DelegatedVesting) + cva.TrackUndelegation(cs(stake(25))) // slashed validator returns half + require.Equal(t, cs(stake(25)), cva.DelegatedFree) + require.Equal(t, cs(stake(50)), cva.DelegatedVesting) - // undelegate from the other validator that did not get slashed - cva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + cva.TrackUndelegation(cs(stake(50))) // healthy validator returns in full require.Nil(t, cva.DelegatedFree) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, cva.DelegatedVesting) + require.Equal(t, cs(stake(25)), cva.DelegatedVesting) } func TestGetVestedCoinsDelVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - - bacc, origCoins := initBaseAccount() - - // require no coins are vested until schedule maturation + now, endTime, bacc, origCoins := vestingFixture() dva := types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) - vestedCoins := dva.GetVestedCoins(now) - require.Nil(t, vestedCoins) - // require all coins be vested at schedule maturation - vestedCoins = dva.GetVestedCoins(endTime) - require.Equal(t, origCoins, vestedCoins) + require.Nil(t, dva.GetVestedCoins(now)) + require.Equal(t, origCoins, dva.GetVestedCoins(endTime)) } func TestGetVestingCoinsDelVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - - bacc, origCoins := initBaseAccount() - - // require all coins vesting at the beginning of the schedule + now, endTime, bacc, origCoins := vestingFixture() dva := types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) - vestingCoins := dva.GetVestingCoins(now) - require.Equal(t, origCoins, vestingCoins) - // require no coins vesting at schedule maturation - vestingCoins = dva.GetVestingCoins(endTime) - require.Nil(t, vestingCoins) + require.Equal(t, origCoins, dva.GetVestingCoins(now)) + require.Nil(t, dva.GetVestingCoins(endTime)) } func TestSpendableCoinsDelVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) + now, endTime, bacc, origCoins := vestingFixture() + dva := types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) - bacc, origCoins := initBaseAccount() + require.True(t, dva.LockedCoins(now).IsEqual(origCoins)) + require.Equal(t, sdk.NewCoins(), dva.LockedCoins(endTime)) + require.True(t, dva.LockedCoins(now.Add(12*time.Hour)).IsEqual(origCoins)) - // require that all coins are locked in the beginning of the vesting - // schedule - dva := types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) - lockedCoins := dva.LockedCoins(now) - require.True(t, lockedCoins.IsEqual(origCoins)) - - // require that all coins are spendable after the maturation of the vesting - // schedule - lockedCoins = dva.LockedCoins(endTime) - require.Equal(t, sdk.NewCoins(), lockedCoins) - - // require that all coins are still vesting after some time - lockedCoins = dva.LockedCoins(now.Add(12 * time.Hour)) - require.True(t, lockedCoins.IsEqual(origCoins)) - - // delegate some locked coins - // require that locked is reduced - delegatedAmount := sdk.NewCoins(sdk.NewInt64Coin(stakeDenom, 50)) - dva.TrackDelegation(now.Add(12*time.Hour), origCoins, delegatedAmount) - lockedCoins = dva.LockedCoins(now.Add(12 * time.Hour)) - require.True(t, lockedCoins.IsEqual(origCoins.Sub(delegatedAmount))) + // Delegating reduces the locked amount. + delegated := sdk.NewCoins(stake(50)) + dva.TrackDelegation(now.Add(12*time.Hour), origCoins, delegated) + require.True(t, dva.LockedCoins(now.Add(12*time.Hour)).IsEqual(origCoins.Sub(delegated))) } func TestTrackDelegationDelVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - - bacc, origCoins := initBaseAccount() + now, endTime, bacc, origCoins := vestingFixture() + newDVA := func() *types.DelayedVestingAccount { + return types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) + } - // require the ability to delegate all vesting coins - dva := types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) + // Before maturation: all delegation is vesting. + dva := newDVA() dva.TrackDelegation(now, origCoins, origCoins) require.Equal(t, origCoins, dva.DelegatedVesting) require.Nil(t, dva.DelegatedFree) - // require the ability to delegate all vested coins - dva = types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) + // After maturation: all delegation is free. + dva = newDVA() dva.TrackDelegation(endTime, origCoins, origCoins) require.Nil(t, dva.DelegatedVesting) require.Equal(t, origCoins, dva.DelegatedFree) - // require the ability to delegate all coins half way through the vesting - // schedule - dva = types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) + // Halfway through: delayed account hasn't vested anything yet, so it's + // all still vesting. + dva = newDVA() dva.TrackDelegation(now.Add(12*time.Hour), origCoins, origCoins) require.Equal(t, origCoins, dva.DelegatedVesting) require.Nil(t, dva.DelegatedFree) - // require no modifications when delegation amount is zero or not enough funds - dva = types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) - + // Over-delegation panics. + dva = newDVA() require.Panics(t, func() { - dva.TrackDelegation(endTime, origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 1000000)}) + dva.TrackDelegation(endTime, origCoins, cs(stake(1000000))) }) require.Nil(t, dva.DelegatedVesting) require.Nil(t, dva.DelegatedFree) } func TestTrackUndelegationDelVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - - bacc, origCoins := initBaseAccount() + now, endTime, bacc, origCoins := vestingFixture() + newDVA := func() *types.DelayedVestingAccount { + return types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) + } - // require the ability to undelegate all vesting coins - dva := types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) + // Undelegate everything delegated while vesting. + dva := newDVA() dva.TrackDelegation(now, origCoins, origCoins) dva.TrackUndelegation(origCoins) require.Nil(t, dva.DelegatedFree) require.Nil(t, dva.DelegatedVesting) - // require the ability to undelegate all vested coins - dva = types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) + // Undelegate everything delegated when vested. + dva = newDVA() dva.TrackDelegation(endTime, origCoins, origCoins) dva.TrackUndelegation(origCoins) require.Nil(t, dva.DelegatedFree) require.Nil(t, dva.DelegatedVesting) - // require no modifications when the undelegation amount is zero - dva = types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) - + // Zero-amount undelegation panics. + dva = newDVA() require.Panics(t, func() { - dva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 0)}) + dva.TrackUndelegation(cs(stake(0))) }) require.Nil(t, dva.DelegatedFree) require.Nil(t, dva.DelegatedVesting) - // vest 50% and delegate to two validators - dva = types.NewDelayedVestingAccount(bacc, origCoins, endTime.Unix(), nil) - dva.TrackDelegation(now.Add(12*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) - dva.TrackDelegation(now.Add(12*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) - - // undelegate from one validator that got slashed 50% - dva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}) + // At t=12h: nothing has vested yet, so both delegations are vesting. + // Undelegate with slashing on one validator. + dva = newDVA() + dva.TrackDelegation(now.Add(12*time.Hour), origCoins, cs(stake(50))) + dva.TrackDelegation(now.Add(12*time.Hour), origCoins, cs(stake(50))) + dva.TrackUndelegation(cs(stake(25))) // slashed require.Nil(t, dva.DelegatedFree) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 75)}, dva.DelegatedVesting) + require.Equal(t, cs(stake(75)), dva.DelegatedVesting) - // undelegate from the other validator that did not get slashed - dva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + dva.TrackUndelegation(cs(stake(50))) // healthy require.Nil(t, dva.DelegatedFree) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, dva.DelegatedVesting) + require.Equal(t, cs(stake(25)), dva.DelegatedVesting) } func TestGetVestedCoinsPeriodicVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - periods := types.Periods{ - types.Period{Length: int64(12 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, - types.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, - types.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, - } - - bacc, origCoins := initBaseAccount() - pva := types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) - - // require no coins vested at the beginning of the vesting schedule - vestedCoins := pva.GetVestedCoins(now) - require.Nil(t, vestedCoins) - - // require all coins vested at the end of the vesting schedule - vestedCoins = pva.GetVestedCoins(endTime) - require.Equal(t, origCoins, vestedCoins) - - // require no coins vested during first vesting period - vestedCoins = pva.GetVestedCoins(now.Add(6 * time.Hour)) - require.Nil(t, vestedCoins) - - // require 50% of coins vested after period 1 - vestedCoins = pva.GetVestedCoins(now.Add(12 * time.Hour)) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins) - - // require period 2 coins don't vest until period is over - vestedCoins = pva.GetVestedCoins(now.Add(15 * time.Hour)) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins) - - // require 75% of coins vested after period 2 - vestedCoins = pva.GetVestedCoins(now.Add(18 * time.Hour)) - require.Equal(t, - sdk.Coins{ - sdk.NewInt64Coin(feeDenom, 750), sdk.NewInt64Coin(stakeDenom, 75)}, vestedCoins) - - // require 100% of coins vested - vestedCoins = pva.GetVestedCoins(now.Add(48 * time.Hour)) - require.Equal(t, origCoins, vestedCoins) + now, endTime, bacc, origCoins := vestingFixture() + pva := types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), defaultPeriods(), nil) + + require.Nil(t, pva.GetVestedCoins(now)) + require.Equal(t, origCoins, pva.GetVestedCoins(endTime)) + require.Nil(t, pva.GetVestedCoins(now.Add(6*time.Hour))) // mid period 1, nothing yet + require.Equal(t, cs(fee(500), stake(50)), pva.GetVestedCoins(now.Add(12*time.Hour))) + require.Equal(t, cs(fee(500), stake(50)), pva.GetVestedCoins(now.Add(15*time.Hour))) // mid period 2 + require.Equal(t, cs(fee(750), stake(75)), pva.GetVestedCoins(now.Add(18*time.Hour))) + require.Equal(t, origCoins, pva.GetVestedCoins(now.Add(48*time.Hour))) } func TestGetVestingCoinsPeriodicVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - periods := types.Periods{ - types.Period{Length: int64(12 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, - types.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, - types.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, - } - - bacc, origCoins := initBaseAccount() - pva := types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) - - // require all coins vesting at the beginning of the vesting schedule - vestingCoins := pva.GetVestingCoins(now) - require.Equal(t, origCoins, vestingCoins) - - // require no coins vesting at the end of the vesting schedule - vestingCoins = pva.GetVestingCoins(endTime) - require.Nil(t, vestingCoins) - - // require 50% of coins vesting - vestingCoins = pva.GetVestingCoins(now.Add(12 * time.Hour)) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestingCoins) - - // require 50% of coins vesting after period 1, but before period 2 completes. - vestingCoins = pva.GetVestingCoins(now.Add(15 * time.Hour)) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestingCoins) - - // require 25% of coins vesting after period 2 - vestingCoins = pva.GetVestingCoins(now.Add(18 * time.Hour)) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}, vestingCoins) - - // require 0% of coins vesting after vesting complete - vestingCoins = pva.GetVestingCoins(now.Add(48 * time.Hour)) - require.Nil(t, vestingCoins) + now, endTime, bacc, origCoins := vestingFixture() + pva := types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), defaultPeriods(), nil) + + require.Equal(t, origCoins, pva.GetVestingCoins(now)) + require.Nil(t, pva.GetVestingCoins(endTime)) + require.Equal(t, cs(fee(500), stake(50)), pva.GetVestingCoins(now.Add(12*time.Hour))) + require.Equal(t, cs(fee(500), stake(50)), pva.GetVestingCoins(now.Add(15*time.Hour))) + require.Equal(t, cs(fee(250), stake(25)), pva.GetVestingCoins(now.Add(18*time.Hour))) + require.Nil(t, pva.GetVestingCoins(now.Add(48*time.Hour))) } func TestSpendableCoinsPeriodicVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - periods := types.Periods{ - types.Period{Length: int64(12 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, - types.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, - types.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, - } - - bacc, origCoins := initBaseAccount() - pva := types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) + now, endTime, bacc, origCoins := vestingFixture() + pva := types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), defaultPeriods(), nil) - // require that there exist no spendable coins at the beginning of the - // vesting schedule - lockedCoins := pva.LockedCoins(now) - require.Equal(t, origCoins, lockedCoins) - - // require that all original coins are spendable at the end of the vesting - // schedule - lockedCoins = pva.LockedCoins(endTime) - require.Equal(t, sdk.NewCoins(), lockedCoins) - - // require that all still vesting coins (50%) are locked - lockedCoins = pva.LockedCoins(now.Add(12 * time.Hour)) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, lockedCoins) + require.Equal(t, origCoins, pva.LockedCoins(now)) + require.Equal(t, sdk.NewCoins(), pva.LockedCoins(endTime)) + require.Equal(t, cs(fee(500), stake(50)), pva.LockedCoins(now.Add(12*time.Hour))) } func TestTrackDelegationPeriodicVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - periods := types.Periods{ - types.Period{Length: int64(12 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, - types.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, - types.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + now, endTime, bacc, origCoins := vestingFixture() + periods := defaultPeriods() + newPVA := func() *types.PeriodicVestingAccount { + return types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) } - bacc, origCoins := initBaseAccount() - - // require the ability to delegate all vesting coins - pva := types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) + // At t=0: all delegation is vesting. + pva := newPVA() pva.TrackDelegation(now, origCoins, origCoins) require.Equal(t, origCoins, pva.DelegatedVesting) require.Nil(t, pva.DelegatedFree) - // require the ability to delegate all vested coins - pva = types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) + // At t=end: all delegation is free. + pva = newPVA() pva.TrackDelegation(endTime, origCoins, origCoins) require.Nil(t, pva.DelegatedVesting) require.Equal(t, origCoins, pva.DelegatedFree) - // delegate half of vesting coins - pva = types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) + // Delegating period[0]'s amount at t=0 is fully vesting. + pva = newPVA() pva.TrackDelegation(now, origCoins, periods[0].Amount) - // require that all delegated coins are delegated vesting require.Equal(t, pva.DelegatedVesting, periods[0].Amount) require.Nil(t, pva.DelegatedFree) - // delegate 75% of coins, split between vested and vesting - pva = types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) + // At t=12h, periods[0] has vested. Delegating periods[0]+periods[1] worth + // prefers to spend the vesting bucket first: periods[0] is vesting, + // periods[1] is free. + pva = newPVA() pva.TrackDelegation(now.Add(12*time.Hour), origCoins, periods[0].Amount.Add(periods[1].Amount...)) - // require that the maximum possible amount of vesting coins are chosen for delegation. require.Equal(t, pva.DelegatedFree, periods[1].Amount) require.Equal(t, pva.DelegatedVesting, periods[0].Amount) - // require the ability to delegate all vesting coins (50%) and all vested coins (50%) - pva = types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) - pva.TrackDelegation(now.Add(12*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, pva.DelegatedVesting) + // At t=12h: split 50/50 between vesting and free across two delegations. + pva = newPVA() + pva.TrackDelegation(now.Add(12*time.Hour), origCoins, cs(stake(50))) + require.Equal(t, cs(stake(50)), pva.DelegatedVesting) require.Nil(t, pva.DelegatedFree) + pva.TrackDelegation(now.Add(12*time.Hour), origCoins, cs(stake(50))) + require.Equal(t, cs(stake(50)), pva.DelegatedVesting) + require.Equal(t, cs(stake(50)), pva.DelegatedFree) - pva.TrackDelegation(now.Add(12*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, pva.DelegatedVesting) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, pva.DelegatedFree) - - // require no modifications when delegation amount is zero or not enough funds - pva = types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) + // Over-delegation panics. + pva = newPVA() require.Panics(t, func() { - pva.TrackDelegation(endTime, origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 1000000)}) + pva.TrackDelegation(endTime, origCoins, cs(stake(1000000))) }) require.Nil(t, pva.DelegatedVesting) require.Nil(t, pva.DelegatedFree) } func TestTrackUndelegationPeriodicVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(24 * time.Hour) - periods := types.Periods{ - types.Period{Length: int64(12 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, - types.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, - types.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + now, endTime, bacc, origCoins := vestingFixture() + periods := defaultPeriods() + newPVA := func() *types.PeriodicVestingAccount { + return types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) } - bacc, origCoins := initBaseAccount() - - // require the ability to undelegate all vesting coins at the beginning of vesting - pva := types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) + pva := newPVA() pva.TrackDelegation(now, origCoins, origCoins) pva.TrackUndelegation(origCoins) require.Nil(t, pva.DelegatedFree) require.Nil(t, pva.DelegatedVesting) - // require the ability to undelegate all vested coins at the end of vesting - pva = types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) - + pva = newPVA() pva.TrackDelegation(endTime, origCoins, origCoins) pva.TrackUndelegation(origCoins) require.Nil(t, pva.DelegatedFree) require.Nil(t, pva.DelegatedVesting) - // require the ability to undelegate half of coins - pva = types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) + // Undelegate periods[0]'s worth after fully vested. + pva = newPVA() pva.TrackDelegation(endTime, origCoins, periods[0].Amount) pva.TrackUndelegation(periods[0].Amount) require.Nil(t, pva.DelegatedFree) require.Nil(t, pva.DelegatedVesting) - // require no modifications when the undelegation amount is zero - pva = types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) - + pva = newPVA() require.Panics(t, func() { - pva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 0)}) + pva.TrackUndelegation(cs(stake(0))) }) require.Nil(t, pva.DelegatedFree) require.Nil(t, pva.DelegatedVesting) - // vest 50% and delegate to two validators - pva = types.NewPeriodicVestingAccount(bacc, origCoins, now.Unix(), periods, nil) - pva.TrackDelegation(now.Add(12*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) - pva.TrackDelegation(now.Add(12*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + // At t=12h: delegate 50 to two validators, then undelegate with slashing. + pva = newPVA() + pva.TrackDelegation(now.Add(12*time.Hour), origCoins, cs(stake(50))) + pva.TrackDelegation(now.Add(12*time.Hour), origCoins, cs(stake(50))) - // undelegate from one validator that got slashed 50% - pva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, pva.DelegatedFree) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, pva.DelegatedVesting) + pva.TrackUndelegation(cs(stake(25))) // slashed + require.Equal(t, cs(stake(25)), pva.DelegatedFree) + require.Equal(t, cs(stake(50)), pva.DelegatedVesting) - // undelegate from the other validator that did not get slashed - pva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + pva.TrackUndelegation(cs(stake(50))) // healthy require.Nil(t, pva.DelegatedFree) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, pva.DelegatedVesting) + require.Equal(t, cs(stake(25)), pva.DelegatedVesting) } func TestGetVestedCoinsPermLockedVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(1000 * 24 * time.Hour) - - bacc, origCoins := initBaseAccount() - - // require no coins are vested + now, _, bacc, origCoins := vestingFixture() + farFuture := now.Add(1000 * 24 * time.Hour) plva := types.NewPermanentLockedAccount(bacc, origCoins, nil) - vestedCoins := plva.GetVestedCoins(now) - require.Nil(t, vestedCoins) - // require no coins be vested at end time - vestedCoins = plva.GetVestedCoins(endTime) - require.Nil(t, vestedCoins) + // Permanently locked: nothing ever vests. + require.Nil(t, plva.GetVestedCoins(now)) + require.Nil(t, plva.GetVestedCoins(farFuture)) } func TestGetVestingCoinsPermLockedVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(1000 * 24 * time.Hour) - - bacc, origCoins := initBaseAccount() - - // require all coins vesting at the beginning of the schedule + now, _, bacc, origCoins := vestingFixture() + farFuture := now.Add(1000 * 24 * time.Hour) plva := types.NewPermanentLockedAccount(bacc, origCoins, nil) - vestingCoins := plva.GetVestingCoins(now) - require.Equal(t, origCoins, vestingCoins) - // require all coins vesting at the end time - vestingCoins = plva.GetVestingCoins(endTime) - require.Equal(t, origCoins, vestingCoins) + // Permanently locked: everything is always vesting. + require.Equal(t, origCoins, plva.GetVestingCoins(now)) + require.Equal(t, origCoins, plva.GetVestingCoins(farFuture)) } func TestSpendableCoinsPermLockedVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(1000 * 24 * time.Hour) + now, _, bacc, origCoins := vestingFixture() + farFuture := now.Add(1000 * 24 * time.Hour) + plva := types.NewPermanentLockedAccount(bacc, origCoins, nil) - bacc, origCoins := initBaseAccount() + require.True(t, plva.LockedCoins(now).IsEqual(origCoins)) + require.True(t, plva.LockedCoins(farFuture).IsEqual(origCoins)) - // require that all coins are locked in the beginning of the vesting - // schedule - plva := types.NewPermanentLockedAccount(bacc, origCoins, nil) - lockedCoins := plva.LockedCoins(now) - require.True(t, lockedCoins.IsEqual(origCoins)) - - // require that all coins are still locked at end time - lockedCoins = plva.LockedCoins(endTime) - require.True(t, lockedCoins.IsEqual(origCoins)) - - // delegate some locked coins - // require that locked is reduced - delegatedAmount := sdk.NewCoins(sdk.NewInt64Coin(stakeDenom, 50)) - plva.TrackDelegation(now.Add(12*time.Hour), origCoins, delegatedAmount) - lockedCoins = plva.LockedCoins(now.Add(12 * time.Hour)) - require.True(t, lockedCoins.IsEqual(origCoins.Sub(delegatedAmount))) + // Delegating reduces the locked amount. + delegated := sdk.NewCoins(stake(50)) + plva.TrackDelegation(now.Add(12*time.Hour), origCoins, delegated) + require.True(t, plva.LockedCoins(now.Add(12*time.Hour)).IsEqual(origCoins.Sub(delegated))) } func TestTrackDelegationPermLockedVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(1000 * 24 * time.Hour) - - bacc, origCoins := initBaseAccount() + now, _, bacc, origCoins := vestingFixture() + farFuture := now.Add(1000 * 24 * time.Hour) + newPLVA := func() *types.PermanentLockedAccount { + return types.NewPermanentLockedAccount(bacc, origCoins, nil) + } - // require the ability to delegate all vesting coins - plva := types.NewPermanentLockedAccount(bacc, origCoins, nil) + // All delegation is always vesting on a permanent-locked account, + // regardless of timestamp. + plva := newPLVA() plva.TrackDelegation(now, origCoins, origCoins) require.Equal(t, origCoins, plva.DelegatedVesting) require.Nil(t, plva.DelegatedFree) - // require the ability to delegate all vested coins at endTime - plva = types.NewPermanentLockedAccount(bacc, origCoins, nil) - plva.TrackDelegation(endTime, origCoins, origCoins) + plva = newPLVA() + plva.TrackDelegation(farFuture, origCoins, origCoins) require.Equal(t, origCoins, plva.DelegatedVesting) require.Nil(t, plva.DelegatedFree) - // require no modifications when delegation amount is zero or not enough funds - plva = types.NewPermanentLockedAccount(bacc, origCoins, nil) - + // Over-delegation panics. + plva = newPLVA() require.Panics(t, func() { - plva.TrackDelegation(endTime, origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 1000000)}) + plva.TrackDelegation(farFuture, origCoins, cs(stake(1000000))) }) require.Nil(t, plva.DelegatedVesting) require.Nil(t, plva.DelegatedFree) } func TestTrackUndelegationPermLockedVestingAcc(t *testing.T) { - now := tmtime.Now() - endTime := now.Add(1000 * 24 * time.Hour) - - bacc, origCoins := initBaseAccount() + now, _, bacc, origCoins := vestingFixture() + farFuture := now.Add(1000 * 24 * time.Hour) + newPLVA := func() *types.PermanentLockedAccount { + return types.NewPermanentLockedAccount(bacc, origCoins, nil) + } - // require the ability to undelegate all vesting coins - plva := types.NewPermanentLockedAccount(bacc, origCoins, nil) + plva := newPLVA() plva.TrackDelegation(now, origCoins, origCoins) plva.TrackUndelegation(origCoins) require.Nil(t, plva.DelegatedFree) require.Nil(t, plva.DelegatedVesting) - // require the ability to undelegate all vesting coins at endTime - plva = types.NewPermanentLockedAccount(bacc, origCoins, nil) - plva.TrackDelegation(endTime, origCoins, origCoins) + plva = newPLVA() + plva.TrackDelegation(farFuture, origCoins, origCoins) plva.TrackUndelegation(origCoins) require.Nil(t, plva.DelegatedFree) require.Nil(t, plva.DelegatedVesting) - // require no modifications when the undelegation amount is zero - plva = types.NewPermanentLockedAccount(bacc, origCoins, nil) + plva = newPLVA() require.Panics(t, func() { - plva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 0)}) + plva.TrackUndelegation(cs(stake(0))) }) require.Nil(t, plva.DelegatedFree) require.Nil(t, plva.DelegatedVesting) - // delegate to two validators - plva = types.NewPermanentLockedAccount(bacc, origCoins, nil) - plva.TrackDelegation(now, origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) - plva.TrackDelegation(now, origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) - - // undelegate from one validator that got slashed 50% - plva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}) + // Delegate 50 to two validators, undelegate with slashing on one. + plva = newPLVA() + plva.TrackDelegation(now, origCoins, cs(stake(50))) + plva.TrackDelegation(now, origCoins, cs(stake(50))) + plva.TrackUndelegation(cs(stake(25))) // slashed require.Nil(t, plva.DelegatedFree) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 75)}, plva.DelegatedVesting) + require.Equal(t, cs(stake(75)), plva.DelegatedVesting) - // undelegate from the other validator that did not get slashed - plva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + plva.TrackUndelegation(cs(stake(50))) // healthy require.Nil(t, plva.DelegatedFree) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, plva.DelegatedVesting) + require.Equal(t, cs(stake(25)), plva.DelegatedVesting) } func TestGenesisAccountValidate(t *testing.T) { @@ -665,27 +475,25 @@ func TestGenesisAccountValidate(t *testing.T) { addr := sdk.AccAddress(pubkey.Address()) baseAcc := authtypes.NewBaseAccount(addr, pubkey, 0, 0) initialVesting := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 50)) - baseVestingWithCoins := types.NewBaseVestingAccount(baseAcc, initialVesting, 100, nil) + baseVesting := types.NewBaseVestingAccount(baseAcc, initialVesting, 100, nil) + + bondCoin := func(amt int64) sdk.Coin { return sdk.NewInt64Coin(sdk.DefaultBondDenom, amt) } + onePeriod := func(length int64, amt sdk.Coins) types.Periods { + return types.Periods{{Length: length, Amount: amt}} + } + tests := []struct { name string acc authtypes.GenesisAccount expErr bool }{ - { - "valid base account", - baseAcc, - false, - }, + {"valid base account", baseAcc, false}, { "invalid base valid account", authtypes.NewBaseAccount(addr, secp256k1.GenPrivKey().PubKey(), 0, 0), true, }, - { - "valid base vesting account", - baseVestingWithCoins, - false, - }, + {"valid base vesting account", baseVesting, false}, { "valid continuous vesting account", types.NewContinuousVestingAccount(baseAcc, initialVesting, 100, 200, nil), @@ -698,60 +506,40 @@ func TestGenesisAccountValidate(t *testing.T) { }, { "valid periodic vesting account", - types.NewPeriodicVestingAccount( - baseAcc, - initialVesting, - 0, - types.Periods{ - types.Period{Length: int64(100), Amount: sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 50)}}, - }, - nil, - ), + types.NewPeriodicVestingAccount(baseAcc, initialVesting, 0, onePeriod(100, sdk.Coins{bondCoin(50)}), nil), false, }, { "invalid vesting period lengths", - types.NewPeriodicVestingAccountRaw( - baseVestingWithCoins, - 0, types.Periods{types.Period{Length: int64(50), Amount: sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 50)}}}), + types.NewPeriodicVestingAccountRaw(baseVesting, 0, onePeriod(50, sdk.Coins{bondCoin(50)})), true, }, { "invalid vesting period amounts", - types.NewPeriodicVestingAccountRaw( - baseVestingWithCoins, - 0, types.Periods{types.Period{Length: int64(100), Amount: sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 25)}}}), + types.NewPeriodicVestingAccountRaw(baseVesting, 0, onePeriod(100, sdk.Coins{bondCoin(25)})), true, }, { - "Empty coin amount should fail", - types.NewPeriodicVestingAccountRaw( - baseVestingWithCoins, - 0, types.Periods{types.Period{Length: int64(100), Amount: sdk.Coins{}}}), + "empty coin amount should fail", + types.NewPeriodicVestingAccountRaw(baseVesting, 0, onePeriod(100, sdk.Coins{})), true, }, { - "Less than 1 coin period should fail", - types.NewPeriodicVestingAccountRaw( - baseVestingWithCoins, - 0, types.Periods{types.Period{Length: int64(0), Amount: sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 25)}}}), + "zero-length period should fail", + types.NewPeriodicVestingAccountRaw(baseVesting, 0, onePeriod(0, sdk.Coins{bondCoin(25)})), true, }, { - "Negative coin amount should fail", - types.NewPeriodicVestingAccountRaw( - baseVestingWithCoins, - 0, types.Periods{types.Period{Length: int64(100), Amount: sdk.Coins{sdk.Coin{ - Denom: sdk.DefaultBondDenom, - Amount: sdk.NewInt(-123), - }}}}), + "negative coin amount should fail", + types.NewPeriodicVestingAccountRaw(baseVesting, 0, onePeriod(100, sdk.Coins{{ + Denom: sdk.DefaultBondDenom, + Amount: sdk.NewInt(-123), + }})), true, }, { - "Less than 1 coin period should fail", - types.NewPeriodicVestingAccountRaw( - baseVestingWithCoins, - 0, types.Periods{types.Period{Length: int64(-1), Amount: sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 25)}}}), + "negative-length period should fail", + types.NewPeriodicVestingAccountRaw(baseVesting, 0, onePeriod(-1, sdk.Coins{bondCoin(25)})), true, }, { @@ -761,14 +549,12 @@ func TestGenesisAccountValidate(t *testing.T) { }, { "invalid positive end time for permanently locked vest account", - &types.PermanentLockedAccount{BaseVestingAccount: baseVestingWithCoins}, + &types.PermanentLockedAccount{BaseVestingAccount: baseVesting}, true, }, } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.expErr, tt.acc.Validate() != nil) }) @@ -778,92 +564,90 @@ func TestGenesisAccountValidate(t *testing.T) { func TestCreateBaseVestingWithAdmin(t *testing.T) { pubkey := secp256k1.GenPrivKey().PubKey() addr := sdk.AccAddress(pubkey.Address()) - adminPubkey := secp256k1.GenPrivKey().PubKey() - adminAddr := sdk.AccAddress(adminPubkey.Address()) + adminAddr := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) baseAcc := authtypes.NewBaseAccount(addr, pubkey, 0, 0) initialVesting := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 50)) - baseVestingWithAdmin := types.NewBaseVestingAccount(baseAcc, initialVesting, 100, adminAddr) - bz, err := a.AccountKeeper.MarshalAccount(baseVestingWithAdmin) - require.Nil(t, err) - - acc2, err := a.AccountKeeper.UnmarshalAccount(bz) - require.Nil(t, err) - require.IsType(t, &types.BaseVestingAccount{}, acc2) - require.Equal(t, baseVestingWithAdmin.String(), acc2.String()) -} - -func TestContinuousVestingAccountMarshal(t *testing.T) { - baseAcc, coins := initBaseAccount() - baseVesting := types.NewBaseVestingAccount(baseAcc, coins, time.Now().Unix(), nil) - acc := types.NewContinuousVestingAccountRaw(baseVesting, baseVesting.EndTime) + acc := types.NewBaseVestingAccount(baseAcc, initialVesting, 100, adminAddr) bz, err := a.AccountKeeper.MarshalAccount(acc) - require.Nil(t, err) + require.NoError(t, err) - acc2, err := a.AccountKeeper.UnmarshalAccount(bz) - require.Nil(t, err) - require.IsType(t, &types.ContinuousVestingAccount{}, acc2) - require.Equal(t, acc.String(), acc2.String()) - - // error on bad bytes - _, err = a.AccountKeeper.UnmarshalAccount(bz[:len(bz)/2]) - require.NotNil(t, err) + got, err := a.AccountKeeper.UnmarshalAccount(bz) + require.NoError(t, err) + require.IsType(t, &types.BaseVestingAccount{}, got) + require.Equal(t, acc.String(), got.String()) } -func TestPeriodicVestingAccountMarshal(t *testing.T) { - baseAcc, coins := initBaseAccount() - acc := types.NewPeriodicVestingAccount(baseAcc, coins, time.Now().Unix(), types.Periods{types.Period{3600, coins}}, nil) +// TestVestingAccountMarshal replaces four near-identical Test*Marshal tests. +// Each variant must round-trip through Marshal/Unmarshal preserving identity +// and string form, and must fail to unmarshal from truncated bytes. +func TestVestingAccountMarshal(t *testing.T) { + baseAcc, origCoins := initBaseAccount() + endTime := time.Now().Unix() + baseVesting := types.NewBaseVestingAccount(baseAcc, origCoins, endTime, nil) + + cases := []struct { + name string + acc authtypes.GenesisAccount + }{ + {"continuous", types.NewContinuousVestingAccountRaw(baseVesting, baseVesting.EndTime)}, + { + "periodic", + types.NewPeriodicVestingAccount(baseAcc, origCoins, endTime, + types.Periods{{Length: 3600, Amount: origCoins}}, nil), + }, + {"delayed", types.NewDelayedVestingAccount(baseAcc, origCoins, endTime, nil)}, + {"permanent_locked", types.NewPermanentLockedAccount(baseAcc, origCoins, nil)}, + } - bz, err := a.AccountKeeper.MarshalAccount(acc) - require.Nil(t, err) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + bz, err := a.AccountKeeper.MarshalAccount(tc.acc) + require.NoError(t, err) - acc2, err := a.AccountKeeper.UnmarshalAccount(bz) - require.Nil(t, err) - require.IsType(t, &types.PeriodicVestingAccount{}, acc2) - require.Equal(t, acc.String(), acc2.String()) + got, err := a.AccountKeeper.UnmarshalAccount(bz) + require.NoError(t, err) + require.IsType(t, tc.acc, got) + require.Equal(t, tc.acc.String(), got.String()) - // error on bad bytes - _, err = a.AccountKeeper.UnmarshalAccount(bz[:len(bz)/2]) - require.NotNil(t, err) + // Truncated bytes must fail to unmarshal. + _, err = a.AccountKeeper.UnmarshalAccount(bz[:len(bz)/2]) + require.Error(t, err) + }) + } } -func TestDelayedVestingAccountMarshal(t *testing.T) { - baseAcc, coins := initBaseAccount() - acc := types.NewDelayedVestingAccount(baseAcc, coins, time.Now().Unix(), nil) - - bz, err := a.AccountKeeper.MarshalAccount(acc) - require.Nil(t, err) +// stake(n) / fee(n) read much better in dense assertions than +// sdk.NewInt64Coin(stakeDenom, n). +func stake(n int64) sdk.Coin { return sdk.NewInt64Coin(stakeDenom, n) } +func fee(n int64) sdk.Coin { return sdk.NewInt64Coin(feeDenom, n) } - acc2, err := a.AccountKeeper.UnmarshalAccount(bz) - require.Nil(t, err) - require.IsType(t, &types.DelayedVestingAccount{}, acc2) - require.Equal(t, acc.String(), acc2.String()) +// cs builds a raw sdk.Coins — equivalent to sdk.Coins{...}, NOT sdk.NewCoins(...). +// Do not switch to sdk.NewCoins: it sorts, validates, and drops zero coins, +// any of which would break tests that assert exact slice equality or that +// pass a zero coin to TrackUndelegation to provoke a panic. +func cs(c ...sdk.Coin) sdk.Coins { return sdk.Coins(c) } - // error on bad bytes - _, err = a.AccountKeeper.UnmarshalAccount(bz[:len(bz)/2]) - require.NotNil(t, err) +func initBaseAccount() (*authtypes.BaseAccount, sdk.Coins) { + _, _, addr := testdata.KeyTestPubAddr() + return authtypes.NewBaseAccountWithAddress(addr), cs(fee(1000), stake(100)) } -func TestPermanentLockedAccountMarshal(t *testing.T) { - baseAcc, coins := initBaseAccount() - acc := types.NewPermanentLockedAccount(baseAcc, coins, nil) - bz, err := a.AccountKeeper.MarshalAccount(acc) - require.Nil(t, err) - - acc2, err := a.AccountKeeper.UnmarshalAccount(bz) - require.Nil(t, err) - require.IsType(t, &types.PermanentLockedAccount{}, acc2) - require.Equal(t, acc.String(), acc2.String()) - - // error on bad bytes - _, err = a.AccountKeeper.UnmarshalAccount(bz[:len(bz)/2]) - require.NotNil(t, err) +// vestingFixture returns the (now, endTime = now+24h, baseAccount, originalVesting) +// tuple shared by every vesting test in this file. +func vestingFixture() (now, endTime time.Time, bacc *authtypes.BaseAccount, orig sdk.Coins) { + now = tmtime.Now() + endTime = now.Add(24 * time.Hour) + bacc, orig = initBaseAccount() + return } -func initBaseAccount() (*authtypes.BaseAccount, sdk.Coins) { - _, _, addr := testdata.KeyTestPubAddr() - origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} - bacc := authtypes.NewBaseAccountWithAddress(addr) - - return bacc, origCoins +// defaultPeriods is the periodic schedule used by every periodic-vesting test: +// 50% vests at +12h, then 25% at +18h, then 25% at +24h. +func defaultPeriods() types.Periods { + return types.Periods{ + {Length: int64(12 * 60 * 60), Amount: cs(fee(500), stake(50))}, + {Length: int64(6 * 60 * 60), Amount: cs(fee(250), stake(25))}, + {Length: int64(6 * 60 * 60), Amount: cs(fee(250), stake(25))}, + } } diff --git a/sei-cosmos/x/bank/keeper/send.go b/sei-cosmos/x/bank/keeper/send.go index a017ec0fa2..832fc1c2a1 100644 --- a/sei-cosmos/x/bank/keeper/send.go +++ b/sei-cosmos/x/bank/keeper/send.go @@ -217,8 +217,11 @@ func (k BaseSendKeeper) SubUnlockedCoins(ctx sdk.Context, addr sdk.AccAddress, a for _, coin := range amt { balance := k.GetBalance(ctx, addr, coin.Denom) if checkNeg { - locked := sdk.NewCoin(coin.Denom, lockedCoins.AmountOf(coin.Denom)) - spendable := balance.Sub(locked) + spendableAmt := balance.Amount.Sub(lockedCoins.AmountOf(coin.Denom)) + if spendableAmt.IsNegative() { + spendableAmt = sdk.ZeroInt() + } + spendable := sdk.NewCoin(coin.Denom, spendableAmt) _, hasNeg := sdk.Coins{spendable}.SafeSub(sdk.Coins{coin}) if hasNeg { @@ -356,39 +359,50 @@ func (k BaseSendKeeper) BlockedAddr(addr sdk.AccAddress) bool { } func (k BaseSendKeeper) SubWei(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) (err error) { - if amt.Equal(sdk.ZeroInt()) { + if amt.IsZero() { return nil } defer func() { if err == nil { - ctx.EventManager().EmitEvent( - types.NewWeiSpentEvent(addr, amt), - ) + ctx.EventManager().EmitEvent(types.NewWeiSpentEvent(addr, amt)) } }() - currentWeiBalance := k.GetWeiBalance(ctx, addr) - if amt.LTE(currentWeiBalance) { - // no need to change usei balance - return k.setWeiBalance(ctx, addr, currentWeiBalance.Sub(amt)) + + denom := sdk.MustGetBaseDenom() + currentWei := k.GetWeiBalance(ctx, addr) + + // Fast path: the subtraction fits entirely within the wei sub-balance, + // so the usei balance is untouched. + if amt.LTE(currentWei) { + return k.setWeiBalance(ctx, addr, currentWei.Sub(amt)) } - currentUseiBalance := k.GetBalance(ctx, addr, sdk.MustGetBaseDenom()).Amount - currentAggregatedBalance := currentUseiBalance.Mul(OneUseiInWei).Add(currentWeiBalance) - postAggregatedbalance := currentAggregatedBalance.Sub(amt) - if postAggregatedbalance.IsNegative() { - return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "%swei is smaller than %swei", currentAggregatedBalance, amt) + + // Slow path: borrow from usei. Aggregate both sub-balances into a single + // wei figure, subtract, then split back into (usei, wei). + currentUsei := k.GetBalance(ctx, addr, denom).Amount + currentTotal := currentUsei.Mul(OneUseiInWei).Add(currentWei) + postTotal := currentTotal.Sub(amt) + if postTotal.IsNegative() { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "%swei is smaller than %swei", currentTotal, amt) + } + + newUsei, newWei := SplitUseiWeiAmount(postTotal) + lockedUsei := k.LockedCoins(ctx, addr).AmountOf(denom) + if newUsei.LT(lockedUsei) { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "%suei is smaller than locked %suei", newUsei, lockedUsei) } - useiBalance, weiBalance := SplitUseiWeiAmount(postAggregatedbalance) - if err := k.setBalance(ctx, addr, sdk.NewCoin(sdk.MustGetBaseDenom(), useiBalance), true); err != nil { + + if err := k.setBalance(ctx, addr, sdk.NewCoin(denom, newUsei), true); err != nil { return err } - return k.setWeiBalance(ctx, addr, weiBalance) + return k.setWeiBalance(ctx, addr, newWei) } func (k BaseSendKeeper) AddWei(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) (err error) { if !k.CanSendTo(ctx, addr) { return sdkerrors.ErrInvalidRecipient } - if amt.Equal(sdk.ZeroInt()) { + if amt.IsZero() { return nil } defer func() { diff --git a/sei-cosmos/x/bank/keeper/send_test.go b/sei-cosmos/x/bank/keeper/send_test.go index ff6128452b..16aa01a6de 100644 --- a/sei-cosmos/x/bank/keeper/send_test.go +++ b/sei-cosmos/x/bank/keeper/send_test.go @@ -12,10 +12,20 @@ import ( func TestBlockedAddr(t *testing.T) { k := keeper.NewBaseSendKeeper(nil, nil, nil, paramtypes.Subspace{}, map[string]bool{}) - txIndexBz := make([]byte, 8) - binary.BigEndian.PutUint64(txIndexBz, uint64(5)) - addr := sdk.AccAddress(append(keeper.CoinbaseAddressPrefix, txIndexBz...)) - require.True(t, k.BlockedAddr(addr)) + + // A coinbase address is the CoinbaseAddressPrefix followed by an 8-byte + // big-endian tx index. Such addresses must be blocked from receiving funds. + coinbaseAddr := func(txIndex uint64) sdk.AccAddress { + idx := make([]byte, 8) + binary.BigEndian.PutUint64(idx, txIndex) + return sdk.AccAddress(append(keeper.CoinbaseAddressPrefix, idx...)) + } + + addr := coinbaseAddr(5) + require.True(t, k.BlockedAddr(addr), "coinbase address should be blocked") + + // Mutating any prefix byte breaks the coinbase pattern, so the address + // should no longer be blocked. addr[0] = 'q' - require.False(t, k.BlockedAddr(addr)) + require.False(t, k.BlockedAddr(addr), "non-coinbase address should not be blocked") } diff --git a/x/evm/keeper/abci.go b/x/evm/keeper/abci.go index c52215aae7..f38b3afd5a 100644 --- a/x/evm/keeper/abci.go +++ b/x/evm/keeper/abci.go @@ -138,6 +138,9 @@ func (k *Keeper) EndBlock(ctx sdk.Context, height int64, blockGasUsed int64) { useiBalance := k.BankKeeper().GetBalance(ctx, coinbaseAddress, denom).Amount lockedUseiBalance := k.BankKeeper().LockedCoins(ctx, coinbaseAddress).AmountOf(denom) balance := useiBalance.Sub(lockedUseiBalance) + if balance.IsNegative() { + balance = sdk.ZeroInt() + } weiBalance := k.BankKeeper().GetWeiBalance(ctx, coinbaseAddress) if !balance.IsZero() || !weiBalance.IsZero() { if err := k.BankKeeper().SendCoinsAndWei(ctx, coinbaseAddress, coinbase, balance, weiBalance); err != nil { diff --git a/x/evm/keeper/balance.go b/x/evm/keeper/balance.go index fac6a1ed4b..6214df2c6c 100644 --- a/x/evm/keeper/balance.go +++ b/x/evm/keeper/balance.go @@ -7,11 +7,21 @@ import ( "github.com/sei-protocol/sei-chain/x/evm/state" ) +// GetBalance returns the spendable EVM balance (in wei) of addr, excluding any +// locked usei (e.g. from vesting accounts). func (k *Keeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress) *big.Int { + bk := k.BankKeeper() denom := k.GetBaseDenom(ctx) - allUsei := k.BankKeeper().GetBalance(ctx, addr, denom).Amount - lockedUsei := k.BankKeeper().LockedCoins(ctx, addr).AmountOf(denom) // LockedCoins doesn't use iterators - usei := allUsei.Sub(lockedUsei) - wei := k.BankKeeper().GetWeiBalance(ctx, addr) - return usei.Mul(state.SdkUseiToSweiMultiplier).Add(wei).BigInt() + + // LockedCoins doesn't use iterators, so this stays cheap. + totalUsei := bk.GetBalance(ctx, addr, denom).Amount + lockedUsei := bk.LockedCoins(ctx, addr).AmountOf(denom) + + spendableUsei := totalUsei.Sub(lockedUsei) + if spendableUsei.IsNegative() { + spendableUsei = sdk.ZeroInt() + } + + wei := bk.GetWeiBalance(ctx, addr) + return spendableUsei.Mul(state.SdkUseiToSweiMultiplier).Add(wei).BigInt() }