#37903 [SC-High] "Potential Underflow Vulnerability in burn Function for total_active_stake_key"
Submitted on Dec 18th 2024 at 15:04:20 UTC by @danvinci_20 for Audit Comp | Folks: Liquid Staking
Report ID: #37903
Report Type: Smart Contract
Report severity: High
Target: https://github.com/Folks-Finance/algo-liquid-staking-contracts/blob/8bd890fde7981335e9b042a99db432e327681e1a/contracts/xalgo/consensus_v2.py
Impacts:
Temporary freezing of funds for at least 1 hour
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Description
Brief/Intro
In the current implementation of the consensus_v2.py
the burn functionality does not explicitly check if the algo_send is less than the total_active_stake before updating the total_active_stake_key.
Vulnerability Details
This is the code that contains this issue:
@router.method(no_op=CallConfig.CALL)
def burn(send_xalgo: abi.AssetTransferTransaction, min_received: abi.Uint64) -> Expr:
burn_amount = send_xalgo.get().asset_amount()
algo_balance = ScratchVar(TealType.uint64)
algo_to_send = ScratchVar(TealType.uint64)
return Seq(
rekey_and_close_to_check(),
# ensure initialised
Assert(App.globalGet(initialised_key)),
# check xALGO sent
check_x_algo_sent(send_xalgo),
# update total rewards
update_total_rewards_and_unclaimed_fees(),
# calculate algo amount to send
algo_balance.store(
App.globalGet(total_active_stake_key)
+ App.globalGet(total_rewards_key)
- App.globalGet(total_unclaimed_fees_key)
),
algo_to_send.store(
mul_scale(
burn_amount,
algo_balance.load(),
get_x_algo_circulating_supply() + burn_amount
)
),
# check amount and send ALGO to user
Assert(algo_to_send.load()),
Assert(algo_to_send.load() >= min_received.get()),
send_algo_from_proposers(Txn.sender(), algo_to_send.load()),
# update total active stake
App.globalPut(total_active_stake_key, App.globalGet(total_active_stake_key) - algo_to_send.load()),
# log burn
Log(Concat(
MethodSignature("Burn(address,uint64,uint64)"),
Txn.sender(),
Itob(burn_amount),
Itob(algo_to_send.load()),
)),
)
When a legitimate user tries to burn an amount of x_algo which equivalent value in algo is greater than the current total_active_key will not be possible this leading to temporary freezing of funds for that user. The cause of this is that during the execution of the burn call we updated the the total_active_stake value like this
update total active stake
App.globalPut(total_active_stake_key, App.globalGet(total_active_stake_key) - algo_to_send.load()),
This should have been implemented like this
If(
algo_to_send.load() > App.globalGet(total_active_stake_key),
# set total_active_stake_key to zero if burn amount exceeds it
App.globalPut(total_active_stake_key, Int(0)),
# proceed with the normal burn logic
App.globalPut(total_active_stake_key, App.globalGet(total_active_stake_key) - algo_to_send.load())
)
to prevent an underflow since if algo_to_send > total_active_stake there will be a revert which should ordinarily shouldn't have occurred and this leads to denial of service for that particular user.
Note: Algo_balance = total_active_staked + total_rewards - unclaimed_fees
Impact Details
The likelihood of this occuring is very low but it will impacts the protocol users preventing from claiming yield temporarily and this could have been prevented
Proof of Concept
Proof of Concept
Test 1 invalid
class LiquidStakingProtocol:
def __init__(self):
self.total_active_stake = 1000 # Initial active stake (example value)
self.total_rewards = 300 # Example value for rewards
self.total_unclaimed_fees = 50 # Example value for unclaimed fees
self.xalgo_circulating_supply = 0 # Example value for circulating supply
def burn(self, burn_amount, min_received):
# Calculate the algo balance to send
algo_balance = self.total_active_stake + self.total_rewards - self.total_unclaimed_fees
# Calculate the amount of ALGO to send based on the burn amount
algo_to_send = (burn_amount * algo_balance) / (self.xalgo_circulating_supply + burn_amount)
print(f"Algo to send {algo_to_send}")
# Check if the algo_to_send exceeds the total_active_stake
if algo_to_send > self.total_active_stake:
print(f"Error: burn amount too large, setting total active stake to underflow.")
self.total_active_stake = 0 # Set total active stake to zero
return
else:
# Proceed with the normal burn logic
print(f"Burning {algo_to_send} ALGO from the active stake.")
self.total_active_stake -= algo_to_send # Update total active stake
# Check if the amount sent is greater than or equal to min_received
if algo_to_send >= min_received:
print(f"Burn successful. {algo_to_send} ALGO sent.")
else:
print(f"Burn failed. Minimum required {min_received} ALGO not received.")
protocol = LiquidStakingProtocol()
# Test 1: Attempt to burn more than the total_active_stake
print("Test 1: Attempting to burn more than the total active stake:")
protocol.burn(1000, 100) # Attempt to burn more than available
Test 2 valid
class LiquidStakingProtocol:
def __init__(self):
self.total_active_stake = 1000 # Initial active stake (example value)
self.total_rewards = 300 # Example value for rewards
self.total_unclaimed_fees = 50 # Example value for unclaimed fees
self.xalgo_circulating_supply = 1000 # Example value for circulating supply
def burn(self, burn_amount, min_received):
# Calculate the algo balance to send
algo_balance = self.total_active_stake + self.total_rewards - self.total_unclaimed_fees
# Calculate the amount of ALGO to send based on the burn amount
algo_to_send = (burn_amount * algo_balance) / (self.xalgo_circulating_supply + burn_amount)
print(f"Algo to send {algo_to_send}")
# Check if the algo_to_send exceeds the total_active_stake
if algo_to_send > self.total_active_stake:
print(f"Error: burn amount too large, setting total active stake to underflow.")
self.total_active_stake = 0 # Set total active stake to zero
return
else:
# Proceed with the normal burn logic
print(f"Burning {algo_to_send} ALGO from the active stake.")
self.total_active_stake -= algo_to_send # Update total active stake
# Check if the amount sent is greater than or equal to min_received
if algo_to_send >= min_received:
print(f"Burn successful. {algo_to_send} ALGO sent.")
else:
print(f"Burn failed. Minimum required {min_received} ALGO not received.")
protocol = LiquidStakingProtocol()
#Test 2: Attempt to burn less than the total_active_stake
print("Test 2: Attempting to burn less than the total active stake:")
protocol.burn(500, 100) # A valid burn
print(f"Total active stake after burn attempt: {protocol.total_active_stake}\n")
Summary: Test 1
: The algo_to_send exceeds the available total_active_stake, so the stake underflows. Test 2
: The algo_to_send is valid, and the total_active_stake is reduced accordingly.
Last updated
Was this helpful?