Accounting validator bonds share could be broken
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.
The malicious user calls
Delegatewith 2,000 stake.The malicious user calls
TokenizeShareswith 1,000 stake.The malicious user calls
ValidatorBondwith 1,000 stake.The malicious user calls
RedeemTokensForShareswith 1,000 stake.The malicious user calls
Undelegatewith 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↗.