VYPR
Low severityNVD Advisory· Published Jun 6, 2024· Updated Aug 2, 2024

evmos allows transferring unvested tokens after delegations

CVE-2024-32873

Description

Evmos is the Ethereum Virtual Machine (EVM) Hub on the Cosmos Network. The spendable balance is not updated properly when delegating vested tokens. The issue allows a clawback vesting account to anticipate the release of unvested tokens. This vulnerability is fixed in 18.0.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Delegating vested tokens on Evmos fails to update the spendable balance, allowing clawback vesting accounts to access unvested tokens early.

Root

Cause The vulnerability in CVE-2024-32873 concerns the improper update of the spendable balance when delegating vested tokens on the Evmos blockchain. Evmos is the Ethereum Virtual Machine (EVM) Hub on the Cosmos Network. The issue stems from a flaw in how clawback vesting accounts handle delegation operations; the spendable balance is not correctly decremented, allowing these accounts to effectively anticipate the release of unvested tokens [3].

Exploitation

The attack surface involves a clawback vesting account initiating a delegation of vested tokens. By exploiting this accounting bug, the account's spendable balance remains higher than intended, enabling the account to use tokens that are still subject to vesting restrictions. This vulnerability is closely related to two other issues fixed in the same release: one that allowed bypassing ante handler checks via Ethereum transactions (CVE-2024–37158) [1], and another that allowed creating validators with vested tokens (CVE-2024–37159) [2].

Impact

An attacker with a clawback vesting account could spend or transfer unvested tokens before the vesting schedule permits, effectively bypassing the intended token release mechanism. This could lead to a loss of funds for the protocol or other users, and undermines the trust assumptions of the vesting system.

Mitigation

The vulnerability is fixed in Evmos version 18.0.0. The commit [4] shows changes to the staking module and related imports to correct the balance accounting. No other workarounds are mentioned; upgrading to the patched version is recommended.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/evmos/evmos/v17Go
< 18.0.018.0.0
github.com/evmos/evmos/v16Go
< 18.0.018.0.0
github.com/evmos/evmos/v15Go
< 18.0.018.0.0
github.com/evmos/evmos/v14Go
< 18.0.018.0.0
github.com/evmos/evmos/v13Go
< 18.0.018.0.0
github.com/evmos/evmos/v12Go
< 18.0.018.0.0
github.com/evmos/evmos/v11Go
< 18.0.018.0.0
github.com/evmos/evmos/v10Go
< 18.0.018.0.0
github.com/evmos/evmos/v9Go
< 18.0.018.0.0
github.com/evmos/evmos/v8Go
< 18.0.018.0.0
github.com/evmos/evmos/v7Go
< 18.0.018.0.0
github.com/evmos/evmos/v6Go
< 18.0.018.0.0

Affected products

14

Patches

1
b2a09ca66613

Merge pull request from GHSA-pxv8-qhrh-jc7v

https://github.com/evmos/evmosTomApr 22, 2024via ghsa
50 files changed · +2489 957
  • app/ante/cosmos/vesting.go+0 123 removed
    @@ -1,123 +0,0 @@
    -// Copyright Tharsis Labs Ltd.(Evmos)
    -// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
    -package cosmos
    -
    -import (
    -	errorsmod "cosmossdk.io/errors"
    -	"github.com/cosmos/cosmos-sdk/codec"
    -	sdk "github.com/cosmos/cosmos-sdk/types"
    -	errortypes "github.com/cosmos/cosmos-sdk/types/errors"
    -	"github.com/cosmos/cosmos-sdk/x/authz"
    -	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
    -	evmtypes "github.com/evmos/evmos/v18/x/evm/types"
    -	vestingtypes "github.com/evmos/evmos/v18/x/vesting/types"
    -)
    -
    -// TODO: remove once Cosmos SDK is upgraded to v0.46
    -
    -// VestingDelegationDecorator validates delegation of vested coins
    -type VestingDelegationDecorator struct {
    -	ak  evmtypes.AccountKeeper
    -	sk  vestingtypes.StakingKeeper
    -	bk  evmtypes.BankKeeper
    -	cdc codec.BinaryCodec
    -}
    -
    -// NewVestingDelegationDecorator creates a new VestingDelegationDecorator
    -func NewVestingDelegationDecorator(ak evmtypes.AccountKeeper, sk vestingtypes.StakingKeeper, bk evmtypes.BankKeeper, cdc codec.BinaryCodec) VestingDelegationDecorator {
    -	return VestingDelegationDecorator{
    -		ak:  ak,
    -		sk:  sk,
    -		bk:  bk,
    -		cdc: cdc,
    -	}
    -}
    -
    -// AnteHandle checks if the tx contains a staking delegation.
    -// It errors if the coins are still locked or the bond amount is greater than
    -// the coins already vested
    -func (vdd VestingDelegationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) {
    -	for _, msg := range tx.GetMsgs() {
    -		switch msg := msg.(type) {
    -		case *authz.MsgExec:
    -			// Check for bypassing authorization
    -			if err := vdd.validateAuthz(ctx, msg); err != nil {
    -				return ctx, err
    -			}
    -		default:
    -			if err := vdd.validateMsg(ctx, msg); err != nil {
    -				return ctx, err
    -			}
    -		}
    -	}
    -
    -	return next(ctx, tx, simulate)
    -}
    -
    -// validateAuthz validates the authorization internal message
    -func (vdd VestingDelegationDecorator) validateAuthz(ctx sdk.Context, execMsg *authz.MsgExec) error {
    -	for _, v := range execMsg.Msgs {
    -		var innerMsg sdk.Msg
    -		if err := vdd.cdc.UnpackAny(v, &innerMsg); err != nil {
    -			return errorsmod.Wrap(err, "cannot unmarshal authz exec msgs")
    -		}
    -
    -		if err := vdd.validateMsg(ctx, innerMsg); err != nil {
    -			return err
    -		}
    -	}
    -
    -	return nil
    -}
    -
    -// validateMsg checks that the only vested coins can be delegated
    -func (vdd VestingDelegationDecorator) validateMsg(ctx sdk.Context, msg sdk.Msg) error {
    -	delegateMsg, ok := msg.(*stakingtypes.MsgDelegate)
    -	if !ok {
    -		return nil
    -	}
    -
    -	for _, addr := range msg.GetSigners() {
    -		acc := vdd.ak.GetAccount(ctx, addr)
    -		if acc == nil {
    -			return errorsmod.Wrapf(
    -				errortypes.ErrUnknownAddress,
    -				"account %s does not exist", addr,
    -			)
    -		}
    -
    -		clawbackAccount, isClawback := acc.(*vestingtypes.ClawbackVestingAccount)
    -		if !isClawback {
    -			// continue to next decorator as this logic only applies to vesting
    -			return nil
    -		}
    -
    -		// error if bond amount is > vested coins
    -		bondDenom := vdd.sk.BondDenom(ctx)
    -		coins := clawbackAccount.GetVestedOnly(ctx.BlockTime())
    -		if coins == nil || coins.Empty() {
    -			return errorsmod.Wrap(
    -				vestingtypes.ErrInsufficientVestedCoins,
    -				"account has no vested coins",
    -			)
    -		}
    -
    -		balance := vdd.bk.GetBalance(ctx, addr, bondDenom)
    -		unvestedOnly := clawbackAccount.GetUnvestedOnly(ctx.BlockTime())
    -		spendable, hasNeg := sdk.Coins{balance}.SafeSub(unvestedOnly...)
    -		if hasNeg {
    -			spendable = sdk.NewCoins()
    -		}
    -
    -		vested := spendable.AmountOf(bondDenom)
    -		if vested.LT(delegateMsg.Amount.Amount) {
    -			return errorsmod.Wrapf(
    -				vestingtypes.ErrInsufficientVestedCoins,
    -				"cannot delegate unvested coins. coins vested < delegation amount (%s < %s)",
    -				vested, delegateMsg.Amount.Amount,
    -			)
    -		}
    -	}
    -
    -	return nil
    -}
    
  • app/ante/handler_options.go+0 2 modified
    @@ -119,7 +119,6 @@ func newCosmosAnteHandler(options HandlerOptions) sdk.AnteHandler {
     		cosmosante.NewMinGasPriceDecorator(options.FeeMarketKeeper, options.EvmKeeper),
     		ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper),
     		cosmosante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.DistributionKeeper, options.FeegrantKeeper, options.StakingKeeper, options.TxFeeChecker),
    -		cosmosante.NewVestingDelegationDecorator(options.AccountKeeper, options.StakingKeeper, options.BankKeeper, options.Cdc),
     		// SetPubKeyDecorator must be called before all signature verification decorators
     		ante.NewSetPubKeyDecorator(options.AccountKeeper),
     		ante.NewValidateSigCountDecorator(options.AccountKeeper),
    @@ -146,7 +145,6 @@ func newLegacyCosmosAnteHandlerEip712(options HandlerOptions) sdk.AnteHandler {
     		ante.NewValidateMemoDecorator(options.AccountKeeper),
     		ante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper),
     		cosmosante.NewDeductFeeDecorator(options.AccountKeeper, options.BankKeeper, options.DistributionKeeper, options.FeegrantKeeper, options.StakingKeeper, options.TxFeeChecker),
    -		cosmosante.NewVestingDelegationDecorator(options.AccountKeeper, options.StakingKeeper, options.BankKeeper, options.Cdc),
     		// SetPubKeyDecorator must be called before all signature verification decorators
     		ante.NewSetPubKeyDecorator(options.AccountKeeper),
     		ante.NewValidateSigCountDecorator(options.AccountKeeper),
    
  • app/app.go+9 8 modified
    @@ -88,8 +88,7 @@ import (
     	"github.com/cosmos/cosmos-sdk/x/slashing"
     	slashingkeeper "github.com/cosmos/cosmos-sdk/x/slashing/keeper"
     	slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
    -	"github.com/cosmos/cosmos-sdk/x/staking"
    -	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
    +	sdkstaking "github.com/cosmos/cosmos-sdk/x/staking"
     	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
     	"github.com/cosmos/cosmos-sdk/x/upgrade"
     	upgradeclient "github.com/cosmos/cosmos-sdk/x/upgrade/client"
    @@ -161,6 +160,8 @@ import (
     	inflation "github.com/evmos/evmos/v18/x/inflation/v1"
     	inflationkeeper "github.com/evmos/evmos/v18/x/inflation/v1/keeper"
     	inflationtypes "github.com/evmos/evmos/v18/x/inflation/v1/types"
    +	"github.com/evmos/evmos/v18/x/staking"
    +	stakingkeeper "github.com/evmos/evmos/v18/x/staking/keeper"
     	revenue "github.com/evmos/evmos/v18/x/revenue/v1"
     	revenuekeeper "github.com/evmos/evmos/v18/x/revenue/v1/keeper"
     	revenuetypes "github.com/evmos/evmos/v18/x/revenue/v1/types"
    @@ -212,7 +213,7 @@ var (
     		genutil.NewAppModuleBasic(genutiltypes.DefaultMessageValidator),
     		bank.AppModuleBasic{},
     		capability.AppModuleBasic{},
    -		staking.AppModuleBasic{},
    +		staking.AppModuleBasic{AppModuleBasic: &sdkstaking.AppModuleBasic{}},
     		distr.AppModuleBasic{},
     		gov.NewAppModuleBasic(
     			[]govclient.ProposalHandler{
    @@ -646,12 +647,12 @@ func NewEvmos(
     		evm.NewAppModule(app.EvmKeeper, app.AccountKeeper, app.GetSubspace(evmtypes.ModuleName)),
     		feemarket.NewAppModule(app.FeeMarketKeeper, app.GetSubspace(feemarkettypes.ModuleName)),
     		// Evmos app modules
    -		inflation.NewAppModule(app.InflationKeeper, app.AccountKeeper, app.StakingKeeper,
    +		inflation.NewAppModule(app.InflationKeeper, app.AccountKeeper, *app.StakingKeeper.Keeper,
     			app.GetSubspace(inflationtypes.ModuleName)),
     		erc20.NewAppModule(app.Erc20Keeper, app.AccountKeeper,
     			app.GetSubspace(erc20types.ModuleName)),
     		epochs.NewAppModule(appCodec, app.EpochsKeeper),
    -		vesting.NewAppModule(app.VestingKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper),
    +		vesting.NewAppModule(app.VestingKeeper, app.AccountKeeper, app.BankKeeper, *app.StakingKeeper.Keeper),
     		revenue.NewAppModule(app.RevenueKeeper, app.AccountKeeper,
     			app.GetSubspace(revenuetypes.ModuleName)),
     	)
    @@ -1195,7 +1196,7 @@ func (app *Evmos) setupUpgradeHandlers() {
     		v10.UpgradeName,
     		v10.CreateUpgradeHandler(
     			app.mm, app.configurator,
    -			app.StakingKeeper,
    +			*app.StakingKeeper.Keeper,
     		),
     	)
     
    @@ -1206,7 +1207,7 @@ func (app *Evmos) setupUpgradeHandlers() {
     			app.mm, app.configurator,
     			app.AccountKeeper,
     			app.BankKeeper,
    -			app.StakingKeeper,
    +			*app.StakingKeeper.Keeper,
     			app.DistrKeeper,
     		),
     	)
    @@ -1252,7 +1253,7 @@ func (app *Evmos) setupUpgradeHandlers() {
     			app.mm, app.configurator,
     			app.BankKeeper,
     			app.EvmKeeper,
    -			app.StakingKeeper,
    +			*app.StakingKeeper.Keeper,
     		),
     	)
     
    
  • app/export.go+1 1 modified
    @@ -50,7 +50,7 @@ func (app *Evmos) ExportAppStateAndValidators(
     		return servertypes.ExportedApp{}, err
     	}
     
    -	validators, err := staking.WriteValidators(ctx, &app.StakingKeeper)
    +	validators, err := staking.WriteValidators(ctx, app.StakingKeeper.Keeper)
     	if err != nil {
     		return servertypes.ExportedApp{}, err
     	}
    
  • app/upgrades/v11/upgrades_test.go+2 2 modified
    @@ -107,7 +107,7 @@ func (suite *UpgradeTestSuite) setValidators(validatorsAddr []string) {
     		validator, err := stakingtypes.NewValidator(valAddr, suite.consKey, stakingtypes.Description{})
     		suite.Require().NoError(err)
     
    -		validator = stakingkeeper.TestingUpdateValidator(&suite.app.StakingKeeper, suite.ctx, validator, true)
    +		validator = stakingkeeper.TestingUpdateValidator(suite.app.StakingKeeper.Keeper, suite.ctx, validator, true)
     
     		err = suite.app.StakingKeeper.Hooks().AfterValidatorCreated(suite.ctx, validator.GetOperator())
     		suite.Require().NoError(err)
    @@ -253,7 +253,7 @@ func (suite *UpgradeTestSuite) TestDistributeRewards() {
     			suite.Require().Equal(math.ZeroInt(), initialDel)
     
     			if utils.IsMainnet(tc.chainID) {
    -				v11.HandleRewardDistribution(suite.ctx, suite.app.Logger(), suite.app.BankKeeper, suite.app.StakingKeeper, suite.app.DistrKeeper)
    +				v11.HandleRewardDistribution(suite.ctx, suite.app.Logger(), suite.app.BankKeeper, *suite.app.StakingKeeper.Keeper, suite.app.DistrKeeper)
     			}
     
     			// account not in list should NOT get rewards
    
  • app/upgrades/v14/migrations_test.go+3 3 modified
    @@ -136,11 +136,11 @@ func (s *UpgradesTestSuite) TestUpdateMigrateNativeMultisigs() {
     	expectedSharesMap[s.validators[0].OperatorAddress] = expectedSharesMap[s.validators[0].OperatorAddress].Sub(delegateShares)
     
     	// Migrate strategic reserves
    -	err = v14.MigrateNativeMultisigs(s.ctx, s.app.BankKeeper, s.app.StakingKeeper, newStrategicReserve.Addr, oldStrategicReservesAddrs...)
    +	err = v14.MigrateNativeMultisigs(s.ctx, s.app.BankKeeper, *s.app.StakingKeeper.Keeper, newStrategicReserve.Addr, oldStrategicReservesAddrs...)
     	s.Require().NoError(err, "failed to migrate strategic reserves")
     
     	// Migrate premint wallet
    -	err = v14.MigrateNativeMultisigs(s.ctx, s.app.BankKeeper, s.app.StakingKeeper, newPremintWallet.Addr, oldPremintWallet.Addr.String())
    +	err = v14.MigrateNativeMultisigs(s.ctx, s.app.BankKeeper, *s.app.StakingKeeper.Keeper, newPremintWallet.Addr, oldPremintWallet.Addr.String())
     	s.Require().NoError(err, "failed to migrate premint wallet")
     
     	// Check that the multisigs have been updated
    @@ -162,7 +162,7 @@ func (s *UpgradesTestSuite) TestInstantUnbonding() {
     	delegation, found := s.app.StakingKeeper.GetDelegation(s.ctx, s.address.Bytes(), s.validators[0].GetOperator())
     	s.Require().True(found, "delegation not found")
     
    -	unbondAmount, err := v14.InstantUnbonding(s.ctx, s.app.BankKeeper, s.app.StakingKeeper, delegation, s.bondDenom)
    +	unbondAmount, err := v14.InstantUnbonding(s.ctx, s.app.BankKeeper, *s.app.StakingKeeper.Keeper, delegation, s.bondDenom)
     	s.Require().NoError(err, "failed to unbond")
     	s.Require().Equal(unbondAmount, math.NewInt(1e18), "expected different unbond amount")
     
    
  • precompiles/distribution/distribution.go+1 2 modified
    @@ -8,15 +8,14 @@ import (
     	"embed"
     	"fmt"
     
    -	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
    -
     	storetypes "github.com/cosmos/cosmos-sdk/store/types"
     	authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper"
     	distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper"
     	"github.com/ethereum/go-ethereum/accounts/abi"
     	"github.com/ethereum/go-ethereum/common"
     	"github.com/ethereum/go-ethereum/core/vm"
     	cmn "github.com/evmos/evmos/v18/precompiles/common"
    +	stakingkeeper "github.com/evmos/evmos/v18/x/staking/keeper"
     )
     
     var _ vm.PrecompiledContract = &Precompile{}
    
  • precompiles/distribution/integration_test.go+2 2 modified
    @@ -204,7 +204,7 @@ var _ = Describe("Calling distribution precompile from EOA", func() {
     			// create a validator with s.address and s.privKey because this account is
     			// used for signing txs
     			stakeAmt = math.NewInt(100)
    -			testutil.CreateValidator(s.ctx, s.T(), s.privKey.PubKey(), s.app.StakingKeeper, stakeAmt)
    +			testutil.CreateValidator(s.ctx, s.T(), s.privKey.PubKey(), *s.app.StakingKeeper.Keeper, stakeAmt)
     
     			// set some validator commission
     			valAddr = s.address.Bytes()
    @@ -793,7 +793,7 @@ var _ = Describe("Calling distribution precompile from another contract", func()
     			// used for signing txs
     			valAddr = s.address.Bytes()
     			stakeAmt := math.NewInt(100)
    -			testutil.CreateValidator(s.ctx, s.T(), s.privKey.PubKey(), s.app.StakingKeeper, stakeAmt)
    +			testutil.CreateValidator(s.ctx, s.T(), s.privKey.PubKey(), *s.app.StakingKeeper.Keeper, stakeAmt)
     
     			// set some commissions to validators
     			var valAddresses []sdk.ValAddress
    
  • precompiles/distribution/utils_test.go+1 1 modified
    @@ -243,7 +243,7 @@ func (s *PrecompileTestSuite) prepareStakingRewards(stkRs ...stakingRewards) {
     		s.Require().NoError(err)
     
     		// end block to bond validator and increase block height
    -		sdkstaking.EndBlocker(s.ctx, &s.app.StakingKeeper)
    +		sdkstaking.EndBlocker(s.ctx, s.app.StakingKeeper.Keeper)
     		// allocate rewards to validator (of these 50% will be paid out to the delegator)
     		allocatedRewards := sdk.NewDecCoins(sdk.NewDecCoin(s.bondDenom, r.RewardAmt.Mul(math.NewInt(2))))
     		s.app.DistrKeeper.AllocateTokensToValidator(s.ctx, r.Validator, allocatedRewards)
    
  • precompiles/outposts/osmosis/osmosis.go+1 1 modified
    @@ -10,7 +10,6 @@ import (
     	storetypes "github.com/cosmos/cosmos-sdk/store/types"
     	authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper"
     	bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
    -	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
     	clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types"
     	channelkeeper "github.com/cosmos/ibc-go/v7/modules/core/04-channel/keeper"
     	"github.com/ethereum/go-ethereum/accounts/abi"
    @@ -20,6 +19,7 @@ import (
     	"github.com/evmos/evmos/v18/precompiles/ics20"
     	erc20keeper "github.com/evmos/evmos/v18/x/erc20/keeper"
     	transferkeeper "github.com/evmos/evmos/v18/x/ibc/transfer/keeper"
    +	stakingkeeper "github.com/evmos/evmos/v18/x/staking/keeper"
     )
     
     const (
    
  • precompiles/outposts/stride/stride.go+1 1 modified
    @@ -9,7 +9,6 @@ import (
     
     	storetypes "github.com/cosmos/cosmos-sdk/store/types"
     	authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper"
    -	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
     	clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types"
     
     	"github.com/ethereum/go-ethereum/accounts/abi"
    @@ -19,6 +18,7 @@ import (
     	"github.com/evmos/evmos/v18/precompiles/ics20"
     	erc20keeper "github.com/evmos/evmos/v18/x/erc20/keeper"
     	transferkeeper "github.com/evmos/evmos/v18/x/ibc/transfer/keeper"
    +	stakingkeeper "github.com/evmos/evmos/v18/x/staking/keeper"
     )
     
     var _ vm.PrecompiledContract = &Precompile{}
    
  • precompiles/staking/integration_test.go+397 1 modified
    @@ -20,6 +20,7 @@ import (
     	"github.com/ethereum/go-ethereum/common"
     	"github.com/ethereum/go-ethereum/core/vm"
     	compiledcontracts "github.com/evmos/evmos/v18/contracts"
    +	"github.com/evmos/evmos/v18/crypto/ethsecp256k1"
     	"github.com/evmos/evmos/v18/precompiles/authorization"
     	cmn "github.com/evmos/evmos/v18/precompiles/common"
     	"github.com/evmos/evmos/v18/precompiles/distribution"
    @@ -29,6 +30,7 @@ import (
     	"github.com/evmos/evmos/v18/precompiles/testutil/contracts"
     	evmosutil "github.com/evmos/evmos/v18/testutil"
     	testutiltx "github.com/evmos/evmos/v18/testutil/tx"
    +	vestingtypes "github.com/evmos/evmos/v18/x/vesting/types"
     )
     
     // General variables used for integration tests
    @@ -736,6 +738,219 @@ var _ = Describe("Calling staking precompile directly", func() {
     		})
     	})
     
    +	Describe("Calling precompile txs from a vesting account", func() {
    +		var (
    +			funder          common.Address
    +			vestAcc         common.Address
    +			vestAccPriv     *ethsecp256k1.PrivKey
    +			clawbackAccount *vestingtypes.ClawbackVestingAccount
    +			unvested        sdk.Coins
    +			vested          sdk.Coins
    +			// unlockedVested are unlocked vested coins of the vesting schedule
    +			unlockedVested      sdk.Coins
    +			defaultDelegateArgs contracts.CallArgs
    +		)
    +
    +		BeforeEach(func() {
    +			// Setup vesting account
    +			funder = s.address
    +			vestAcc, vestAccPriv = testutiltx.NewAddrKey()
    +			vestingAmtTotal := evmosutil.TestVestingSchedule.TotalVestingCoins
    +
    +			clawbackAccount = s.setupVestingAccount(funder.Bytes(), vestAcc.Bytes())
    +
    +			// Check if all tokens are unvested at vestingStart
    +			unvested = clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
    +			vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +			Expect(vestingAmtTotal).To(Equal(unvested))
    +			Expect(vested.IsZero()).To(BeTrue())
    +
    +			// populate the default delegate args
    +			defaultDelegateArgs = defaultCallArgs.WithMethodName(staking.DelegateMethod)
    +			defaultDelegateArgs = defaultDelegateArgs.WithPrivKey(vestAccPriv)
    +		})
    +
    +		Context("before first vesting period - all tokens locked and unvested", func() {
    +			BeforeEach(func() {
    +				s.NextBlock()
    +
    +				// Ensure no tokens are vested
    +				vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +				unvested = clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
    +				unlocked := clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
    +				zeroCoins := sdk.NewCoins(sdk.NewCoin(s.bondDenom, math.ZeroInt()))
    +				Expect(vested).To(Equal(zeroCoins), "expected different vested coins")
    +				Expect(unvested).To(Equal(evmosutil.TestVestingSchedule.TotalVestingCoins), "expected different unvested coins")
    +				Expect(unlocked).To(Equal(zeroCoins), "expected different unlocked coins")
    +			})
    +
    +			It("Should not be able to delegate unvested tokens", func() {
    +				delegateArgs := defaultDelegateArgs.WithArgs(
    +					vestAcc, valAddr.String(), unvested.AmountOf(s.bondDenom).BigInt(),
    +				)
    +
    +				failCheck := defaultLogCheck.
    +					WithErrContains("cannot delegate unvested coins")
    +
    +				_, _, err := contracts.CallContractAndCheckLogs(s.ctx, s.app, delegateArgs, failCheck)
    +				Expect(err).NotTo(BeNil(), "error while calling the smart contract: %v", err)
    +				Expect(err.Error()).To(ContainSubstring("coins available for delegation < delegation amount"))
    +			})
    +
    +			It("Should be able to delegate tokens not involved in vesting schedule", func() {
    +				// send some coins to the vesting account
    +				coinsToDelegate := sdk.NewCoins(sdk.NewCoin(s.bondDenom, math.NewInt(1e18)))
    +				err := evmosutil.FundAccount(s.ctx, s.app.BankKeeper, clawbackAccount.GetAddress(), coinsToDelegate)
    +				Expect(err).To(BeNil())
    +
    +				// check balance is updated
    +				balance := s.app.BankKeeper.GetBalance(s.ctx, clawbackAccount.GetAddress(), s.bondDenom)
    +				Expect(balance).To(Equal(accountGasCoverage[0].Add(evmosutil.TestVestingSchedule.TotalVestingCoins[0]).Add(coinsToDelegate[0])))
    +
    +				delegateArgs := defaultDelegateArgs.WithArgs(
    +					vestAcc, valAddr.String(), coinsToDelegate.AmountOf(s.bondDenom).BigInt(),
    +				)
    +
    +				logCheckArgs := passCheck.WithExpEvents(staking.EventTypeDelegate)
    +
    +				_, _, err = contracts.CallContractAndCheckLogs(s.ctx, s.app, delegateArgs, logCheckArgs)
    +				Expect(err).To(BeNil(), "error while calling the smart contract: %v", err)
    +
    +				delegation, found := s.app.StakingKeeper.GetDelegation(s.ctx, vestAcc.Bytes(), valAddr)
    +				Expect(found).To(BeTrue(), "expected delegation to be found")
    +				Expect(delegation.Shares.BigInt()).To(Equal(coinsToDelegate[0].Amount.BigInt()))
    +
    +				// check vesting balance is untouched
    +				balancePost := s.app.BankKeeper.GetBalance(s.ctx, clawbackAccount.GetAddress(), s.bondDenom)
    +				Expect(balancePost.IsGTE(evmosutil.TestVestingSchedule.TotalVestingCoins[0])).To(BeTrue())
    +			})
    +		})
    +
    +		Context("after first vesting period and before lockup - some vested tokens, but still all locked", func() {
    +			BeforeEach(func() {
    +				// Surpass cliff but none of lockup duration
    +				cliffDuration := time.Duration(evmosutil.TestVestingSchedule.CliffPeriodLength)
    +				s.NextBlockAfter(cliffDuration * time.Second)
    +
    +				// Check if some, but not all tokens are vested
    +				vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +				expVested := sdk.NewCoins(sdk.NewCoin(s.bondDenom, evmosutil.TestVestingSchedule.VestedCoinsPerPeriod[0].Amount.Mul(math.NewInt(evmosutil.TestVestingSchedule.CliffMonths))))
    +				Expect(vested).NotTo(Equal(evmosutil.TestVestingSchedule.TotalVestingCoins), "expected some tokens to have been vested")
    +				Expect(vested).To(Equal(expVested), "expected different vested amount")
    +
    +				// check the vested tokens are still locked
    +				unlockedVested = clawbackAccount.GetUnlockedVestedCoins(s.ctx.BlockTime())
    +				Expect(unlockedVested).To(Equal(sdk.Coins{}))
    +
    +				vestingAmtTotal := evmosutil.TestVestingSchedule.TotalVestingCoins
    +				res, err := s.app.VestingKeeper.Balances(s.ctx, &vestingtypes.QueryBalancesRequest{Address: clawbackAccount.Address})
    +				Expect(err).To(BeNil())
    +				Expect(res.Vested).To(Equal(expVested))
    +				Expect(res.Unvested).To(Equal(vestingAmtTotal.Sub(expVested...)))
    +				// All coins from vesting schedule should be locked
    +				Expect(res.Locked).To(Equal(vestingAmtTotal))
    +			})
    +
    +			It("Should be able to delegate locked vested tokens", func() {
    +				delegateArgs := defaultDelegateArgs.WithArgs(
    +					vestAcc, valAddr.String(), vested[0].Amount.BigInt(),
    +				)
    +
    +				logCheckArgs := passCheck.WithExpEvents(staking.EventTypeDelegate)
    +
    +				_, _, err := contracts.CallContractAndCheckLogs(s.ctx, s.app, delegateArgs, logCheckArgs)
    +				Expect(err).To(BeNil(), "error while calling the smart contract: %v", err)
    +
    +				delegation, found := s.app.StakingKeeper.GetDelegation(s.ctx, vestAcc.Bytes(), valAddr)
    +				Expect(found).To(BeTrue(), "expected delegation to be found")
    +				Expect(delegation.Shares.BigInt()).To(Equal(vested[0].Amount.BigInt()))
    +			})
    +
    +			It("Should be able to delegate locked vested tokens + free tokens (not in vesting schedule)", func() {
    +				// send some coins to the vesting account
    +				amt := sdk.NewCoins(sdk.NewCoin(s.bondDenom, math.NewInt(1e18)))
    +				err := evmosutil.FundAccount(s.ctx, s.app.BankKeeper, clawbackAccount.GetAddress(), amt)
    +				Expect(err).To(BeNil())
    +
    +				// check balance is updated
    +				balance := s.app.BankKeeper.GetBalance(s.ctx, clawbackAccount.GetAddress(), s.bondDenom)
    +				Expect(balance).To(Equal(accountGasCoverage[0].Add(evmosutil.TestVestingSchedule.TotalVestingCoins[0]).Add(amt[0])))
    +
    +				coinsToDelegate := amt.Add(vested...)
    +
    +				delegateArgs := defaultDelegateArgs.WithArgs(
    +					vestAcc, valAddr.String(), coinsToDelegate[0].Amount.BigInt(),
    +				)
    +
    +				logCheckArgs := passCheck.WithExpEvents(staking.EventTypeDelegate)
    +
    +				_, _, err = contracts.CallContractAndCheckLogs(s.ctx, s.app, delegateArgs, logCheckArgs)
    +				Expect(err).To(BeNil(), "error while calling the smart contract: %v", err)
    +
    +				delegation, found := s.app.StakingKeeper.GetDelegation(s.ctx, vestAcc.Bytes(), valAddr)
    +				Expect(found).To(BeTrue(), "expected delegation to be found")
    +				Expect(delegation.Shares.BigInt()).To(Equal(coinsToDelegate[0].Amount.BigInt()))
    +			})
    +		})
    +
    +		Context("Between first and second lockup periods - vested coins are unlocked", func() {
    +			BeforeEach(func() {
    +				// Surpass first lockup
    +				vestDuration := time.Duration(evmosutil.TestVestingSchedule.LockupPeriodLength)
    +				s.NextBlockAfter(vestDuration * time.Second)
    +
    +				// Check if some, but not all tokens are vested and unlocked
    +				vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +				unlocked := clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
    +				unlockedVested = clawbackAccount.GetUnlockedVestedCoins(s.ctx.BlockTime())
    +
    +				expVested := sdk.NewCoins(sdk.NewCoin(s.bondDenom, evmosutil.TestVestingSchedule.VestedCoinsPerPeriod[0].Amount.Mul(math.NewInt(evmosutil.TestVestingSchedule.LockupMonths))))
    +				expUnlockedVested := expVested
    +
    +				Expect(vested).NotTo(Equal(evmosutil.TestVestingSchedule.TotalVestingCoins), "expected not all tokens to be vested")
    +				Expect(vested).To(Equal(expVested), "expected different amount of vested tokens")
    +				// all vested coins are unlocked
    +				Expect(unlockedVested).To(Equal(vested))
    +				Expect(unlocked).To(Equal(evmosutil.TestVestingSchedule.UnlockedCoinsPerLockup))
    +				Expect(unlockedVested).To(Equal(expUnlockedVested))
    +			})
    +			It("Should be able to delegate unlocked vested tokens", func() {
    +				delegateArgs := defaultDelegateArgs.WithArgs(
    +					vestAcc, valAddr.String(), unlockedVested[0].Amount.BigInt(),
    +				)
    +
    +				logCheckArgs := passCheck.WithExpEvents(staking.EventTypeDelegate)
    +
    +				_, _, err := contracts.CallContractAndCheckLogs(s.ctx, s.app, delegateArgs, logCheckArgs)
    +				Expect(err).To(BeNil(), "error while calling the smart contract: %v", err)
    +
    +				delegation, found := s.app.StakingKeeper.GetDelegation(s.ctx, vestAcc.Bytes(), valAddr)
    +				Expect(found).To(BeTrue(), "expected delegation to be found")
    +				Expect(delegation.Shares.BigInt()).To(Equal(unlockedVested[0].Amount.BigInt()))
    +			})
    +
    +			It("Cannot delegate more than vested tokens (and free tokens)", func() {
    +				// calculate the delegatable amount
    +				balance := s.app.BankKeeper.GetBalance(s.ctx, vestAcc.Bytes(), s.bondDenom)
    +				unvestedOnly := clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
    +				delegatable := balance.Sub(unvestedOnly[0])
    +
    +				delegateArgs := defaultDelegateArgs.WithArgs(
    +					vestAcc, valAddr.String(), delegatable.Amount.Add(sdk.OneInt()).BigInt(),
    +				)
    +
    +				logCheckArgs := passCheck.WithExpEvents(staking.EventTypeDelegate)
    +
    +				_, _, err := contracts.CallContractAndCheckLogs(s.ctx, s.app, delegateArgs, logCheckArgs)
    +				Expect(err).NotTo(BeNil(), "error while calling the smart contract: %v", err)
    +				Expect(err.Error()).To(ContainSubstring("cannot delegate unvested coins"))
    +
    +				_, found := s.app.StakingKeeper.GetDelegation(s.ctx, vestAcc.Bytes(), valAddr)
    +				Expect(found).To(BeFalse(), "expected delegation NOT to be found")
    +			})
    +		})
    +	})
    +
     	Describe("to query allowance", func() {
     		var (
     			defaultAllowanceArgs contracts.CallArgs
    @@ -1649,7 +1864,7 @@ var _ = Describe("Calling staking precompile via Solidity", func() {
     
     			It("shouldn't delegate to a validator that is not in the allow list of the approval", func() {
     				// create a new validator, which is not included in the active set of the last block
    -				testutil.CreateValidator(s.ctx, s.T(), s.privKey.PubKey(), s.app.StakingKeeper, math.NewInt(100))
    +				testutil.CreateValidator(s.ctx, s.T(), s.privKey.PubKey(), *s.app.StakingKeeper.Keeper, math.NewInt(100))
     				newValAddr := sdk.ValAddress(s.address.Bytes())
     
     				delegateArgs := defaultDelegateArgs.WithArgs(
    @@ -1663,6 +1878,187 @@ var _ = Describe("Calling staking precompile via Solidity", func() {
     				Expect(delegation.GetShares()).To(Equal(math.LegacyNewDecFromInt(math.NewInt(100))), "expected only the delegation from creating the validator, no more")
     			})
     		})
    +		Describe("delegation from a vesting account", func() {
    +			var (
    +				funder          common.Address
    +				vestAcc         common.Address
    +				vestAccPriv     *ethsecp256k1.PrivKey
    +				clawbackAccount *vestingtypes.ClawbackVestingAccount
    +				unvested        sdk.Coins
    +				vested          sdk.Coins
    +				// unlockedVested are unlocked vested coins of the vesting schedule
    +				unlockedVested sdk.Coins
    +				defaultArgs    contracts.CallArgs
    +			)
    +
    +			BeforeEach(func() {
    +				// Setup vesting account
    +				funder = s.address
    +				vestAcc, vestAccPriv = testutiltx.NewAddrKey()
    +
    +				clawbackAccount = s.setupVestingAccount(funder.Bytes(), vestAcc.Bytes())
    +
    +				// Check if all tokens are unvested at vestingStart
    +				totalVestingCoins := evmosutil.TestVestingSchedule.TotalVestingCoins
    +				unvested = clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
    +				vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +				Expect(unvested).To(Equal(totalVestingCoins))
    +				Expect(vested.IsZero()).To(BeTrue())
    +
    +				// create approval to allow spending all vesting coins
    +				cArgs := defaultApproveArgs.WithArgs(
    +					contractAddr, []string{staking.DelegateMsg}, totalVestingCoins.AmountOf(s.bondDenom).BigInt(),
    +				).WithPrivKey(vestAccPriv)
    +				s.SetupApprovalWithContractCalls(cArgs)
    +
    +				// add the vesting account priv key to the delegate args
    +				defaultArgs = defaultDelegateArgs.WithPrivKey(vestAccPriv)
    +			})
    +
    +			Context("before first vesting period - all tokens locked and unvested", func() {
    +				BeforeEach(func() {
    +					s.NextBlock()
    +
    +					// Ensure no tokens are vested
    +					vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +					unvested = clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
    +					unlocked := clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
    +					zeroCoins := sdk.NewCoins(sdk.NewCoin(s.bondDenom, math.ZeroInt()))
    +					Expect(vested).To(Equal(zeroCoins))
    +					Expect(unvested).To(Equal(evmosutil.TestVestingSchedule.TotalVestingCoins))
    +					Expect(unlocked).To(Equal(zeroCoins))
    +				})
    +
    +				It("Should not be able to delegate unvested tokens", func() {
    +					delegateArgs := defaultArgs.WithArgs(
    +						vestAcc, valAddr.String(), unvested.AmountOf(s.bondDenom).BigInt(),
    +					)
    +
    +					_, _, err := contracts.CallContractAndCheckLogs(s.ctx, s.app, delegateArgs, execRevertedCheck)
    +					Expect(err).To(HaveOccurred(), "error while calling the smart contract: %v", err)
    +				})
    +
    +				It("Should be able to delegate tokens not involved in vesting schedule", func() {
    +					// send some coins to the vesting account
    +					coinsToDelegate := sdk.NewCoins(sdk.NewCoin(s.bondDenom, math.NewInt(1e18)))
    +					err := evmosutil.FundAccount(s.ctx, s.app.BankKeeper, clawbackAccount.GetAddress(), coinsToDelegate)
    +					Expect(err).To(BeNil())
    +
    +					delegateArgs := defaultArgs.WithArgs(
    +						vestAcc, valAddr.String(), coinsToDelegate.AmountOf(s.bondDenom).BigInt(),
    +					)
    +
    +					logCheckArgs := passCheck.WithExpEvents(staking.EventTypeDelegate)
    +
    +					_, _, err = contracts.CallContractAndCheckLogs(s.ctx, s.app, delegateArgs, logCheckArgs)
    +					Expect(err).To(BeNil(), "error while calling the smart contract: %v", err)
    +
    +					delegation, found := s.app.StakingKeeper.GetDelegation(s.ctx, vestAcc.Bytes(), valAddr)
    +					Expect(found).To(BeTrue(), "expected delegation to be found")
    +					Expect(delegation.Shares.BigInt()).To(Equal(coinsToDelegate[0].Amount.BigInt()))
    +				})
    +			})
    +
    +			Context("after first vesting period and before lockup - some vested tokens, but still all locked", func() {
    +				BeforeEach(func() {
    +					// Surpass cliff but none of lockup duration
    +					cliffDuration := time.Duration(evmosutil.TestVestingSchedule.CliffPeriodLength)
    +					s.NextBlockAfter(cliffDuration * time.Second)
    +
    +					// Check if some, but not all tokens are vested
    +					vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +					expVested := sdk.NewCoins(sdk.NewCoin(s.bondDenom, evmosutil.TestVestingSchedule.VestedCoinsPerPeriod[0].Amount.Mul(math.NewInt(evmosutil.TestVestingSchedule.CliffMonths))))
    +					Expect(vested).NotTo(Equal(evmosutil.TestVestingSchedule.TotalVestingCoins))
    +					Expect(vested).To(Equal(expVested))
    +
    +					// check the vested tokens are still locked
    +					unlockedVested = clawbackAccount.GetUnlockedVestedCoins(s.ctx.BlockTime())
    +					Expect(unlockedVested).To(Equal(sdk.Coins{}))
    +
    +					vestingAmtTotal := evmosutil.TestVestingSchedule.TotalVestingCoins
    +					res, err := s.app.VestingKeeper.Balances(s.ctx, &vestingtypes.QueryBalancesRequest{Address: clawbackAccount.Address})
    +					Expect(err).To(BeNil())
    +					Expect(res.Vested).To(Equal(expVested))
    +					Expect(res.Unvested).To(Equal(vestingAmtTotal.Sub(expVested...)))
    +					// All coins from vesting schedule should be locked
    +					Expect(res.Locked).To(Equal(vestingAmtTotal))
    +				})
    +
    +				It("Should be able to delegate locked vested tokens", func() {
    +					delegateArgs := defaultArgs.WithArgs(
    +						vestAcc, valAddr.String(), vested[0].Amount.BigInt(),
    +					)
    +
    +					logCheckArgs := passCheck.WithExpEvents(staking.EventTypeDelegate)
    +
    +					_, _, err := contracts.CallContractAndCheckLogs(s.ctx, s.app, delegateArgs, logCheckArgs)
    +					Expect(err).To(BeNil(), "error while calling the smart contract: %v", err)
    +
    +					delegation, found := s.app.StakingKeeper.GetDelegation(s.ctx, vestAcc.Bytes(), valAddr)
    +					Expect(found).To(BeTrue(), "expected delegation to be found")
    +					Expect(delegation.Shares.BigInt()).To(Equal(vested[0].Amount.BigInt()))
    +				})
    +
    +				It("Should be able to delegate locked vested tokens + free tokens (not in vesting schedule)", func() {
    +					// send some coins to the vesting account
    +					amt := sdk.NewCoins(sdk.NewCoin(s.bondDenom, math.NewInt(1e18)))
    +					err := evmosutil.FundAccount(s.ctx, s.app.BankKeeper, clawbackAccount.GetAddress(), amt)
    +					Expect(err).To(BeNil())
    +
    +					coinsToDelegate := amt.Add(vested...)
    +
    +					delegateArgs := defaultArgs.WithArgs(
    +						vestAcc, valAddr.String(), coinsToDelegate[0].Amount.BigInt(),
    +					)
    +
    +					logCheckArgs := passCheck.WithExpEvents(staking.EventTypeDelegate)
    +
    +					_, _, err = contracts.CallContractAndCheckLogs(s.ctx, s.app, delegateArgs, logCheckArgs)
    +					Expect(err).To(BeNil(), "error while calling the smart contract: %v", err)
    +
    +					delegation, found := s.app.StakingKeeper.GetDelegation(s.ctx, vestAcc.Bytes(), valAddr)
    +					Expect(found).To(BeTrue(), "expected delegation to be found")
    +					Expect(delegation.Shares.BigInt()).To(Equal(coinsToDelegate[0].Amount.BigInt()))
    +				})
    +			})
    +
    +			Context("Between first and second lockup periods - vested coins are unlocked", func() {
    +				BeforeEach(func() {
    +					// Surpass first lockup
    +					vestDuration := time.Duration(evmosutil.TestVestingSchedule.LockupPeriodLength)
    +					s.NextBlockAfter(vestDuration * time.Second)
    +
    +					// Check if some, but not all tokens are vested and unlocked
    +					vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +					unlocked := clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
    +					unlockedVested = clawbackAccount.GetUnlockedVestedCoins(s.ctx.BlockTime())
    +
    +					expVested := sdk.NewCoins(sdk.NewCoin(s.bondDenom, evmosutil.TestVestingSchedule.VestedCoinsPerPeriod[0].Amount.Mul(math.NewInt(evmosutil.TestVestingSchedule.LockupMonths))))
    +					expUnlockedVested := expVested
    +
    +					Expect(vested).NotTo(Equal(evmosutil.TestVestingSchedule.TotalVestingCoins))
    +					Expect(vested).To(Equal(expVested))
    +					// all vested coins are unlocked
    +					Expect(unlockedVested).To(Equal(vested))
    +					Expect(unlocked).To(Equal(evmosutil.TestVestingSchedule.UnlockedCoinsPerLockup))
    +					Expect(unlockedVested).To(Equal(expUnlockedVested))
    +				})
    +				It("Should be able to delegate unlocked vested tokens", func() {
    +					delegateArgs := defaultArgs.WithArgs(
    +						vestAcc, valAddr.String(), unlockedVested[0].Amount.BigInt(),
    +					)
    +
    +					logCheckArgs := passCheck.WithExpEvents(staking.EventTypeDelegate)
    +
    +					_, _, err := contracts.CallContractAndCheckLogs(s.ctx, s.app, delegateArgs, logCheckArgs)
    +					Expect(err).To(BeNil(), "error while calling the smart contract: %v", err)
    +
    +					delegation, found := s.app.StakingKeeper.GetDelegation(s.ctx, vestAcc.Bytes(), valAddr)
    +					Expect(found).To(BeTrue(), "expected delegation to be found")
    +					Expect(delegation.Shares.BigInt()).To(Equal(unlockedVested[0].Amount.BigInt()))
    +				})
    +			})
    +		})
     	})
     
     	Context("unbonding", func() {
    
  • precompiles/staking/query.go+5 5 modified
    @@ -50,7 +50,7 @@ func (p Precompile) Delegation(
     		return nil, err
     	}
     
    -	queryServer := stakingkeeper.Querier{Keeper: &p.stakingKeeper}
    +	queryServer := stakingkeeper.Querier{Keeper: p.stakingKeeper.Keeper}
     
     	res, err := queryServer.Delegation(sdk.WrapSDKContext(ctx), req)
     	if err != nil {
    @@ -80,7 +80,7 @@ func (p Precompile) UnbondingDelegation(
     		return nil, err
     	}
     
    -	queryServer := stakingkeeper.Querier{Keeper: &p.stakingKeeper}
    +	queryServer := stakingkeeper.Querier{Keeper: p.stakingKeeper.Keeper}
     
     	res, err := queryServer.UnbondingDelegation(sdk.WrapSDKContext(ctx), req)
     	if err != nil {
    @@ -109,7 +109,7 @@ func (p Precompile) Validator(
     		return nil, err
     	}
     
    -	queryServer := stakingkeeper.Querier{Keeper: &p.stakingKeeper}
    +	queryServer := stakingkeeper.Querier{Keeper: p.stakingKeeper.Keeper}
     
     	res, err := queryServer.Validator(sdk.WrapSDKContext(ctx), req)
     	if err != nil {
    @@ -138,7 +138,7 @@ func (p Precompile) Validators(
     		return nil, err
     	}
     
    -	queryServer := stakingkeeper.Querier{Keeper: &p.stakingKeeper}
    +	queryServer := stakingkeeper.Querier{Keeper: p.stakingKeeper.Keeper}
     
     	res, err := queryServer.Validators(sdk.WrapSDKContext(ctx), req)
     	if err != nil {
    @@ -184,7 +184,7 @@ func (p Precompile) Redelegations(
     		return nil, err
     	}
     
    -	queryServer := stakingkeeper.Querier{Keeper: &p.stakingKeeper}
    +	queryServer := stakingkeeper.Querier{Keeper: p.stakingKeeper.Keeper}
     
     	res, err := queryServer.Redelegations(ctx, req)
     	if err != nil {
    
  • precompiles/staking/staking.go+1 1 modified
    @@ -11,12 +11,12 @@ import (
     	"github.com/cometbft/cometbft/libs/log"
     	storetypes "github.com/cosmos/cosmos-sdk/store/types"
     	authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper"
    -	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
     	"github.com/ethereum/go-ethereum/accounts/abi"
     	"github.com/ethereum/go-ethereum/common"
     	"github.com/ethereum/go-ethereum/core/vm"
     	"github.com/evmos/evmos/v18/precompiles/authorization"
     	cmn "github.com/evmos/evmos/v18/precompiles/common"
    +	stakingkeeper "github.com/evmos/evmos/v18/x/staking/keeper"
     )
     
     var _ vm.PrecompiledContract = &Precompile{}
    
  • precompiles/staking/tx.go+1 1 modified
    @@ -8,13 +8,13 @@ import (
     	"time"
     
     	sdk "github.com/cosmos/cosmos-sdk/types"
    -	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
     	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
     	"github.com/ethereum/go-ethereum/accounts/abi"
     	"github.com/ethereum/go-ethereum/common"
     	"github.com/ethereum/go-ethereum/core/vm"
     	"github.com/evmos/evmos/v18/precompiles/authorization"
     	"github.com/evmos/evmos/v18/x/evm/statedb"
    +	stakingkeeper "github.com/evmos/evmos/v18/x/staking/keeper"
     )
     
     const (
    
  • precompiles/staking/utils_test.go+57 4 modified
    @@ -2,6 +2,7 @@ package staking_test
     
     import (
     	"encoding/json"
    +	"fmt"
     	"math/big"
     	"time"
     
    @@ -20,12 +21,13 @@ import (
     	sdk "github.com/cosmos/cosmos-sdk/types"
     	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
     	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
    -	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
     	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
     	"github.com/ethereum/go-ethereum/accounts/abi"
     	"github.com/ethereum/go-ethereum/common"
     	ethtypes "github.com/ethereum/go-ethereum/core/types"
    +	"github.com/ethereum/go-ethereum/crypto"
     	evmosapp "github.com/evmos/evmos/v18/app"
    +	"github.com/evmos/evmos/v18/crypto/ethsecp256k1"
     	"github.com/evmos/evmos/v18/precompiles/authorization"
     	cmn "github.com/evmos/evmos/v18/precompiles/common"
     	"github.com/evmos/evmos/v18/precompiles/staking"
    @@ -38,9 +40,14 @@ import (
     	"github.com/evmos/evmos/v18/x/evm/statedb"
     	evmtypes "github.com/evmos/evmos/v18/x/evm/types"
     	inflationtypes "github.com/evmos/evmos/v18/x/inflation/v1/types"
    +	stakingkeeper "github.com/evmos/evmos/v18/x/staking/keeper"
    +	vestingtypes "github.com/evmos/evmos/v18/x/vesting/types"
     	"golang.org/x/exp/slices"
     )
     
    +// stipend to pay EVM tx fees
    +var accountGasCoverage = sdk.NewCoins(sdk.NewCoin(utils.BaseDenom, math.NewInt(1e16)))
    +
     // SetupWithGenesisValSet initializes a new EvmosApp with a validator set and genesis accounts
     // that also act as delegators. For simplicity, each validator is bonded with a delegation
     // of one consensus engine unit (10^6) in the default token of the simapp from first genesis
    @@ -328,6 +335,13 @@ func (s *PrecompileTestSuite) SetupApprovalWithContractCalls(approvalArgs contra
     	_, _, err := contracts.CallContractAndCheckLogs(s.ctx, s.app, approvalArgs, logCheckArgs)
     	Expect(err).To(BeNil(), "error while approving: %v", err)
     
    +	// get granter address from private key provided
    +	pk, ok := approvalArgs.PrivKey.(*ethsecp256k1.PrivKey)
    +	Expect(ok).To(BeTrue(), fmt.Sprintf("expected a ethsecp256k1.PrivKey, but got %T", approvalArgs.PrivKey))
    +	key, err := pk.ToECDSA()
    +	Expect(err).To(BeNil())
    +	granter := crypto.PubkeyToAddress(key.PublicKey)
    +
     	// iterate over args
     	var expectedAuthz stakingtypes.AuthorizationType
     	for _, msgType := range msgTypes {
    @@ -341,7 +355,7 @@ func (s *PrecompileTestSuite) SetupApprovalWithContractCalls(approvalArgs contra
     		case staking.CancelUnbondingDelegationMsg:
     			expectedAuthz = staking.CancelUnbondingDelegationAuthz
     		}
    -		authz, expirationTime := s.CheckAuthorization(expectedAuthz, approvalArgs.ContractAddr, s.address)
    +		authz, expirationTime := s.CheckAuthorization(expectedAuthz, approvalArgs.ContractAddr, granter)
     		Expect(authz).ToNot(BeNil(), "expected authorization to be set")
     		Expect(authz.MaxTokens.Amount).To(Equal(math.NewInt(expAmount.Int64())), "expected different allowance")
     		Expect(authz.MsgTypeURL()).To(Equal(msgType), "expected different message type")
    @@ -363,8 +377,13 @@ func (s *PrecompileTestSuite) DeployContract(contract evmtypes.CompiledContract)
     
     // NextBlock commits the current block and sets up the next block.
     func (s *PrecompileTestSuite) NextBlock() {
    +	s.NextBlockAfter(time.Second)
    +}
    +
    +// NextBlock commits the current block and sets up the next block.
    +func (s *PrecompileTestSuite) NextBlockAfter(t time.Duration) {
     	var err error
    -	s.ctx, err = evmosutil.CommitAndCreateNewCtx(s.ctx, s.app, time.Second, nil)
    +	s.ctx, err = evmosutil.CommitAndCreateNewCtx(s.ctx, s.app, t, nil)
     	Expect(err).To(BeNil(), "failed to commit block")
     }
     
    @@ -499,7 +518,7 @@ func (s *PrecompileTestSuite) setupRedelegations(redelAmt *big.Int) error {
     
     	// create a validator with s.address and s.privKey
     	// then create a redelegation from validator[0] to this new validator
    -	testutil.CreateValidator(s.ctx, s.T(), s.privKey.PubKey(), s.app.StakingKeeper, math.NewInt(100))
    +	testutil.CreateValidator(s.ctx, s.T(), s.privKey.PubKey(), *s.app.StakingKeeper.Keeper, math.NewInt(100))
     	msg.ValidatorDstAddress = sdk.ValAddress(s.address.Bytes()).String()
     	_, err := msgSrv.BeginRedelegate(s.ctx, &msg)
     	return err
    @@ -514,3 +533,37 @@ func (s *PrecompileTestSuite) CheckValidatorOutput(valOut staking.ValidatorInfo)
     	Expect(slices.Contains(validatorAddrs, valOut.OperatorAddress)).To(BeTrue(), "operator address not found in test suite validators")
     	Expect(valOut.DelegatorShares).To(Equal(big.NewInt(1e18)), "expected different delegator shares")
     }
    +
    +// setupVestingAccount is a helper function used in integraiton tests to setup a vesting account
    +// using the TestVestingSchedule. Also, funds the account with extra funds to pay for transaction fees
    +func (s *PrecompileTestSuite) setupVestingAccount(funder, vestAcc sdk.AccAddress) *vestingtypes.ClawbackVestingAccount {
    +	vestingAmtTotal := evmosutil.TestVestingSchedule.TotalVestingCoins
    +
    +	vestingStart := s.ctx.BlockTime()
    +	baseAccount := authtypes.NewBaseAccountWithAddress(vestAcc.Bytes())
    +	clawbackAccount := vestingtypes.NewClawbackVestingAccount(
    +		baseAccount,
    +		funder,
    +		vestingAmtTotal,
    +		vestingStart,
    +		evmosutil.TestVestingSchedule.LockupPeriods,
    +		evmosutil.TestVestingSchedule.VestingPeriods,
    +	)
    +
    +	err := evmosutil.FundAccount(s.ctx, s.app.BankKeeper, clawbackAccount.GetAddress(), vestingAmtTotal)
    +	Expect(err).To(BeNil())
    +	acc := s.app.AccountKeeper.NewAccount(s.ctx, clawbackAccount)
    +	s.app.AccountKeeper.SetAccount(s.ctx, acc)
    +
    +	// Check all coins are locked up
    +	lockedUp := clawbackAccount.GetLockedUpCoins(s.ctx.BlockTime())
    +	Expect(vestingAmtTotal).To(Equal(lockedUp))
    +
    +	// Grant gas stipend to cover EVM fees
    +	err = evmosutil.FundAccount(s.ctx, s.app.BankKeeper, clawbackAccount.GetAddress(), accountGasCoverage)
    +	Expect(err).To(BeNil())
    +	granteeBalance := s.app.BankKeeper.GetBalance(s.ctx, clawbackAccount.GetAddress(), s.bondDenom)
    +	Expect(granteeBalance).To(Equal(accountGasCoverage[0].Add(vestingAmtTotal[0])))
    +
    +	return clawbackAccount
    +}
    
  • tests/nix_tests/test_patches.py+5 3 modified
    @@ -12,6 +12,7 @@
         deploy_contract,
         eth_to_bech32,
         get_fees_from_tx_result,
    +    wait_for_cosmos_tx_receipt,
         wait_for_new_blocks,
     )
     
    @@ -371,7 +372,8 @@ def test_unvested_token_delegation(evmos_cluster):
         )
         tx = cli.sign_tx_json(tx, address, max_priority_price=0)
         rsp = cli.broadcast_tx_json(tx, broadcast_mode="sync")
    -
    +    # get tx receipt to check if tx failed as expected 
    +    receipt = wait_for_cosmos_tx_receipt(cli, rsp["txhash"])
         # assert tx fails with corresponding error message
    -    assert rsp["code"] == 2
    -    assert "insufficient vested coins" in rsp["raw_log"]
    +    assert receipt["tx_result"]["code"] == 2
    +    assert "insufficient vested coins" in receipt["tx_result"]["log"]
    
  • testutil/integration/evmos/network/clients.go+1 1 modified
    @@ -85,6 +85,6 @@ func (n *IntegrationNetwork) GetAuthzClient() authz.QueryClient {
     
     func (n *IntegrationNetwork) GetStakingClient() stakingtypes.QueryClient {
     	queryHelper := getQueryHelper(n.GetContext())
    -	stakingtypes.RegisterQueryServer(queryHelper, stakingkeeper.Querier{Keeper: &n.app.StakingKeeper})
    +	stakingtypes.RegisterQueryServer(queryHelper, stakingkeeper.Querier{Keeper: n.app.StakingKeeper.Keeper})
     	return stakingtypes.NewQueryClient(queryHelper)
     }
    
  • testutil/integration/evmos/network/config.go+29 0 modified
    @@ -9,6 +9,8 @@ import (
     	"github.com/evmos/evmos/v18/utils"
     
     	sdktypes "github.com/cosmos/cosmos-sdk/types"
    +	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
    +	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
     	evmostypes "github.com/evmos/evmos/v18/types"
     )
     
    @@ -20,6 +22,7 @@ type Config struct {
     	eip155ChainID      *big.Int
     	amountOfValidators int
     	preFundedAccounts  []sdktypes.AccAddress
    +	balances           []banktypes.Balance
     	denom              string
     }
     
    @@ -36,6 +39,24 @@ func DefaultConfig() Config {
     	}
     }
     
    +// getGenAccountsAndBalances takes the network configuration and returns the used
    +// genesis accounts and balances.
    +//
    +// NOTE: If the balances are set, the pre-funded accounts are ignored.
    +func getGenAccountsAndBalances(cfg Config) (genAccounts []authtypes.GenesisAccount, balances []banktypes.Balance) {
    +	if len(cfg.balances) > 0 {
    +		balances = cfg.balances
    +		accounts := getAccAddrsFromBalances(balances)
    +		genAccounts = createGenesisAccounts(accounts)
    +	} else {
    +		coin := sdktypes.NewCoin(cfg.denom, PrefundedAccountInitialBalance)
    +		genAccounts = createGenesisAccounts(cfg.preFundedAccounts)
    +		balances = createBalances(cfg.preFundedAccounts, coin)
    +	}
    +
    +	return
    +}
    +
     // ConfigOption defines a function that can modify the NetworkConfig.
     // The purpose of this is to force to be declarative when the default configuration
     // requires to be changed.
    @@ -67,6 +88,14 @@ func WithPreFundedAccounts(accounts ...sdktypes.AccAddress) ConfigOption {
     	}
     }
     
    +// WithBalances sets the specific balances for the pre-funded accounts, that
    +// are being set up for the network.
    +func WithBalances(balances ...banktypes.Balance) ConfigOption {
    +	return func(cfg *Config) {
    +		cfg.balances = append(cfg.balances, balances...)
    +	}
    +}
    +
     // WithDenom sets the denom for the network.
     func WithDenom(denom string) ConfigOption {
     	return func(cfg *Config) {
    
  • testutil/integration/evmos/network/network.go+5 3 modified
    @@ -7,6 +7,7 @@ import (
     	"encoding/json"
     	"math"
     	"math/big"
    +	"time"
     
     	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
     
    @@ -105,9 +106,7 @@ var (
     func (n *IntegrationNetwork) configureAndInitChain() error {
     	// Create funded accounts based on the config and
     	// create genesis accounts
    -	coin := sdktypes.NewCoin(n.cfg.denom, PrefundedAccountInitialBalance)
    -	genAccounts := createGenesisAccounts(n.cfg.preFundedAccounts)
    -	fundedAccountBalances := createBalances(n.cfg.preFundedAccounts, coin)
    +	genAccounts, fundedAccountBalances := getGenAccountsAndBalances(n.cfg)
     
     	// Create validator set with the amount of validators specified in the config
     	// with the default power of 1.
    @@ -154,8 +153,10 @@ func (n *IntegrationNetwork) configureAndInitChain() error {
     		return err
     	}
     
    +	now := time.Now()
     	evmosApp.InitChain(
     		abcitypes.RequestInitChain{
    +			Time:            now,
     			ChainId:         n.cfg.chainID,
     			Validators:      []abcitypes.ValidatorUpdate{},
     			ConsensusParams: app.DefaultConsensusParams,
    @@ -168,6 +169,7 @@ func (n *IntegrationNetwork) configureAndInitChain() error {
     	header := tmproto.Header{
     		ChainID:            n.cfg.chainID,
     		Height:             evmosApp.LastBlockHeight() + 1,
    +		Time:               now,
     		AppHash:            evmosApp.LastCommitID().Hash,
     		ValidatorsHash:     valSet.Hash(),
     		NextValidatorsHash: valSet.Hash(),
    
  • testutil/integration/evmos/network/setup.go+19 1 modified
    @@ -23,6 +23,8 @@ import (
     	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
     	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
     	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
    +	"github.com/ethereum/go-ethereum/crypto"
    +	evmostypes "github.com/evmos/evmos/v18/types"
     	epochstypes "github.com/evmos/evmos/v18/x/epochs/types"
     	infltypes "github.com/evmos/evmos/v18/x/inflation/v1/types"
     )
    @@ -50,9 +52,25 @@ func createValidatorSetAndSigners(numberOfValidators int) (*tmtypes.ValidatorSet
     func createGenesisAccounts(accounts []sdktypes.AccAddress) []authtypes.GenesisAccount {
     	numberOfAccounts := len(accounts)
     	genAccounts := make([]authtypes.GenesisAccount, 0, numberOfAccounts)
    +	emptyCodeHash := crypto.Keccak256Hash(nil).String()
     	for _, acc := range accounts {
     		baseAcc := authtypes.NewBaseAccount(acc, nil, 0, 0)
    -		genAccounts = append(genAccounts, baseAcc)
    +		ethAcc := &evmostypes.EthAccount{
    +			BaseAccount: baseAcc,
    +			CodeHash:    emptyCodeHash,
    +		}
    +		genAccounts = append(genAccounts, ethAcc)
    +	}
    +	return genAccounts
    +}
    +
    +// getAccAddrsFromBalances returns a slice of genesis accounts from the
    +// given balances.
    +func getAccAddrsFromBalances(balances []banktypes.Balance) []sdktypes.AccAddress {
    +	numberOfBalances := len(balances)
    +	genAccounts := make([]sdktypes.AccAddress, 0, numberOfBalances)
    +	for _, balance := range balances {
    +		genAccounts = append(genAccounts, balance.GetAddress())
     	}
     	return genAccounts
     }
    
  • testutil/integration/ibc/coordinator/coordinator.go+2 5 modified
    @@ -41,10 +41,7 @@ type Coordinator interface {
     }
     
     // TODO: Replace for a config
    -var (
    -	AmountOfDummyChains = 2
    -	GlobalTime          = time.Date(time.Now().Year()+1, 1, 2, 0, 0, 0, 0, time.UTC)
    -)
    +var AmountOfDummyChains = 2
     
     var _ Coordinator = (*IntegrationCoordinator)(nil)
     
    @@ -62,7 +59,7 @@ type IntegrationCoordinator struct {
     func NewIntegrationCoordinator(t *testing.T, preConfiguredChains []network.Network) *IntegrationCoordinator {
     	coord := &ibctesting.Coordinator{
     		T:           t,
    -		CurrentTime: GlobalTime,
    +		CurrentTime: time.Now(),
     	}
     	ibcChains := getIBCChains(t, coord, preConfiguredChains)
     	dummyChains, dummyChainsIds := generateDummyChains(t, coord, AmountOfDummyChains)
    
  • testutil/staking-rewards.go+2 2 modified
    @@ -110,7 +110,7 @@ func PrepareAccountsForDelegationRewards(t *testing.T, ctx sdk.Context, app *app
     		err = app.StakingKeeper.SetParams(ctx, stakingParams)
     		require.NoError(t, err)
     
    -		stakingHelper := teststaking.NewHelper(t, ctx, &app.StakingKeeper)
    +		stakingHelper := teststaking.NewHelper(t, ctx, app.StakingKeeper.Keeper)
     		stakingHelper.Commission = stakingtypes.NewCommissionRates(zeroDec, zeroDec, zeroDec)
     		stakingHelper.Denom = utils.BaseDenom
     
    @@ -122,7 +122,7 @@ func PrepareAccountsForDelegationRewards(t *testing.T, ctx sdk.Context, app *app
     
     		// end block to bond validator and increase block height
     		// Not using Commit() here because code panics due to invalid block height
    -		staking.EndBlocker(ctx, &app.StakingKeeper)
    +		staking.EndBlocker(ctx, app.StakingKeeper.Keeper)
     
     		// allocate rewards to validator (of these 50% will be paid out to the delegator)
     		validator := app.StakingKeeper.Validator(ctx, valAddr)
    
  • testutil/vesting.go+88 0 added
    @@ -0,0 +1,88 @@
    +// Copyright Tharsis Labs Ltd.(Evmos)
    +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
    +package testutil
    +
    +import (
    +	"cosmossdk.io/math"
    +	sdk "github.com/cosmos/cosmos-sdk/types"
    +	sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
    +	"github.com/evmos/evmos/v18/utils"
    +)
    +
    +type vestingSchedule struct {
    +	CliffMonths int64
    +	// CliffPeriodLength in seconds
    +	CliffPeriodLength int64
    +	NumLockupPeriods  int64
    +	LockupMonths      int64
    +	// LockupPeriodLength in seconds
    +	LockupPeriodLength int64
    +	NumVestingPeriods  int64
    +	// VestingPeriodLength in seconds
    +	VestingPeriodLength    int64
    +	TotalVestingCoins      sdk.Coins
    +	VestedCoinsPerPeriod   sdk.Coins
    +	UnlockedCoinsPerLockup sdk.Coins
    +	VestingPeriods         []sdkvesting.Period
    +	LockupPeriods          []sdkvesting.Period
    +}
    +
    +// Vesting schedule for tests that use vesting account
    +var (
    +	TestVestingSchedule vestingSchedule
    +	// Monthly vesting period
    +	stakeDenom    = utils.BaseDenom
    +	amt           = math.NewInt(1e17)
    +	vestingLength = int64(60 * 60 * 24 * 30) // in seconds
    +	vestingAmt    = sdk.NewCoins(sdk.NewCoin(stakeDenom, amt))
    +	vestingPeriod = sdkvesting.Period{Length: vestingLength, Amount: vestingAmt}
    +
    +	// 4 years vesting total
    +	periodsTotal    = int64(48)
    +	vestingAmtTotal = sdk.NewCoins(sdk.NewCoin(stakeDenom, amt.Mul(math.NewInt(periodsTotal))))
    +
    +	// 6 month cliff
    +	cliff       = int64(6)
    +	cliffLength = vestingLength * cliff
    +	cliffAmt    = sdk.NewCoins(sdk.NewCoin(stakeDenom, amt.Mul(math.NewInt(cliff))))
    +	cliffPeriod = sdkvesting.Period{Length: cliffLength, Amount: cliffAmt}
    +
    +	// 12 month lockup
    +	lockup       = int64(12) // 12 months
    +	lockupLength = vestingLength * lockup
    +	// Unlock at 12 and 24 months
    +	numLockupPeriods = int64(2)
    +	// Unlock half of the total vest in each unlock event. By default, all tokens are
    +	// unlocked after surpassing the final period.
    +	unlockedPerLockup = vestingAmtTotal.QuoInt(math.NewInt(numLockupPeriods))
    +	lockupPeriod      = sdkvesting.Period{Length: lockupLength, Amount: unlockedPerLockup}
    +	lockupPeriods     = make(sdkvesting.Periods, numLockupPeriods)
    +	// add initial cliff to vesting periods
    +	vestingPeriods = sdkvesting.Periods{cliffPeriod}
    +)
    +
    +func init() {
    +	for i := range lockupPeriods {
    +		lockupPeriods[i] = lockupPeriod
    +	}
    +
    +	// Create vesting periods with initial cliff
    +	for p := int64(1); p <= periodsTotal-cliff; p++ {
    +		vestingPeriods = append(vestingPeriods, vestingPeriod)
    +	}
    +
    +	TestVestingSchedule = vestingSchedule{
    +		CliffMonths:            cliff,
    +		CliffPeriodLength:      cliffLength,
    +		NumLockupPeriods:       numLockupPeriods,
    +		NumVestingPeriods:      periodsTotal,
    +		LockupMonths:           lockup,
    +		LockupPeriodLength:     lockupLength,
    +		VestingPeriodLength:    vestingLength,
    +		TotalVestingCoins:      vestingAmtTotal,
    +		VestedCoinsPerPeriod:   vestingAmt,
    +		UnlockedCoinsPerLockup: unlockedPerLockup,
    +		VestingPeriods:         vestingPeriods,
    +		LockupPeriods:          lockupPeriods,
    +	}
    +}
    
  • x/erc20/keeper/utils_test.go+1 1 modified
    @@ -103,7 +103,7 @@ func (suite *KeeperTestSuite) DoSetupTest(t require.TestingT) {
     	valAddr := sdk.ValAddress(suite.address.Bytes())
     	validator, err := stakingtypes.NewValidator(valAddr, privCons.PubKey(), stakingtypes.Description{})
     	require.NoError(t, err)
    -	validator = stakingkeeper.TestingUpdateValidator(&suite.app.StakingKeeper, suite.ctx, validator, true)
    +	validator = stakingkeeper.TestingUpdateValidator(suite.app.StakingKeeper.Keeper, suite.ctx, validator, true)
     	err = suite.app.StakingKeeper.Hooks().AfterValidatorCreated(suite.ctx, validator.GetOperator())
     	require.NoError(t, err)
     	err = suite.app.StakingKeeper.SetValidatorByConsAddr(suite.ctx, validator)
    
  • x/evm/keeper/precompiles.go+1 1 modified
    @@ -20,7 +20,6 @@ import (
     	authzkeeper "github.com/cosmos/cosmos-sdk/x/authz/keeper"
     	bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
     	distributionkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper"
    -	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
     	channelkeeper "github.com/cosmos/ibc-go/v7/modules/core/04-channel/keeper"
     	bankprecompile "github.com/evmos/evmos/v18/precompiles/bank"
     	distprecompile "github.com/evmos/evmos/v18/precompiles/distribution"
    @@ -33,6 +32,7 @@ import (
     	vestingprecompile "github.com/evmos/evmos/v18/precompiles/vesting"
     	erc20Keeper "github.com/evmos/evmos/v18/x/erc20/keeper"
     	transferkeeper "github.com/evmos/evmos/v18/x/ibc/transfer/keeper"
    +	stakingkeeper "github.com/evmos/evmos/v18/x/staking/keeper"
     	vestingkeeper "github.com/evmos/evmos/v18/x/vesting/keeper"
     )
     
    
  • x/feemarket/keeper/utils_test.go+1 1 modified
    @@ -70,7 +70,7 @@ func (suite *KeeperTestSuite) SetupApp(checkTx bool, chainID string) {
     	valAddr := sdk.ValAddress(suite.address.Bytes())
     	validator, err := stakingtypes.NewValidator(valAddr, priv.PubKey(), stakingtypes.Description{})
     	require.NoError(t, err)
    -	validator = stakingkeeper.TestingUpdateValidator(&suite.app.StakingKeeper, suite.ctx, validator, true)
    +	validator = stakingkeeper.TestingUpdateValidator(suite.app.StakingKeeper.Keeper, suite.ctx, validator, true)
     	err = suite.app.StakingKeeper.Hooks().AfterValidatorCreated(suite.ctx, validator.GetOperator())
     	require.NoError(t, err)
     
    
  • x/ibc/transfer/keeper/keeper_test.go+1 1 modified
    @@ -122,7 +122,7 @@ func (suite *KeeperTestSuite) DoSetupTest(t require.TestingT) {
     	valAddr := sdk.ValAddress(suite.address.Bytes())
     	validator, err := stakingtypes.NewValidator(valAddr, privCons.PubKey(), stakingtypes.Description{})
     	require.NoError(t, err)
    -	validator = stakingkeeper.TestingUpdateValidator(&suite.app.StakingKeeper, suite.ctx, validator, true)
    +	validator = stakingkeeper.TestingUpdateValidator(suite.app.StakingKeeper.Keeper, suite.ctx, validator, true)
     	err = suite.app.StakingKeeper.Hooks().AfterValidatorCreated(suite.ctx, validator.GetOperator())
     	require.NoError(t, err)
     	err = suite.app.StakingKeeper.SetValidatorByConsAddr(suite.ctx, validator)
    
  • x/revenue/v1/keeper/utils_test.go+1 1 modified
    @@ -74,7 +74,7 @@ func (suite *KeeperTestSuite) SetupApp(chainID string) {
     	valAddr := sdk.ValAddress(suite.address.Bytes())
     	validator, err := stakingtypes.NewValidator(valAddr, privCons.PubKey(), stakingtypes.Description{})
     	require.NoError(t, err)
    -	validator = stakingkeeper.TestingUpdateValidator(&suite.app.StakingKeeper, suite.ctx, validator, true)
    +	validator = stakingkeeper.TestingUpdateValidator(suite.app.StakingKeeper.Keeper, suite.ctx, validator, true)
     	err = suite.app.StakingKeeper.Hooks().AfterValidatorCreated(suite.ctx, validator.GetOperator())
     	require.NoError(t, err)
     	err = suite.app.StakingKeeper.SetValidatorByConsAddr(suite.ctx, validator)
    
  • x/staking/keeper/integration_test.go+395 0 added
    @@ -0,0 +1,395 @@
    +package keeper_test
    +
    +import (
    +	"testing"
    +	"time"
    +
    +	"cosmossdk.io/math"
    +	"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
    +
    +	sdk "github.com/cosmos/cosmos-sdk/types"
    +	"github.com/cosmos/cosmos-sdk/x/authz"
    +	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
    +	"github.com/cosmos/cosmos-sdk/x/staking/types"
    +
    +	"github.com/evmos/evmos/v18/testutil"
    +	"github.com/evmos/evmos/v18/testutil/integration/common/factory"
    +	evmosfactory "github.com/evmos/evmos/v18/testutil/integration/evmos/factory"
    +	"github.com/evmos/evmos/v18/testutil/integration/evmos/grpc"
    +	"github.com/evmos/evmos/v18/testutil/integration/evmos/keyring"
    +	"github.com/evmos/evmos/v18/testutil/integration/evmos/network"
    +	"github.com/evmos/evmos/v18/utils"
    +	vestingtypes "github.com/evmos/evmos/v18/x/vesting/types"
    +
    +	//nolint:revive // dot imports are fine for Ginkgo
    +	. "github.com/onsi/ginkgo/v2"
    +	//nolint:revive // dot imports are fine for Ginkgo
    +	. "github.com/onsi/gomega"
    +)
    +
    +func TestKeeperIntegrationTestSuite(t *testing.T) {
    +	// Run Ginkgo integration tests
    +	RegisterFailHandler(Fail)
    +	RunSpecs(t, "Keeper Suite")
    +}
    +
    +var _ = Describe("Staking module tests", func() {
    +	var (
    +		nw   *network.UnitTestNetwork
    +		gh   grpc.Handler
    +		keys keyring.Keyring
    +		tf   evmosfactory.TxFactory
    +	)
    +
    +	Context("using a vesting account", func() {
    +		var (
    +			clawbackAccount       *vestingtypes.ClawbackVestingAccount
    +			funder                keyring.Key
    +			vestingAccount        keyring.Key
    +			otherAccount          keyring.Key
    +			vestAccInitialBalance *sdk.Coin
    +			// initialized vars
    +			gasPrice = math.NewInt(700_000_000)
    +			gas      = uint64(500_000)
    +		)
    +
    +		BeforeEach(func() {
    +			// setup network
    +			// create 3 prefunded accounts:
    +			keys = keyring.New(3)
    +			funder = keys.GetKey(0)
    +			vestingAccount = keys.GetKey(1)
    +			otherAccount = keys.GetKey(2)
    +
    +			// set a higher initial balance for the funder to have
    +			// enough for the vesting schedule
    +			funderInitialBalance, ok := math.NewIntFromString("100_000_000_000_000_000_000")
    +			Expect(ok).To(BeTrue())
    +			balances := []banktypes.Balance{
    +				{
    +					Address: funder.AccAddr.String(),
    +					Coins:   sdk.NewCoins(sdk.NewCoin(utils.BaseDenom, funderInitialBalance)),
    +				},
    +				{
    +					Address: vestingAccount.AccAddr.String(),
    +					Coins:   sdk.NewCoins(sdk.NewCoin(utils.BaseDenom, network.PrefundedAccountInitialBalance)),
    +				},
    +				{
    +					Address: otherAccount.AccAddr.String(),
    +					Coins:   sdk.NewCoins(sdk.NewCoin(utils.BaseDenom, network.PrefundedAccountInitialBalance)),
    +				},
    +			}
    +
    +			nw = network.NewUnitTestNetwork(
    +				network.WithBalances(balances...),
    +			)
    +			gh = grpc.NewIntegrationHandler(nw)
    +			tf = evmosfactory.New(nw, gh)
    +
    +			Expect(nw.NextBlock()).To(BeNil())
    +
    +			// setup vesting account
    +			createAccMsg := vestingtypes.NewMsgCreateClawbackVestingAccount(funder.AccAddr, vestingAccount.AccAddr, false)
    +			res, err := tf.ExecuteCosmosTx(vestingAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{createAccMsg}})
    +			Expect(err).To(BeNil())
    +			Expect(res.IsOK()).To(BeTrue())
    +			Expect(nw.NextBlock()).To(BeNil())
    +
    +			// get vesting account initial balance (free tokens)
    +			balRes, err := gh.GetBalance(vestingAccount.AccAddr, nw.GetDenom())
    +			Expect(err).To(BeNil())
    +			vestAccInitialBalance = balRes.Balance
    +
    +			// Fund the clawback vesting accounts
    +			vestingStart := nw.GetContext().BlockTime()
    +			fundMsg := vestingtypes.NewMsgFundVestingAccount(funder.AccAddr, vestingAccount.AccAddr, vestingStart, testutil.TestVestingSchedule.LockupPeriods, testutil.TestVestingSchedule.VestingPeriods)
    +			res, err = tf.ExecuteCosmosTx(funder.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{fundMsg}})
    +			Expect(err).To(BeNil())
    +			Expect(res.IsOK()).To(BeTrue())
    +			Expect(nw.NextBlock()).To(BeNil())
    +
    +			// check vesting account was created successfully
    +			acc, err := gh.GetAccount(vestingAccount.AccAddr.String())
    +			Expect(err).To(BeNil())
    +			clawbackAccount, ok = acc.(*vestingtypes.ClawbackVestingAccount)
    +			Expect(ok).To(BeTrue())
    +		})
    +
    +		Context("delegate", func() {
    +			var delMsg *types.MsgDelegate
    +
    +			Context("using MsgDelegate", func() {
    +				BeforeEach(func() {
    +					// create a MsgDelegate to delegate the free tokens (balance previous to be converted to clawback account) + vested coins per period
    +					delMsg = types.NewMsgDelegate(vestingAccount.AccAddr, nw.GetValidators()[0].GetOperator(), testutil.TestVestingSchedule.VestedCoinsPerPeriod.Add(*vestAccInitialBalance)[0])
    +				})
    +
    +				It("should not allow to delegate unvested tokens", func() {
    +					// all coins in vesting schedule should be unvested
    +					unvestedCoins := clawbackAccount.GetVestingCoins(nw.GetContext().BlockTime())
    +					Expect(unvestedCoins).To(Equal(testutil.TestVestingSchedule.TotalVestingCoins))
    +
    +					balRes, err := gh.GetBalance(vestingAccount.AccAddr, nw.GetDenom())
    +					Expect(err).To(BeNil())
    +					delegatableBalance := balRes.Balance.Sub(unvestedCoins[0])
    +					Expect(delegatableBalance.Amount.LT(delMsg.Amount.Amount)).To(BeTrue())
    +
    +					_, err = tf.ExecuteCosmosTx(vestingAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{delMsg}})
    +					Expect(err).To(HaveOccurred())
    +					Expect(err.Error()).To(ContainSubstring("cannot delegate unvested coins"))
    +				})
    +
    +				It("should allow to delegate free tokens when all tokens in vesting schedule are unvested", func() {
    +					// calculate fees to deduct from free balance
    +					// to get the proper delegation amount
    +					fees := sdk.NewCoin(nw.GetDenom(), gasPrice.Mul(math.NewIntFromUint64(gas)))
    +					delMsg.Amount = vestAccInitialBalance.Sub(fees)
    +
    +					res, err := tf.ExecuteCosmosTx(vestingAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{delMsg}, Gas: gas, GasPrice: &gasPrice})
    +					Expect(err).To(BeNil())
    +					Expect(res.IsOK()).To(BeTrue())
    +					Expect(nw.NextBlock()).To(BeNil())
    +
    +					// check delegation was created successfully
    +					delRes, err := gh.GetDelegation(vestingAccount.AccAddr.String(), nw.GetValidators()[0].OperatorAddress)
    +					Expect(err).To(BeNil())
    +					Expect(delRes.DelegationResponse).NotTo(BeNil())
    +					Expect(delRes.DelegationResponse.Balance).To(Equal(delMsg.Amount))
    +				})
    +
    +				It("should allow to delegate locked vested tokens", func() {
    +					// cliff period passes - some tokens vested, but still all locked
    +					vestingPeriod := time.Duration(testutil.TestVestingSchedule.CliffPeriodLength)
    +					Expect(nw.NextBlockAfter(vestingPeriod * time.Second)).To(BeNil())
    +
    +					// check there're some vested coins
    +					denom := nw.GetDenom()
    +					expCoins := sdk.NewCoins(sdk.NewCoin(denom, testutil.TestVestingSchedule.VestedCoinsPerPeriod.AmountOf(denom).MulRaw(testutil.TestVestingSchedule.CliffMonths)))
    +					lockedVestedCoins := clawbackAccount.GetLockedUpVestedCoins(nw.GetContext().BlockTime())
    +					Expect(lockedVestedCoins).To(Equal(expCoins))
    +
    +					// deduct fees from delegation amount to pay the tx
    +					fees := sdk.NewCoin(nw.GetDenom(), gasPrice.Mul(math.NewIntFromUint64(gas)))
    +					delMsg.Amount = vestAccInitialBalance.Add(lockedVestedCoins[0]).Sub(fees)
    +
    +					res, err := tf.ExecuteCosmosTx(vestingAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{delMsg}, Gas: gas, GasPrice: &gasPrice})
    +					Expect(err).To(BeNil())
    +					Expect(res.IsOK()).To(BeTrue())
    +					Expect(nw.NextBlock()).To(BeNil())
    +
    +					// check delegation was created successfully
    +					delRes, err := gh.GetDelegation(vestingAccount.AccAddr.String(), nw.GetValidators()[0].OperatorAddress)
    +					Expect(err).To(BeNil())
    +					Expect(delRes.DelegationResponse).NotTo(BeNil())
    +					Expect(delRes.DelegationResponse.Balance).To(Equal(delMsg.Amount))
    +				})
    +
    +				It("should allow to delegate unlocked vested tokens", func() {
    +					// first lockup period passes
    +					lockupPeriod := time.Duration(testutil.TestVestingSchedule.LockupPeriodLength)
    +					Expect(nw.NextBlockAfter(lockupPeriod * time.Second)).To(BeNil())
    +
    +					// check there're some vested coins
    +					denom := nw.GetDenom()
    +					expVested := sdk.NewCoins(sdk.NewCoin(denom, testutil.TestVestingSchedule.VestedCoinsPerPeriod.AmountOf(denom).Mul(math.NewInt(testutil.TestVestingSchedule.LockupMonths))))
    +					vestedCoins := clawbackAccount.GetVestedCoins(nw.GetContext().BlockTime())
    +					Expect(vestedCoins).To(Equal(expVested))
    +
    +					// all vested coins should be unlocked
    +					unlockedVestedCoins := clawbackAccount.GetUnlockedVestedCoins(nw.GetContext().BlockTime())
    +					Expect(unlockedVestedCoins).To(Equal(vestedCoins))
    +
    +					// delegation amount is all free coins + unlocked vested - fee to pay tx
    +					fees := sdk.NewCoin(nw.GetDenom(), gasPrice.Mul(math.NewIntFromUint64(gas)))
    +					delMsg.Amount = vestAccInitialBalance.Add(unlockedVestedCoins[0]).Sub(fees)
    +
    +					res, err := tf.ExecuteCosmosTx(vestingAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{delMsg}, Gas: gas, GasPrice: &gasPrice})
    +					Expect(err).To(BeNil())
    +					Expect(res.IsOK()).To(BeTrue())
    +					Expect(nw.NextBlock()).To(BeNil())
    +
    +					// check delegation was created successfully
    +					delRes, err := gh.GetDelegation(vestingAccount.AccAddr.String(), nw.GetValidators()[0].OperatorAddress)
    +					Expect(err).To(BeNil())
    +					Expect(delRes.DelegationResponse).NotTo(BeNil())
    +					Expect(delRes.DelegationResponse.Balance).To(Equal(delMsg.Amount))
    +				})
    +			})
    +
    +			Context("MsgDelegate nested in MsgExec", func() {
    +				BeforeEach(func() {
    +					expiration := time.Now().Add(time.Hour * 24 * 365 * 2) // 2years
    +					// create a grant for other account
    +					// to send a MsgDelegate
    +					grantMsg, err := authz.NewMsgGrant(vestingAccount.AccAddr, otherAccount.AccAddr, authz.NewGenericAuthorization("/cosmos.staking.v1beta1.MsgDelegate"), &expiration)
    +					Expect(err).To(BeNil())
    +					res, err := tf.ExecuteCosmosTx(vestingAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{grantMsg}})
    +					Expect(err).To(BeNil())
    +					Expect(res.IsOK()).To(BeTrue())
    +					Expect(nw.NextBlock()).To(BeNil())
    +
    +					// create a MsgDelegate to delegate vested coins per period
    +					delMsg = types.NewMsgDelegate(vestingAccount.AccAddr, nw.GetValidators()[0].GetOperator(), testutil.TestVestingSchedule.VestedCoinsPerPeriod.Add(*vestAccInitialBalance)[0])
    +				})
    +
    +				It("should not allow to delegate unvested tokens", func() {
    +					// all coins in vesting schedule should be unvested
    +					unvestedCoins := clawbackAccount.GetVestingCoins(nw.GetContext().BlockTime())
    +					Expect(unvestedCoins).To(Equal(testutil.TestVestingSchedule.TotalVestingCoins))
    +
    +					balRes, err := gh.GetBalance(vestingAccount.AccAddr, nw.GetDenom())
    +					Expect(err).To(BeNil())
    +					delegatableBalance := balRes.Balance.Sub(unvestedCoins[0])
    +					Expect(delegatableBalance.Amount.LT(delMsg.Amount.Amount)).To(BeTrue())
    +
    +					execMsg := authz.NewMsgExec(otherAccount.AccAddr, []sdk.Msg{delMsg})
    +					_, err = tf.ExecuteCosmosTx(otherAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{&execMsg}})
    +					Expect(err).To(HaveOccurred())
    +					Expect(err.Error()).To(ContainSubstring("cannot delegate unvested coins."))
    +				})
    +
    +				It("should allow to delegate locked vested tokens", func() {
    +					// cliff period passes - some tokens vested, but still all locked
    +					vestingPeriod := time.Duration(testutil.TestVestingSchedule.CliffPeriodLength)
    +					Expect(nw.NextBlockAfter(vestingPeriod * time.Second)).To(BeNil())
    +
    +					// check there're some vested coins
    +					denom := nw.GetDenom()
    +					expCoins := sdk.NewCoins(sdk.NewCoin(denom, testutil.TestVestingSchedule.VestedCoinsPerPeriod.AmountOf(denom).MulRaw(testutil.TestVestingSchedule.CliffMonths)))
    +					lockedVestedCoins := clawbackAccount.GetLockedUpVestedCoins(nw.GetContext().BlockTime())
    +					Expect(lockedVestedCoins).To(Equal(expCoins))
    +
    +					// update delegation amount to be the free balance + locked vested coins - fees
    +					fees := sdk.NewCoin(nw.GetDenom(), gasPrice.Mul(math.NewIntFromUint64(gas)))
    +					delMsg.Amount = vestAccInitialBalance.Add(lockedVestedCoins[0]).Sub(fees)
    +
    +					execMsg := authz.NewMsgExec(otherAccount.AccAddr, []sdk.Msg{delMsg})
    +					res, err := tf.ExecuteCosmosTx(otherAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{&execMsg}, Gas: gas, GasPrice: &gasPrice})
    +					Expect(err).To(BeNil())
    +					Expect(res.IsOK()).To(BeTrue())
    +					Expect(nw.NextBlock()).To(BeNil())
    +
    +					// check delegation was created successfully
    +					delRes, err := gh.GetDelegation(vestingAccount.AccAddr.String(), nw.GetValidators()[0].OperatorAddress)
    +					Expect(err).To(BeNil())
    +					Expect(delRes.DelegationResponse).NotTo(BeNil())
    +					Expect(delRes.DelegationResponse.Balance).To(Equal(delMsg.Amount))
    +				})
    +
    +				It("after first lockup period - should allow to delegate unlocked vested tokens", func() {
    +					// first lockup period passes
    +					lockupPeriod := time.Duration(testutil.TestVestingSchedule.LockupPeriodLength)
    +					Expect(nw.NextBlockAfter(lockupPeriod * time.Second)).To(BeNil())
    +
    +					// check there're some vested coins
    +					denom := nw.GetDenom()
    +					expVested := sdk.NewCoins(sdk.NewCoin(denom, testutil.TestVestingSchedule.VestedCoinsPerPeriod.AmountOf(denom).Mul(math.NewInt(testutil.TestVestingSchedule.LockupMonths))))
    +					vestedCoins := clawbackAccount.GetVestedCoins(nw.GetContext().BlockTime())
    +					Expect(vestedCoins).To(Equal(expVested))
    +
    +					execMsg := authz.NewMsgExec(otherAccount.AccAddr, []sdk.Msg{delMsg})
    +					res, err := tf.ExecuteCosmosTx(otherAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{&execMsg}, Gas: gas})
    +					Expect(err).To(BeNil())
    +					Expect(res.IsOK()).To(BeTrue())
    +					Expect(nw.NextBlock()).To(BeNil())
    +
    +					// check delegation was created successfully
    +					delRes, err := gh.GetDelegation(vestingAccount.AccAddr.String(), nw.GetValidators()[0].OperatorAddress)
    +					Expect(err).To(BeNil())
    +					Expect(delRes.DelegationResponse).NotTo(BeNil())
    +					Expect(delRes.DelegationResponse.Balance).To(Equal(delMsg.Amount))
    +				})
    +			})
    +		})
    +
    +		Context("create validator with self delegation", func() {
    +			var createValMsg *types.MsgCreateValidator
    +
    +			BeforeEach(func() {
    +				// create a MsgCreateValidator to create a validator.
    +				// Self delegate coins in the vesting schedule
    +				var err error
    +				pubKey := ed25519.GenPrivKey().PubKey()
    +				commissions := types.NewCommissionRates(
    +					sdk.NewDecWithPrec(5, 2),
    +					sdk.NewDecWithPrec(2, 1),
    +					sdk.NewDecWithPrec(5, 2),
    +				)
    +				createValMsg, err = types.NewMsgCreateValidator(
    +					sdk.ValAddress(vestingAccount.AccAddr),
    +					pubKey,
    +					testutil.TestVestingSchedule.VestedCoinsPerPeriod.Add(*vestAccInitialBalance)[0],
    +					types.NewDescription("T", "E", "S", "T", "Z"),
    +					commissions,
    +					sdk.OneInt(),
    +				)
    +				Expect(err).To(BeNil())
    +			})
    +
    +			It("should not allow to create validator with unvested tokens in self delegation", func() {
    +				// all coins in vesting schedule should be unvested
    +				unvestedCoins := clawbackAccount.GetVestingCoins(nw.GetContext().BlockTime())
    +				Expect(unvestedCoins).To(Equal(testutil.TestVestingSchedule.TotalVestingCoins))
    +
    +				balRes, err := gh.GetBalance(vestingAccount.AccAddr, nw.GetDenom())
    +				Expect(err).To(BeNil())
    +				delegatableBalance := balRes.Balance.Sub(unvestedCoins[0])
    +				Expect(delegatableBalance.Amount.LT(createValMsg.Value.Amount)).To(BeTrue())
    +
    +				_, err = tf.ExecuteCosmosTx(vestingAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{createValMsg}})
    +				Expect(err).To(HaveOccurred())
    +				Expect(err.Error()).To(ContainSubstring("cannot delegate unvested coins."))
    +			})
    +
    +			It("should allow to create validator with locked vested tokens", func() {
    +				// cliff period passes - some tokens vested, but still all locked
    +				vestingPeriod := time.Duration(testutil.TestVestingSchedule.CliffPeriodLength)
    +				Expect(nw.NextBlockAfter(vestingPeriod * time.Second)).To(BeNil())
    +
    +				// check there're some vested coins
    +				denom := nw.GetDenom()
    +				expCoins := sdk.NewCoins(sdk.NewCoin(denom, testutil.TestVestingSchedule.VestedCoinsPerPeriod.AmountOf(denom).MulRaw(testutil.TestVestingSchedule.CliffMonths)))
    +				lockedVestedCoins := clawbackAccount.GetLockedUpVestedCoins(nw.GetContext().BlockTime())
    +				Expect(lockedVestedCoins).To(Equal(expCoins))
    +
    +				// update delegation amount to be the free balance + locked vested coins - fees
    +				fees := sdk.NewCoin(nw.GetDenom(), gasPrice.Mul(math.NewIntFromUint64(gas)))
    +				createValMsg.Value = vestAccInitialBalance.Add(lockedVestedCoins[0]).Sub(fees)
    +
    +				res, err := tf.ExecuteCosmosTx(vestingAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{createValMsg}, Gas: gas, GasPrice: &gasPrice})
    +				Expect(err).To(BeNil())
    +				Expect(res.IsOK()).To(BeTrue())
    +				Expect(nw.NextBlock()).To(BeNil())
    +
    +				// check validator was created successfully
    +				qc := nw.GetStakingClient()
    +				valRes, err := qc.Validator(nw.GetContext(), &types.QueryValidatorRequest{ValidatorAddr: sdk.ValAddress(vestingAccount.AccAddr).String()})
    +				Expect(err).To(BeNil())
    +				Expect(valRes.Validator.Status).To(Equal(types.Bonded))
    +			})
    +
    +			It("after first lockup period - should allow to create validator with a delegation of vested tokens", func() {
    +				// first lockup period passes
    +				lockupPeriod := time.Duration(testutil.TestVestingSchedule.LockupPeriodLength)
    +				Expect(nw.NextBlockAfter(lockupPeriod * time.Second)).To(BeNil())
    +
    +				// check there're some vested coins
    +				denom := nw.GetDenom()
    +				expVested := sdk.NewCoins(sdk.NewCoin(denom, testutil.TestVestingSchedule.VestedCoinsPerPeriod.AmountOf(denom).Mul(math.NewInt(testutil.TestVestingSchedule.LockupMonths))))
    +				vestedCoins := clawbackAccount.GetVestedCoins(nw.GetContext().BlockTime())
    +				Expect(vestedCoins).To(Equal(expVested))
    +
    +				res, err := tf.ExecuteCosmosTx(vestingAccount.Priv, factory.CosmosTxArgs{Msgs: []sdk.Msg{createValMsg}, Gas: 500_000})
    +				Expect(err).To(BeNil())
    +				Expect(res.IsOK()).To(BeTrue())
    +				Expect(nw.NextBlock()).To(BeNil())
    +
    +				// check validator was created successfully
    +				qc := nw.GetStakingClient()
    +				valRes, err := qc.Validator(nw.GetContext(), &types.QueryValidatorRequest{ValidatorAddr: sdk.ValAddress(vestingAccount.AccAddr).String()})
    +				Expect(err).To(BeNil())
    +				Expect(valRes.Validator.Status).To(Equal(types.Bonded))
    +			})
    +		})
    +	})
    +})
    
  • x/staking/keeper/keeper.go+33 0 added
    @@ -0,0 +1,33 @@
    +// Copyright Tharsis Labs Ltd.(Evmos)
    +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
    +
    +package keeper
    +
    +import (
    +	"github.com/cosmos/cosmos-sdk/codec"
    +	storetypes "github.com/cosmos/cosmos-sdk/store/types"
    +	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
    +	"github.com/cosmos/cosmos-sdk/x/staking/types"
    +)
    +
    +// Keeper is a wrapper around the Cosmos SDK staking keeper.
    +type Keeper struct {
    +	*stakingkeeper.Keeper
    +	ak types.AccountKeeper
    +	bk types.BankKeeper
    +}
    +
    +// NewKeeper creates a new staking Keeper wrapper instance.
    +func NewKeeper(
    +	cdc codec.BinaryCodec,
    +	key storetypes.StoreKey,
    +	ak types.AccountKeeper,
    +	bk types.BankKeeper,
    +	authority string,
    +) *Keeper {
    +	return &Keeper{
    +		stakingkeeper.NewKeeper(cdc, key, ak, bk, authority),
    +		ak,
    +		bk,
    +	}
    +}
    
  • x/staking/keeper/msg_server.go+107 0 added
    @@ -0,0 +1,107 @@
    +// Copyright Tharsis Labs Ltd.(Evmos)
    +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
    +
    +package keeper
    +
    +import (
    +	"context"
    +
    +	errorsmod "cosmossdk.io/errors"
    +	"cosmossdk.io/math"
    +	sdk "github.com/cosmos/cosmos-sdk/types"
    +	errortypes "github.com/cosmos/cosmos-sdk/types/errors"
    +	sdkstakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
    +	"github.com/cosmos/cosmos-sdk/x/staking/types"
    +
    +	vestingtypes "github.com/evmos/evmos/v18/x/vesting/types"
    +)
    +
    +// msgServer is a wrapper around the Cosmos SDK message server.
    +type msgServer struct {
    +	types.MsgServer
    +	*Keeper
    +}
    +
    +var _ types.MsgServer = msgServer{}
    +
    +// NewMsgServerImpl returns an implementation of the staking MsgServer interface
    +// for the provided Keeper.
    +func NewMsgServerImpl(keeper *Keeper) types.MsgServer {
    +	baseMsgServer := sdkstakingkeeper.NewMsgServerImpl(keeper.Keeper)
    +	return &msgServer{baseMsgServer, keeper}
    +}
    +
    +// Delegate defines a method for performing a delegation of coins from a delegator to a validator.
    +// The method performs some checks if the sender of the tx is a clawback vesting account and then
    +// relay the message to the Cosmos SDK staking method.
    +func (k msgServer) Delegate(goCtx context.Context, msg *types.MsgDelegate) (*types.MsgDelegateResponse, error) {
    +	if err := k.validateDelegationAmountNotUnvested(goCtx, msg.DelegatorAddress, msg.Amount.Amount); err != nil {
    +		return nil, err
    +	}
    +
    +	return k.MsgServer.Delegate(goCtx, msg)
    +}
    +
    +// CreateValidator defines a method to create a validator. The method performs some checks if the
    +// sender of the tx is a clawback vesting account and then relay the message to the Cosmos SDK staking
    +// method.
    +func (k msgServer) CreateValidator(goCtx context.Context, msg *types.MsgCreateValidator) (*types.MsgCreateValidatorResponse, error) {
    +	if err := k.validateDelegationAmountNotUnvested(goCtx, msg.DelegatorAddress, msg.Value.Amount); err != nil {
    +		return nil, err
    +	}
    +
    +	return k.MsgServer.CreateValidator(goCtx, msg)
    +}
    +
    +// validateDelegationAmountNotUnvested checks if the delegator is a clawback vesting account.
    +// In such case, checks that the provided delegation amount is available according
    +// to the current vesting schedule (unvested coins cannot be delegated).
    +func (k msgServer) validateDelegationAmountNotUnvested(goCtx context.Context, delegatorAddress string, amount math.Int) error {
    +	ctx := sdk.UnwrapSDKContext(goCtx)
    +	addr, err := sdk.AccAddressFromBech32(delegatorAddress)
    +	if err != nil {
    +		return err
    +	}
    +
    +	acc := k.ak.GetAccount(ctx, addr)
    +	if acc == nil {
    +		return errorsmod.Wrapf(
    +			errortypes.ErrUnknownAddress,
    +			"account %s does not exist", addr,
    +		)
    +	}
    +	// check if delegator address is a clawback vesting account. If not, no check
    +	// is required.
    +	clawbackAccount, isClawback := acc.(*vestingtypes.ClawbackVestingAccount)
    +	if !isClawback {
    +		return nil
    +	}
    +
    +	// vesting account can only delegate
    +	// if enough free balance (coins not in vesting schedule)
    +	// plus the vested coins (locked/unlocked)
    +	bondDenom := k.BondDenom(ctx)
    +	// GetBalance returns entire account balance
    +	// balance = free coins + all coins in vesting schedule
    +	balance := k.bk.GetBalance(ctx, addr, bondDenom)
    +	unvestedOnly := clawbackAccount.GetVestingCoins(ctx.BlockTime())
    +	// delegatable coins are going to be all the free coins + vested coins
    +	// Can only delegate bondable coins
    +	unvestedBondableAmt := unvestedOnly.AmountOf(bondDenom)
    +	// A ClawbackVestingAccount can delegate coins from the vesting schedule
    +	// when having vested locked coins or unlocked vested coins.
    +	// It CANNOT delegate unvested coins
    +	delegatableAmt := balance.Amount.Sub(unvestedBondableAmt)
    +	if delegatableAmt.IsNegative() {
    +		delegatableAmt = math.ZeroInt()
    +	}
    +
    +	if delegatableAmt.LT(amount) {
    +		return errorsmod.Wrapf(
    +			vestingtypes.ErrInsufficientVestedCoins,
    +			"cannot delegate unvested coins. coins available for delegation < delegation amount (%s < %s)",
    +			delegatableAmt, amount,
    +		)
    +	}
    +	return nil
    +}
    
  • x/staking/keeper/msg_server_test.go+271 0 added
    @@ -0,0 +1,271 @@
    +// Copyright Tharsis Labs Ltd.(Evmos)
    +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
    +
    +package keeper_test
    +
    +import (
    +	"testing"
    +	"time"
    +
    +	"cosmossdk.io/math"
    +	"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
    +	sdk "github.com/cosmos/cosmos-sdk/types"
    +	"github.com/cosmos/cosmos-sdk/x/staking/types"
    +
    +	"github.com/evmos/evmos/v18/testutil"
    +	"github.com/evmos/evmos/v18/testutil/integration/evmos/network"
    +	utiltx "github.com/evmos/evmos/v18/testutil/tx"
    +	"github.com/evmos/evmos/v18/utils"
    +	"github.com/evmos/evmos/v18/x/staking/keeper"
    +	vestingtypes "github.com/evmos/evmos/v18/x/vesting/types"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func TestMsgDelegate(t *testing.T) {
    +	var (
    +		ctx              sdk.Context
    +		nw               *network.UnitTestNetwork
    +		defaultDelCoin   = sdk.NewCoin(utils.BaseDenom, math.NewInt(1e18))
    +		delegatorAddr, _ = utiltx.NewAccAddressAndKey()
    +		funderAddr, _    = utiltx.NewAccAddressAndKey()
    +	)
    +
    +	testCases := []struct {
    +		name   string
    +		setup  func() sdk.Coin
    +		expErr bool
    +		errMsg string
    +	}{
    +		{
    +			name: "can delegate from a common EthAccount",
    +			setup: func() sdk.Coin {
    +				// Send some funds to delegator account
    +				err := testutil.FundAccountWithBaseDenom(ctx, nw.App.BankKeeper, delegatorAddr, defaultDelCoin.Amount.Int64())
    +				require.NoError(t, err)
    +				return defaultDelCoin
    +			},
    +			expErr: false,
    +		},
    +		{
    +			name: "can delegate free coins from a ClawbackVestingAccount",
    +			setup: func() sdk.Coin {
    +				err := setupClawbackVestingAccount(ctx, nw, delegatorAddr, funderAddr, testutil.TestVestingSchedule.TotalVestingCoins.Add(defaultDelCoin))
    +				require.NoError(t, err)
    +				return defaultDelCoin
    +			},
    +			expErr: false,
    +		},
    +		{
    +			name: "cannot delegate unvested coins from a ClawbackVestingAccount",
    +			setup: func() sdk.Coin {
    +				err := setupClawbackVestingAccount(ctx, nw, delegatorAddr, funderAddr, testutil.TestVestingSchedule.TotalVestingCoins)
    +				require.NoError(t, err)
    +				return defaultDelCoin
    +			},
    +			expErr: true,
    +			errMsg: "cannot delegate unvested coins",
    +		},
    +		{
    +			name: "can delegate locked vested coins from a ClawbackVestingAccount",
    +			setup: func() sdk.Coin {
    +				err := setupClawbackVestingAccount(ctx, nw, delegatorAddr, funderAddr, testutil.TestVestingSchedule.TotalVestingCoins)
    +				require.NoError(t, err)
    +
    +				// after first vesting period and before lockup
    +				// some vested tokens, but still all locked
    +				cliffDuration := time.Duration(testutil.TestVestingSchedule.CliffPeriodLength)
    +				nw.NextBlockAfter(cliffDuration * time.Second)
    +				ctx = nw.GetContext()
    +
    +				acc := nw.App.AccountKeeper.GetAccount(ctx, delegatorAddr)
    +				vestAcc, ok := acc.(*vestingtypes.ClawbackVestingAccount)
    +				require.True(t, ok)
    +
    +				// check that locked vested is > 0
    +				lockedVested := vestAcc.GetLockedUpVestedCoins(ctx.BlockTime())
    +				require.True(t, lockedVested.IsAllGT(sdk.NewCoins()))
    +
    +				// returned delegation coins are the locked vested coins
    +				return lockedVested[0]
    +			},
    +			expErr: false,
    +		},
    +		{
    +			name: "can delegate unlocked vested coins from a ClawbackVestingAccount",
    +			setup: func() sdk.Coin {
    +				err := setupClawbackVestingAccount(ctx, nw, delegatorAddr, funderAddr, testutil.TestVestingSchedule.TotalVestingCoins)
    +				require.NoError(t, err)
    +
    +				// Between first and second lockup periods
    +				// vested coins are unlocked
    +				lockDuration := time.Duration(testutil.TestVestingSchedule.LockupPeriodLength)
    +				nw.NextBlockAfter(lockDuration * time.Second)
    +				ctx = nw.GetContext()
    +
    +				acc := nw.App.AccountKeeper.GetAccount(ctx, delegatorAddr)
    +				vestAcc, ok := acc.(*vestingtypes.ClawbackVestingAccount)
    +				require.True(t, ok)
    +
    +				unlockedVested := vestAcc.GetUnlockedVestedCoins(ctx.BlockTime())
    +				require.True(t, unlockedVested.IsAllGT(sdk.NewCoins()))
    +
    +				// returned delegation coins are the locked vested coins
    +				return unlockedVested[0]
    +			},
    +			expErr: false,
    +		},
    +	}
    +
    +	for _, tc := range testCases {
    +		t.Run(tc.name, func(t *testing.T) {
    +			nw = network.NewUnitTestNetwork()
    +			ctx = nw.GetContext()
    +			delCoin := tc.setup()
    +
    +			srv := keeper.NewMsgServerImpl(&nw.App.StakingKeeper)
    +			res, err := srv.Delegate(ctx, &types.MsgDelegate{
    +				DelegatorAddress: delegatorAddr.String(),
    +				ValidatorAddress: nw.GetValidators()[0].OperatorAddress,
    +				Amount:           delCoin,
    +			})
    +
    +			if tc.expErr {
    +				require.Error(t, err)
    +				require.Contains(t, err.Error(), tc.errMsg)
    +			} else {
    +				require.NoError(t, err)
    +				require.NotNil(t, res)
    +			}
    +		})
    +	}
    +}
    +
    +func TestMsgCreateValidator(t *testing.T) {
    +	var (
    +		ctx              sdk.Context
    +		nw               *network.UnitTestNetwork
    +		defaultDelCoin   = sdk.NewCoin(utils.BaseDenom, math.NewInt(1e18))
    +		validatorAddr, _ = utiltx.NewAccAddressAndKey()
    +		funderAddr, _    = utiltx.NewAccAddressAndKey()
    +	)
    +
    +	testCases := []struct {
    +		name   string
    +		setup  func() sdk.Coin
    +		expErr bool
    +		errMsg string
    +	}{
    +		{
    +			name: "can create a validator using a common EthAccount",
    +			setup: func() sdk.Coin {
    +				// Send some funds to delegator account
    +				err := testutil.FundAccountWithBaseDenom(ctx, nw.App.BankKeeper, validatorAddr, defaultDelCoin.Amount.Int64())
    +				require.NoError(t, err)
    +				return defaultDelCoin
    +			},
    +			expErr: false,
    +		},
    +		{
    +			name: "can create a validator using a ClawbackVestingAccount and free tokens in self delegation",
    +			setup: func() sdk.Coin {
    +				err := setupClawbackVestingAccount(ctx, nw, validatorAddr, funderAddr, testutil.TestVestingSchedule.TotalVestingCoins.Add(defaultDelCoin))
    +				require.NoError(t, err)
    +				return defaultDelCoin
    +			},
    +			expErr: false,
    +		},
    +		{
    +			name: "cannot create a validator using a ClawbackVestingAccount and unvested tokens in self delegation",
    +			setup: func() sdk.Coin {
    +				err := setupClawbackVestingAccount(ctx, nw, validatorAddr, funderAddr, testutil.TestVestingSchedule.TotalVestingCoins)
    +				require.NoError(t, err)
    +				return defaultDelCoin
    +			},
    +			expErr: true,
    +			errMsg: "cannot delegate unvested coins",
    +		},
    +		{
    +			name: "can create a validator using a ClawbackVestingAccount and locked vested coins in self delegation",
    +			setup: func() sdk.Coin {
    +				err := setupClawbackVestingAccount(ctx, nw, validatorAddr, funderAddr, testutil.TestVestingSchedule.TotalVestingCoins)
    +				require.NoError(t, err)
    +
    +				// after first vesting period and before lockup
    +				// some vested tokens, but still all locked
    +				cliffDuration := time.Duration(testutil.TestVestingSchedule.CliffPeriodLength)
    +				nw.NextBlockAfter(cliffDuration * time.Second)
    +				ctx = nw.GetContext()
    +
    +				acc := nw.App.AccountKeeper.GetAccount(ctx, validatorAddr)
    +				vestAcc, ok := acc.(*vestingtypes.ClawbackVestingAccount)
    +				require.True(t, ok)
    +
    +				// check that locked vested is > 0
    +				lockedVested := vestAcc.GetLockedUpVestedCoins(ctx.BlockTime())
    +				require.True(t, lockedVested.IsAllGT(sdk.NewCoins()))
    +
    +				// returned delegation coins are the locked vested coins
    +				return lockedVested[0]
    +			},
    +			expErr: false,
    +		},
    +		{
    +			name: "can create a validator using a ClawbackVestingAccount and unlocked vested coins in self delegation",
    +			setup: func() sdk.Coin {
    +				err := setupClawbackVestingAccount(ctx, nw, validatorAddr, funderAddr, testutil.TestVestingSchedule.TotalVestingCoins)
    +				require.NoError(t, err)
    +
    +				// Between first and second lockup periods
    +				// vested coins are unlocked
    +				lockDuration := time.Duration(testutil.TestVestingSchedule.LockupPeriodLength)
    +				nw.NextBlockAfter(lockDuration * time.Second)
    +				ctx = nw.GetContext()
    +
    +				acc := nw.App.AccountKeeper.GetAccount(ctx, validatorAddr)
    +				vestAcc, ok := acc.(*vestingtypes.ClawbackVestingAccount)
    +				require.True(t, ok)
    +
    +				unlockedVested := vestAcc.GetUnlockedVestedCoins(ctx.BlockTime())
    +				require.True(t, unlockedVested.IsAllGT(sdk.NewCoins()))
    +
    +				// returned delegation coins are the locked vested coins
    +				return unlockedVested[0]
    +			},
    +			expErr: false,
    +		},
    +	}
    +
    +	for _, tc := range testCases {
    +		t.Run(tc.name, func(t *testing.T) {
    +			nw = network.NewUnitTestNetwork()
    +			ctx = nw.GetContext()
    +			coinToSelfBond := tc.setup()
    +
    +			pubKey := ed25519.GenPrivKey().PubKey()
    +			commissions := types.NewCommissionRates(
    +				sdk.NewDecWithPrec(5, 2),
    +				sdk.NewDecWithPrec(2, 1),
    +				sdk.NewDecWithPrec(5, 2),
    +			)
    +			msg, err := types.NewMsgCreateValidator(
    +				sdk.ValAddress(validatorAddr),
    +				pubKey,
    +				coinToSelfBond,
    +				types.NewDescription("T", "E", "S", "T", "Z"),
    +				commissions,
    +				sdk.OneInt(),
    +			)
    +			require.NoError(t, err)
    +			srv := keeper.NewMsgServerImpl(&nw.App.StakingKeeper)
    +			res, err := srv.CreateValidator(ctx, msg)
    +
    +			if tc.expErr {
    +				require.Error(t, err)
    +				require.Contains(t, err.Error(), tc.errMsg)
    +			} else {
    +				require.NoError(t, err)
    +				require.NotNil(t, res)
    +			}
    +		})
    +	}
    +}
    
  • x/staking/keeper/util_test.go+48 0 added
    @@ -0,0 +1,48 @@
    +// Copyright Tharsis Labs Ltd.(Evmos)
    +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
    +
    +package keeper_test
    +
    +import (
    +	"errors"
    +	"time"
    +
    +	sdk "github.com/cosmos/cosmos-sdk/types"
    +	"github.com/evmos/evmos/v18/testutil"
    +	"github.com/evmos/evmos/v18/testutil/integration/evmos/network"
    +	"github.com/evmos/evmos/v18/x/vesting/types"
    +)
    +
    +// setupClawbackVestingAccount sets up a clawback vesting account
    +// using the TestVestingSchedule. If exceeded balance is provided,
    +// will fund the vesting account with it.
    +func setupClawbackVestingAccount(ctx sdk.Context, nw *network.UnitTestNetwork, vestingAcc, funderAcc sdk.AccAddress, balances sdk.Coins) error {
    +	totalVestingCoins := testutil.TestVestingSchedule.TotalVestingCoins
    +	if totalVestingCoins.IsAllGT(balances) {
    +		return errors.New("should provide enough balance for the vesting schedule")
    +	}
    +	// fund the vesting account to set the account and then
    +	// send funds over to the funder account so free balance remains
    +	err := testutil.FundAccount(ctx, nw.App.BankKeeper, vestingAcc, balances)
    +	if err != nil {
    +		return err
    +	}
    +	err = nw.App.BankKeeper.SendCoins(ctx, vestingAcc, funderAcc, totalVestingCoins)
    +	if err != nil {
    +		return err
    +	}
    +
    +	// create a clawback vesting account
    +	msgCreate := types.NewMsgCreateClawbackVestingAccount(funderAcc, vestingAcc, false)
    +	if _, err = nw.App.VestingKeeper.CreateClawbackVestingAccount(ctx, msgCreate); err != nil {
    +		return err
    +	}
    +
    +	// fund vesting account
    +	msgFund := types.NewMsgFundVestingAccount(funderAcc, vestingAcc, time.Now(), testutil.TestVestingSchedule.LockupPeriods, testutil.TestVestingSchedule.VestingPeriods)
    +	if _, err = nw.App.VestingKeeper.FundVestingAccount(ctx, msgFund); err != nil {
    +		return err
    +	}
    +
    +	return nil
    +}
    
  • x/staking/module.go+61 0 added
    @@ -0,0 +1,61 @@
    +// Copyright Tharsis Labs Ltd.(Evmos)
    +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
    +
    +package staking
    +
    +import (
    +	"github.com/cosmos/cosmos-sdk/codec"
    +	"github.com/cosmos/cosmos-sdk/types/module"
    +	"github.com/cosmos/cosmos-sdk/x/auth/exported"
    +	"github.com/cosmos/cosmos-sdk/x/staking"
    +	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
    +	"github.com/cosmos/cosmos-sdk/x/staking/types"
    +
    +	"github.com/evmos/evmos/v18/x/staking/keeper"
    +)
    +
    +var (
    +	_ module.BeginBlockAppModule = AppModule{}
    +	_ module.EndBlockAppModule   = AppModule{}
    +	_ module.AppModuleBasic      = AppModuleBasic{}
    +	_ module.AppModuleSimulation = AppModule{}
    +)
    +
    +// AppModuleBasic defines the basic application module used by the staking module.
    +type AppModuleBasic struct {
    +	*staking.AppModuleBasic
    +}
    +
    +// AppModule represents a wrapper around the Cosmos SDK staking module AppModule and
    +// the Evmos custom staking module keeper.
    +type AppModule struct {
    +	*staking.AppModule
    +	keeper *keeper.Keeper
    +}
    +
    +// NewAppModule creates a wrapper for the staking module.
    +func NewAppModule(
    +	cdc codec.Codec,
    +	k *keeper.Keeper,
    +	ak types.AccountKeeper,
    +	bk types.BankKeeper,
    +	ls exported.Subspace,
    +) AppModule {
    +	am := staking.NewAppModule(cdc, k.Keeper, ak, bk, ls)
    +	return AppModule{
    +		AppModule: &am,
    +		keeper:    k,
    +	}
    +}
    +
    +// RegisterServices registers module services.
    +func (am AppModule) RegisterServices(cfg module.Configurator) {
    +	// Override Staking Msg Server
    +	types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper))
    +	querier := stakingkeeper.Querier{Keeper: am.keeper.Keeper}
    +	types.RegisterQueryServer(cfg.QueryServer(), querier)
    +
    +	// !! NOTE: when upgrading to a new cosmos-sdk version
    +	// !! Check if there're store migrations for the staking module
    +	// !! if so, you'll need to add them here
    +}
    
  • x/vesting/keeper/gov_test.go+6 2 modified
    @@ -6,7 +6,9 @@ import (
     )
     
     func (suite *KeeperTestSuite) TestGovClawbackStore() {
    -	suite.SetupTest()
    +	if err := suite.SetupTest(); err != nil {
    +		panic(err)
    +	}
     
     	addr := sdk.AccAddress(suite.address.Bytes())
     
    @@ -30,7 +32,9 @@ func (suite *KeeperTestSuite) TestGovClawbackStore() {
     }
     
     func (suite *KeeperTestSuite) TestGovClawbackNoOps() {
    -	suite.SetupTest()
    +	if err := suite.SetupTest(); err != nil {
    +		panic(err)
    +	}
     
     	addr := sdk.AccAddress(suite.address.Bytes())
     	addr2 := sdk.AccAddress(testutiltx.GenerateAddress().Bytes())
    
  • x/vesting/keeper/grpc_query.go+3 3 modified
    @@ -40,9 +40,9 @@ func (k Keeper) Balances(
     		)
     	}
     
    -	locked := clawbackAccount.GetLockedOnly(ctx.BlockTime())
    -	unvested := clawbackAccount.GetUnvestedOnly(ctx.BlockTime())
    -	vested := clawbackAccount.GetVestedOnly(ctx.BlockTime())
    +	locked := clawbackAccount.GetLockedUpCoins(ctx.BlockTime())
    +	unvested := clawbackAccount.GetVestingCoins(ctx.BlockTime())
    +	vested := clawbackAccount.GetVestedCoins(ctx.BlockTime())
     
     	return &types.QueryBalancesResponse{
     		Locked:   locked,
    
  • x/vesting/keeper/grpc_query_test.go+1 1 modified
    @@ -117,7 +117,7 @@ func (suite *KeeperTestSuite) TestBalances() {
     
     	for _, tc := range testCases {
     		suite.Run(fmt.Sprintf("Case %s", tc.name), func() {
    -			suite.SetupTest() // reset
    +			suite.Require().NoError(suite.SetupTest()) // reset
     			ctx := sdk.WrapSDKContext(suite.ctx)
     			tc.malleate()
     			suite.Commit()
    
  • x/vesting/keeper/integration_test.go+412 380 modified
    @@ -4,6 +4,7 @@ import (
     	"fmt"
     	"math/big"
     	"strings"
    +	"testing"
     	"time"
     
     	//nolint:revive // dot imports are fine for Ginkgo
    @@ -12,14 +13,17 @@ import (
     	. "github.com/onsi/gomega"
     
     	"cosmossdk.io/math"
    +	"github.com/cosmos/cosmos-sdk/baseapp"
     	sdk "github.com/cosmos/cosmos-sdk/types"
     	errortypes "github.com/cosmos/cosmos-sdk/types/errors"
     	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
     	sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
    +	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
     	govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
     	govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
     	govv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
     	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
    +	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
     	"github.com/ethereum/go-ethereum/common"
     	"github.com/evmos/evmos/v18/contracts"
     	"github.com/evmos/evmos/v18/crypto/ethsecp256k1"
    @@ -31,6 +35,15 @@ import (
     	"github.com/evmos/evmos/v18/x/vesting/types"
     )
     
    +func TestKeeperIntegrationTestSuite(t *testing.T) {
    +	s = new(KeeperTestSuite)
    +	s.SetT(t)
    +
    +	// Run Ginkgo integration tests
    +	RegisterFailHandler(Fail)
    +	RunSpecs(t, "Keeper Suite")
    +}
    +
     // TestClawbackAccount is a struct to store all relevant information that is corresponding
     // to a clawback vesting account.
     type TestClawbackAccount struct {
    @@ -40,7 +53,20 @@ type TestClawbackAccount struct {
     }
     
     // Initialize general error variable for easier handling in loops throughout this test suite.
    -var err error
    +var (
    +	err                error
    +	stakeDenom         = utils.BaseDenom
    +	accountGasCoverage = sdk.NewCoins(sdk.NewCoin(stakeDenom, math.NewInt(1e16)))
    +	amt                = testutil.TestVestingSchedule.VestedCoinsPerPeriod[0].Amount
    +	cliff              = testutil.TestVestingSchedule.CliffMonths
    +	cliffLength        = testutil.TestVestingSchedule.CliffPeriodLength
    +	vestingAmtTotal    = testutil.TestVestingSchedule.TotalVestingCoins
    +	vestingLength      = testutil.TestVestingSchedule.VestingPeriodLength
    +	numLockupPeriods   = testutil.TestVestingSchedule.NumLockupPeriods
    +	periodsTotal       = testutil.TestVestingSchedule.NumVestingPeriods
    +	lockup             = testutil.TestVestingSchedule.LockupMonths
    +	unlockedPerLockup  = testutil.TestVestingSchedule.UnlockedCoinsPerLockup
    +)
     
     // Clawback vesting with Cliff and Lock. In this case the cliff is reached
     // before the lockup period is reached to represent the scenario in which an
    @@ -52,44 +78,6 @@ var err error
     // 22/09 Cliff ends
     // 23/02 Lock ends
     var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
    -	// Monthly vesting period
    -	stakeDenom := utils.BaseDenom
    -	amt := math.NewInt(1e17)
    -	vestingLength := int64(60 * 60 * 24 * 30) // in seconds
    -	vestingAmt := sdk.NewCoins(sdk.NewCoin(stakeDenom, amt))
    -	vestingPeriod := sdkvesting.Period{Length: vestingLength, Amount: vestingAmt}
    -
    -	// 4 years vesting total
    -	periodsTotal := int64(48)
    -	vestingAmtTotal := sdk.NewCoins(sdk.NewCoin(stakeDenom, amt.Mul(math.NewInt(periodsTotal))))
    -
    -	// 6 month cliff
    -	cliff := int64(6)
    -	cliffLength := vestingLength * cliff
    -	cliffAmt := sdk.NewCoins(sdk.NewCoin(stakeDenom, amt.Mul(math.NewInt(cliff))))
    -	cliffPeriod := sdkvesting.Period{Length: cliffLength, Amount: cliffAmt}
    -
    -	// 12 month lockup
    -	lockup := int64(12) // 12 months
    -	lockupLength := vestingLength * lockup
    -	// Unlock at 12 and 24 months
    -	numLockupPeriods := int64(2)
    -	// Unlock 1/4th of the total vest in each unlock event. By default, all tokens are
    -	// unlocked after surpassing the final period.
    -	unlockedPerLockup := vestingAmtTotal.QuoInt(math.NewInt(4))
    -	unlockedPerLockupAmt := unlockedPerLockup[0].Amount
    -	lockupPeriod := sdkvesting.Period{Length: lockupLength, Amount: unlockedPerLockup}
    -	lockupPeriods := make(sdkvesting.Periods, numLockupPeriods)
    -	for i := range lockupPeriods {
    -		lockupPeriods[i] = lockupPeriod
    -	}
    -
    -	// Create vesting periods with initial cliff
    -	vestingPeriods := sdkvesting.Periods{cliffPeriod}
    -	for p := int64(1); p <= periodsTotal-cliff; p++ {
    -		vestingPeriods = append(vestingPeriods, vestingPeriod)
    -	}
    -
     	// Create test accounts with private keys for signing
     	numTestAccounts := 4
     	testAccounts := make([]TestClawbackAccount, numTestAccounts)
    @@ -102,20 +90,20 @@ var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
     	}
     	numTestMsgs := 3
     
    -	accountGasCoverage := sdk.NewCoins(sdk.NewCoin(stakeDenom, math.NewInt(1e16)))
    -
     	var (
    -		clawbackAccount   *types.ClawbackVestingAccount
    -		unvested          sdk.Coins
    -		vested            sdk.Coins
    +		clawbackAccount *types.ClawbackVestingAccount
    +		unvested        sdk.Coins
    +		vested          sdk.Coins
    +		// freeCoins are unlocked vested coins of the vesting schedule
    +		freeCoins         sdk.Coins
     		twoThirdsOfVested sdk.Coins
     	)
     
     	dest := sdk.AccAddress(utiltx.GenerateAddress().Bytes())
     	funder := sdk.AccAddress(utiltx.GenerateAddress().Bytes())
     
     	BeforeEach(func() {
    -		s.SetupTest()
    +		Expect(s.SetupTest()).To(BeNil()) // reset
     
     		// Initialize all test accounts
     		for i, account := range testAccounts {
    @@ -127,65 +115,64 @@ var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
     				funder,
     				vestingAmtTotal,
     				vestingStart,
    -				lockupPeriods,
    -				vestingPeriods,
    +				testutil.TestVestingSchedule.LockupPeriods,
    +				testutil.TestVestingSchedule.VestingPeriods,
     			)
     
     			err := testutil.FundAccount(s.ctx, s.app.BankKeeper, account.address, vestingAmtTotal)
    -			s.Require().NoError(err)
    +			Expect(err).To(BeNil())
     			acc := s.app.AccountKeeper.NewAccount(s.ctx, clawbackAccount)
     			s.app.AccountKeeper.SetAccount(s.ctx, acc)
     
     			// Check if all tokens are unvested at vestingStart
    -			unvested = clawbackAccount.GetUnvestedOnly(s.ctx.BlockTime())
    -			vested = clawbackAccount.GetVestedOnly(s.ctx.BlockTime())
    -			s.Require().Equal(vestingAmtTotal, unvested)
    -			s.Require().True(vested.IsZero())
    +			unvested = clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
    +			vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +			Expect(vestingAmtTotal).To(Equal(unvested))
    +			Expect(vested.IsZero()).To(BeTrue())
     
     			// Grant gas stipend to cover EVM fees
     			err = testutil.FundAccount(s.ctx, s.app.BankKeeper, clawbackAccount.GetAddress(), accountGasCoverage)
    -			s.Require().NoError(err)
    +			Expect(err).To(BeNil())
     			granteeBalance := s.app.BankKeeper.GetBalance(s.ctx, account.address, stakeDenom)
    -			s.Require().Equal(granteeBalance, accountGasCoverage[0].Add(vestingAmtTotal[0]))
    +			Expect(granteeBalance).To(Equal(accountGasCoverage[0].Add(vestingAmtTotal[0])))
     
     			// Update testAccounts clawbackAccount reference
     			testAccounts[i].clawbackAccount = clawbackAccount
     		}
     	})
    -
     	Context("before first vesting period", func() {
     		BeforeEach(func() {
     			// Add a commit to instantiate blocks
     			s.Commit()
     
     			// Ensure no tokens are vested
    -			vested := clawbackAccount.GetVestedOnly(s.ctx.BlockTime())
    -			unlocked := clawbackAccount.GetUnlockedOnly(s.ctx.BlockTime())
    +			vested := clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +			unlocked := clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
     			zeroCoins := sdk.NewCoins(sdk.NewCoin(stakeDenom, math.ZeroInt()))
    -			s.Require().Equal(zeroCoins, vested)
    -			s.Require().Equal(zeroCoins, unlocked)
    +			Expect(zeroCoins).To(Equal(vested))
    +			Expect(zeroCoins).To(Equal(unlocked))
     		})
     
     		It("cannot delegate tokens", func() {
    -			_, err := delegate(testAccounts[0], accountGasCoverage.Add(sdk.NewCoin(stakeDenom, math.NewInt(1))))
    +			_, err := testutil.Delegate(s.ctx, s.app, testAccounts[0].privKey, accountGasCoverage.Add(sdk.NewCoin(stakeDenom, math.NewInt(1)))[0], s.validator)
     			Expect(err).ToNot(BeNil())
     		})
     
     		It("can transfer spendable tokens", func() {
     			account := testAccounts[0]
     			// Fund account with new spendable tokens
    -			err := testutil.FundAccount(s.ctx, s.app.BankKeeper, account.address, unvested)
    +			amt := unvested
    +			err := testutil.FundAccount(s.ctx, s.app.BankKeeper, account.address, amt)
     			Expect(err).To(BeNil())
     
     			err = s.app.BankKeeper.SendCoins(
     				s.ctx,
     				account.address,
     				dest,
    -				unvested,
    +				amt,
     			)
     			Expect(err).To(BeNil())
     		})
    -
     		It("cannot transfer unvested tokens", func() {
     			err := s.app.BankKeeper.SendCoins(
     				s.ctx,
    @@ -195,77 +182,162 @@ var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
     			)
     			Expect(err).ToNot(BeNil())
     		})
    -
     		It("can perform Ethereum tx with spendable balance", func() {
     			account := testAccounts[0]
     			// Fund account with new spendable tokens
    -			err := testutil.FundAccount(s.ctx, s.app.BankKeeper, account.address, unlockedPerLockup)
    +			coins := testutil.TestVestingSchedule.UnlockedCoinsPerLockup
    +			err := testutil.FundAccount(s.ctx, s.app.BankKeeper, account.address, coins)
     			Expect(err).To(BeNil())
     
    -			txAmount := unlockedPerLockupAmt.BigInt()
    +			txAmount := coins.AmountOf(stakeDenom).BigInt()
     			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount, 0)
     			Expect(err).To(BeNil())
     
    -			assertEthSucceeds([]TestClawbackAccount{account}, funder, dest, unlockedPerLockupAmt, stakeDenom, msg)
    +			assertEthSucceeds([]TestClawbackAccount{account}, funder, dest, coins.AmountOf(stakeDenom), stakeDenom, msg)
     		})
     
     		It("cannot perform Ethereum tx with unvested balance", func() {
     			account := testAccounts[0]
    -			txAmount := unlockedPerLockupAmt.BigInt()
    +			unlockedCoins := testutil.TestVestingSchedule.UnlockedCoinsPerLockup
    +			txAmount := unlockedCoins.AmountOf(stakeDenom).BigInt()
     			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount, 0)
     			Expect(err).To(BeNil())
     
     			assertEthFails(msg)
     		})
     	})
    -
     	Context("after first vesting period and before lockup", func() {
     		BeforeEach(func() {
     			// Surpass cliff but none of lockup duration
    -			cliffDuration := time.Duration(cliffLength)
    +			cliffDuration := time.Duration(testutil.TestVestingSchedule.CliffPeriodLength)
     			s.CommitAfter(cliffDuration * time.Second)
     
     			// Check if some, but not all tokens are vested
    -			vested = clawbackAccount.GetVestedOnly(s.ctx.BlockTime())
    -			expVested := sdk.NewCoins(sdk.NewCoin(stakeDenom, amt.Mul(math.NewInt(cliff))))
    -			s.Require().NotEqual(vestingAmtTotal, vested)
    -			s.Require().Equal(expVested, vested)
    +			vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +			expVested := sdk.NewCoins(sdk.NewCoin(stakeDenom, amt.Mul(math.NewInt(testutil.TestVestingSchedule.CliffMonths))))
    +			Expect(vested).NotTo(Equal(vestingAmtTotal))
    +			Expect(vested).To(Equal(expVested))
    +
    +			// check the vested tokens are still locked
    +			freeCoins = clawbackAccount.GetUnlockedVestedCoins(s.ctx.BlockTime())
    +			Expect(freeCoins).To(Equal(sdk.Coins{}))
     
     			twoThirdsOfVested = vested.Sub(vested.QuoInt(math.NewInt(3))...)
    +
    +			res, err := s.app.VestingKeeper.Balances(s.ctx, &types.QueryBalancesRequest{Address: clawbackAccount.Address})
    +			Expect(err).To(BeNil())
    +			Expect(res.Vested).To(Equal(expVested))
    +			Expect(res.Unvested).To(Equal(vestingAmtTotal.Sub(expVested...)))
    +			// All coins from vesting schedule should be locked
    +			Expect(res.Locked).To(Equal(vestingAmtTotal))
     		})
     
    -		It("can delegate vested tokens and update spendable balance", func() {
    +		It("can delegate vested locked tokens", func() {
     			testAccount := testAccounts[0]
    -			// Verify that the total spendable coins decreases after staking
    -			// vested tokens.
    +			// Verify that the total spendable coins should only be coins
    +			// not in the vesting schedule. Because all coins from the vesting
    +			// schedule are still locked
     			spendablePre := s.app.BankKeeper.SpendableCoins(s.ctx, testAccount.address)
    +			Expect(spendablePre).To(Equal(accountGasCoverage))
    +
    +			// delegate the vested locked coins.
    +			_, err := testutil.Delegate(s.ctx, s.app, testAccount.privKey, vested[0], s.validator)
    +			Expect(err).To(BeNil(), "expected no error during delegation")
     
    -			_, err := delegate(testAccount, vested)
    +			// check spendable coins have only been reduced by the gas paid for the transaction to show that the delegated coins were taken from the locked but vested amount
    +			spendablePost := s.app.BankKeeper.SpendableCoins(s.ctx, testAccount.address)
    +			Expect(spendablePost).To(Equal(spendablePre.Sub(accountGasCoverage...)))
    +
    +			// check delegation was created successfully
    +			stkQuerier := stakingkeeper.Querier{Keeper: s.app.StakingKeeper.Keeper}
    +			delRes, err := stkQuerier.DelegatorDelegations(s.ctx, &stakingtypes.QueryDelegatorDelegationsRequest{DelegatorAddr: testAccount.clawbackAccount.Address})
     			Expect(err).To(BeNil())
    +			Expect(delRes.DelegationResponses).To(HaveLen(1))
    +			Expect(delRes.DelegationResponses[0].Balance.Amount).To(Equal(vested[0].Amount))
    +		})
     
    +		It("can delegate tokens from account balance - tokens not in vesting schedule", func() {
    +			testAccount := testAccounts[0]
    +
    +			// send some funds to the account to delegate
    +			coinsToDelegate := sdk.NewCoins(sdk.NewCoin(stakeDenom, math.NewInt(1e18)))
    +			err = testutil.FundAccount(s.ctx, s.app.BankKeeper, testAccount.address, coinsToDelegate)
    +			Expect(err).To(BeNil())
    +
    +			// Verify that the total spendable coins should only be coins
    +			// not in the vesting schedule. Because all coins from the vesting
    +			// schedule are still locked
    +			spendablePre := s.app.BankKeeper.SpendableCoins(s.ctx, testAccount.address)
    +			Expect(spendablePre).To(Equal(accountGasCoverage.Add(coinsToDelegate...)))
    +
    +			// delegate funds not in vesting schedule
    +			res, err := testutil.Delegate(s.ctx, s.app, testAccount.privKey, coinsToDelegate[0], s.validator)
    +			Expect(err).NotTo(HaveOccurred(), "expected no error during delegation")
    +			Expect(res.IsOK()).To(BeTrue())
    +
    +			// check spendable balance is updated properly
     			spendablePost := s.app.BankKeeper.SpendableCoins(s.ctx, testAccount.address)
    -			Expect(spendablePost.AmountOf(stakeDenom).GT(spendablePre.AmountOf(stakeDenom)))
    +			Expect(spendablePost).To(Equal(spendablePre.Sub(coinsToDelegate...).Sub(accountGasCoverage...)))
     		})
     
    -		It("cannot delegate unvested tokens", func() {
    -			_, err := delegate(testAccounts[0], vestingAmtTotal)
    +		It("can delegate tokens from account balance (free tokens) + locked vested tokens", func() {
    +			testAccount := testAccounts[0]
    +
    +			// send some funds to the account to delegate
    +			amt := sdk.NewCoins(sdk.NewCoin(stakeDenom, math.NewInt(1e18)))
    +			err = testutil.FundAccount(s.ctx, s.app.BankKeeper, testAccount.address, amt)
    +			Expect(err).To(BeNil())
    +
    +			// Verify that the total spendable coins should only be coins
    +			// not in the vesting schedule. Because all coins from the vesting
    +			// schedule are still locked
    +			spendablePre := s.app.BankKeeper.SpendableCoins(s.ctx, testAccount.address)
    +			Expect(spendablePre).To(Equal(accountGasCoverage.Add(amt...)))
    +
    +			// delegate some tokens from the account balance + locked vested coins
    +			coinsToDelegate := amt.Add(vested...)
    +
    +			res, err := testutil.Delegate(s.ctx, s.app, testAccount.privKey, coinsToDelegate[0], s.validator)
    +			Expect(err).NotTo(HaveOccurred(), "expected no error during delegation")
    +			Expect(res.IsOK()).To(BeTrue())
    +
    +			// check spendable balance is updated properly
    +			spendablePost := s.app.BankKeeper.SpendableCoins(s.ctx, testAccount.address)
    +			Expect(spendablePost).To(Equal(spendablePre.Sub(amt...).Sub(accountGasCoverage...)))
    +		})
    +
    +		It("cannot delegate unvested tokens in sequetial txs", func() {
    +			_, err := testutil.Delegate(s.ctx, s.app, testAccounts[0].privKey, twoThirdsOfVested[0], s.validator)
    +			Expect(err).To(BeNil(), "error while executing the delegate message")
    +			_, err = testutil.Delegate(s.ctx, s.app, testAccounts[0].privKey, twoThirdsOfVested[0], s.validator)
     			Expect(err).ToNot(BeNil())
     		})
     
    -		It("cannot delegate unvested tokens in batches", func() {
    -			msg, err := delegate(testAccounts[0], twoThirdsOfVested)
    +		It("cannot delegate then send tokens", func() {
    +			_, err := testutil.Delegate(s.ctx, s.app, testAccounts[0].privKey, twoThirdsOfVested[0], s.validator)
     			Expect(err).To(BeNil())
     
    -			msgServer := stakingkeeper.NewMsgServerImpl(&s.app.StakingKeeper)
    -			_, err = msgServer.Delegate(s.ctx, msg)
    -			Expect(err).ToNot(HaveOccurred(), "error while executing the delegate message")
    +			err = s.app.BankKeeper.SendCoins(
    +				s.ctx,
    +				clawbackAccount.GetAddress(),
    +				dest,
    +				twoThirdsOfVested,
    +			)
    +			Expect(err).ToNot(BeNil())
    +		})
     
    -			_, err = delegate(testAccounts[0], twoThirdsOfVested)
    +		It("cannot delegate more than the locked vested tokens", func() {
    +			_, err := testutil.Delegate(s.ctx, s.app, testAccounts[0].privKey, vested[0].Add(sdk.NewCoin(stakeDenom, math.NewInt(1))), s.validator)
     			Expect(err).ToNot(BeNil())
     		})
     
    -		It("cannot delegate then send tokens", func() {
    -			_, err := delegate(testAccounts[0], twoThirdsOfVested)
    +		It("cannot delegate free tokens and then send locked/unvested tokens", func() {
    +			// send some funds to the account to delegate
    +			coinsToDelegate := sdk.NewCoins(sdk.NewCoin(stakeDenom, math.NewInt(1e18)))
    +			err = testutil.FundAccount(s.ctx, s.app.BankKeeper, testAccounts[0].address, coinsToDelegate)
    +			Expect(err).To(BeNil())
    +
    +			_, err := testutil.Delegate(s.ctx, s.app, testAccounts[0].privKey, coinsToDelegate[0], s.validator)
     			Expect(err).To(BeNil())
     
     			err = s.app.BankKeeper.SendCoins(
    @@ -277,7 +349,7 @@ var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
     			Expect(err).ToNot(BeNil())
     		})
     
    -		It("cannot transfer vested tokens", func() {
    +		It("cannot transfer locked vested tokens", func() {
     			err := s.app.BankKeeper.SendCoins(
     				s.ctx,
     				clawbackAccount.GetAddress(),
    @@ -289,85 +361,141 @@ var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
     
     		It("can perform Ethereum tx with spendable balance", func() {
     			account := testAccounts[0]
    +			coins := testutil.TestVestingSchedule.UnlockedCoinsPerLockup
     			// Fund account with new spendable tokens
    -			err := testutil.FundAccount(s.ctx, s.app.BankKeeper, account.address, unlockedPerLockup)
    +			err := testutil.FundAccount(s.ctx, s.app.BankKeeper, account.address, coins)
     			Expect(err).To(BeNil())
     
    -			txAmount := unlockedPerLockupAmt.BigInt()
    -			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount, 0)
    +			txAmount := coins.AmountOf(stakeDenom)
    +			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount.BigInt(), 0)
     			Expect(err).To(BeNil())
     
    -			assertEthSucceeds([]TestClawbackAccount{account}, funder, dest, unlockedPerLockupAmt, stakeDenom, msg)
    +			assertEthSucceeds([]TestClawbackAccount{account}, funder, dest, txAmount, stakeDenom, msg)
     		})
     
    -		It("cannot perform Ethereum tx with locked balance", func() {
    +		It("cannot perform Ethereum tx with locked vested balance", func() {
     			account := testAccounts[0]
     			txAmount := vested.AmountOf(stakeDenom).BigInt()
     			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount, 0)
     			Expect(err).To(BeNil())
    -
     			assertEthFails(msg)
     		})
     	})
    -
     	Context("Between first and second lockup periods", func() {
     		BeforeEach(func() {
     			// Surpass first lockup
    -			vestDuration := time.Duration(lockupLength)
    +			vestDuration := time.Duration(testutil.TestVestingSchedule.LockupPeriodLength)
     			s.CommitAfter(vestDuration * time.Second)
     
    +			// after first lockup period
    +			// half of total vesting tokens are unlocked
    +			// but only 12 vesting periods passed
     			// Check if some, but not all tokens are vested and unlocked
     			for _, account := range testAccounts {
    -				vested := account.clawbackAccount.GetVestedOnly(s.ctx.BlockTime())
    -				unlocked := account.clawbackAccount.GetUnlockedOnly(s.ctx.BlockTime())
    +				vested = account.clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +				unlocked := account.clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
    +				freeCoins = account.clawbackAccount.GetUnlockedVestedCoins(s.ctx.BlockTime())
    +
     				expVested := sdk.NewCoins(sdk.NewCoin(stakeDenom, amt.Mul(math.NewInt(lockup))))
    +				expUnlockedVested := expVested
     
    -				s.Require().NotEqual(vestingAmtTotal, vested)
    -				s.Require().Equal(expVested, vested)
    -				s.Require().Equal(unlocked, unlockedPerLockup)
    +				Expect(vested).NotTo(Equal(vestingAmtTotal))
    +				Expect(vested).To(Equal(expVested))
    +				Expect(unlocked).To(Equal(unlockedPerLockup))
    +				Expect(freeCoins).To(Equal(expUnlockedVested))
     			}
     		})
     
    -		It("should enable access to unlocked EVM tokens (single-account, single-msg)", func() {
    +		It("delegate unlocked vested tokens and spendable balance is updated properly", func() {
    +			account := testAccounts[0]
    +			balance := s.app.BankKeeper.GetBalance(s.ctx, account.address, stakeDenom)
    +			// the returned balance should be the account's initial balance and
    +			// the total amount of the vesting schedule
    +			Expect(balance.Amount).To(Equal(accountGasCoverage.Add(vestingAmtTotal...)[0].Amount))
    +
    +			spReq := &banktypes.QuerySpendableBalanceByDenomRequest{Address: account.address.String(), Denom: stakeDenom}
    +			spRes, err := s.app.BankKeeper.SpendableBalanceByDenom(s.ctx, spReq)
    +			Expect(err).To(BeNil())
    +			// spendable balance should be the initial account balance + vested tokens
    +			initialSpendableBalance := spRes.Balance
    +			Expect(initialSpendableBalance.Amount).To(Equal(accountGasCoverage.Add(freeCoins...)[0].Amount))
    +
    +			// can delegate vested tokens
    +			// fees paid is accountGasCoverage amount
    +			res, err := testutil.Delegate(s.ctx, s.app, account.privKey, freeCoins[0], s.validator)
    +			Expect(err).ToNot(HaveOccurred(), "expected no error during delegation")
    +			Expect(res.Code).To(BeZero(), "expected delegation to succeed")
    +
    +			// spendable balance should be updated to be prevSpendableBalance - delegatedAmt - fees
    +			spRes, err = s.app.BankKeeper.SpendableBalanceByDenom(s.ctx, spReq)
    +			Expect(err).To(BeNil())
    +			Expect(spRes.Balance.Amount.Int64()).To(Equal(int64(0)))
    +
    +			// try to send coins - should error
    +			err = s.app.BankKeeper.SendCoins(s.ctx, account.address, funder, vested)
    +			Expect(err).NotTo(BeNil())
    +			Expect(err.Error()).To(ContainSubstring("spendable balance"))
    +			Expect(err.Error()).To(ContainSubstring("is smaller than"))
    +		})
    +
    +		It("cannot delegate more than vested tokens", func() {
     			account := testAccounts[0]
    +			balance := s.app.BankKeeper.GetBalance(s.ctx, account.address, stakeDenom)
    +			// the returned balance should be the account's initial balance and
    +			// the total amount of the vesting schedule
    +			Expect(balance.Amount).To(Equal(accountGasCoverage.Add(vestingAmtTotal...)[0].Amount))
     
    -			txAmount := unlockedPerLockupAmt.BigInt()
    -			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount, 0)
    +			spReq := &banktypes.QuerySpendableBalanceByDenomRequest{Address: account.address.String(), Denom: stakeDenom}
    +			spRes, err := s.app.BankKeeper.SpendableBalanceByDenom(s.ctx, spReq)
    +			Expect(err).To(BeNil())
    +			// spendable balance should be the initial account balance + vested tokens
    +			initialSpendableBalance := spRes.Balance
    +			Expect(initialSpendableBalance.Amount).To(Equal(accountGasCoverage.Add(freeCoins...)[0].Amount))
    +
    +			// cannot delegate more than vested tokens
    +			_, err = testutil.Delegate(s.ctx, s.app, account.privKey, freeCoins[0].Add(sdk.NewCoin(stakeDenom, math.NewInt(1))), s.validator)
    +			Expect(err).To(HaveOccurred(), "expected no error during delegation")
    +			Expect(err.Error()).To(ContainSubstring("cannot delegate unvested coins"))
    +		})
    +
    +		It("should enable access to unlocked and vested EVM tokens (single-account, single-msg)", func() {
    +			account := testAccounts[0]
    +			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, vested[0].Amount.BigInt(), 0)
     			Expect(err).To(BeNil())
     
    -			assertEthSucceeds([]TestClawbackAccount{account}, funder, dest, unlockedPerLockupAmt, stakeDenom, msg)
    +			assertEthSucceeds([]TestClawbackAccount{account}, funder, dest, vested[0].Amount, stakeDenom, msg)
     		})
     
     		It("should enable access to unlocked EVM tokens (single-account, multiple-msgs)", func() {
     			account := testAccounts[0]
     
     			// Split the total unlocked amount into numTestMsgs equally sized tx's
     			msgs := make([]sdk.Msg, numTestMsgs)
    -			txAmount := unlockedPerLockupAmt.QuoRaw(int64(numTestMsgs)).BigInt()
    +			txAmount := vested[0].Amount.QuoRaw(int64(numTestMsgs)).BigInt()
     
     			for i := 0; i < numTestMsgs; i++ {
     				msgs[i], err = utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount, i)
     				Expect(err).To(BeNil())
     			}
     
    -			assertEthSucceeds([]TestClawbackAccount{account}, funder, dest, unlockedPerLockupAmt, stakeDenom, msgs...)
    +			assertEthSucceeds([]TestClawbackAccount{account}, funder, dest, vested[0].Amount, stakeDenom, msgs...)
     		})
     
     		It("should enable access to unlocked EVM tokens (multi-account, single-msg)", func() {
    -			txAmount := unlockedPerLockupAmt.BigInt()
    +			txAmount := vested[0].Amount.BigInt()
     
     			msgs := make([]sdk.Msg, numTestAccounts)
     			for i, grantee := range testAccounts {
     				msgs[i], err = utiltx.CreateEthTx(s.ctx, s.app, grantee.privKey, grantee.address, dest, txAmount, 0)
     				Expect(err).To(BeNil())
     			}
     
    -			assertEthSucceeds(testAccounts, funder, dest, unlockedPerLockupAmt, stakeDenom, msgs...)
    +			assertEthSucceeds(testAccounts, funder, dest, vested[0].Amount, stakeDenom, msgs...)
     		})
     
     		It("should enable access to unlocked EVM tokens (multi-account, multiple-msgs)", func() {
     			msgs := []sdk.Msg{}
    -			txAmount := unlockedPerLockupAmt.QuoRaw(int64(numTestMsgs)).BigInt()
    +			txAmount := vested[0].Amount.QuoRaw(int64(numTestMsgs)).BigInt()
     
     			for _, grantee := range testAccounts {
     				for j := 0; j < numTestMsgs; j++ {
    @@ -377,7 +505,7 @@ var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
     				}
     			}
     
    -			assertEthSucceeds(testAccounts, funder, dest, unlockedPerLockupAmt, stakeDenom, msgs...)
    +			assertEthSucceeds(testAccounts, funder, dest, vested[0].Amount, stakeDenom, msgs...)
     		})
     
     		It("should not enable access to locked EVM tokens (single-account, single-msg)", func() {
    @@ -386,43 +514,39 @@ var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
     			txAmount := vestingAmtTotal.AmountOf(stakeDenom).BigInt()
     			msg, err := utiltx.CreateEthTx(s.ctx, s.app, testAccount.privKey, testAccount.address, dest, txAmount, 0)
     			Expect(err).To(BeNil())
    -
     			assertEthFails(msg)
     		})
     
     		It("should not enable access to locked EVM tokens (single-account, multiple-msgs)", func() {
     			msgs := make([]sdk.Msg, numTestMsgs+1)
    -			txAmount := unlockedPerLockupAmt.QuoRaw(int64(numTestMsgs)).BigInt()
    +			txAmount := vested[0].Amount.QuoRaw(int64(numTestMsgs)).BigInt()
     			testAccount := testAccounts[0]
     
     			// Add additional message that exceeds unlocked balance
     			for i := 0; i < numTestMsgs+1; i++ {
     				msgs[i], err = utiltx.CreateEthTx(s.ctx, s.app, testAccount.privKey, testAccount.address, dest, txAmount, i)
     				Expect(err).To(BeNil())
     			}
    -
     			assertEthFails(msgs...)
     		})
     
     		It("should not enable access to locked EVM tokens (multi-account, single-msg)", func() {
     			msgs := make([]sdk.Msg, numTestAccounts+1)
    -			txAmount := unlockedPerLockupAmt.BigInt()
    +			txAmount := vested[0].Amount.BigInt()
     
     			for i, account := range testAccounts {
     				msgs[i], err = utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount, 0)
     				Expect(err).To(BeNil())
     			}
    -
     			// Add additional message that exceeds unlocked balance
     			msgs[numTestAccounts], err = utiltx.CreateEthTx(s.ctx, s.app, testAccounts[0].privKey, testAccounts[0].address, dest, txAmount, 1)
     			Expect(err).To(BeNil())
    -
     			assertEthFails(msgs...)
     		})
     
     		It("should not enable access to locked EVM tokens (multi-account, multiple-msgs)", func() {
     			msgs := []sdk.Msg{}
    -			txAmount := unlockedPerLockupAmt.QuoRaw(int64(numTestMsgs)).BigInt()
    +			txAmount := vested[0].Amount.QuoRaw(int64(numTestMsgs)).BigInt()
     			var addedMsg sdk.Msg
     
     			for _, account := range testAccounts {
    @@ -431,95 +555,94 @@ var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
     					msgs = append(msgs, addedMsg)
     				}
     			}
    -
     			// Add additional message that exceeds unlocked balance
     			addedMsg, err = utiltx.CreateEthTx(s.ctx, s.app, testAccounts[0].privKey, testAccounts[0].address, dest, txAmount, numTestMsgs)
     			Expect(err).To(BeNil())
     			msgs = append(msgs, addedMsg)
    -
     			assertEthFails(msgs...)
     		})
    -
     		It("should not short-circuit with a normal account", func() {
     			account := testAccounts[0]
     			address, privKey := utiltx.NewAccAddressAndKey()
    -
     			txAmount := vestingAmtTotal.AmountOf(stakeDenom).BigInt()
    -
     			// Fund a normal account to try to short-circuit the AnteHandler
     			err = testutil.FundAccount(s.ctx, s.app.BankKeeper, address, vestingAmtTotal.MulInt(math.NewInt(2)))
     			Expect(err).To(BeNil())
     			normalAccMsg, err := utiltx.CreateEthTx(s.ctx, s.app, privKey, address, dest, txAmount, 0)
     			Expect(err).To(BeNil())
    -
     			// Attempt to spend entire balance
     			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount, 0)
     			Expect(err).To(BeNil())
     			err = validateEthVestingTransactionDecorator(normalAccMsg, msg)
     			Expect(err).ToNot(BeNil())
    -
     			_, err = testutil.DeliverEthTx(s.app, nil, msg)
     			Expect(err).ToNot(BeNil())
     		})
     	})
     
     	Context("after first lockup and additional vest", func() {
     		BeforeEach(func() {
    -			vestDuration := time.Duration(lockupLength + vestingLength)
    +			vestDuration := time.Duration(testutil.TestVestingSchedule.LockupPeriodLength + vestingLength)
     			s.CommitAfter(vestDuration * time.Second)
     
    -			vested = clawbackAccount.GetVestedOnly(s.ctx.BlockTime())
    +			// after first lockup period
    +			// half of total vesting tokens are unlocked
    +			// now only 13 vesting periods passed
    +
    +			vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
     			expVested := sdk.NewCoins(sdk.NewCoin(stakeDenom, amt.Mul(math.NewInt(lockup+1))))
     
    -			unlocked := clawbackAccount.GetUnlockedOnly(s.ctx.BlockTime())
    +			unlocked := clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
     			expUnlocked := unlockedPerLockup
     
    -			s.Require().Equal(expVested, vested)
    -			s.Require().Equal(expUnlocked, unlocked)
    +			Expect(expVested).To(Equal(vested))
    +			Expect(expUnlocked).To(Equal(unlocked))
     		})
     
     		It("should enable access to unlocked EVM tokens", func() {
     			testAccount := testAccounts[0]
     
    -			txAmount := unlockedPerLockupAmt.BigInt()
    +			txAmount := vested[0].Amount.BigInt()
     			msg, err := utiltx.CreateEthTx(s.ctx, s.app, testAccount.privKey, testAccount.address, dest, txAmount, 0)
     			Expect(err).To(BeNil())
     
    -			assertEthSucceeds([]TestClawbackAccount{testAccount}, funder, dest, unlockedPerLockupAmt, stakeDenom, msg)
    +			assertEthSucceeds([]TestClawbackAccount{testAccount}, funder, dest, vested[0].Amount, stakeDenom, msg)
     		})
     
    -		It("should not enable access to locked EVM tokens", func() {
    +		It("should not enable access to unvested EVM tokens", func() {
     			testAccount := testAccounts[0]
     
    -			txAmount := vested.AmountOf(stakeDenom).BigInt()
    +			txAmount := vested[0].Amount.Add(amt).BigInt()
     			msg, err := utiltx.CreateEthTx(s.ctx, s.app, testAccount.privKey, testAccount.address, dest, txAmount, 0)
     			Expect(err).To(BeNil())
     
     			assertEthFails(msg)
     		})
     	})
    -
     	Context("after half of vesting period and both lockups", func() {
     		BeforeEach(func() {
     			// Surpass lockup duration
    -			lockupDuration := time.Duration(lockupLength * numLockupPeriods)
    +			lockupDuration := time.Duration(testutil.TestVestingSchedule.LockupPeriodLength * numLockupPeriods)
     			s.CommitAfter(lockupDuration * time.Second)
    +			// after two lockup period
    +			// total vesting tokens are unlocked
    +			// and 24/48 vesting periods passed
     
     			// Check if some, but not all tokens are vested
    -			unvested = clawbackAccount.GetUnvestedOnly(s.ctx.BlockTime())
    -			vested = clawbackAccount.GetVestedOnly(s.ctx.BlockTime())
    +			unvested = clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
    +			vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
     			expVested := sdk.NewCoins(sdk.NewCoin(stakeDenom, amt.Mul(math.NewInt(lockup*numLockupPeriods))))
    -			s.Require().NotEqual(vestingAmtTotal, vested)
    -			s.Require().Equal(expVested, vested)
    +			Expect(vestingAmtTotal).NotTo(Equal(vested))
    +			Expect(expVested).To(Equal(vested))
     		})
     
     		It("can delegate vested tokens", func() {
    -			_, err := delegate(testAccounts[0], vested)
    +			_, err := testutil.Delegate(s.ctx, s.app, testAccounts[0].privKey, vested[0], s.validator)
     			Expect(err).To(BeNil())
     		})
     
     		It("cannot delegate unvested tokens", func() {
    -			_, err := delegate(testAccounts[0], vestingAmtTotal)
    +			_, err := testutil.Delegate(s.ctx, s.app, testAccounts[0].privKey, vestingAmtTotal[0], s.validator)
     			Expect(err).ToNot(BeNil())
     		})
     
    @@ -532,7 +655,6 @@ var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
     			)
     			Expect(err).To(BeNil())
     		})
    -
     		It("cannot transfer unvested tokens", func() {
     			err := s.app.BankKeeper.SendCoins(
     				s.ctx,
    @@ -542,63 +664,57 @@ var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
     			)
     			Expect(err).ToNot(BeNil())
     		})
    -
     		It("can perform Ethereum tx with spendable balance", func() {
     			account := testAccounts[0]
    -
     			txAmount := vested.AmountOf(stakeDenom).BigInt()
     			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount, 0)
     			Expect(err).To(BeNil())
    -
     			assertEthSucceeds([]TestClawbackAccount{account}, funder, dest, vested.AmountOf(stakeDenom), stakeDenom, msg)
     		})
     	})
    -
     	Context("after entire vesting period and both lockups", func() {
     		BeforeEach(func() {
     			// Surpass vest duration
     			vestDuration := time.Duration(vestingLength * periodsTotal)
     			s.CommitAfter(vestDuration * time.Second)
     
     			// Check that all tokens are vested and unlocked
    -			unvested = clawbackAccount.GetUnvestedOnly(s.ctx.BlockTime())
    -			vested = clawbackAccount.GetVestedOnly(s.ctx.BlockTime())
    -			locked := clawbackAccount.LockedCoins(s.ctx.BlockTime())
    +			unvested = clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
    +			vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +			unlocked := clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
    +			unlockedVested := clawbackAccount.GetUnlockedVestedCoins(s.ctx.BlockTime())
    +			notSpendable := clawbackAccount.LockedCoins(s.ctx.BlockTime())
    +
    +			// all vested coins should be unlocked
    +			Expect(vested).To(Equal(unlockedVested))
     
     			zeroCoins := sdk.NewCoins(sdk.NewCoin(stakeDenom, math.ZeroInt()))
    -			s.Require().Equal(vestingAmtTotal, vested)
    -			s.Require().Equal(zeroCoins, locked)
    -			s.Require().Equal(zeroCoins, unvested)
    +			Expect(vestingAmtTotal).To(Equal(vested))
    +			Expect(vestingAmtTotal).To(Equal(unlocked))
    +			Expect(zeroCoins).To(Equal(notSpendable))
    +			Expect(zeroCoins).To(Equal(unvested))
     		})
     
     		It("can send entire balance", func() {
     			account := testAccounts[0]
    -
     			txAmount := vestingAmtTotal.AmountOf(stakeDenom)
     			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount.BigInt(), 0)
     			Expect(err).To(BeNil())
    -
     			assertEthSucceeds([]TestClawbackAccount{account}, funder, dest, txAmount, stakeDenom, msg)
     		})
    -
     		It("cannot exceed balance", func() {
     			account := testAccounts[0]
    -
     			txAmount := vestingAmtTotal.AmountOf(stakeDenom).Mul(math.NewInt(2))
     			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, txAmount.BigInt(), 0)
     			Expect(err).To(BeNil())
    -
     			assertEthFails(msg)
     		})
    -
     		It("should short-circuit with zero balance", func() {
     			account := testAccounts[0]
     			balance := s.app.BankKeeper.GetBalance(s.ctx, account.address, stakeDenom)
    -
     			// Drain account balance
     			err := s.app.BankKeeper.SendCoins(s.ctx, account.address, dest, sdk.NewCoins(balance))
     			Expect(err).To(BeNil())
    -
     			msg, err := utiltx.CreateEthTx(s.ctx, s.app, account.privKey, account.address, dest, big.NewInt(0), 0)
     			Expect(err).To(BeNil())
     			err = validateEthVestingTransactionDecorator(msg)
    @@ -614,36 +730,6 @@ var _ = Describe("Clawback Vesting Accounts", Ordered, func() {
     // 22/09 Cliff ends
     // 23/02 Lock ends
     var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
    -	// Monthly vesting period
    -	stakeDenom := utils.BaseDenom
    -	amt := math.NewInt(1)
    -	vestingLength := int64(60 * 60 * 24 * 30) // in seconds
    -	vestingAmt := sdk.NewCoins(sdk.NewCoin(stakeDenom, amt))
    -	vestingPeriod := sdkvesting.Period{Length: vestingLength, Amount: vestingAmt}
    -
    -	// 4 years vesting total
    -	periodsTotal := int64(48)
    -	vestingTotal := amt.Mul(math.NewInt(periodsTotal))
    -	vestingAmtTotal := sdk.NewCoins(sdk.NewCoin(stakeDenom, vestingTotal))
    -
    -	// 6 month cliff
    -	cliff := int64(6)
    -	cliffLength := vestingLength * cliff
    -	cliffAmt := sdk.NewCoins(sdk.NewCoin(stakeDenom, amt.Mul(math.NewInt(cliff))))
    -	cliffPeriod := sdkvesting.Period{Length: cliffLength, Amount: cliffAmt}
    -
    -	// 12 month lockup
    -	lockup := int64(12) // 12 year
    -	lockupLength := vestingLength * lockup
    -	lockupPeriod := sdkvesting.Period{Length: lockupLength, Amount: vestingAmtTotal}
    -	lockupPeriods := sdkvesting.Periods{lockupPeriod}
    -
    -	// Create vesting periods with initial cliff
    -	vestingPeriods := sdkvesting.Periods{cliffPeriod}
    -	for p := int64(1); p <= periodsTotal-cliff; p++ {
    -		vestingPeriods = append(vestingPeriods, vestingPeriod)
    -	}
    -
     	var (
     		clawbackAccount *types.ClawbackVestingAccount
     		vesting         sdk.Coins
    @@ -653,12 +739,13 @@ var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
     		isClawback      bool
     	)
     
    -	vestingAddr := sdk.AccAddress(utiltx.GenerateAddress().Bytes())
    +	vestingAddr, vestingPriv := utiltx.NewAccAddressAndKey()
     	funder, funderPriv := utiltx.NewAccAddressAndKey()
     	dest := sdk.AccAddress(utiltx.GenerateAddress().Bytes())
     
     	BeforeEach(func() {
    -		s.SetupTest()
    +		Expect(s.SetupTest()).To(BeNil()) // reset
    +
     		vestingStart := s.ctx.BlockTime()
     
     		// Initialize account at vesting address by funding it with tokens
    @@ -668,46 +755,45 @@ var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
     		err = s.app.BankKeeper.SendCoins(s.ctx, vestingAddr, funder, vestingAmtTotal)
     		Expect(err).ToNot(HaveOccurred(), "failed to send coins to funder")
     
    +		// Send some tokens to the vesting account to cover tx fees
    +		err = testutil.FundAccount(s.ctx, s.app.BankKeeper, vestingAddr, accountGasCoverage)
    +		Expect(err).ToNot(HaveOccurred(), "failed to fund target account")
    +
     		balanceFunder := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		balanceGrantee := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     		balanceDest := s.app.BankKeeper.GetBalance(s.ctx, dest, stakeDenom)
     		Expect(balanceFunder).To(Equal(vestingAmtTotal[0]), "expected different funder balance")
    -		Expect(balanceGrantee.IsZero()).To(BeTrue(), "expected balance of vesting account to be zero")
    +		Expect(balanceGrantee.Amount).To(Equal(accountGasCoverage[0].Amount))
     		Expect(balanceDest.IsZero()).To(BeTrue(), "expected destination balance to be zero")
     
     		msg := types.NewMsgCreateClawbackVestingAccount(funder, vestingAddr, true)
    -
     		_, err = s.app.VestingKeeper.CreateClawbackVestingAccount(sdk.WrapSDKContext(s.ctx), msg)
     		Expect(err).ToNot(HaveOccurred(), "expected creating clawback vesting account to succeed")
    -
     		acc := s.app.AccountKeeper.GetAccount(s.ctx, vestingAddr)
     		clawbackAccount, isClawback = acc.(*types.ClawbackVestingAccount)
     		Expect(isClawback).To(BeTrue(), "expected account to be clawback vesting account")
    -
     		// fund the vesting account
    -		msgFund := types.NewMsgFundVestingAccount(funder, vestingAddr, vestingStart, lockupPeriods, vestingPeriods)
    +		msgFund := types.NewMsgFundVestingAccount(funder, vestingAddr, vestingStart, testutil.TestVestingSchedule.LockupPeriods, testutil.TestVestingSchedule.VestingPeriods)
     		_, err = s.app.VestingKeeper.FundVestingAccount(sdk.WrapSDKContext(s.ctx), msgFund)
     		Expect(err).ToNot(HaveOccurred(), "expected funding vesting account to succeed")
    -
     		acc = s.app.AccountKeeper.GetAccount(s.ctx, vestingAddr)
     		Expect(acc).ToNot(BeNil(), "expected account to exist")
     		clawbackAccount, isClawback = acc.(*types.ClawbackVestingAccount)
     		Expect(isClawback).To(BeTrue(), "expected account to be clawback vesting account")
     
     		// Check if all tokens are unvested and locked at vestingStart
     		vesting = clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
    -		vested = clawbackAccount.GetVestedOnly(s.ctx.BlockTime())
    -		unlocked = clawbackAccount.GetUnlockedOnly(s.ctx.BlockTime())
    +		vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +		unlocked = clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
     		Expect(vesting).To(Equal(vestingAmtTotal), "expected difference vesting tokens")
     		Expect(vested.IsZero()).To(BeTrue(), "expected no tokens to be vested")
     		Expect(unlocked.IsZero()).To(BeTrue(), "expected no tokens to be unlocked")
    -
     		bF := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		balanceGrantee = s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     		balanceDest = s.app.BankKeeper.GetBalance(s.ctx, dest, stakeDenom)
     
     		Expect(bF.IsZero()).To(BeTrue(), "expected funder balance to be zero")
    -		Expect(balanceGrantee).To(Equal(vestingAmtTotal[0]), "expected all tokens to be locked")
    +		Expect(balanceGrantee).To(Equal(vestingAmtTotal.Add(accountGasCoverage...)[0]), "expected all tokens to be locked")
     		Expect(balanceDest.IsZero()).To(BeTrue(), "expected no tokens to be unlocked")
     	})
     
    @@ -716,37 +802,30 @@ var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
     		emptyVestingAddr := sdk.AccAddress(utiltx.GenerateAddress().Bytes())
     		err := testutil.FundAccount(s.ctx, s.app.BankKeeper, emptyVestingAddr, vestingAmtTotal)
     		Expect(err).ToNot(HaveOccurred(), "failed to fund target account")
    -
     		msg := types.NewMsgCreateClawbackVestingAccount(funder, emptyVestingAddr, false)
    -
     		_, err = s.app.VestingKeeper.CreateClawbackVestingAccount(sdk.WrapSDKContext(s.ctx), msg)
     		Expect(err).ToNot(HaveOccurred(), "expected creating clawback vesting account to succeed")
    -
     		clawbackMsg := types.NewMsgClawback(funder, emptyVestingAddr, dest)
     		_, err = s.app.VestingKeeper.Clawback(ctx, clawbackMsg)
     		Expect(err).To(HaveOccurred())
     		Expect(err.Error()).To(ContainSubstring("has no vesting or lockup periods"))
     	})
    -
     	It("should claw back unvested amount before cliff", func() {
     		ctx := sdk.WrapSDKContext(s.ctx)
    -
     		balanceFunder := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		balanceDest := s.app.BankKeeper.GetBalance(s.ctx, dest, stakeDenom)
    -
     		// Perform clawback before cliff
     		msg := types.NewMsgClawback(funder, vestingAddr, dest)
     		res, err := s.app.VestingKeeper.Clawback(ctx, msg)
     		Expect(err).To(BeNil())
     		Expect(res.Coins).To(Equal(vestingAmtTotal), "expected different coins to be clawed back")
    -
     		// All initial vesting amount goes to dest
     		bF := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		bG := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     		bD := s.app.BankKeeper.GetBalance(s.ctx, dest, stakeDenom)
     
     		Expect(bF).To(Equal(balanceFunder), "expected funder balance to be unchanged")
    -		Expect(bG.IsZero()).To(BeTrue(), "expected all tokens to be clawed back")
    +		Expect(bG.Amount).To(Equal(accountGasCoverage[0].Amount), "expected all tokens to be clawed back")
     		Expect(bD).To(Equal(balanceDest.Add(vestingAmtTotal[0])), "expected all tokens to be clawed back to the destination account")
     	})
     
    @@ -756,82 +835,78 @@ var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
     		s.CommitAfter(cliffDuration * time.Second)
     
     		// Check that all tokens are locked and some, but not all tokens are vested
    -		vested = clawbackAccount.GetVestedOnly(s.ctx.BlockTime())
    -		unlocked = clawbackAccount.GetUnlockedOnly(s.ctx.BlockTime())
    -		free = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +		vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +		unlocked = clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
    +		free = clawbackAccount.GetUnlockedVestedCoins(s.ctx.BlockTime())
     		vesting = clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
     		expVestedAmount := amt.Mul(math.NewInt(cliff))
     		expVested := sdk.NewCoins(sdk.NewCoin(stakeDenom, expVestedAmount))
     		unvested := vestingAmtTotal.Sub(vested...)
     
    -		s.Require().Equal(expVested, vested)
    -		s.Require().True(expVestedAmount.GT(math.NewInt(0)))
    -		s.Require().True(free.IsZero())
    -		s.Require().Equal(vesting, vestingAmtTotal)
    +		Expect(expVested).To(Equal(vested))
    +		Expect(expVestedAmount.GT(math.NewInt(0))).To(BeTrue())
    +		Expect(free.IsZero()).To(BeTrue())
    +		Expect(vesting).To(Equal(vestingAmtTotal.Sub(expVested...)))
     
     		balanceFunder := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		balanceGrantee := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     		balanceDest := s.app.BankKeeper.GetBalance(s.ctx, dest, stakeDenom)
    -
     		// Perform clawback
     		msg := types.NewMsgClawback(funder, vestingAddr, dest)
     		ctx := sdk.WrapSDKContext(s.ctx)
     		res, err := s.app.VestingKeeper.Clawback(ctx, msg)
     		Expect(err).To(BeNil())
     		Expect(res.Coins).To(Equal(unvested), "expected unvested coins to be clawed back")
    -
     		bF := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		bG := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     		bD := s.app.BankKeeper.GetBalance(s.ctx, dest, stakeDenom)
     
    -		expClawback := clawbackAccount.GetUnvestedOnly(s.ctx.BlockTime())
    +		expClawback := clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
     
     		// Any unvested amount is clawed back
    -		s.Require().Equal(balanceFunder, bF)
    -		s.Require().Equal(balanceGrantee.Sub(expClawback[0]).Amount.Uint64(), bG.Amount.Uint64())
    -		s.Require().Equal(balanceDest.Add(expClawback[0]).Amount.Uint64(), bD.Amount.Uint64())
    +		Expect(balanceFunder).To(Equal(bF))
    +		Expect(balanceGrantee.Sub(expClawback[0]).Amount.Uint64()).To(Equal(bG.Amount.Uint64()))
    +		Expect(balanceDest.Add(expClawback[0]).Amount.Uint64()).To(Equal(bD.Amount.Uint64()))
     	})
     
     	It("should claw back any unvested amount after cliff and unlocking", func() {
     		// Surpass lockup duration
     		// A strict `if t < clawbackTime` comparison is used in ComputeClawback
     		// so, we increment the duration with 1 for the free token calculation to match
    -		lockupDuration := time.Duration(lockupLength + 1)
    +		lockupDuration := time.Duration(testutil.TestVestingSchedule.LockupPeriodLength + 1)
     		s.CommitAfter(lockupDuration * time.Second)
     
     		// Check if some, but not all tokens are vested and unlocked
    -		vested = clawbackAccount.GetVestedOnly(s.ctx.BlockTime())
    -		unlocked = clawbackAccount.GetUnlockedOnly(s.ctx.BlockTime())
    +		vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +		unlocked = clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
     		free = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
     		vesting = clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
     		expVestedAmount := amt.Mul(math.NewInt(lockup))
     		expVested := sdk.NewCoins(sdk.NewCoin(stakeDenom, expVestedAmount))
     		unvested := vestingAmtTotal.Sub(vested...)
     
    -		s.Require().Equal(free, vested)
    -		s.Require().Equal(expVested, vested)
    -		s.Require().True(expVestedAmount.GT(math.NewInt(0)))
    -		s.Require().Equal(vesting, unvested)
    +		Expect(free).To(Equal(vested))
    +		Expect(expVested).To(Equal(vested))
    +		Expect(expVestedAmount.GT(math.NewInt(0))).To(BeTrue())
    +		Expect(vesting).To(Equal(unvested))
     
     		balanceFunder := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		balanceGrantee := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     		balanceDest := s.app.BankKeeper.GetBalance(s.ctx, dest, stakeDenom)
    -
     		// Perform clawback
     		msg := types.NewMsgClawback(funder, vestingAddr, dest)
     		ctx := sdk.WrapSDKContext(s.ctx)
     		res, err := s.app.VestingKeeper.Clawback(ctx, msg)
     		Expect(err).To(BeNil())
     		Expect(res.Coins).To(Equal(unvested), "expected only coins to be clawed back")
    -
     		bF := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		bG := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     		bD := s.app.BankKeeper.GetBalance(s.ctx, dest, stakeDenom)
     
     		// Any unvested amount is clawed back
    -		s.Require().Equal(balanceFunder, bF)
    -		s.Require().Equal(balanceGrantee.Sub(vesting[0]).Amount.Uint64(), bG.Amount.Uint64())
    -		s.Require().Equal(balanceDest.Add(vesting[0]).Amount.Uint64(), bD.Amount.Uint64())
    +		Expect(balanceFunder).To(Equal(bF))
    +		Expect(balanceGrantee.Sub(vesting[0]).Amount.Uint64()).To(Equal(bG.Amount.Uint64()))
    +		Expect(balanceDest.Add(vesting[0]).Amount.Uint64()).To(Equal(bD.Amount.Uint64()))
     	})
     
     	It("should not claw back any amount after vesting periods end", func() {
    @@ -840,46 +915,43 @@ var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
     		s.CommitAfter(vestingDuration * time.Second)
     
     		// Check if some, but not all tokens are vested and unlocked
    -		vested = clawbackAccount.GetVestedOnly(s.ctx.BlockTime())
    -		unlocked = clawbackAccount.GetUnlockedOnly(s.ctx.BlockTime())
    +		vested = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
    +		unlocked = clawbackAccount.GetUnlockedCoins(s.ctx.BlockTime())
     		free = clawbackAccount.GetVestedCoins(s.ctx.BlockTime())
     		vesting = clawbackAccount.GetVestingCoins(s.ctx.BlockTime())
     
     		expVested := sdk.NewCoins(sdk.NewCoin(stakeDenom, amt.Mul(math.NewInt(periodsTotal))))
     		unvested := vestingAmtTotal.Sub(vested...)
     
    -		s.Require().Equal(free, vested)
    -		s.Require().Equal(expVested, vested)
    -		s.Require().Equal(expVested, vestingAmtTotal)
    -		s.Require().Equal(unlocked, vestingAmtTotal)
    -		s.Require().Equal(vesting, unvested)
    -		s.Require().True(vesting.IsZero())
    +		Expect(free).To(Equal(vested))
    +		Expect(expVested).To(Equal(vested))
    +		Expect(expVested).To(Equal(vestingAmtTotal))
    +		Expect(unlocked).To(Equal(vestingAmtTotal))
    +		Expect(vesting).To(Equal(unvested))
    +		Expect(vesting.IsZero()).To(BeTrue())
     
     		balanceFunder := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		balanceGrantee := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     		balanceDest := s.app.BankKeeper.GetBalance(s.ctx, dest, stakeDenom)
    -
     		// Perform clawback
     		msg := types.NewMsgClawback(funder, vestingAddr, dest)
     		ctx := sdk.WrapSDKContext(s.ctx)
     		res, err := s.app.VestingKeeper.Clawback(ctx, msg)
     		Expect(err).To(BeNil(), "expected no error during clawback")
     		Expect(res).ToNot(BeNil(), "expected response not to be nil")
     		Expect(res.Coins).To(BeEmpty(), "expected nothing to be clawed back")
    -
     		bF := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		bG := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     		bD := s.app.BankKeeper.GetBalance(s.ctx, dest, stakeDenom)
     
     		// No amount is clawed back
    -		s.Require().Equal(balanceFunder, bF)
    -		s.Require().Equal(balanceGrantee, bG)
    -		s.Require().Equal(balanceDest, bD)
    +		Expect(balanceFunder).To(Equal(bF))
    +		Expect(balanceGrantee).To(Equal(bG))
    +		Expect(balanceDest).To(Equal(bD))
     	})
     
     	Context("while there is an active governance proposal for the vesting account", func() {
     		var clawbackProposalID uint64
    -
     		BeforeEach(func() {
     			// submit a different proposal to simulate having multiple proposals of different types
     			// on chain.
    @@ -893,20 +965,16 @@ var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
     				s.address.Bytes(),
     			)
     			Expect(err).ToNot(HaveOccurred(), "expected no error creating the proposal submission message")
    -
     			_, err = testutil.DeliverTx(s.ctx, s.app, s.priv, nil, msgSubmitProposal)
     			Expect(err).ToNot(HaveOccurred(), "expected no error during proposal submission")
    -
     			// submit clawback proposal
     			govClawbackProposal := &types.ClawbackProposal{
     				Title:              "test gov clawback",
     				Description:        "this is an example of a governance proposal to clawback vesting coins",
     				Address:            vestingAddr.String(),
     				DestinationAddress: funder.String(),
     			}
    -
     			deposit := sdk.Coins{sdk.Coin{Denom: stakeDenom, Amount: math.NewInt(1)}}
    -
     			// Create the message to submit the proposal
     			msgSubmit, err := govv1beta1.NewMsgSubmitProposal(
     				govClawbackProposal, deposit, s.address.Bytes(),
    @@ -915,9 +983,7 @@ var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
     			// deliver the proposal
     			_, err = testutil.DeliverTx(s.ctx, s.app, s.priv, nil, msgSubmit)
     			Expect(err).ToNot(HaveOccurred(), "expected no error during proposal submission")
    -
     			s.Commit()
    -
     			// Check if the proposal was submitted
     			proposals := s.app.GovKeeper.GetProposals(s.ctx)
     			Expect(len(proposals)).To(Equal(2), "expected two proposals to be found")
    @@ -926,49 +992,40 @@ var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
     			Expect(proposal.GetTitle()).To(Equal("test gov clawback"), "expected different proposal title")
     			Expect(proposal.Status).To(Equal(govv1.StatusDepositPeriod), "expected proposal to be in deposit period")
     		})
    -
     		Context("with deposit made", func() {
     			BeforeEach(func() {
     				params := s.app.GovKeeper.GetParams(s.ctx)
     				depositAmount := params.MinDeposit[0].Amount.Sub(math.NewInt(1))
     				deposit := sdk.Coins{sdk.Coin{Denom: params.MinDeposit[0].Denom, Amount: depositAmount}}
    -
     				// Deliver the deposit
     				msgDeposit := govv1beta1.NewMsgDeposit(s.address.Bytes(), clawbackProposalID, deposit)
     				_, err := testutil.DeliverTx(s.ctx, s.app, s.priv, nil, msgDeposit)
     				Expect(err).ToNot(HaveOccurred(), "expected no error during proposal deposit")
    -
     				s.Commit()
    -
     				// Check the proposal is in voting period
     				proposal, found := s.app.GovKeeper.GetProposal(s.ctx, clawbackProposalID)
     				Expect(found).To(BeTrue(), "expected proposal to be found")
     				Expect(proposal.Status).To(Equal(govv1.StatusVotingPeriod), "expected proposal to be in voting period")
    -
     				// Check the store entry was set correctly
     				hasActivePropposal := s.app.VestingKeeper.HasActiveClawbackProposal(s.ctx, vestingAddr)
     				Expect(hasActivePropposal).To(BeTrue(), "expected an active clawback proposal for the vesting account")
     			})
    -
     			It("should not allow clawback", func() {
     				// Try to clawback tokens
     				msgClawback := types.NewMsgClawback(funder, vestingAddr, dest)
     				_, err = s.app.VestingKeeper.Clawback(sdk.WrapSDKContext(s.ctx), msgClawback)
     				Expect(err).To(HaveOccurred(), "expected error during clawback while there is an active governance proposal")
     				Expect(err.Error()).To(ContainSubstring("clawback is disabled while there is an active clawback proposal"))
    -
     				// Check that the clawback was not performed
     				acc := s.app.AccountKeeper.GetAccount(s.ctx, vestingAddr)
     				Expect(acc).ToNot(BeNil(), "expected account to exist")
     				_, isClawback := acc.(*types.ClawbackVestingAccount)
     				Expect(isClawback).To(BeTrue(), "expected account to be clawback vesting account")
    -
     				balances, err := s.app.VestingKeeper.Balances(s.ctx, &types.QueryBalancesRequest{
     					Address: vestingAddr.String(),
     				})
     				Expect(err).ToNot(HaveOccurred(), "expected no error during balances query")
     				Expect(balances.Unvested).To(Equal(vestingAmtTotal), "expected no tokens to be clawed back")
    -
     				// Delegate some funds to the suite validators in order to vote on proposal with enough voting power
     				// using only the suite private key
     				priv, ok := s.priv.(*ethsecp256k1.PrivKey)
    @@ -981,39 +1038,32 @@ var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
     					Expect(err).ToNot(HaveOccurred(), "expected no error during delegation")
     					Expect(res.Code).To(BeZero(), "expected delegation to succeed")
     				}
    -
     				// Vote on proposal
     				res, err := testutil.Vote(s.ctx, s.app, priv, clawbackProposalID, govv1beta1.OptionYes)
     				Expect(err).ToNot(HaveOccurred(), "failed to vote on proposal %d", clawbackProposalID)
     				Expect(res.Code).To(BeZero(), "expected proposal voting to succeed")
    -
     				// Check that the funds are clawed back after the proposal has ended
     				s.CommitAfter(time.Hour * 24 * 365) // one year
     				// Commit again because EndBlocker is run with time of the previous block and gov proposals are ended in EndBlocker
     				s.Commit()
    -
     				// Check that proposal has passed
     				proposal, found := s.app.GovKeeper.GetProposal(s.ctx, clawbackProposalID)
     				Expect(found).To(BeTrue(), "expected proposal to exist")
     				Expect(proposal.Status).ToNot(Equal(govv1.StatusVotingPeriod), "expected proposal to not be in voting period anymore")
     				Expect(proposal.Status).To(Equal(govv1.StatusPassed), "expected proposal to have passed")
    -
     				// Check that the account was converted to a normal account
     				acc = s.app.AccountKeeper.GetAccount(s.ctx, vestingAddr)
     				Expect(acc).ToNot(BeNil(), "expected account to exist")
     				_, isClawback = acc.(*types.ClawbackVestingAccount)
     				Expect(isClawback).To(BeFalse(), "expected account to be a normal account")
    -
     				hasActiveProposal := s.app.VestingKeeper.HasActiveClawbackProposal(s.ctx, vestingAddr)
     				Expect(hasActiveProposal).To(BeFalse(), "expected no active clawback proposal")
     			})
    -
     			It("should not allow changing the vesting funder", func() {
     				msgUpdateFunder := types.NewMsgUpdateVestingFunder(funder, dest, vestingAddr)
     				_, err = s.app.VestingKeeper.UpdateVestingFunder(sdk.WrapSDKContext(s.ctx), msgUpdateFunder)
     				Expect(err).To(HaveOccurred(), "expected error during update funder while there is an active governance proposal")
     				Expect(err.Error()).To(ContainSubstring("cannot update funder while there is an active clawback proposal"))
    -
     				// Check that the funder was not updated
     				acc := s.app.AccountKeeper.GetAccount(s.ctx, vestingAddr)
     				Expect(acc).ToNot(BeNil(), "expected account to exist")
    @@ -1022,48 +1072,39 @@ var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
     				Expect(clawbackAcc.FunderAddress).To(Equal(funder.String()), "expected funder to be unchanged")
     			})
     		})
    -
     		Context("without deposit made", func() {
     			It("allows clawback and changing the funder before the deposit period ends", func() {
     				newFunder, newPriv := utiltx.NewAccAddressAndKey()
    -
     				// fund accounts
     				err = testutil.FundAccountWithBaseDenom(s.ctx, s.app.BankKeeper, newFunder, 5e18)
     				Expect(err).ToNot(HaveOccurred(), "failed to fund target account")
     				err = testutil.FundAccountWithBaseDenom(s.ctx, s.app.BankKeeper, funder, 5e18)
     				Expect(err).ToNot(HaveOccurred(), "failed to fund target account")
    -
     				msgUpdateFunder := types.NewMsgUpdateVestingFunder(funder, newFunder, vestingAddr)
     				_, err = testutil.DeliverTx(s.ctx, s.app, funderPriv, nil, msgUpdateFunder)
     				Expect(err).ToNot(HaveOccurred(), "expected no error during update funder while there is an active governance proposal")
    -
     				// Check that the funder was updated
     				acc := s.app.AccountKeeper.GetAccount(s.ctx, vestingAddr)
     				Expect(acc).ToNot(BeNil(), "expected account to exist")
     				_, isClawback := acc.(*types.ClawbackVestingAccount)
     				Expect(isClawback).To(BeTrue(), "expected account to be clawback vesting account")
    -
     				// Claw back tokens
     				msgClawback := types.NewMsgClawback(newFunder, vestingAddr, funder)
     				_, err = testutil.DeliverTx(s.ctx, s.app, newPriv, nil, msgClawback)
     				Expect(err).ToNot(HaveOccurred(), "expected no error during clawback while there is no deposit made")
    -
     				// Check account is converted to a normal account
     				acc = s.app.AccountKeeper.GetAccount(s.ctx, vestingAddr)
     				Expect(acc).ToNot(BeNil(), "expected account to exist")
     				_, isClawback = acc.(*types.ClawbackVestingAccount)
     				Expect(isClawback).To(BeFalse(), "expected account to be a normal account")
     			})
    -
     			It("should remove the store entry after the deposit period ends", func() {
     				s.CommitAfter(time.Hour * 24 * 365) // one year
     				// Commit again because EndBlocker is run with time of the previous block and gov proposals are ended in EndBlocker
     				s.Commit()
    -
     				// Check that the proposal has ended -- since deposit failed it's removed from the store
     				_, found := s.app.GovKeeper.GetProposal(s.ctx, clawbackProposalID)
     				Expect(found).To(BeFalse(), "expected proposal not to be found")
    -
     				// Check that the store entry was removed
     				hasActiveProposal := s.app.VestingKeeper.HasActiveClawbackProposal(s.ctx, vestingAddr)
     				Expect(hasActiveProposal).To(BeFalse(),
    @@ -1073,97 +1114,88 @@ var _ = Describe("Clawback Vesting Accounts - claw back tokens", func() {
     			})
     		})
     	})
    -
     	It("should update vesting funder and claw back unvested amount before cliff", func() {
     		ctx := sdk.WrapSDKContext(s.ctx)
     		newFunder := sdk.AccAddress(utiltx.GenerateAddress().Bytes())
    -
     		balanceFunder := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		balanceNewFunder := s.app.BankKeeper.GetBalance(s.ctx, newFunder, stakeDenom)
     		balanceGrantee := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
    -
     		// Update clawback vesting account funder
     		updateFunderMsg := types.NewMsgUpdateVestingFunder(funder, newFunder, vestingAddr)
     		_, err := s.app.VestingKeeper.UpdateVestingFunder(ctx, updateFunderMsg)
    -		s.Require().NoError(err)
    +		Expect(err).To(BeNil())
     
     		// Perform clawback before cliff - funds should go to new funder (no dest address defined)
     		msg := types.NewMsgClawback(newFunder, vestingAddr, sdk.AccAddress([]byte{}))
     		res, err := s.app.VestingKeeper.Clawback(ctx, msg)
     		Expect(err).To(BeNil())
     		Expect(res.Coins).To(Equal(vestingAmtTotal), "expected different coins to be clawed back")
    -
     		// All initial vesting amount goes to funder
     		bF := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		bNewF := s.app.BankKeeper.GetBalance(s.ctx, newFunder, stakeDenom)
     		bG := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     
     		// Original funder balance should not change
    -		s.Require().Equal(bF, balanceFunder)
    +		Expect(bF).To(Equal(balanceFunder))
     		// New funder should get the vested tokens
    -		s.Require().Equal(balanceNewFunder.Add(vestingAmtTotal[0]).Amount.Uint64(), bNewF.Amount.Uint64())
    -		s.Require().Equal(balanceGrantee.Sub(vestingAmtTotal[0]).Amount.Uint64(), bG.Amount.Uint64())
    +		Expect(balanceNewFunder.Add(vestingAmtTotal[0]).Amount.Uint64()).To(Equal(bNewF.Amount.Uint64()))
    +		Expect(balanceGrantee.Sub(vestingAmtTotal[0]).Amount.Uint64()).To(Equal(bG.Amount.Uint64()))
     	})
     
     	It("should update vesting funder and first funder cannot claw back unvested before cliff", func() {
     		ctx := sdk.WrapSDKContext(s.ctx)
     		newFunder := sdk.AccAddress(utiltx.GenerateAddress().Bytes())
    -
     		balanceFunder := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		balanceNewFunder := s.app.BankKeeper.GetBalance(s.ctx, newFunder, stakeDenom)
     		balanceGrantee := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
    -
     		// Update clawback vesting account funder
     		updateFunderMsg := types.NewMsgUpdateVestingFunder(funder, newFunder, vestingAddr)
     		_, err := s.app.VestingKeeper.UpdateVestingFunder(ctx, updateFunderMsg)
    -		s.Require().NoError(err)
    +		Expect(err).To(BeNil())
     
     		// Original funder tries to perform clawback before cliff - is not the current funder
     		msg := types.NewMsgClawback(funder, vestingAddr, sdk.AccAddress([]byte{}))
     		_, err = s.app.VestingKeeper.Clawback(ctx, msg)
    -		s.Require().Error(err)
    +		Expect(err).NotTo(BeNil())
     
     		// All balances should remain the same
     		bF := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     		bNewF := s.app.BankKeeper.GetBalance(s.ctx, newFunder, stakeDenom)
     		bG := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     
    -		s.Require().Equal(bF, balanceFunder)
    -		s.Require().Equal(balanceNewFunder, bNewF)
    -		s.Require().Equal(balanceGrantee, bG)
    +		Expect(bF).To(Equal(balanceFunder))
    +		Expect(balanceNewFunder).To(Equal(bNewF))
    +		Expect(balanceGrantee).To(Equal(bG))
     	})
     
     	Context("governance clawback to community pool", func() {
     		It("should claw back unvested amount before cliff", func() {
     			ctx := sdk.WrapSDKContext(s.ctx)
    -
     			// initial balances
     			balanceFunder := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     			balanceGrantee := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     			balanceDest := s.app.BankKeeper.GetBalance(s.ctx, dest, stakeDenom)
     			pool := s.app.DistrKeeper.GetFeePool(s.ctx)
     			balanceCommPool := pool.CommunityPool[0]
    -
     			// Perform clawback before cliff
     			msg := types.NewMsgClawback(authtypes.NewModuleAddress(govtypes.ModuleName), vestingAddr, dest)
     			res, err := s.app.VestingKeeper.Clawback(ctx, msg)
     			Expect(err).To(BeNil())
     			Expect(res.Coins).To(Equal(vestingAmtTotal), "expected different coins to be clawed back")
    -
     			// All initial vesting amount goes to community pool instead of dest
     			bF := s.app.BankKeeper.GetBalance(s.ctx, funder, stakeDenom)
     			bG := s.app.BankKeeper.GetBalance(s.ctx, vestingAddr, stakeDenom)
     			bD := s.app.Ban
    ... [truncated]
    
  • x/vesting/keeper/migrations.go+10 1 modified
    @@ -6,9 +6,13 @@ import (
     	sdk "github.com/cosmos/cosmos-sdk/types"
     	"github.com/cosmos/cosmos-sdk/types/module"
     	v2 "github.com/evmos/evmos/v18/x/vesting/migrations/v2"
    +	v3 "github.com/evmos/evmos/v18/x/vesting/migrations/v3"
     )
     
    -var _ module.MigrationHandler = Migrator{}.Migrate1to2
    +var (
    +	_ module.MigrationHandler = Migrator{}.Migrate1to2
    +	_ module.MigrationHandler = Migrator{}.Migrate2to3
    +)
     
     // Migrator is a struct for handling in-place store migrations.
     type Migrator struct {
    @@ -26,3 +30,8 @@ func NewMigrator(keeper Keeper) Migrator {
     func (m Migrator) Migrate1to2(ctx sdk.Context) error {
     	return v2.MigrateStore(ctx, m.keeper.accountKeeper)
     }
    +
    +// Migrate2to3 migrates the store from consensus version 2 to 3
    +func (m Migrator) Migrate2to3(ctx sdk.Context) error {
    +	return v3.MigrateStore(ctx, m.keeper.accountKeeper)
    +}
    
  • x/vesting/keeper/migrations_test.go+90 2 modified
    @@ -3,6 +3,7 @@ package keeper_test
     import (
     	"time"
     
    +	"github.com/cosmos/cosmos-sdk/types"
     	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
     	sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
     	testutiltx "github.com/evmos/evmos/v18/testutil/tx"
    @@ -11,8 +12,10 @@ import (
     	vestingtypes "github.com/evmos/evmos/v18/x/vesting/types"
     )
     
    -func (suite *KeeperTestSuite) TestMigration() {
    -	suite.SetupTest()
    +func (suite *KeeperTestSuite) TestMigrate1to2() {
    +	if err := suite.SetupTest(); err != nil {
    +		panic(err)
    +	}
     
     	// Create account addresses for testing
     	vestingAddr, _ := testutiltx.NewAccAddressAndKey()
    @@ -45,3 +48,88 @@ func (suite *KeeperTestSuite) TestMigration() {
     	suite.Require().NotNil(foundAcc, "vesting account not found")
     	suite.Require().IsType(&vestingtypes.ClawbackVestingAccount{}, foundAcc, "vesting account is not a v2 base vesting account")
     }
    +
    +func (suite *KeeperTestSuite) TestMigrate2to3() {
    +	var emtpyCoins types.Coins
    +	if err := suite.SetupTest(); err != nil {
    +		panic(err)
    +	}
    +
    +	// Create account addresses for testing
    +	vestingAddr, _ := testutiltx.NewAccAddressAndKey()
    +	funder, _ := testutiltx.NewAccAddressAndKey()
    +
    +	// create a base vesting account instead of a clawback vesting account at the vesting address
    +	baseAccount := authtypes.NewBaseAccountWithAddress(vestingAddr)
    +	acc := sdkvesting.NewBaseVestingAccount(baseAccount, balances, 500000)
    +
    +	testCases := []struct {
    +		name                    string
    +		initialDelegatedVesting types.Coins
    +		initialDelegatedFree    types.Coins
    +		expectedDelegatedFree   types.Coins
    +	}{
    +		{
    +			name:                    "delegated vesting > 0 and delegated free == 0",
    +			initialDelegatedVesting: quarter,
    +			initialDelegatedFree:    emtpyCoins,
    +			expectedDelegatedFree:   quarter,
    +		},
    +		{
    +			name:                    "delegated vesting > 0 and delegated free > 0",
    +			initialDelegatedVesting: quarter,
    +			initialDelegatedFree:    quarter,
    +			expectedDelegatedFree:   types.NewCoins(types.NewInt64Coin("test", 500)),
    +		},
    +		{
    +			name:                    "delegated vesting == 0 and delegated free > 0",
    +			initialDelegatedVesting: emtpyCoins,
    +			initialDelegatedFree:    quarter,
    +			expectedDelegatedFree:   quarter,
    +		},
    +		{
    +			name:                    "delegated vesting == 0 and delegated free == 0",
    +			initialDelegatedVesting: emtpyCoins,
    +			initialDelegatedFree:    emtpyCoins,
    +			expectedDelegatedFree:   emtpyCoins,
    +		},
    +	}
    +
    +	for _, tc := range testCases {
    +		suite.Run(tc.name, func() {
    +			acc.DelegatedVesting = tc.initialDelegatedVesting
    +			acc.DelegatedFree = tc.initialDelegatedFree
    +
    +			vestAcc := &vestingtypes.ClawbackVestingAccount{
    +				BaseVestingAccount: acc,
    +				FunderAddress:      funder.String(),
    +				StartTime:          time.Now(),
    +				LockupPeriods:      lockupPeriods,
    +				VestingPeriods:     vestingPeriods,
    +			}
    +			suite.app.AccountKeeper.SetAccount(suite.ctx, vestAcc)
    +
    +			// check account was created successfully
    +			foundAcc := suite.app.AccountKeeper.GetAccount(suite.ctx, vestingAddr)
    +			suite.Require().NotNil(foundAcc, "vesting account not found")
    +			vestAcc, ok := foundAcc.(*vestingtypes.ClawbackVestingAccount)
    +			suite.Require().True(ok)
    +			suite.Require().Equal(tc.initialDelegatedVesting, vestAcc.DelegatedVesting)
    +			suite.Require().Equal(tc.initialDelegatedFree, vestAcc.DelegatedFree)
    +
    +			// migrate
    +			migrator := keeper.NewMigrator(suite.app.VestingKeeper)
    +			err = migrator.Migrate2to3(suite.ctx)
    +			suite.Require().NoError(err, "migration failed")
    +
    +			// check that the account delegated vesting coins were migrated
    +			// to the delegated free coins
    +			foundAcc = suite.app.AccountKeeper.GetAccount(suite.ctx, vestingAddr)
    +			suite.Require().NotNil(foundAcc, "vesting account not found")
    +			vestAcc, ok = foundAcc.(*vestingtypes.ClawbackVestingAccount)
    +			suite.Require().True(ok)
    +			suite.Require().Equal(emtpyCoins, vestAcc.DelegatedVesting)
    +			suite.Require().Equal(tc.expectedDelegatedFree, vestAcc.DelegatedFree)
    +		})
    +	}
    +}
    
  • x/vesting/keeper/msg_server.go+5 0 modified
    @@ -371,6 +371,11 @@ func (k Keeper) ConvertVestingAccount(
     		return nil, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "vesting coins still left in account: %s", msg.VestingAddress)
     	}
     
    +	// check if account has any locked up coins left
    +	if vestingAcc.HasLockedCoins(ctx.BlockTime()) {
    +		return nil, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "locked up coins still left in account: %s", msg.VestingAddress)
    +	}
    +	
     	// if gov clawback is disabled, remove the entry from the store.
     	// if no entry is found for the address, this will no-op
     	k.DeleteGovClawbackDisabled(ctx, address)
    
  • x/vesting/keeper/setup_test.go+127 13 modified
    @@ -1,23 +1,34 @@
     package keeper_test
     
     import (
    +	"errors"
    +	"math"
     	"testing"
    +	"time"
     
    -	//nolint:revive // dot imports are fine for Ginkgo
    -	. "github.com/onsi/ginkgo/v2"
    -	//nolint:revive // dot imports are fine for Ginkgo
    -	. "github.com/onsi/gomega"
    -
    +	sdkmath "cosmossdk.io/math"
    +	"github.com/cosmos/cosmos-sdk/baseapp"
     	"github.com/cosmos/cosmos-sdk/client"
     	"github.com/cosmos/cosmos-sdk/crypto/keyring"
     	cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
     	sdk "github.com/cosmos/cosmos-sdk/types"
    +	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
    +	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
     	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
     	"github.com/ethereum/go-ethereum/common"
    +	"github.com/ethereum/go-ethereum/crypto"
     	"github.com/stretchr/testify/suite"
     
     	ethtypes "github.com/ethereum/go-ethereum/core/types"
    -	evm "github.com/evmos/evmos/v18/x/evm/types"
    +
    +	"github.com/evmos/evmos/v18/crypto/ethsecp256k1"
    +	"github.com/evmos/evmos/v18/encoding"
    +	"github.com/evmos/evmos/v18/testutil"
    +	utiltx "github.com/evmos/evmos/v18/testutil/tx"
    +	evmostypes "github.com/evmos/evmos/v18/types"
    +	"github.com/evmos/evmos/v18/utils"
    +	epochstypes "github.com/evmos/evmos/v18/x/epochs/types"
    +	evmtypes "github.com/evmos/evmos/v18/x/evm/types"
     
     	"github.com/evmos/evmos/v18/app"
     	"github.com/evmos/evmos/v18/x/vesting/types"
    @@ -41,7 +52,7 @@ type KeeperTestSuite struct {
     
     	ctx            sdk.Context
     	app            *app.Evmos
    -	queryClientEvm evm.QueryClient
    +	queryClientEvm evmtypes.QueryClient
     	queryClient    types.QueryClient
     	address        common.Address
     	consAddress    sdk.ConsAddress
    @@ -57,12 +68,115 @@ var s *KeeperTestSuite
     func TestKeeperTestSuite(t *testing.T) {
     	s = new(KeeperTestSuite)
     	suite.Run(t, s)
    -
    -	// Run Ginkgo integration tests
    -	RegisterFailHandler(Fail)
    -	RunSpecs(t, "Keeper Suite")
     }
     
    -func (suite *KeeperTestSuite) SetupTest() {
    -	suite.DoSetupTest(suite.T())
    +func (suite *KeeperTestSuite) SetupTest() error {
    +	checkTx := false
    +
    +	// account key
    +	priv, err := ethsecp256k1.GenerateKey()
    +	if err != nil {
    +		return err
    +	}
    +	suite.address = common.BytesToAddress(priv.PubKey().Address().Bytes())
    +	suite.signer = utiltx.NewSigner(priv)
    +	suite.priv = priv
    +
    +	// consensus key
    +	priv, err = ethsecp256k1.GenerateKey()
    +	if err != nil {
    +		return err
    +	}
    +	suite.consAddress = sdk.ConsAddress(priv.PubKey().Address())
    +
    +	// Init app
    +	chainID := utils.TestnetChainID + "-1"
    +	suite.app = app.Setup(checkTx, nil, chainID)
    +
    +	// Set Context
    +	header := testutil.NewHeader(
    +		1, time.Now().UTC(), chainID, suite.consAddress, nil, nil,
    +	)
    +	suite.ctx = suite.app.BaseApp.NewContext(false, header)
    +
    +	// Setup query helpers
    +	queryHelperEvm := baseapp.NewQueryServerTestHelper(suite.ctx, suite.app.InterfaceRegistry())
    +	evmtypes.RegisterQueryServer(queryHelperEvm, suite.app.EvmKeeper)
    +	suite.queryClientEvm = evmtypes.NewQueryClient(queryHelperEvm)
    +
    +	queryHelper := baseapp.NewQueryServerTestHelper(suite.ctx, suite.app.InterfaceRegistry())
    +	types.RegisterQueryServer(queryHelper, suite.app.VestingKeeper)
    +	suite.queryClient = types.NewQueryClient(queryHelper)
    +
    +	// Set epoch start time and height for all epoch identifiers from the epoch
    +	// module
    +	identifiers := []string{epochstypes.WeekEpochID, epochstypes.DayEpochID}
    +	for _, identifier := range identifiers {
    +		epoch, found := suite.app.EpochsKeeper.GetEpochInfo(suite.ctx, identifier)
    +		if !found {
    +			return errors.New("epoch info not found")
    +		}
    +		epoch.StartTime = suite.ctx.BlockTime()
    +		epoch.CurrentEpochStartHeight = suite.ctx.BlockHeight()
    +		suite.app.EpochsKeeper.SetEpochInfo(suite.ctx, epoch)
    +	}
    +
    +	acc := &evmostypes.EthAccount{
    +		BaseAccount: authtypes.NewBaseAccount(sdk.AccAddress(suite.address.Bytes()), nil, 0, 0),
    +		CodeHash:    common.BytesToHash(crypto.Keccak256(nil)).String(),
    +	}
    +
    +	suite.app.AccountKeeper.SetAccount(suite.ctx, acc)
    +
    +	// fund signer acc to pay for tx fees
    +	amt := sdkmath.NewInt(int64(math.Pow10(18) * 2))
    +	err = testutil.FundAccount(
    +		suite.ctx,
    +		suite.app.BankKeeper,
    +		suite.priv.PubKey().Address().Bytes(),
    +		sdk.NewCoins(sdk.NewCoin(utils.BaseDenom, amt)),
    +	)
    +	if err != nil {
    +		return err
    +	}
    +
    +	// Set Validator
    +	valAddr := sdk.ValAddress(suite.address.Bytes())
    +	validator, err := stakingtypes.NewValidator(valAddr, priv.PubKey(), stakingtypes.Description{})
    +	if err != nil {
    +		return err
    +	}
    +	validator = stakingkeeper.TestingUpdateValidator(suite.app.StakingKeeper.Keeper, suite.ctx, validator, true)
    +	err = suite.app.StakingKeeper.Hooks().AfterValidatorCreated(suite.ctx, validator.GetOperator())
    +	if err != nil {
    +		return err
    +	}
    +	err = suite.app.StakingKeeper.SetValidatorByConsAddr(suite.ctx, validator)
    +	if err != nil {
    +		return err
    +	}
    +	validators := s.app.StakingKeeper.GetValidators(s.ctx, 1)
    +	suite.validator = validators[0]
    +
    +	encodingConfig := encoding.MakeConfig(app.ModuleBasics)
    +	suite.clientCtx = client.Context{}.WithTxConfig(encodingConfig.TxConfig)
    +	suite.ethSigner = ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID())
    +
    +	// Deploy contracts
    +	contract, err = suite.DeployContract(erc20Name, erc20Symbol, erc20Decimals)
    +	if err != nil {
    +		return err
    +	}
    +	contract2, err = suite.DeployContract(erc20Name2, erc20Symbol2, erc20Decimals)
    +	if err != nil {
    +		return err
    +	}
    +
    +	// Set correct denom in govKeeper
    +	govParams := suite.app.GovKeeper.GetParams(suite.ctx)
    +	govParams.MinDeposit = sdk.NewCoins(sdk.NewCoin(utils.BaseDenom, sdkmath.NewInt(1e6)))
    +	votingPeriod := time.Second
    +	govParams.VotingPeriod = &votingPeriod
    +
    +	return suite.app.GovKeeper.SetParams(suite.ctx, govParams)
     }
    
  • x/vesting/keeper/utils_test.go+0 120 modified
    @@ -1,134 +1,23 @@
     package keeper_test
     
     import (
    -	"math"
     	"strings"
     	"time"
     
     	//nolint:revive // dot imports are fine for Ginkgo
     	. "github.com/onsi/gomega"
     
     	sdkmath "cosmossdk.io/math"
    -	"github.com/cosmos/cosmos-sdk/baseapp"
    -	"github.com/cosmos/cosmos-sdk/client"
     	sdk "github.com/cosmos/cosmos-sdk/types"
     	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
    -	stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
    -	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
     	"github.com/ethereum/go-ethereum/common"
    -	ethtypes "github.com/ethereum/go-ethereum/core/types"
    -	"github.com/ethereum/go-ethereum/crypto"
     
    -	"github.com/evmos/evmos/v18/app"
    -	cosmosante "github.com/evmos/evmos/v18/app/ante/cosmos"
     	evmante "github.com/evmos/evmos/v18/app/ante/evm"
     	"github.com/evmos/evmos/v18/contracts"
    -	"github.com/evmos/evmos/v18/crypto/ethsecp256k1"
    -	"github.com/evmos/evmos/v18/encoding"
     	"github.com/evmos/evmos/v18/testutil"
    -	utiltx "github.com/evmos/evmos/v18/testutil/tx"
    -	evmostypes "github.com/evmos/evmos/v18/types"
    -	"github.com/evmos/evmos/v18/utils"
    -	epochstypes "github.com/evmos/evmos/v18/x/epochs/types"
    -	evmtypes "github.com/evmos/evmos/v18/x/evm/types"
     	"github.com/evmos/evmos/v18/x/vesting/types"
    -
    -	"github.com/stretchr/testify/require"
     )
     
    -func (suite *KeeperTestSuite) DoSetupTest(t require.TestingT) {
    -	checkTx := false
    -
    -	// account key
    -	priv, err := ethsecp256k1.GenerateKey()
    -	require.NoError(t, err)
    -	suite.address = common.BytesToAddress(priv.PubKey().Address().Bytes())
    -	suite.signer = utiltx.NewSigner(priv)
    -	suite.priv = priv
    -
    -	// consensus key
    -	priv, err = ethsecp256k1.GenerateKey()
    -	require.NoError(t, err)
    -	suite.consAddress = sdk.ConsAddress(priv.PubKey().Address())
    -
    -	// Init app
    -	chainID := utils.TestnetChainID + "-1"
    -	suite.app = app.Setup(checkTx, nil, chainID)
    -
    -	// Set Context
    -	header := testutil.NewHeader(
    -		1, time.Now().UTC(), chainID, suite.consAddress, nil, nil,
    -	)
    -	suite.ctx = suite.app.BaseApp.NewContext(false, header)
    -
    -	// Setup query helpers
    -	queryHelperEvm := baseapp.NewQueryServerTestHelper(suite.ctx, suite.app.InterfaceRegistry())
    -	evmtypes.RegisterQueryServer(queryHelperEvm, suite.app.EvmKeeper)
    -	suite.queryClientEvm = evmtypes.NewQueryClient(queryHelperEvm)
    -
    -	queryHelper := baseapp.NewQueryServerTestHelper(suite.ctx, suite.app.InterfaceRegistry())
    -	types.RegisterQueryServer(queryHelper, suite.app.VestingKeeper)
    -	suite.queryClient = types.NewQueryClient(queryHelper)
    -
    -	// Set epoch start time and height for all epoch identifiers from the epoch
    -	// module
    -	identifiers := []string{epochstypes.WeekEpochID, epochstypes.DayEpochID}
    -	for _, identifier := range identifiers {
    -		epoch, found := suite.app.EpochsKeeper.GetEpochInfo(suite.ctx, identifier)
    -		suite.Require().True(found)
    -		epoch.StartTime = suite.ctx.BlockTime()
    -		epoch.CurrentEpochStartHeight = suite.ctx.BlockHeight()
    -		suite.app.EpochsKeeper.SetEpochInfo(suite.ctx, epoch)
    -	}
    -
    -	acc := &evmostypes.EthAccount{
    -		BaseAccount: authtypes.NewBaseAccount(sdk.AccAddress(suite.address.Bytes()), nil, 0, 0),
    -		CodeHash:    common.BytesToHash(crypto.Keccak256(nil)).String(),
    -	}
    -
    -	suite.app.AccountKeeper.SetAccount(suite.ctx, acc)
    -
    -	// fund signer acc to pay for tx fees
    -	amt := sdkmath.NewInt(int64(math.Pow10(18) * 2))
    -	err = testutil.FundAccount(
    -		suite.ctx,
    -		suite.app.BankKeeper,
    -		suite.priv.PubKey().Address().Bytes(),
    -		sdk.NewCoins(sdk.NewCoin(utils.BaseDenom, amt)),
    -	)
    -	suite.Require().NoError(err)
    -
    -	// Set Validator
    -	valAddr := sdk.ValAddress(suite.address.Bytes())
    -	validator, err := stakingtypes.NewValidator(valAddr, priv.PubKey(), stakingtypes.Description{})
    -	require.NoError(t, err)
    -	validator = stakingkeeper.TestingUpdateValidator(&suite.app.StakingKeeper, suite.ctx, validator, true)
    -	err = suite.app.StakingKeeper.Hooks().AfterValidatorCreated(suite.ctx, validator.GetOperator())
    -	require.NoError(t, err)
    -	err = suite.app.StakingKeeper.SetValidatorByConsAddr(suite.ctx, validator)
    -	require.NoError(t, err)
    -	validators := s.app.StakingKeeper.GetValidators(s.ctx, 1)
    -	suite.validator = validators[0]
    -
    -	encodingConfig := encoding.MakeConfig(app.ModuleBasics)
    -	suite.clientCtx = client.Context{}.WithTxConfig(encodingConfig.TxConfig)
    -	suite.ethSigner = ethtypes.LatestSignerForChainID(suite.app.EvmKeeper.ChainID())
    -
    -	// Deploy contracts
    -	contract, err = suite.DeployContract(erc20Name, erc20Symbol, erc20Decimals)
    -	require.NoError(t, err)
    -	contract2, err = suite.DeployContract(erc20Name2, erc20Symbol2, erc20Decimals)
    -	require.NoError(t, err)
    -
    -	// Set correct denom in govKeeper
    -	govParams := suite.app.GovKeeper.GetParams(suite.ctx)
    -	govParams.MinDeposit = sdk.NewCoins(sdk.NewCoin(utils.BaseDenom, sdkmath.NewInt(1e6)))
    -	votingPeriod := time.Second
    -	govParams.VotingPeriod = &votingPeriod
    -	err = suite.app.GovKeeper.SetParams(suite.ctx, govParams)
    -	suite.Require().NoError(err, "failed to set gov params")
    -}
    -
     // Commit commits and starts a new block with an updated context.
     func (suite *KeeperTestSuite) Commit() {
     	suite.CommitAfter(time.Second * 0)
    @@ -218,15 +107,6 @@ func assertEthSucceeds(testAccounts []TestClawbackAccount, funder sdk.AccAddress
     	}
     }
     
    -// delegate is a helper function which creates a message to delegate a given amount of tokens
    -// to a validator and checks if the Cosmos vesting delegation decorator returns no error.
    -func delegate(account TestClawbackAccount, coins sdk.Coins) (*stakingtypes.MsgDelegate, error) {
    -	msg := stakingtypes.NewMsgDelegate(account.address, s.validator.GetOperator(), coins[0])
    -	dec := cosmosante.NewVestingDelegationDecorator(s.app.AccountKeeper, s.app.StakingKeeper, s.app.BankKeeper, types.ModuleCdc)
    -	err = testutil.ValidateAnteForMsgs(s.ctx, dec, msg)
    -	return msg, err
    -}
    -
     // validateEthVestingTransactionDecorator is a helper function to execute the eth vesting transaction decorator
     // with 1 or more given messages and return any occurring error.
     func validateEthVestingTransactionDecorator(msgs ...sdk.Msg) error {
    
  • x/vesting/migrations/v3/migrate.go+38 0 added
    @@ -0,0 +1,38 @@
    +// Copyright Tharsis Labs Ltd.(Evmos)
    +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
    +package v3
    +
    +import (
    +	sdk "github.com/cosmos/cosmos-sdk/types"
    +	accounttypes "github.com/cosmos/cosmos-sdk/x/auth/types"
    +	vestingtypes "github.com/evmos/evmos/v18/x/vesting/types"
    +)
    +
    +// MigrateStore migrates the x/vesting module state from the consensus version 2 to
    +// version 3.
    +// Specifically, it adds the DelegatedVesting (that should always be 0)
    +// to the DelegatedFree
    +func MigrateStore(
    +	ctx sdk.Context,
    +	ak vestingtypes.AccountKeeper,
    +) error {
    +	ak.IterateAccounts(ctx, func(account accounttypes.AccountI) bool {
    +		if vestAcc, ok := account.(*vestingtypes.ClawbackVestingAccount); ok {
    +			// if DelegatedVesting == 0, skip it
    +			if !vestAcc.DelegatedVesting.IsAllPositive() {
    +				return false
    +			}
    +			// add DelegatedVesting to DelegatedFree,
    +			// because it is not possible to delegate vesting coins.
    +			// ONLY vested (free) coins can be delegated
    +			vestAcc.DelegatedFree = vestAcc.DelegatedFree.Add(vestAcc.DelegatedVesting...)
    +			vestAcc.DelegatedVesting = sdk.NewCoins()
    +
    +			ak.SetAccount(ctx, vestAcc)
    +		}
    +
    +		return false
    +	})
    +
    +	return nil
    +}
    
  • x/vesting/migrations/v3/migrate_test.go+3 0 added
    @@ -0,0 +1,3 @@
    +package v3
    +
    +// NOTE: Migrations are tested in keeper package.
    
  • x/vesting/module.go+5 1 modified
    @@ -28,7 +28,7 @@ import (
     )
     
     // consensusVersion defines the current x/vesting module consensus version.
    -const consensusVersion = 2
    +const consensusVersion = 3
     
     var (
     	_ module.AppModule      = AppModule{}
    @@ -133,6 +133,10 @@ func (am AppModule) RegisterServices(cfg module.Configurator) {
     	if err := cfg.RegisterMigration(types.ModuleName, 1, migrator.Migrate1to2); err != nil {
     		panic(fmt.Errorf("failed to migrate %s to v2: %w", types.ModuleName, err))
     	}
    +
    +	if err := cfg.RegisterMigration(types.ModuleName, 2, migrator.Migrate2to3); err != nil {
    +		panic(fmt.Errorf("failed to migrate %s to v3: %w", types.ModuleName, err))
    +	}
     }
     
     // InitGenesis performs a no-op.
    
  • x/vesting/types/clawback_vesting_account.go+51 35 modified
    @@ -51,35 +51,55 @@ func NewClawbackVestingAccount(
     	}
     }
     
    -// GetVestedCoins returns the total number of vested coins that are still in lockup. If no coins are
    -// vested, nil is returned.
    -func (va ClawbackVestingAccount) GetVestedCoins(blockTime time.Time) sdk.Coins {
    -	// It's likely that one or the other schedule will be nearly trivial,
    -	// so there should be little overhead in recomputing the conjunction each time.
    -	coins := va.GetUnlockedOnly(blockTime).Min(va.GetVestedOnly(blockTime))
    +// GetLockedUpVestedCoins returns the total number of vested coins that are locked.
    +func (va ClawbackVestingAccount) GetLockedUpVestedCoins(blockTime time.Time) sdk.Coins {
    +	return va.GetVestedCoins(blockTime).Sub(va.GetUnlockedVestedCoins(blockTime)...)
    +}
    +
    +// GetUnlockedVestedCoins returns the total number of vested coins that are unlocked.
    +// If no coins are vested and unlocked, nil is returned.
    +func (va ClawbackVestingAccount) GetUnlockedVestedCoins(blockTime time.Time) sdk.Coins {
    +	coins := va.GetUnlockedCoins(blockTime).Min(va.GetVestedCoins(blockTime))
     	if coins.IsZero() {
    -		return nil
    +		return sdk.Coins{}
     	}
     	return coins
     }
     
    -// GetVestingCoins returns the total number of vesting coins. If no coins are
    -// vesting, nil is returned.
    +// GetVestingCoins returns the total number of vesting coins (unvested coins).
    +// If no coins are vesting, nil is returned.
     func (va ClawbackVestingAccount) GetVestingCoins(blockTime time.Time) sdk.Coins {
     	return va.OriginalVesting.Sub(va.GetVestedCoins(blockTime)...)
     }
     
    -// LockedCoins returns the set of coins that are not spendable (i.e. locked),
    -// defined as the vesting coins that are not delegated.
    +// LockedCoins returns the set of coins that are not spendable (i.e. locked or unvested),
    +// defined as the vesting coins (unvested) plus locked vested coins.
    +//
    +// totalAmt = vesting(un/locked) + lockedVested + unlockedVested
    +//
    +//	(all)   =   (cannot spend)    (cannot spend)   (CAN spend)
    +//
    +// lockedCoins = totalAmt - unlockedVested
     func (va ClawbackVestingAccount) LockedCoins(blockTime time.Time) sdk.Coins {
    -	return va.BaseVestingAccount.LockedCoinsFromVesting(va.GetVestingCoins(blockTime))
    +	return va.OriginalVesting.Sub(va.GetUnlockedVestedCoins(blockTime)...)
     }
     
     // TrackDelegation tracks a desired delegation amount by setting the appropriate
    -// values for the amount of delegated vesting, delegated free, and reducing the
    -// overall amount of base coins.
    -func (va *ClawbackVestingAccount) TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) {
    -	va.BaseVestingAccount.TrackDelegation(balance, va.GetVestingCoins(blockTime), amount)
    +// values for the amount of delegated free coins.
    +// The 'balance' input parameter is the delegator account balance.
    +// The 'amount' input parameter are the delegated coins
    +// Note that unvested coins cannot be delegated
    +func (va *ClawbackVestingAccount) TrackDelegation(_ time.Time, balance, amount sdk.Coins) {
    +	// Can only delegate vested (free) coins
    +	for _, coin := range amount {
    +		baseAmt := balance.AmountOf(coin.Denom)
    +		// Panic if the delegation amount is zero or if the base coins does not
    +		// exceed the desired delegation amount.
    +		if coin.Amount.IsZero() || baseAmt.LT(coin.Amount) {
    +			panic("delegation attempt with zero coins or insufficient funds")
    +		}
    +		va.DelegatedFree = va.DelegatedFree.Add(coin)
    +	}
     }
     
     // GetStartTime returns the time when vesting starts for a periodic vesting
    @@ -125,7 +145,7 @@ func (va ClawbackVestingAccount) Validate() error {
     	}
     
     	if vestingEnd > va.EndTime {
    -		return errors.New("vesting schedule exteds beyond account end time")
    +		return errors.New("vesting schedule extends beyond account end time")
     	}
     
     	if !CoinEq(vestingCoins, va.OriginalVesting) {
    @@ -135,29 +155,25 @@ func (va ClawbackVestingAccount) Validate() error {
     	return va.BaseVestingAccount.Validate()
     }
     
    -// GetUnlockedOnly returns the unlocking schedule at blockTime.
    -func (va ClawbackVestingAccount) GetUnlockedOnly(blockTime time.Time) sdk.Coins {
    +// GetUnlockedCoins returns the unlocked coins at blockTime.
    +// Note that these unlocked coins can be vested or unvested
    +// and is determined by the lockup periods
    +func (va ClawbackVestingAccount) GetUnlockedCoins(blockTime time.Time) sdk.Coins {
     	return ReadSchedule(va.GetStartTime(), va.EndTime, va.LockupPeriods, va.OriginalVesting, blockTime.Unix())
     }
     
    -// GetLockedOnly returns the locking schedule at blockTime.
    -func (va ClawbackVestingAccount) GetLockedOnly(blockTime time.Time) sdk.Coins {
    -	return va.OriginalVesting.Sub(va.GetUnlockedOnly(blockTime)...)
    +// GetLockedUpCoins returns the locked coins at blockTime.
    +// Note that these locked up coins can be vested or unvested,
    +// and is determined by the lockup periods
    +func (va ClawbackVestingAccount) GetLockedUpCoins(blockTime time.Time) sdk.Coins {
    +	return va.OriginalVesting.Sub(va.GetUnlockedCoins(blockTime)...)
     }
     
    -// GetVestedOnly returns the vesting schedule at blockTime.
    -func (va ClawbackVestingAccount) GetVestedOnly(blockTime time.Time) sdk.Coins {
    +// GetVestedCoins returns the vested coins at blockTime.
    +func (va ClawbackVestingAccount) GetVestedCoins(blockTime time.Time) sdk.Coins {
     	return ReadSchedule(va.GetStartTime(), va.EndTime, va.VestingPeriods, va.OriginalVesting, blockTime.Unix())
     }
     
    -// GetUnvestedOnly returns the unvesting schedule at blockTime.
    -func (va ClawbackVestingAccount) GetUnvestedOnly(blockTime time.Time) sdk.Coins {
    -	totalUnvested := va.OriginalVesting.Sub(va.GetVestedOnly(blockTime)...)
    -	if totalUnvested == nil {
    -		totalUnvested = sdk.Coins{}
    -	}
    -	return totalUnvested
    -}
     
     // GetPassedPeriodCount returns the amount of passed periods at blockTime.
     func (va ClawbackVestingAccount) GetPassedPeriodCount(blockTime time.Time) int {
    @@ -170,8 +186,8 @@ func (va ClawbackVestingAccount) GetPassedPeriodCount(blockTime time.Time) int {
     func (va ClawbackVestingAccount) ComputeClawback(
     	clawbackTime int64,
     ) (ClawbackVestingAccount, sdk.Coins) {
    -	totalVested := va.GetVestedOnly(time.Unix(clawbackTime, 0))
    -	totalUnvested := va.GetUnvestedOnly(time.Unix(clawbackTime, 0))
    +	totalVested := va.GetVestedCoins(time.Unix(clawbackTime, 0))
    +	totalUnvested := va.GetVestingCoins(time.Unix(clawbackTime, 0))
     
     	// Remove all unvested periods from the schedule
     	passedPeriodID := va.GetPassedPeriodCount(time.Unix(clawbackTime, 0))
    @@ -203,5 +219,5 @@ func (va ClawbackVestingAccount) ComputeClawback(
     // HasLockedCoins returns true if the block time has not passed all clawback
     // account's lockup periods
     func (va ClawbackVestingAccount) HasLockedCoins(blockTime time.Time) bool {
    -	return !va.GetLockedOnly(blockTime).IsZero()
    +	return !va.GetLockedUpCoins(blockTime).IsZero()
     }
    
  • x/vesting/types/clawback_vesting_account_test.go+184 220 modified
    @@ -19,12 +19,24 @@ var (
     	stakeDenom    = "stake"
     	feeDenom      = "fee"
     	lockupPeriods = sdkvesting.Periods{
    -		sdkvesting.Period{Length: int64(16 * 60 * 60), Amount: sdk.NewCoins(sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100))},
    +		sdkvesting.Period{
    +			Length: int64(16 * 60 * 60), // 16hs
    +			Amount: sdk.NewCoins(sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)),
    +		},
     	}
     	vestingPeriods = sdkvesting.Periods{
    -		sdkvesting.Period{Length: int64(12 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}},
    -		sdkvesting.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}},
    -		sdkvesting.Period{Length: int64(6 * 60 * 60), Amount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}},
    +		sdkvesting.Period{
    +			Length: int64(12 * 60 * 60), // 12hs
    +			Amount: getPercentOfVestingCoins(50),
    +		},
    +		sdkvesting.Period{
    +			Length: int64(6 * 60 * 60), // 6hs
    +			Amount: getPercentOfVestingCoins(25),
    +		},
    +		sdkvesting.Period{
    +			Length: int64(6 * 60 * 60), // 6hs
    +			Amount: getPercentOfVestingCoins(25),
    +		},
     	}
     	origCoins = sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)}
     )
    @@ -38,30 +50,31 @@ func TestVestingAccountSuite(t *testing.T) {
     }
     
     func (suite *VestingAccountTestSuite) TestClawbackAccountNew() {
    -	addr := sdk.AccAddress(utiltx.GenerateAddress().Bytes())
    +	addr := sdk.AccAddress("test_address")
     	baseAcc := authtypes.NewBaseAccountWithAddress(addr)
     	initialVesting := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 50))
     
     	testCases := []struct {
    -		name   string
    -		acc    authtypes.GenesisAccount
    -		expErr bool
    +		name      string
    +		acc       authtypes.GenesisAccount
    +		expErr    bool
    +		expErrMsg string
     	}{
     		{
    -			"Clawback vesting account - pass",
    -			types.NewClawbackVestingAccount(
    +			name: "Clawback vesting account - pass",
    +			acc: types.NewClawbackVestingAccount(
     				baseAcc,
    -				sdk.AccAddress([]byte("the funder")),
    +				sdk.AccAddress("the funder"),
     				initialVesting,
     				time.Now(),
     				sdkvesting.Periods{sdkvesting.Period{Length: 101, Amount: initialVesting}},
     				sdkvesting.Periods{sdkvesting.Period{Length: 201, Amount: initialVesting}},
     			),
    -			false,
    +			expErr: false,
     		},
     		{
    -			"Clawback vesting account - invalid vesting end",
    -			&types.ClawbackVestingAccount{
    +			name: "Clawback vesting account - invalid vesting end",
    +			acc: &types.ClawbackVestingAccount{
     				BaseVestingAccount: &sdkvesting.BaseVestingAccount{
     					BaseAccount:     baseAcc,
     					OriginalVesting: initialVesting,
    @@ -72,11 +85,12 @@ func (suite *VestingAccountTestSuite) TestClawbackAccountNew() {
     				LockupPeriods:  sdkvesting.Periods{sdkvesting.Period{Length: 10, Amount: initialVesting}},
     				VestingPeriods: sdkvesting.Periods{sdkvesting.Period{Length: 10, Amount: initialVesting}},
     			},
    -			true,
    +			expErr:    true,
    +			expErrMsg: "vesting start-time must be before end-time",
     		},
     		{
    -			"Clawback vesting account - lockup too long",
    -			&types.ClawbackVestingAccount{
    +			name: "Clawback vesting account - lockup too long",
    +			acc: &types.ClawbackVestingAccount{
     				BaseVestingAccount: &sdkvesting.BaseVestingAccount{
     					BaseAccount:     baseAcc,
     					OriginalVesting: initialVesting,
    @@ -87,11 +101,12 @@ func (suite *VestingAccountTestSuite) TestClawbackAccountNew() {
     				LockupPeriods:  sdkvesting.Periods{sdkvesting.Period{Length: 20, Amount: initialVesting}},
     				VestingPeriods: sdkvesting.Periods{sdkvesting.Period{Length: 10, Amount: initialVesting}},
     			},
    -			true,
    +			expErr:    true,
    +			expErrMsg: "lockup schedule extends beyond account end time",
     		},
     		{
    -			"Clawback vesting account - invalid lockup coins",
    -			&types.ClawbackVestingAccount{
    +			name: "Clawback vesting account - invalid lockup coins",
    +			acc: &types.ClawbackVestingAccount{
     				BaseVestingAccount: &sdkvesting.BaseVestingAccount{
     					BaseAccount:     baseAcc,
     					OriginalVesting: initialVesting,
    @@ -102,11 +117,12 @@ func (suite *VestingAccountTestSuite) TestClawbackAccountNew() {
     				LockupPeriods:  sdkvesting.Periods{sdkvesting.Period{Length: 10, Amount: initialVesting.Add(initialVesting...)}},
     				VestingPeriods: sdkvesting.Periods{sdkvesting.Period{Length: 10, Amount: initialVesting}},
     			},
    -			true,
    +			expErr:    true,
    +			expErrMsg: "original vesting coins does not match the sum of all coins in lockup periods",
     		},
     		{
    -			"Clawback vesting account - vesting too long",
    -			&types.ClawbackVestingAccount{
    +			name: "Clawback vesting account - vesting too long",
    +			acc: &types.ClawbackVestingAccount{
     				BaseVestingAccount: &sdkvesting.BaseVestingAccount{
     					BaseAccount:     baseAcc,
     					OriginalVesting: initialVesting,
    @@ -117,11 +133,12 @@ func (suite *VestingAccountTestSuite) TestClawbackAccountNew() {
     				LockupPeriods:  sdkvesting.Periods{sdkvesting.Period{Length: 10, Amount: initialVesting}},
     				VestingPeriods: sdkvesting.Periods{sdkvesting.Period{Length: 20, Amount: initialVesting}},
     			},
    -			true,
    +			expErr:    true,
    +			expErrMsg: "vesting schedule extends beyond account end time",
     		},
     		{
    -			"Clawback vesting account - invalid vesting coins",
    -			&types.ClawbackVestingAccount{
    +			name: "Clawback vesting account - invalid vesting coins",
    +			acc: &types.ClawbackVestingAccount{
     				BaseVestingAccount: &sdkvesting.BaseVestingAccount{
     					BaseAccount:     baseAcc,
     					OriginalVesting: initialVesting,
    @@ -132,242 +149,186 @@ func (suite *VestingAccountTestSuite) TestClawbackAccountNew() {
     				LockupPeriods:  sdkvesting.Periods{sdkvesting.Period{Length: 10, Amount: initialVesting}},
     				VestingPeriods: sdkvesting.Periods{sdkvesting.Period{Length: 10, Amount: initialVesting.Add(initialVesting...)}},
     			},
    -			true,
    +			expErr:    true,
    +			expErrMsg: "original vesting coins does not match the sum of all coins in vesting periods",
     		},
     	}
     
     	for _, tc := range testCases {
     		suite.Run(tc.name, func() {
    -			suite.Require().Equal(tc.expErr, tc.acc.Validate() != nil)
    +			err := tc.acc.Validate()
    +			if tc.expErr {
    +				suite.Require().Error(err)
    +				suite.Require().Contains(err.Error(), tc.expErrMsg)
    +				return
    +			}
    +			suite.Require().NoError(err)
     		})
     	}
     }
     
    -func (suite *VestingAccountTestSuite) TestGetVestedVestingLockedCoins() {
    +func (suite *VestingAccountTestSuite) TestGetCoinsFunctions() {
     	now := tmtime.Now()
     	endTime := now.Add(24 * time.Hour)
     	addr := sdk.AccAddress(utiltx.GenerateAddress().Bytes())
     	bacc := authtypes.NewBaseAccountWithAddress(addr)
     	va := types.NewClawbackVestingAccount(bacc, sdk.AccAddress([]byte("funder")), origCoins, now, lockupPeriods, vestingPeriods)
     
     	testCases := []struct {
    -		name              string
    -		time              time.Time
    -		expVestedCoins    sdk.Coins
    -		expUnvestedCoins  sdk.Coins
    -		expSpendableCoins sdk.Coins
    +		name                   string
    +		time                   time.Time
    +		expVestedCoins         sdk.Coins
    +		expLockedUpVestedCoins sdk.Coins
    +		expUnlockedVestedCoins sdk.Coins
    +		expUnvestedCoins       sdk.Coins
    +		expLockedUpCoins       sdk.Coins
    +		expUnlockedCoins       sdk.Coins
    +		expNotSpendable        sdk.Coins
     	}{
     		{
    -			"no coins vested at the beginning of the vesting schedule",
    -			now,
    -			nil,
    -			origCoins,
    -			origCoins,
    -		},
    -		{
    -			"all coins vested at the end of the vesting schedule",
    -			endTime,
    -			origCoins,
    -			sdk.Coins{},
    -			sdk.NewCoins(),
    -		},
    -		{
    -			"no coins vested during first vesting period",
    -			now.Add(6 * time.Hour),
    -			nil,
    -			origCoins,
    -			origCoins,
    -		},
    -		{
    -			"no coins vested after period 1 before unlocking",
    -			now.Add(14 * time.Hour),
    -			nil,
    -			origCoins,
    -			origCoins,
    -		},
    -		{
    -			"50 percent of coins vested after period 1 at unlocking",
    -			now.Add(16 * time.Hour),
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -		},
    -		{
    -			"period 2 coins don't vest until period is over",
    -			now.Add(17 * time.Hour),
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -		},
    -		{
    -			"75 percent of coins vested after period 2",
    -			now.Add(18 * time.Hour),
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 750), sdk.NewInt64Coin(stakeDenom, 75)},
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)},
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)},
    -		},
    -		{
    -			"100 percent of coins vested",
    -			now.Add(48 * time.Hour),
    -			origCoins,
    -			sdk.Coins{},
    -			sdk.NewCoins(),
    +			name:                   "no coins vested at the beginning of the vesting schedule, all locked",
    +			time:                   now,
    +			expVestedCoins:         sdk.Coins{},
    +			expLockedUpVestedCoins: sdk.Coins{},
    +			expUnlockedVestedCoins: sdk.Coins{},
    +			expUnvestedCoins:       origCoins,
    +			expLockedUpCoins:       origCoins,
    +			expUnlockedCoins:       sdk.Coins{},
    +			expNotSpendable:        origCoins,
    +		},
    +		{
    +			name:                   "all coins vested and unlocked at the end of the vesting schedule",
    +			time:                   endTime,
    +			expVestedCoins:         origCoins,
    +			expLockedUpVestedCoins: sdk.Coins{},
    +			expUnlockedVestedCoins: origCoins,
    +			expUnvestedCoins:       sdk.Coins{},
    +			expLockedUpCoins:       sdk.Coins{},
    +			expUnlockedCoins:       origCoins,
    +			expNotSpendable:        sdk.Coins{},
    +		},
    +		{
    +			name:                   "no coins vested during first vesting period, all still locked",
    +			time:                   now.Add(6 * time.Hour),
    +			expVestedCoins:         sdk.Coins{},
    +			expLockedUpVestedCoins: sdk.Coins{},
    +			expUnlockedVestedCoins: sdk.Coins{},
    +			expUnvestedCoins:       origCoins,
    +			expLockedUpCoins:       origCoins,
    +			expUnlockedCoins:       sdk.Coins{},
    +			expNotSpendable:        origCoins,
    +		},
    +		{
    +			name:                   "50 percent of coins are vested after 1st vesting period, but before unlocking (all locked coins)",
    +			time:                   now.Add(12 * time.Hour),
    +			expVestedCoins:         getPercentOfVestingCoins(50),
    +			expLockedUpVestedCoins: getPercentOfVestingCoins(50),
    +			expUnlockedVestedCoins: sdk.Coins{},
    +			expUnvestedCoins:       getPercentOfVestingCoins(50),
    +			expLockedUpCoins:       origCoins,
    +			expUnlockedCoins:       sdk.Coins{},
    +			expNotSpendable:        origCoins,
    +		},
    +		{
    +			name:                   "after lockup period (all coins unlocked) - 50 percent of coins already vested",
    +			time:                   now.Add(16 * time.Hour),
    +			expVestedCoins:         getPercentOfVestingCoins(50),
    +			expLockedUpVestedCoins: sdk.Coins{},
    +			expUnlockedVestedCoins: getPercentOfVestingCoins(50),
    +			expUnvestedCoins:       getPercentOfVestingCoins(50),
    +			expLockedUpCoins:       sdk.Coins{},
    +			expUnlockedCoins:       origCoins,
    +			expNotSpendable:        getPercentOfVestingCoins(50),
    +		},
    +		{
    +			name:                   "in between vesting periods 1 and 2 - no new coins don't vested",
    +			time:                   now.Add(17 * time.Hour),
    +			expVestedCoins:         getPercentOfVestingCoins(50),
    +			expLockedUpVestedCoins: sdk.Coins{},
    +			expUnlockedVestedCoins: getPercentOfVestingCoins(50),
    +			expUnvestedCoins:       getPercentOfVestingCoins(50),
    +			expLockedUpCoins:       sdk.Coins{},
    +			expUnlockedCoins:       origCoins,
    +			expNotSpendable:        getPercentOfVestingCoins(50),
    +		},
    +		{
    +			name:                   "75 percent of coins vested after period 2",
    +			time:                   now.Add(18 * time.Hour),
    +			expVestedCoins:         getPercentOfVestingCoins(75),
    +			expLockedUpVestedCoins: sdk.Coins{},
    +			expUnlockedVestedCoins: getPercentOfVestingCoins(75),
    +			expUnvestedCoins:       getPercentOfVestingCoins(25),
    +			expLockedUpCoins:       sdk.Coins{},
    +			expUnlockedCoins:       origCoins,
    +			expNotSpendable:        getPercentOfVestingCoins(25),
    +		},
    +		{
    +			name:                   "100 percent of coins vested",
    +			time:                   now.Add(48 * time.Hour),
    +			expVestedCoins:         origCoins,
    +			expLockedUpVestedCoins: sdk.Coins{},
    +			expUnlockedVestedCoins: origCoins,
    +			expUnvestedCoins:       sdk.Coins{},
    +			expLockedUpCoins:       sdk.Coins{},
    +			expUnlockedCoins:       origCoins,
    +			expNotSpendable:        sdk.Coins{},
     		},
     	}
     
     	for _, tc := range testCases {
     		suite.Run(tc.name, func() {
     			vestedCoins := va.GetVestedCoins(tc.time)
     			suite.Require().Equal(tc.expVestedCoins, vestedCoins)
    +			lockedUpVested := va.GetLockedUpVestedCoins(tc.time)
    +			suite.Require().Equal(tc.expLockedUpVestedCoins, lockedUpVested)
    +			unlockedVestedCoins := va.GetUnlockedVestedCoins(tc.time)
    +			suite.Require().Equal(tc.expUnlockedVestedCoins, unlockedVestedCoins)
     			unvestedCoins := va.GetVestingCoins(tc.time)
     			suite.Require().Equal(tc.expUnvestedCoins, unvestedCoins)
    -			spendableCoins := va.LockedCoins(tc.time)
    -			suite.Require().Equal(tc.expSpendableCoins, spendableCoins)
    -		})
    -	}
    -}
    -
    -func (suite *VestingAccountTestSuite) TestGetVestedUnvestedLockedOnly() {
    -	now := tmtime.Now()
    -	endTime := now.Add(24 * time.Hour)
    -	addr := sdk.AccAddress(utiltx.GenerateAddress().Bytes())
    -	bacc := authtypes.NewBaseAccountWithAddress(addr)
    -	va := types.NewClawbackVestingAccount(bacc, sdk.AccAddress([]byte("funder")), origCoins, now, lockupPeriods, vestingPeriods)
    -
    -	testCases := []struct {
    -		name             string
    -		time             time.Time
    -		expVestedCoins   sdk.Coins
    -		expUnvestedCoins sdk.Coins
    -		expLockedCoins   sdk.Coins
    -	}{
    -		{
    -			"no coins vested at the beginning of the vesting schedule",
    -			now,
    -			sdk.Coins{},
    -			origCoins,
    -			origCoins,
    -		},
    -		{
    -			"all coins vested at the end of the vesting schedule",
    -			endTime,
    -			origCoins,
    -			sdk.Coins{},
    -			sdk.Coins{},
    -		},
    -		{
    -			"no coins vested during first vesting period",
    -			now.Add(6 * time.Hour),
    -			sdk.Coins{},
    -			origCoins,
    -			origCoins,
    -		},
    -		{
    -			"50 percent of coins vested after period 1 before unlocking",
    -			now.Add(14 * time.Hour),
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -			origCoins,
    -		},
    -		{
    -			"50 percent of coins vested after period 1 at unlocking",
    -			now.Add(16 * time.Hour),
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{},
    -		},
    -		{
    -			"period 2 coins don't vest until period is over",
    -			now.Add(17 * time.Hour),
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{},
    -		},
    -		{
    -			"75 percent of coins vested after period 2",
    -			now.Add(18 * time.Hour),
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 750), sdk.NewInt64Coin(stakeDenom, 75)},
    -			sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)},
    -			sdk.Coins{},
    -		},
    -		{
    -			"100 percent of coins vested",
    -			now.Add(48 * time.Hour),
    -			origCoins,
    -			sdk.Coins{},
    -			sdk.Coins{},
    -		},
    -	}
    -
    -	for _, tc := range testCases {
    -		suite.Run(tc.name, func() {
    -			vestedCoins := va.GetVestedOnly(tc.time)
    -			suite.Require().Equal(tc.expVestedCoins, vestedCoins)
    -			unvestedCoins := va.GetUnvestedOnly(tc.time)
    -			suite.Require().Equal(tc.expUnvestedCoins, unvestedCoins)
    -			lockedCoins := va.GetLockedOnly(tc.time)
    -			suite.Require().Equal(tc.expLockedCoins, lockedCoins)
    +			lockedUpCoins := va.GetLockedUpCoins(tc.time)
    +			suite.Require().Equal(tc.expLockedUpCoins, lockedUpCoins)
    +			unlockedCoins := va.GetUnlockedCoins(tc.time)
    +			suite.Require().Equal(tc.expUnlockedCoins, unlockedCoins)
    +			suite.Require().Equal(tc.expNotSpendable, va.LockedCoins(tc.time))
     		})
     	}
     }
    -
     func (suite *VestingAccountTestSuite) TestTrackDelegationUndelegation() {
     	now := tmtime.Now()
     	endTime := now.Add(24 * time.Hour)
     
     	testCases := []struct {
    -		name                   string
    -		delegate               func(*types.ClawbackVestingAccount)
    -		expDelegatedUnvested   sdk.Coins
    -		expDelegatedFree       sdk.Coins
    -		undelegate             func(*types.ClawbackVestingAccount)
    -		expUndelegatedUnvested sdk.Coins
    -		expUndelegatedFree     sdk.Coins
    -		expDelegationPanic     bool
    -		expUndelegationPanic   bool
    +		name                 string
    +		delegate             func(*types.ClawbackVestingAccount)
    +		expDelegatedFree     sdk.Coins
    +		undelegate           func(*types.ClawbackVestingAccount)
    +		expUndelegatedFree   sdk.Coins
    +		expDelegationPanic   bool
    +		expUndelegationPanic bool
     	}{
    -		{
    -			"delegate and undelegate all unvested coins",
    -			func(va *types.ClawbackVestingAccount) {
    -				va.TrackDelegation(now, origCoins, origCoins)
    -			},
    -			origCoins,
    -			nil,
    -			func(va *types.ClawbackVestingAccount) {
    -				va.TrackUndelegation(origCoins)
    -			},
    -			sdk.Coins{},
    -			nil,
    -			false,
    -			false,
    -		},
     		{
     			"delegate and undelegated all vested coins",
     			func(va *types.ClawbackVestingAccount) {
     				va.TrackDelegation(endTime, origCoins, origCoins)
     			},
    -			nil,
     			origCoins,
     			func(va *types.ClawbackVestingAccount) {
     				va.TrackUndelegation(origCoins)
     			},
    -			nil,
     			sdk.Coins{},
     			false,
     			false,
     		},
     		{
    -			"delegate and undelegate half of unvested coins",
    +			"delegate and undelegate half of vested coins",
     			func(va *types.ClawbackVestingAccount) {
     				va.TrackDelegation(now, origCoins, vestingPeriods[0].Amount)
     			},
     			vestingPeriods[0].Amount,
    -			nil,
     			func(va *types.ClawbackVestingAccount) {
     				va.TrackUndelegation(vestingPeriods[0].Amount)
     			},
     			sdk.Coins{},
    -			nil,
     			false,
     			false,
     		},
    @@ -377,12 +338,10 @@ func (suite *VestingAccountTestSuite) TestTrackDelegationUndelegation() {
     				va.TrackDelegation(now, origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 1000000)})
     			},
     			vestingPeriods[0].Amount,
    -			nil,
     			func(va *types.ClawbackVestingAccount) {
     				va.TrackUndelegation(vestingPeriods[0].Amount)
     			},
     			sdk.Coins{},
    -			sdk.Coins{},
     			true,
     			false,
     		},
    @@ -392,12 +351,10 @@ func (suite *VestingAccountTestSuite) TestTrackDelegationUndelegation() {
     				va.TrackDelegation(now, origCoins, origCoins)
     			},
     			vestingPeriods[0].Amount,
    -			nil,
     			func(va *types.ClawbackVestingAccount) {
     				va.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 0)})
     			},
     			sdk.Coins{},
    -			sdk.Coins{},
     			false,
     			true,
     		},
    @@ -407,13 +364,11 @@ func (suite *VestingAccountTestSuite) TestTrackDelegationUndelegation() {
     				va.TrackDelegation(now.Add(17*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)})
     				va.TrackDelegation(now.Add(17*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)})
     			},
    -			sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)},
    +			sdk.Coins{sdk.NewInt64Coin(stakeDenom, 100)},
     			func(va *types.ClawbackVestingAccount) {
     				va.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)})
     			},
    -			sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)},
    +			sdk.Coins{sdk.NewInt64Coin(stakeDenom, 75)},
     			false,
     			false,
     		},
    @@ -423,26 +378,21 @@ func (suite *VestingAccountTestSuite) TestTrackDelegationUndelegation() {
     				va.TrackDelegation(now.Add(17*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)})
     				va.TrackDelegation(now.Add(17*time.Hour), origCoins, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)})
     			},
    -			sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)},
    -			sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)},
    +			sdk.Coins{sdk.NewInt64Coin(stakeDenom, 100)},
     			func(va *types.ClawbackVestingAccount) {
     				va.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)})
     				va.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)})
     			},
     			sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)},
    -			sdk.Coins{},
     			false,
     			false,
     		},
     	}
    -
     	for _, tc := range testCases {
     		suite.Run(tc.name, func() {
     			addr := sdk.AccAddress(utiltx.GenerateAddress().Bytes())
     			bacc := authtypes.NewBaseAccountWithAddress(addr)
    -
     			va := types.NewClawbackVestingAccount(bacc, sdk.AccAddress([]byte("funder")), origCoins, now, lockupPeriods, vestingPeriods)
    -
     			if tc.expDelegationPanic { //nolint:gocritic
     				suite.Require().Panics(func() {
     					tc.delegate(va)
    @@ -452,14 +402,15 @@ func (suite *VestingAccountTestSuite) TestTrackDelegationUndelegation() {
     					tc.undelegate(va)
     				})
     			} else {
    +				var emptyCoins sdk.Coins
     				// Track Delegation
     				tc.delegate(va)
    -				suite.Require().Equal(tc.expDelegatedUnvested, va.DelegatedVesting)
    +				suite.Require().Equal(emptyCoins, va.DelegatedVesting)
     				suite.Require().Equal(tc.expDelegatedFree, va.DelegatedFree)
     
     				// Track Undelegation
     				tc.undelegate(va)
    -				suite.Require().Equal(tc.expUndelegatedUnvested, va.DelegatedVesting)
    +				suite.Require().Equal(emptyCoins, va.DelegatedVesting)
     				suite.Require().Equal(tc.expUndelegatedFree, va.DelegatedFree)
     			}
     		})
    @@ -538,3 +489,16 @@ func (suite *VestingAccountTestSuite) TestComputeClawback() {
     		})
     	}
     }
    +
    +// getPercentOfVestingCoins is a helper function to calculate
    +// the specified percentage of the coins in the vesting schedule
    +func getPercentOfVestingCoins(percentage int64) sdk.Coins {
    +	if percentage < 0 || percentage > 100 {
    +		panic("invalid percentage passed!")
    +	}
    +	var retCoins sdk.Coins
    +	for _, coin := range origCoins {
    +		retCoins = retCoins.Add(sdk.NewCoin(coin.Denom, coin.Amount.MulRaw(percentage).QuoRaw(100)))
    +	}
    +	return retCoins
    +}
    
  • x/vesting/types/schedule.go+1 1 modified
    @@ -8,7 +8,7 @@ import (
     	sdkvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
     )
     
    -// ReadSchedule returns the value of a schedule at readTime.
    +// ReadSchedule returns the coins of a schedule at readTime.
     //
     // A "schedule" is an increasing step function of Coins over time. It's
     // specified as an absolute start time and a sequence of relative periods, with
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.