Assessment reports>Cosmos SDK Liquid Stake Module>High findings>Accounting validator bonds share could be broken
Category: Coding Mistakes

Accounting validator bonds share could be broken

High Severity
High Impact
Medium Likelihood

Description

The ValidatorBond function sets the delegation.ValidatorBond to true and increases the validator.ValidatorBondShares by the delegation shares. If once the delegation is marked as ValidatorBond, ValidatorBondShares will be increased or decreased based on the delegation shares. The ValidatorBondShares is used to account for the validator bond shares that affect the liquid shares of the validator.

func (k msgServer) ValidatorBond(goCtx context.Context, msg *types.MsgValidatorBond) (*types.MsgValidatorBondResponse, error) {
    // ...
    if !delegation.ValidatorBond {
		delegation.ValidatorBond = true
		k.SetDelegation(ctx, delegation)
		validator.ValidatorBondShares = validator.ValidatorBondShares.Add(delegation.Shares)
    // ...
    
func (k msgServer) Delegate(ctx context.Context, msg *types.MsgDelegate) (*types.MsgDelegateResponse, error) {
    // ...
    	if delegation.ValidatorBond {
		if err := k.IncreaseValidatorBondShares(ctx, valAddr, newShares); err != nil {
			return nil, err
		}
	}
    // ...

func (k msgServer) Undelegate(ctx context.Context, msg *types.MsgUndelegate) (*types.MsgUndelegateResponse, error) {
    // ...
    if delegation.ValidatorBond {
		if err := k.SafelyDecreaseValidatorBond(ctx, addr, shares); err != nil {
			return nil, err
		}
	}
    // ...

In current implementation, the TokenizeShares function will revert if the delegation is for a validator bond. However, the ValidatorBond function does not check if the part of delegation shares are already tokenized or if other users could send tokens to this delegator. This tokenized share is not accounted for in the ValidatorBondShares.

In this case, if the delegator redeems the tokenized shares, its unbonded and undelegatable amount will be increased by the tokenized shares not accounted for in the ValidatorBondShares. This will result in a balance mismatch between the ValidatorBondShares and the actual unbonded and undelegatable amount.

func (k msgServer) TokenizeShares(goCtx context.Context, msg *types.MsgTokenizeShares) (*types.MsgTokenizeSharesResponse, error) {
    // ...
    if delegation.ValidatorBond {
		return nil, types.ErrValidatorBondNotAllowedForTokenizeShare
	}
    // ...

Impact

Using the following scenario, a malicious user can manipulate ValidatorBondShares to become a negative value.

  1. The malicious user calls Delegate with 2,000 stake.

  2. The malicious user calls TokenizeShares with 1,000 stake.

  3. The malicious user calls ValidatorBond with 1,000 stake.

  4. The malicious user calls RedeemTokensForShares with 1,000 stake.

  5. The malicious user calls Undelegate with 2,000 stake.

As a result, the ValidatorBondShares will be -1,000 compared to the initial ValidatorBondShares.

This results in a permanent balance mismatch and can even lead to negative values. Once the ValidatorBondShares becomes negative, the function SafelyIncreaseValidatorLiquidShares and SafelyDecreaseValidatorBond, which are used in delegate or undelegate, will always fail. Ultimately, this undermines the stability of the protocol.

Recommendations

In a situation where a delegator holds validator bond shares, the validator bond shares should increase when redeeming tokenized shares from the delegator.

Remediation

The issue has been patched in commit 2436420.

Zellic © 2025Back to top ↑