Audit finding: Missing logic
Project: AvaProtocol
Auditor: Slow Mist
Location: pallets/parachain-staking/src/lib.rs
Impact: Low
Vulnerability class (audit): Others
Vulnerability class (Scout): Business Logic
Status: Ignored; Planned for future updates.

[N5] [Low] Missing logic
Category: Others
Content
pallets/parachain-staking/src/lib.rs
It is necessary to make a judgment in the case of deposit_into_existing failure. If the transfer fails, the entire transaction needs to be rolled back.
fn prepare_staking_payouts(now: RoundIndex) {
// payout is now - delay rounds ago => now - delay > 0 else return early
let delay = T::RewardPaymentDelay::get();
if now <= delay {
return;
}
let round_to_payout = now.saturating_sub(delay);
let total_points = <Points<T>>::get(round_to_payout);
if total_points.is_zero() {
return;
}
let total_staked = <Staked<T>>::take(round_to_payout);
let total_issuance = Self::compute_issuance(total_staked);
let mut left_issuance = total_issuance;
// reserve portion of issuance for parachain bond account
let bond_config = <ParachainBondInfo<T>>::get();
let parachain_bond_reserve = bond_config.percent * total_issuance;
if let Ok(imb) = T::Currency::deposit_into_existing(&bond_config.account, parachain_bond_reserve) {
// update round issuance iff transfer succeeds
left_issuance = left_issuance.saturating_sub(imb.peek());
Self::deposit_event(Event::ReservedForParachainBond {
account: bond_config.account,
value: imb.peek(),
});
}
let payout = DelayedPayout {
round_issuance: total_issuance,
total_staking_reward: left_issuance,
collator_commission: <CollatorCommission<T>>::get(),
};
<DelayedPayouts<T>>::insert(round_to_payout, payout);
}
pub(crate) fn pay_one_collator_reward(paid_for_round: RoundIndex, payout_info: DelayedPayout<BalanceOf<T>>, ) -> (Option<(T::AccountId, BalanceOf<T>)>, Weight) {
// TODO: it would probably be optimal to roll Points into the DelayedPayouts storage
// item so that we do fewer reads each block
let total_points = <Points<T>>::get(paid_for_round);
if total_points.is_zero() {
// TODO: this case is obnoxious... it's a value query, so it could mean one
// of two different logic errors:
// 1. we removed it before we should have
// 2. we called pay_one_collator_reward when we were actually done with deferred payouts
log::warn!("pay_one_collator_reward called with no <Points<T>> for the round!");
return (None, 0u64.into());
}
let mint = |amt: BalanceOf<T>, to: T::AccountId| {
if let Ok(amount_transferred) = T::Currency::deposit_into_existing(&to, amt) {
Self::deposit_event(Event::Rewarded {
account: to.clone(),
rewards: amount_transferred.peek(),
});
}
};
let collator_fee = payout_info.collator_commission;
let collator_issuance = collator_fee * payout_info.round_issuance;
if let Some((collator, pts)) = <AwardedPts<T>>::iter_prefix(paid_for_round).drain().next() {
let mut extra_weight = 0;
let pct_due = Perbill::from_rational(pts, total_points);
let total_paid = pct_due * payout_info.total_staking_reward;
let mut amt_due = total_paid;
// Take the snapshot of block author and delegations
let state = <AtStake<T>>::take(paid_for_round, &collator);
let num_delegators = state.delegations.len();
if state.delegations.is_empty() {
// solo collator with no delegators
mint(amt_due, collator.clone());
extra_weight += T::OnCollatorPayout::on_collator_payout(paid_for_round, collator.clone(), amt_due, );
} else {
// pay collator first; commission + due_portion
let collator_pct = Perbill::from_rational(state.bond, state.total);
let commission = pct_due * collator_issuance;
amt_due = amt_due.saturating_sub(commission);
let collator_reward = (collator_pct * amt_due).saturating_add(commission);
mint(collator_reward, collator.clone());
extra_weight += T::OnCollatorPayout::on_collator_payout(paid_for_round, collator.clone(), collator_reward, );
// pay delegators due portion
for Bond { owner, amount } in state.delegations {
let percent = Perbill::from_rational(amount, state.total);
let due = percent * amt_due;
if !due.is_zero() {
mint(due, owner.clone());
}
}
}
(Some((collator, total_paid)), T::WeightInfo::pay_one_collator_reward(num_delegators as u32) + extra_weight, )
} else {
// Note that we don't clean up storage here; it is cleaned up in handle_delayed_payouts()
(None, 0u64.into())
}
}
Solution
If the transfer fails, the transaction should be rolled back.
Status
Ignored; The project party considers that it will be updated in subsequent versions.

Summary: Missing rollback logic for failed transfers in staking payouts.