Skip to content

Latest commit

 

History

History
87 lines (67 loc) · 2.57 KB

040.md

File metadata and controls

87 lines (67 loc) · 2.57 KB

Slow Misty Iguana

Medium

The chain cannot be recovered after the number of stakers reaches 0

Summary

When the total stake amount is 0(fp.totalVotingPower = 0), the block can never enter Finalized state and cannot be restored after being restake.

Root Cause

Internal Pre-conditions

  1. At some point the total stake amount is 0.

External Pre-conditions

Attack Path

  1. At some point the stake amount is 0.
  2. Since the total voting power is 0, the block can never be Finalized.
  3. The staker restakes BTC, but since the old block does not enter the Finalized state, the staked block cannot enter the Finalized state either.
  4. ResumeFinalityProposal cannot recover this situation.

Impact

halt of the chain

PoC

When the number of votes is greater than 2/3, the block will enter the Finalized state:

func (k Keeper) TallyBlocks(ctx context.Context) {
	   ......
		switch {
		case fpSet != nil && !ib.Finalized:
			// has finality providers, non-finalised: tally and try to finalise the block
			voterBTCPKs := k.GetVoters(ctx, ib.Height)
			if tally(fpSet, voterBTCPKs) {
				// if this block gets >2/3 votes, finalise it
				k.finalizeBlock(ctx, ib)
			} else {
				// if not, then this block and all subsequent blocks should not be finalised
				// thus, we need to break here
				break finalizationLoop
			}
		......
	}
}

func tally(fpSet map[string]uint64, voterBTCPKs map[string]struct{}) bool {
	totalPower := uint64(0)
	votedPower := uint64(0)
	for pkStr, power := range fpSet {
		totalPower += power
		if _, ok := voterBTCPKs[pkStr]; ok {
			votedPower += power
		}
	}

->	return votedPower*3 > totalPower*2
}

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/finality/keeper/tallying.go#L103

The problem is that if totalPower = 0, the tally function returns false, so blocks can never be Finalized.

This situation cannot be restored, even if totalPower is reset ina later block, since the previous block is not Finalized, the later block will not be Finalized.

The ResumeFinalityProposal is used to recover the abnormal block, but it also cannot handle this situation. The ResumeFinalityProposal removes non-voting FPs, but block still cannot be Finalized if totalPower is 0.

Mitigation

func tally(fpSet map[string]uint64, voterBTCPKs map[string]struct{}) bool {
	totalPower := uint64(0)
	votedPower := uint64(0)
	for pkStr, power := range fpSet {
		totalPower += power
		if _, ok := voterBTCPKs[pkStr]; ok {
			votedPower += power
		}
	}

-	return votedPower*3 > totalPower*2
+	return votedPower*3 >= totalPower*2
}