Skip to content

Latest commit

 

History

History
143 lines (100 loc) · 5.11 KB

036.md

File metadata and controls

143 lines (100 loc) · 5.11 KB

Slow Misty Iguana

Medium

The voting power table of the historical block is not updated

Summary

The voting power table of the historical block is not updated, and the voting power of jailed fp will be counted in the totalPower.

Root Cause

The voting power table of the historical block is not updated

Internal Pre-conditions

External Pre-conditions

Attack Path

Impact

It may result in votes that cannot pass and blocks cannot enter Finalize state.

PoC

HandleLiveness is used to add sluggish fp to the jail and send event updates voting power table:

x/finality/abci.go/EndBlocker -> HandleLiveness -> HandleFinalityProviderLiveness

func (k Keeper) HandleFinalityProviderLiveness(ctx context.Context, fpPk *types.BIP340PubKey, missed bool, height int64) error {
	......

	// if the number of missed block reaches the threshold within the sliding window
	// jail the finality provider
	if height > minHeight && signInfo.MissedBlocksCounter > maxMissed {
		updated = true

->		if err := k.jailSluggishFinalityProvider(ctx, fpPk); err != nil {
			return fmt.Errorf("failed to jail sluggish finality provider %s: %w", fpPk.MarshalHex(), err)
		}

->		signInfo.JailedUntil = sdkCtx.HeaderInfo().Time.Add(params.JailDuration)
		// we need to reset the counter & bitmap so that the finality provider won't be
		// immediately jailed after unjailing.
		signInfo.MissedBlocksCounter = 0
		if err := k.DeleteMissedBlockBitmap(ctx, fpPk); err != nil {
			return fmt.Errorf("failed to remove the missed block bit map: %w", err)
		}

		k.Logger(sdkCtx).Info(
			"finality provider is jailed",
			"height", height,
			"public_key", fpPk.MarshalHex(),
		)
	}

	// Set the updated signing info
	if updated {
->		return k.FinalityProviderSigningTracker.Set(ctx, fpPk.MustMarshal(), *signInfo)
	}

	return nil
}

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

But the problem is that the jailed event is sent based on the btcHight height corresponding to the current block, so only the voting power table(VotingPowerDistCache) of the current block is updated after the jailed event:

func (k Keeper) JailFinalityProvider(ctx context.Context, fpBTCPK []byte) error {
	// ensure finality provider exists
	fp, err := k.GetFinalityProvider(ctx, fpBTCPK)
	if err != nil {
		return err
	}

	// ensure finality provider is not slashed yet
	if fp.IsSlashed() {
		return types.ErrFpAlreadySlashed
	}

	// ensure finality provider is not jailed yet
	if fp.IsJailed() {
		return types.ErrFpAlreadyJailed
	}

	// set finality provider to be jailed
	fp.Jailed = true
	k.setFinalityProvider(ctx, fp)

	btcTip := k.btclcKeeper.GetTipInfo(ctx)
	if btcTip == nil {
		return fmt.Errorf("failed to get current BTC tip")
	}

	// record jailed event. The next `BeginBlock` will consume this
	// event for updating the finality provider set
	powerUpdateEvent := types.NewEventPowerDistUpdateWithJailedFP(fp.BtcPk)
->	k.addPowerDistUpdateEvent(ctx, btcTip.Height, powerUpdateEvent)

	return nil
}

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/btcstaking/keeper/finality_providers.go#L148

After being jailed, fp cannot vote, not only for the current block, but also for the historical block:

// AddFinalitySig adds a new vote to a given block
func (ms msgServer) AddFinalitySig(goCtx context.Context, req *types.MsgAddFinalitySig) (*types.MsgAddFinalitySigResponse, error) {
    ......
->	if fp.IsJailed() {
		return nil, bstypes.ErrFpAlreadyJailed.Wrapf("finality provider public key: %s", fpPK.MarshalHex())
	}
    ......

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

However, the problem is that the voting power of jailed fp is still retained in the voting power table of the historical block, which may cause that the voting cannot reach the quorum of 2/3. In particular, some fp with more voting power have been jailed or a lot of fp have been jailed.

Assume that block 1, 2, and 3 are all in UNFinalize state, and fp is required to vote: The current block height is that fp0 is jailed in block 4, the jailed event is sent and processed, and the voting power table of block 4 is updated with a new value and no longer contains the voting power of fp0. But the voting power table in blocks 1, 2, 3 is still the old value, At this time, fp0 can no longer vote, but the value of fp0 is protected when calculating totalPower.

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

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

UpdatePowerDist updates only the events between currentHeight-1 and the btcHeight associated before currentHeight, and does not process the events in the old block:

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/finality/keeper/power_dist_change.go#L24-L38

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/finality/keeper/power_dist_change.go#L85-L92

Mitigation

The totalPower in the historical block needs to be subtracted from the voting power of jailed fp.