None of the state-changing external functions in dsc_engine.vy use the @nonreentrant decorator:
The protocol description states: "someone could fork this codebase, swap out WETH & WBTC for any basket of assets they like, and the code would work the same."
While WETH and WBTC are standard ERC-20 tokens without callbacks, if the code is forked with ERC-777 tokens or other callback-enabled tokens, reentrancy attacks become possible.
The most critical reentrancy vector is in liquidate() (lines 112-135):
With an ERC-777 collateral token, step 3's IERC20.transfer() triggers a tokensReceived callback on the liquidator before step 4 executes. At this point, the user's collateral has been reduced but their debt has NOT been reduced, making their health factor even worse. A malicious liquidator contract could re-enter liquidate() again, extracting additional collateral.
Likelihood: Low -- Current deployment uses WETH/WBTC which have no callbacks. However, the protocol is explicitly designed to be forked with different tokens.
Impact: High -- With callback-enabled tokens, a malicious liquidator could drain the liquidated user's collateral through repeated re-entrant liquidation calls before debt is ever reduced.
Real-World Precedent:
Vyper @nonreentrant Lock Bypass / Curve Finance ($73.5M, 2023): Vyper compiler bug in versions 0.2.15-0.3.0 caused reentrancy. Note: Vyper 0.4.0 is NOT affected by this bug.
Lendf.Me ($25M, 2020): ERC-777 callback reentrancy in supply/borrow functions.
How the attack works (with ERC-777 collateral fork):
Victim has an underwater position: 10 ERC777_TOKEN collateral, 5000 DSC debt
Attacker deploys a malicious liquidator contract with a tokensReceived callback
Attacker calls liquidate(ERC777_TOKEN, victim, 2500) via the malicious contract
_redeem_collateral transfers collateral to the attacker contract
ERC-777 tokensReceived callback fires on the attacker contract
Attacker re-enters liquidate(ERC777_TOKEN, victim, 2500) again
Second liquidation proceeds: victim's health factor is even worse (collateral reduced, debt unchanged)
Total: attacker burned 5000 DSC but extracted double the collateral + bonus
Expected outcome: Attacker drains excess collateral from the victim's position.
Add @nonreentrant to all state-changing external functions:
Additionally, consider reordering liquidate() to follow strict CEI (Checks-Effects-Interactions): reduce debt BEFORE transferring collateral.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.