Algo Ssstablecoinsss

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

Unchecked External Call in `_mint_dsc` Leads to Debt Without Funds

Summary

The _mint_dsc internal function in dsc_engine.vy updates the user's debt state (user_to_dsc_minted) before executing the external call to DSC.mint(). Because the success of the extcall is not verified, if the minting operation fails, the user's debt is permanently increased without them receiving any DSC tokens.

Description

In the _mint_dsc function, the protocol updates the accounting state and then attempts to mint the tokens:

@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)

In Vyper, extcall does not automatically revert the transaction if the external call fails; it simply returns False. If DSC.mint() fails (due to a bug in the DSC contract, a paused state, or a malicious token implementation), the transaction will still succeed. However, self.user_to_dsc_minted[msg.sender] has already been increased.

This results in a severe accounting mismatch where the user owes debt to the protocol but never received the corresponding stablecoins.

Risk

Severity: High
Likelihood: Medium
Impact: High

If a user calls deposit_collateral_and_mint_dsc or mint_dsc and the underlying DSC.mint fails:

  1. The user's collateral remains locked in the engine.

  2. The user's debt (user_to_dsc_minted) increases.

  3. The user receives 0 DSC tokens.

This leads to a direct loss of funds and borrowing power for the user, as they are now in debt for tokens they do not possess. It also breaks the fundamental invariant of the stablecoin system (1 DSC debt = 1 DSC in circulation).

Proof of Concept

Add this test to tests/test_dsc_engine.py:

import boa
from eth_utils import to_wei
def test_mint_dsc_fails_but_debt_increases(dsce, dsc, some_user, weth):
# 1. User deposits collateral
collateral_amount = to_wei(10, "ether")
with boa.env.prank(some_user):
weth.approve(dsce.address, collateral_amount)
dsce.deposit_collateral(weth.address, collateral_amount)
# 2. Mock the DSC contract to make mint() fail
# (In a real scenario, this could happen if DSC is paused or has a bug)
with boa.env.prank(dsc.owner()):
# Assuming we can pause or break the mint function for the test
dsc.set_minter(dsce.address, False)
amount_to_mint = to_wei(100, "ether")
# 3. User attempts to mint DSC
with boa.env.prank(some_user):
# The transaction will NOT revert because extcall success is ignored
dsce.mint_dsc(amount_to_mint)
# 4. Check user balance and debt
user_dsc_balance = dsc.balanceOf(some_user)
user_dsc_minted, _ = dsce.get_account_information(some_user)
# ASSERTION: User has debt but no tokens!
assert user_dsc_balance == 0, "User should have 0 DSC"
assert user_dsc_minted == amount_to_mint, "User debt should be increased despite mint failure"
print("✅ HIGH VULNERABILITY EXPLOITED!")
print(f"User debt: {user_dsc_minted}, User DSC balance: {user_dsc_balance}")

Run: mox test -k test_mint_dsc_fails_but_debt_increases

Expected Output:

✅ HIGH VULNERABILITY EXPLOITED!
User debt: 100000000000000000000, User DSC balance: 0

Recommended Mitigation

Always verify the success of external calls in Vyper using extcall return values, or use the raw_call with revert_on_failure=True (though extcall is preferred for type safety).

Update _mint_dsc to check the return value:

@internal
def _mint_dsc(amount_dsc_to_mint: uint256):
assert amount_dsc_to_mint > 0, "DSCEngine__NeedsMoreThanZero"
# INTERACTIONS: Perform external call FIRST
success: bool = extcall DSC.mint(msg.sender, amount_dsc_to_mint)
assert success, "DSCEngine_MintFailed"
# EFFECTS: Update state AFTER successful external call
self.user_to_dsc_minted[msg.sender] += amount_dsc_to_mint
self._revert_if_health_factor_is_broken(msg.sender)

Why This Works:
By checking the success boolean returned by extcall, the function will explicitly revert if the minting fails, preventing the state update and ensuring the user is never left with debt without the corresponding tokens.

Updates

Lead Judging Commences

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