Skip to content

Latest commit

 

History

History
146 lines (113 loc) · 5.32 KB

066.md

File metadata and controls

146 lines (113 loc) · 5.32 KB

Slow Misty Iguana

High

Send coins to feeCollector for DoS attack.

Summary

Send coins to feeCollector for DoS attack.

Root Cause

HandleCoinsInFeeCollector will obtain all coins on the feeCollector address, not a specific coin.

Internal Pre-conditions

External Pre-conditions

Attack Path

  1. The attacker creates multiple tokens.
  2. The attacker sends multiple tokens to the feeCollector address.
  3. When calculating and distributing benefits, the attacker's tokens will be calculated even if they are invalid.

Impact

Slow ABCI methods

PoC

func (k Keeper) HandleCoinsInFeeCollector(ctx context.Context) {
	params := k.GetParams(ctx)

	// find the fee collector account
	feeCollector := k.accountKeeper.GetModuleAccount(ctx, k.feeCollectorName)
	// get all balances in the fee collector account,
	// where the balance includes minted tokens in the previous block
->	feesCollectedInt := k.bankKeeper.GetAllBalances(ctx, feeCollector.GetAddress())

	// don't intercept if there is no fee in fee collector account
	if !feesCollectedInt.IsAllPositive() {
		return
	}

	// record BTC staking gauge for the current height, and transfer corresponding amount
	// from fee collector account to incentive module account
	// TODO: maybe we should not transfer reward to BTC staking gauge before BTC staking is activated
	// this is tricky to implement since finality module will depend on incentive and incentive cannot
	// depend on finality module due to cyclic dependency
	btcStakingPortion := params.BTCStakingPortion()
	btcStakingReward := types.GetCoinsPortion(feesCollectedInt, btcStakingPortion)
->	k.accumulateBTCStakingReward(ctx, btcStakingReward)
}

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/incentive/keeper/intercept_fee_collector.go#L20

func (k Keeper) accumulateBTCStakingReward(ctx context.Context, btcStakingReward sdk.Coins) {
	// update BTC staking gauge
	height := uint64(sdk.UnwrapSDKContext(ctx).HeaderInfo().Height)
->	gauge := types.NewGauge(btcStakingReward...)
->	k.SetBTCStakingGauge(ctx, height, gauge)

	// transfer the BTC staking reward from fee collector account to incentive module account
	err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, k.feeCollectorName, types.ModuleName, btcStakingReward)
	if err != nil {
		// this can only be programming error and is unrecoverable
		panic(err)
	}
}

func (k Keeper) GetBTCStakingGauge(ctx context.Context, height uint64) *types.Gauge {
	store := k.btcStakingGaugeStore(ctx)
	gaugeBytes := store.Get(sdk.Uint64ToBigEndian(height))
	if gaugeBytes == nil {
		return nil
	}

	var gauge types.Gauge
	k.cdc.MustUnmarshal(gaugeBytes, &gauge)
	return &gauge
}

All tokens under the feeCollector address are stored in the gauge, and the GetBTCStakingGauge function is used to obtain them.

RewardBTCStaking will obtain these tokens and use them to distribute fp and staker rewards:

func (k Keeper) RewardBTCStaking(ctx context.Context, height uint64, dc *ftypes.VotingPowerDistCache, voters map[string]struct{}) {
->	gauge := k.GetBTCStakingGauge(ctx, height)
	if gauge == nil {
		// failing to get a reward gauge at previous height is a programming error
		panic("failed to get a reward gauge at previous height")
	}

	// calculate total voting power of voters
	var totalVotingPowerOfVoters uint64
	for i, fp := range dc.FinalityProviders {
		if i >= int(dc.NumActiveFps) {
			break
		}
		if _, ok := voters[fp.BtcPk.MarshalHex()]; ok {
			totalVotingPowerOfVoters += fp.TotalBondedSat 
		}
	}

	// distribute rewards according to voting power portions for voters
	for i, fp := range dc.FinalityProviders {
		if i >= int(dc.NumActiveFps) {
			break
		}

		if _, ok := voters[fp.BtcPk.MarshalHex()]; !ok {
			continue
		}

		// calculate the portion of a finality provider's voting power out of the total voting power of the voters
		// fpPortion = fp.TotalBondedSat / totalVotingPowerOfVoters
		fpPortion := sdkmath.LegacyNewDec(int64(fp.TotalBondedSat)).QuoTruncate(sdkmath.LegacyNewDec(int64(totalVotingPowerOfVoters)))
		//	portionCoins := coins.MulDecTruncate(portion)  coins = gauge.Coins
	    //  portionCoinsInt, _ := portionCoins.TruncateDecimal()
->		coinsForFpsAndDels := gauge.GetCoinsPortion(fpPortion)

		// reward the finality provider with commission
		coinsForCommission := types.GetCoinsPortion(coinsForFpsAndDels, *fp.Commission)
		k.accumulateRewardGauge(ctx, types.FinalityProviderType, fp.GetAddress(), coinsForCommission)

		// reward the rest of coins to each BTC delegation proportional to its voting power portion
		coinsForBTCDels := coinsForFpsAndDels.Sub(coinsForCommission...)
		// GetFinalityProviderCurrentRewards(fp)
		// fpCurrentRwd.AddRewards(rwd)
		// setFinalityProviderCurrentRewards(fp)
		if err := k.AddFinalityProviderRewardsForBtcDelegations(ctx, fp.GetAddress(), coinsForBTCDels); err != nil {
			panic(fmt.Errorf("failed to add fp rewards for btc delegation %s at height %d: %w", fp.GetAddress().String(), height, err))
		}
	}
	// TODO: prune unnecessary state (delete BTCStakingGauge after the amount is used)
}

https://github.com/sherlock-audit/2024-12-babylon/blob/main/babylon/x/incentive/keeper/btc_staking_gauge.go#L19

Since Babylon supports cross-chain calls, users can create custom tokens through other chains and then send them to the feeCollector address.

Mitigation

Do not obtain all tokens under the feeCollector address.