Algo Ssstablecoinsss

AI First Flight #2
Beginner FriendlyDeFi
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Liquidator Receives Collateral Before DSC Is Burned Enabling Exploitation

Root + Impact

Root Cause: The liquidate() function transfers collateral to the liquidator before burning their DSC tokens. The liquidator's health factor is also checked only after receiving assets.

Impact: If the burn operation fails after collateral has been transferred, the liquidator receives collateral without paying. This allows attackers to extract collateral from underwater positions without providing any DSC, directly stealing from affected users.

Description

Normal Behavior: During liquidation, the liquidator should atomically: burn DSC from their balance, reduce the user's debt, and receive collateral. The liquidator's health factor should remain valid throughout.

Issue: The liquidate() function redeems collateral to the liquidator before burning DSC. If the burn operation fails after collateral transfer, the liquidator receives free collateral. Additionally, the health factor check for the liquidator happens after they've already received assets.
# dsc_engine.vy
@external
def liquidate(collateral: address, user: address, debt_to_cover: uint256):
# ... validation ...
# @> Step 1: Collateral transferred to liquidator FIRST
self._redeem_collateral(
collateral,
token_amount_from_debt_covered + bonus_collateral,
user,
msg.sender, # Liquidator receives collateral
)
# @> Step 2: DSC burned AFTER collateral transfer
self._burn_dsc(debt_to_cover, user, msg.sender) # If this fails, liquidator keeps collateral
# @> Step 3: Health factor checked LAST
self._revert_if_health_factor_is_broken(msg.sender)

Risk

Likelihood:HIGH

  • Reason 1 : Burn operation can fail if liquidator hasn't approved sufficient DSC

  • Reason 2 : Malicious liquidator can intentionally trigger failure after receiving collateral

Impact:

  • Impact 1 : Liquidator extracts collateral without paying DSC

  • Impact 2 : User's collateral is stolen without debt being reduced

Proof of Concept

An attacker with zero DSC tokens calls liquidate() on an underwater position. The _redeem_collateral() successfully transfers the victim's ETH to the attacker. When _burn_dsc() executes, it attempts to burn tokens the attacker doesn't have. If this fails silently (per H-03), the attacker keeps the collateral without payment.

# Attack scenario:
def test_liquidator_steals_collateral():
# Setup:
# - Victim has 1 ETH collateral ($2000) and 900 DSC debt
# - ETH price drops, victim is liquidatable
# - Attacker has 0 DSC tokens but calls liquidate
# In liquidate():
# 1. _redeem_collateral() executes successfully
# - victim's collateral reduced by 1 ETH
# - attacker receives 1 ETH
# - ERC20 transfer completes
# 2. _burn_dsc() attempts to burn from attacker
# - extcall DSC.burn_from(attacker, 900e18)
# - Attacker has 0 DSC tokens
# - If burn doesn't check balance properly, this could silently fail
# - OR if snekmate's burn_from checks and reverts, the whole tx reverts
# If using a custom DSC that doesn't revert:
# Result: Attacker stole 1 ETH, victim lost collateral, debt unchanged

Recommended Mitigation

Reorder the operations to follow the checks-effects-interactions pattern: burn DSC from the liquidator first, then transfer collateral. This ensures the liquidator must pay before receiving any assets.

# dsc_engine.vy
@external
def liquidate(collateral: address, user: address, debt_to_cover: uint256):
assert debt_to_cover > 0, "DSCEngine__NeedsMoreThanZero"
starting_user_health_factor: uint256 = self._health_factor(user)
assert starting_user_health_factor < MIN_HEALTH_FACTOR, "DSCEngine__HealthFactorOk"
token_amount_from_debt_covered: uint256 = self._get_token_amount_from_usd(collateral, debt_to_cover)
bonus_collateral: uint256 = (token_amount_from_debt_covered * LIQUIDATION_BONUS) // LIQUIDATION_PRECISION
+ # Burn DSC FIRST - liquidator must pay before receiving
+ self._burn_dsc(debt_to_cover, user, msg.sender)
+ # Transfer collateral AFTER payment confirmed
self._redeem_collateral(
collateral,
token_amount_from_debt_covered + bonus_collateral,
user,
msg.sender,
)
- self._burn_dsc(debt_to_cover, user, msg.sender)
# ... rest of function
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 3 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!