Slow Misty Iguana
High
Send coins to feeCollector
for DoS attack.
HandleCoinsInFeeCollector
will obtain all coins on the feeCollector
address, not a specific coin.
- The attacker creates multiple tokens.
- The attacker sends multiple tokens to the
feeCollector
address. - When calculating and distributing benefits, the attacker's tokens will be calculated even if they are invalid.
Slow ABCI methods
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)
}
Since Babylon supports cross-chain calls, users can create custom tokens through other chains and then send them to the feeCollector
address.
Do not obtain all tokens under the feeCollector address.