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
Delegate
with 2,000 stake.The malicious user calls
TokenizeShares
with 1,000 stake.The malicious user calls
ValidatorBond
with 1,000 stake.The malicious user calls
RedeemTokensForShares
with 1,000 stake.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↗.