Skip to content

Latest commit

 

History

History
155 lines (121 loc) · 5.69 KB

067.md

File metadata and controls

155 lines (121 loc) · 5.69 KB

Slow Misty Iguana

High

Sending 1 unit token to feeCollector will cause the rewards unable to be distributed.

Summary

Sending 1 unit token to feeCollector will cause the rewards unable to be distributed.

Root Cause

HandleCoinsInFeeCollector will obtain all tokens under the feeCollector address. The number of one token is 0, causing other tokens to be unable to be extracted.

Internal Pre-conditions

External Pre-conditions

Attack Path

Impact

All FPs cannot get rewards

PoC

HandleCoinsInFeeCollector will obtain all tokens under the feeCollector address:

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)
}

func GetCoinsPortion(coinsInt sdk.Coins, portion math.LegacyDec) sdk.Coins {
	// coins with decimal value
	coins := sdk.NewDecCoinsFromCoins(coinsInt...)
	// portion of coins with decimal values
	portionCoins := coins.MulDecTruncate(portion)
	// truncate back
	// TODO: how to deal with changes?
->	portionCoinsInt, _ := portionCoins.TruncateDecimal()
	return portionCoinsInt
}

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

If an attacker sends 1 unit of any type of token to feeCollector, since 1 unit is too small btcStakingReward = 0; btcStakingReward = btcStakingPortion * 1 unit

The GetCoinsPortion function will TruncateDecimal, so btcStakingReward = 0

fpReward (coinsForCommission) = btcStakingReward * fp.Commission = 0

If fpReward is 0, the accumulateRewardGauge function will return early, so all fps cannot accumulate rewards.

RewardBTCStaking -> accumulateRewardGauge :

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)
}

func (k Keeper) accumulateRewardGauge(ctx context.Context, sType types.StakeholderType, addr sdk.AccAddress, reward sdk.Coins) {
	// if reward contains nothing, do nothing
->	if !reward.IsAllPositive() {
		return
	}
	// get reward gauge, or create a new one if it does not exist
	rg := k.GetRewardGauge(ctx, sType, addr)
	if rg == nil {
		rg = types.NewRewardGauge()
	}
	// add the given reward to reward gauge
	rg.Add(reward)
	// set back
	k.SetRewardGauge(ctx, sType, addr, rg)
}

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

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

The actual feeCoin is not 0, only the token sent tofeeCollector by the attack is 0. The number of one token is 0, causing other tokens to be unable to be extracted.

Mitigation

Do not obtain all tokens under the feeCollector address.