Root + Impact
Integer division in price conversions and health factor calculations truncates fractional values, causing users to lose dust amounts that accumulate over many operations and can be exploited through repeated small transactions.
Description
-
The protocol converts between token amounts and USD values using integer division with 18-decimal precision, calculating collateral values and health factors for all operations.
-
Integer division rounds down (truncates), causing precision loss on every conversion, where small amounts lose proportionally more value and repeated operations accumulate losses.
# dsc_engine.vy line 313-315
@internal
@view
def _get_usd_value(token: address, amount: uint256) -> uint256:
# ... get price ...
return (
(convert(price, uint256) * ADDITIONAL_FEED_PRECISION) * amount
) // PRECISION
# dsc_engine.vy line 325-327
@internal
@pure
def _calculate_health_factor(total_dsc_minted: uint256, collateral_value_in_usd: uint256) -> uint256:
collateral_adjusted_for_threshold: uint256 = (
collateral_value_in_usd * LIQUIDATION_THRESHOLD
) // LIQUIDATION_PRECISION
return (collateral_adjusted_for_threshold * (10**18))
# dsc_engine.vy line 362-365
@internal
@view
def _get_token_amount_from_usd(token: address, usd_amount_in_wei: uint256) -> uint256:
# ... get price ...
return (
(usd_amount_in_wei * PRECISION)
convert(price, uint256) * ADDITIONAL_FEED_PRECISION
)
) # @> Division truncates
Risk
Likelihood:
-
Every price conversion and health factor calculation performs integer division, affecting all deposits, mints, redeems, and liquidations.
-
Users performing many small transactions accumulate more precision loss than single large transactions for the same total amount.
Impact:
-
Small amounts round to lower values, users lose dust on every operation (e.g., 999 wei becomes 0 after conversion).
-
Attackers exploit by performing thousands of tiny operations to extract accumulated truncated amounts favoring protocol or themselves.
Proof of Concept
tiny_eth = 100
usd_value = dsce.get_usd_value(weth, tiny_eth)
total_loss = 0
for i in range(1000):
deposited = 1000 * 1e18 + 999
usd_val = dsce.get_usd_value(weth, deposited)
eth_back = dsce.get_token_amount_from_usd(weth, usd_val)
loss = deposited - eth_back
total_loss += loss
Recommended Mitigation
# dsc_engine.vy
+MIN_COLLATERAL_AMOUNT: constant(uint256) = 1000 # Minimum 1000 wei
+MIN_DSC_AMOUNT: constant(uint256) = 1 * 10**18 # Minimum 1 DSC
@internal
def _deposit_collateral(token_collateral_address: address, amount_collateral: uint256):
- assert amount_collateral > 0, "DSCEngine_NeedsMoreThanZero"
+ assert amount_collateral >= MIN_COLLATERAL_AMOUNT, "DSCEngine_AmountTooSmall"
# ... rest of function
@internal
def _mint_dsc(amount_dsc_to_mint: uint256):
- assert amount_dsc_to_mint > 0, "DSCEngine__NeedsMoreThanZero"
+ assert amount_dsc_to_mint >= MIN_DSC_AMOUNT, "DSCEngine_AmountTooSmall"
# ... rest of function