#37854 [SC-Insight] Missing state validation upon Upgrade
Submitted on Dec 17th 2024 at 13:51:59 UTC by @k13n for Audit Comp | Folks: Liquid Staking
Report ID: #37854
Report Type: Smart Contract
Report severity: Insight
Target: https://github.com/Folks-Finance/algo-liquid-staking-contracts/blob/8bd890fde7981335e9b042a99db432e327681e1a/contracts/xalgo/consensus_v2.py
Impacts:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
The contract is updatable meaning that the code of the contract can be changed. Right now an old version of this contract is deployed on Algorand and according to the developers this old contract will be updated to contain the code subject to this audit competition. This new code does not validate the global state of the contract upon update. Therefore, certain invariants cannot be guaranteed after an update. This can lead to unexpected behavior and loss of user funds.
Vulnerability Details
Function initialise
must be called after upgrading the contract and before any other meaningful operation (minting, burning, etc.) can be executed. This function assumes that certain global state exists and is valid without further checks. In case this global state was manipulated prior to the upgrade, certain invariants may not hold.
Function update_fee
ensures the invariant that the fee is between 0 and 100%. This invariant is asserted in function check_fee
which checks that the fee is below 100% (since Algorand only supports unsigned integers, the fee must be >= 0%). This check_fee()
(and other similar sanity checks) are missing in the initialise
function. Therefore, it is possible that the invariant doesn't hold.
Impact Details
Assume that through the above loophole the fee is set to 10000% by a rogue admin. As a result, the admin can collect more fees than he should possibly get and these additional fees are taken from the users' stake, resulting in the theft of user funds.
Recommendation
Check that the given invariants (check_fee
, check_premium
, ...) are maintained after updating in initialise
.
References
https://github.com/Folks-Finance/algo-liquid-staking-contracts/blob/8bd890fde7981335e9b042a99db432e327681e1a/contracts/xalgo/consensus_v2.py#L283
https://github.com/Folks-Finance/algo-liquid-staking-contracts/blob/8bd890fde7981335e9b042a99db432e327681e1a/contracts/xalgo/consensus_v2.py#L72
Proof of Concept
Proof of Concept
Initial State
The old code (code that is not subject to this audit) is deployed on Algorand
(Rogue) Admin prepares Attack
A (rouge) admin manipulates the global state and for example sets the fee to 10'000%
Admin deploys update
An admin updates the contract and deploys the code subject to this audit competition
Attacker withdraws user funds
The attacker makes a protocol donation to ensure that rewards are > 0
The attacker calls
claim_fee
, which in turns callssend_unclaimed_fees
andupdate_total_rewards_and_unclaimed_fees
.Here the code computes the new rewards (which are > 0 because of the donation) and multiplies that with the fee percentage that the attacker set to > 100%. As a result, the global state
total_unclaimed_fees
is updated to a large value, larger than the actual rewards that have been collectedFunction
claim_fee
transfers the fees to the admin account which results in the theft of user funds