Slow Misty Iguana
High
Sending 1 unit token to feeCollector
will cause the rewards unable to be distributed.
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.
All FPs cannot get rewards
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
}
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)
}
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.
Do not obtain all tokens under the feeCollector
address.