Algo Ssstablecoinsss

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

DSC Mint and Burn Operations Do Not Verify Success

Root + Impact

Root Cause: The _mint_dsc() and _burn_dsc() functions call external DSC token functions without checking the return values. The code explicitly comments that success is not being checked.

Impact: If mint fails, users have debt recorded but receive no tokens, effectively stealing their collateral value. If burn fails, users get debt reduction without tokens being destroyed, allowing them to extract unbacked value from the protocol and causing undercollateralization.

Description

Normal Behavior: When a user mints DSC, the engine should record their debt and transfer newly minted tokens to them. When burning, tokens should be destroyed and debt reduced. Both operations should succeed atomically.

Issue: The _mint_dsc() and _burn_dsc() functions call external DSC token functions but explicitly do not check return values. If the external call fails silently, the internal accounting becomes desynced from actual token balances.
# dsc_engine.vy
@internal
def _mint_dsc(amount_dsc_to_mint: uint256):
assert amount_dsc_to_mint > 0, "DSCEngine__NeedsMoreThanZero"
self.user_to_dsc_minted[msg.sender] += amount_dsc_to_mint # @> Debt recorded
self._revert_if_health_factor_is_broken(msg.sender)
# @> Note, we are not checking success here
extcall DSC.mint(msg.sender, amount_dsc_to_mint) # @> If this fails, debt is recorded but no tokens sent
@internal
def _burn_dsc(amount_dsc_to_burn: uint256, on_behalf_of: address, dsc_from: address):
self.user_to_dsc_minted[on_behalf_of] -= amount_dsc_to_burn # @> Debt reduced
# @> Note, we are not checking success here
extcall DSC.burn_from(dsc_from, amount_dsc_to_burn) # @> If this fails, debt reduced but tokens not burned

Risk

Likelihood:HIGH

  • Reason 1 : DSC token's mint function could fail if minter is not properly set

  • Reason 2 : DSC token's burn_from could fail if insufficient allowance is set

Impact:

  • Impact 1 : Burn failure: User's debt is reduced but tokens are not burned - user gets free debt reduction, protocol becomes undercollateralized

  • Impact 2 : Mint failure: User's debt is recorded but they receive no tokens - funds are effectively stolen from user

Proof of Concept

When a user calls burn_dsc(500e18) without first approving the DSCEngine to spend their DSC tokens, the internal accounting reduces their debt by 500 DSC, but the burn_from call fails due to insufficient allowance. The user now has 500 DSC less debt but still holds all their tokens, effectively creating 500 unbacked DSC in circulation.

# Scenario 1: Mint fails but debt is recorded
def test_mint_failure_records_debt():
# DSCEngine is not set as minter on DSC token
# User calls mint_dsc(1000e18)
# In _mint_dsc:
# 1. user_to_dsc_minted[user] += 1000e18 # Debt recorded: 1000 DSC
# 2. health_factor check passes
# 3. extcall DSC.mint(user, 1000e18) # FAILS - engine not authorized
# Result:
# - User has 1000 DSC debt on the books
# - User received 0 DSC tokens
# - User's collateral is now backing phantom debt
# Scenario 2: Burn fails but debt is reduced
def test_burn_failure_reduces_debt():
# User has 1000 DSC debt and 1000 DSC tokens
# User did not approve DSCEngine to burn their tokens
# User calls burn_dsc(500e18)
# In _burn_dsc:
# 1. user_to_dsc_minted[user] -= 500e18 # Debt reduced to 500 DSC
# 2. extcall DSC.burn_from(user, 500e18) # FAILS - no approval
# Result:
# - User's debt is now 500 DSC
# - User still has 1000 DSC tokens
# - User gained 500 DSC of free money
# - Protocol is now undercollateralized by 500 DSC

Recommended Mitigation

Capture the return value from external calls and assert that the operation succeeded. This ensures internal accounting only persists when the actual token operations complete successfully.

# dsc_engine.vy
@internal
def _mint_dsc(amount_dsc_to_mint: uint256):
assert amount_dsc_to_mint > 0, "DSCEngine__NeedsMoreThanZero"
self.user_to_dsc_minted[msg.sender] += amount_dsc_to_mint
self._revert_if_health_factor_is_broken(msg.sender)
- # Note, we are not checking success here
- extcall DSC.mint(msg.sender, amount_dsc_to_mint)
+ success: bool = extcall DSC.mint(msg.sender, amount_dsc_to_mint)
+ assert success, "DSCEngine__MintFailed"
@internal
def _burn_dsc(amount_dsc_to_burn: uint256, on_behalf_of: address, dsc_from: address):
self.user_to_dsc_minted[on_behalf_of] -= amount_dsc_to_burn
- # Note, we are not checking success here
- extcall DSC.burn_from(dsc_from, amount_dsc_to_burn)
+ success: bool = extcall DSC.burn_from(dsc_from, amount_dsc_to_burn)
+ assert success, "DSCEngine__BurnFailed"
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!