Summary
The _mint_dsc
and _burn_dsc
functions in the DSC Engine contract were found to lack sufficient validation when interacting with the external DSC.mint
and DSC.burn_from
functions. The absence of success checks led to potential inconsistencies between the contract's internal state and the actual token balances. This could cause incorrect assumptions about the system's financial health, exposing the contract to potential exploits.
Vulnerability Details
Root Cause:
-
The functions originally used extcall DSC.mint
and extcall DSC.burn_from
without verifying the return status.
-
No post-operation balance verification was performed, allowing silent failures of the mint or burn operations.
Vulnerability Mechanics:
-
If the mint
or burn_from
function in the DSC
token implementation failed internally but did not revert, the contract’s internal records (user_to_dsc_minted
) would still be modified.
-
This could lead to a dangerous mismatch between the recorded and actual token balances.
Impact
-
State Inconsistency: Internal records of minted or burned tokens could become desynchronized from the actual token balances.
-
Financial Risk: The system could experience miscalculations during liquidation or accounting processes, leading to insolvency or other financial risks.
-
Exploit Potential: A malicious token implementation could manipulate the state without performing mint or burn operations, causing irreversible inconsistencies.
Proof
To demonstrate the vulnerability, a test was constructed using a mock implementation of the DSC contract where mint
function intentionally failed without reverting. After calling the mint
function from the DSC Engine
contract, the internal state of the system still reflected successful operations, while actual balances remained unchanged.
Unit Test
def test_mint_burn_success_check(dsce, weth, eth_usd):
USER = boa.env.generate_address()
DEPOSIT_AMOUNT = to_wei(1, "ether")
MINT_AMOUNT = to_wei(100, "ether")
INITIAL_PRICE = 2_000 * 10**8
eth_usd.updateAnswer(INITIAL_PRICE)
failing_dsc = boa.load('tests/mocks/FailingDSC.vy')
dsce_vulnerable = boa.load(
'src/dsc_engine.vy',
[weth.address, weth.address],
[eth_usd.address, eth_usd.address],
failing_dsc.address
)
weth.mint(USER, DEPOSIT_AMOUNT)
with boa.env.prank(USER):
weth.approve(dsce_vulnerable.address, DEPOSIT_AMOUNT)
dsce_vulnerable.deposit_collateral(weth, DEPOSIT_AMOUNT)
dsce_vulnerable.mint_dsc(MINT_AMOUNT)
minted_amount = dsce_vulnerable.user_to_dsc_minted(USER)
actual_balance = failing_dsc.balanceOf(USER)
print(f"Internal record of minted DSC: {minted_amount/1e18}")
print(f"Actual DSC balance: {actual_balance/1e18}")
assert minted_amount != actual_balance, "State should be inconsistent due to failed mint"
assert minted_amount > 0, "Internal state updated despite failed mint"
assert actual_balance == 0, "No tokens should have been minted"
Mock DSC to be added in tests/mocks
"""
@title Mock DSC that fails silently on mint
"""
from ethereum.ercs import IERC20
implements: IERC20
totalSupply: public(uint256)
balanceOf: public(HashMap[address, uint256])
allowance: public(HashMap[address, HashMap[address, uint256]])
@external
def transfer(_to: address, _value: uint256) -> bool:
return True
@external
def transferFrom(_from: address, _to: address, _value: uint256) -> bool:
return True
@external
def approve(_spender: address, _value: uint256) -> bool:
return True
@external
def mint(to: address, amount: uint256) -> bool:
return False
@external
def burn_from(from_: address, amount: uint256) -> bool:
return False
Tools Used
Manual Code Review
AI (Chat GPT, Claude)
Recommendations
** Check Balances Before and After Operations:** Verify token balances before and after mint and burn operations to ensure proper execution.
Corrected Implementation Example:
@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)
# Get initial balance
initial_balance_response: Bytes[32] = raw_call(
DSC.address,
abi_encode(msg.sender, method_id=method_id("balanceOf(address)")),
max_outsize=32,
revert_on_failure=True
)
initial_balance: uint256 = convert(initial_balance_response, uint256)
# Attempt to mint tokens
extcall DSC.mint(msg.sender, amount_dsc_to_mint)
# Get final balance
final_balance_response: Bytes[32] = raw_call(
DSC.address,
abi_encode(msg.sender, method_id=method_id("balanceOf(address)")),
max_outsize=32,
revert_on_failure=True
)
final_balance: uint256 = convert(final_balance_response, uint256)
assert final_balance == initial_balance + amount_dsc_to_mint, "DSCEngine_MintingFailed"
This implementation ensures accurate balance validation before and after mint operations, minimizing the risk of state inconsistency.