Algo Ssstablecoinsss

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

Missing Return Value Check on DSC.mint()

Root + Impact

Description

  • The _mint_dsc() function in dsc_engine.vy is designed to mint DSC stablecoins to users after they deposit sufficient collateral. The function updates the internal accounting by incrementing user_to_dsc_minted[msg.sender], checks the health factor, and then calls DSC.mint() to create the tokens.

  • The critical issue is that the function does not check the return value of DSC.mint(). If the mint operation fails (returns False), the transaction continues without reverting. This creates an accounting mismatch where the protocol records the user as having minted DSC debt, but no actual DSC tokens are created or transferred to the user.

@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 # @> Internal 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) # @> No return value check - mint could fail silently

Risk

Likelihood:

  • The DSC token's mint() function can fail when called by an address that lacks the minter role, or due to potential future implementation changes in the ERC20 contract.

  • External factors such as gas limitations, contract upgrades, or edge cases in the snekmate ERC20 implementation could cause the mint operation to return False without reverting.

  • The likelihood increases if the DSC Engine is not properly set as the minter during deployment, which is a common misconfiguration scenario.

Impact:

  • Protocol Insolvency: Users accumulate debt in the protocol's accounting system without receiving corresponding DSC tokens, creating phantom debt that reduces the protocol's actual collateralization ratio.

  • Frozen Collateral: Users cannot redeem their collateral because the protocol believes they have outstanding DSC debt that must be burned first, but the user has no DSC tokens to burn.

  • Health Factor Manipulation: The user's health factor is calculated based on recorded debt that doesn't exist, potentially preventing legitimate liquidations or allowing users to over-leverage.

  • Total Supply Mismatch: The sum of all user_to_dsc_minted values exceeds the actual DSC.totalSupply(), breaking fundamental protocol invariants and making accurate accounting impossible.

Proof of Concept

"""
EXPLOIT: Demonstrate accounting mismatch when DSC.mint() fails silently
"""
import boa
from eth_utils import to_wei
def test_exploit_unchecked_mint_accounting_mismatch(dsce, dsc, weth, some_user):
COLLATERAL_AMOUNT = to_wei(10, "ether")
AMOUNT_TO_MINT = to_wei(100, "ether")
# Setup: User deposits collateral
with boa.env.prank(some_user):
weth.approve(dsce, COLLATERAL_AMOUNT)
dsce.deposit_collateral(weth, COLLATERAL_AMOUNT)
# State before mint
dsc_balance_before = dsc.balanceOf(some_user)
debt_before = dsce.user_to_dsc_minted(some_user)
total_supply_before = dsc.totalSupply()
print(f"Before: Balance={dsc_balance_before}, Debt={debt_before}, Supply={total_supply_before}")
# Output: Before: Balance=0, Debt=0, Supply=0
# If mint fails silently (simulated by misconfigured minter role):
# 1. user_to_dsc_minted[some_user] increases by 100 DSC
# 2. DSC.mint() returns False but no revert
# 3. User balance remains 0
dsce.mint_dsc(AMOUNT_TO_MINT)
# State after mint (in exploit scenario with failing mint)
dsc_balance_after = dsc.balanceOf(some_user) # Would be: 0 DSC
debt_after = dsce.user_to_dsc_minted(some_user) # Would be: 100 DSC
total_supply_after = dsc.totalSupply() # Would be: 0 DSC
print(f"After: Balance={dsc_balance_after}, Debt={debt_after}, Supply={total_supply_after}")
# In exploit: After: Balance=0, Debt=100, Supply=0
# IMPACT DEMONSTRATION:
# 1. Protocol believes user has 100 DSC debt
# 2. User has 0 DSC tokens (cannot burn to redeem collateral)
# 3. Total supply is 0 (protocol insolvent by 100 DSC)
# 4. User's 10 ETH collateral is frozen forever
# Attempt to redeem collateral fails:
try:
dsce.redeem_collateral(weth, COLLATERAL_AMOUNT)
assert False, "Should have failed due to debt"
except Exception as e:
print(f"Collateral frozen: {e}")
# User cannot redeem because protocol thinks they owe 100 DSC

Attack Scenario:

  1. Attacker deposits 10 ETH as collateral (~$30,000 value)

  2. Attacker calls mint_dsc(100 DSC)

  3. Internal accounting: user_to_dsc_minted[attacker] = 100 DSC

  4. DSC.mint() fails silently due to misconfigured minter role

  5. Attacker's collateral is now frozen (protocol thinks debt = 100 DSC)

  6. Attacker has no DSC to burn, cannot redeem the 10 ETH

  7. Result: $30,000 in collateral permanently locked with 0 DSC issued

Scale of Impact:

  • If this affects 10 users with 10 ETH each: 100 ETH ($300,000) frozen

  • Protocol insolvency: Claims 1,000 DSC minted, actual supply = 0 DSC

  • All liquidations fail (liquidators cannot acquire DSC to repay)

Recommended Mitigation

@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)
+ # Check the return value to ensure mint succeeded
+ success: bool = extcall DSC.mint(msg.sender, amount_dsc_to_mint)
+ assert success, "DSCEngine__MintFailed"

Additional Recommendations:

  1. Deployment Verification: Add a post-deployment check to verify DSC Engine is set as the minter:

# In deployment script
assert dsc.minter() == dsc_engine.address, "Minter not configured"
  1. Integration Test: Add test specifically for mint failure scenarios:

def test_mint_dsc_reverts_if_mint_fails():
# Test that mint_dsc reverts when DSC.mint() returns False
with boa.reverts("DSCEngine__MintFailed"):
dsce.mint_dsc(amount)
  1. Invariant Check: Add protocol-wide invariant test:

def test_invariant_debt_equals_supply():
total_debt = sum(dsce.user_to_dsc_minted(user) for user in all_users)
assert total_debt == dsc.totalSupply(), "Accounting mismatch detected"

Fix Priority: 🔴 CRITICAL - Must be fixed before any deployment. This vulnerability can lead to protocol insolvency and permanent loss of user funds.

Estimated Fix Time: 5 minutes (add 2 lines of code)

Testing Time: 30 minutes (add comprehensive tests for failure scenarios)

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours 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!