Audit finding: Missing validation in multiple StakingLedger methods
Project: Parallel
Auditor: Trail of Bits
Location: pallets/liquid-staking/src/types.rs:199-219,223-226,230-253
Impact: Undetermined
Likelihood: Difficulty-High
Vulnerability class (audit): Data Validation
Vulnerability class (Scout): Error Handling and Validation

4. Missing validation in multiple StakingLedger methods
Severity: Undetermined
Difficulty: High
Type: Data Validation
Finding ID: TOB-PLF-4
Target: pallets/liquid-staking/src/types.rs
Description
The staking ledger is used to keep track of the total amount of staked funds in the system. It is updated in response to cross-consensus messaging (XCM) requests to the parent chain (either Polkadot or Kusama). A number of the StakingLedger methods lack sufficient input validation before they update the staking ledger’s internal state. Even though the input is validated as part of the original XCM call, there could still be issues due to implementation errors or overlooked corner cases.
First, the StakingLedger::rebond method does not use checked arithmetic to update the active balance. The method should also check that the computed unlocking_balance is equal to the input value at the end of the loop to ensure that the system remains consistent.
pub fn rebond(&mut self, value: Balance) {
let mut unlocking_balance: Balance = Zero::zero();
while let Some(last) = self.unlocking.last_mut() {
if unlocking_balance + last.value <= value {
unlocking_balance += last.value;
self.active += last.value;
self.unlocking.pop();
} else {
let diff = value - unlocking_balance;
unlocking_balance += diff;
self.active += diff;
last.value -= diff;
}
if unlocking_balance >= value {
break;
}
}
}
Figure 4.1: pallets/liquid-staking/src/types.rs:199-219
Second, the StakingLedger::bond_extra method does not use checked arithmetic to update the total and active balances.
pub fn bond_extra(&mut self, value: Balance) {
self.total += value;
self.active += value;
}
Figure 4.2: pallets/liquid-staking/src/types.rs:223-226
Finally, the StakingLedger::unbond method does not use checked arithmetic when updating the active balance.
pub fn unbond(&mut self, value: Balance, target_era: EraIndex) {
if let Some(mut chunk) = self
.unlocking
.last_mut()
.filter(|chunk| chunk.era == target_era)
{
chunk.value = chunk.value.saturating_add(value);
} else {
self.unlocking.push(UnlockChunk {
value,
era: target_era,
});
};
self.active -= value;
}
Figure 4.3: pallets/liquid-staking/src/types.rs:230-253
Since the staking ledger is updated by a number of the XCM response handlers, and XCM responses may return out of order, it is important to ensure that input to the staking ledger methods is validated to prevent issues due to race conditions and corner cases.
We could not find a way to exploit this issue, but we cannot rule out the risk that it could be used to cause a denial-of-service condition in the system.
Exploit Scenario
The staking ledger's state is updated as part of a WithdrawUnbonded request, leaving the unlocking vector in the staking ledger empty. Later, when the response to a previous call to rebond is handled, the ledger is updated again, which leaves it in an inconsistent state.
Recommendations
Short term, ensure that the balance represented by the staking ledger’s unlocking vector is enough to cover the input balance passed to StakingLedger::rebond. Use checked arithmetic in all staking ledger methods that update the ledger’s internal state to ensure that issues due to data races are detected and handled correctly.

Summary: StakingLedger methods lack sufficient input validation and unchecked arithmetic, potentially leading to inconsistent states or race conditions.