Root + Impact
The protocol records collateral balances at deposit time but rebasing tokens (stETH, aTokens) automatically change balances over time, causing recorded amounts to diverge from actual holdings and creating protocol insolvency or unfair liquidations.
Description
-
The protocol records user collateral deposits in a state mapping and uses this recorded amount for all collateral value calculations, health factor checks, and liquidations.
-
Rebasing tokens automatically adjust their balances in holder wallets (increase for rewards, decrease for slashing), but the protocol's recorded amounts remain static, causing mismatches between actual contract balance and accounting records.
# dsc_engine.vy lines 223-225
self.user_to_token_address_to_amount_deposited[msg.sender][
token_collateral_address
] += amount_collateral
# dsc_engine.vy lines 335-340
@internal
@view
def _get_account_collateral_value(user: address) -> uint256:
total_collateral_value_in_usd: uint256 = 0
for token: address in COLLATERAL_TOKENS:
amount: uint256 = self.user_to_token_address_to_amount_deposited[user][token]
total_collateral_value_in_usd += self._get_usd_value(token, amount)
return total_collateral_value_in_usd
# @> No check against actual balanceOf(this)
Risk
Likelihood:
-
Protocol whitelists rebasing tokens like stETH (Lido staked ETH with daily rewards) or aTokens (Aave interest-bearing tokens), where balance changes occur automatically every block.
-
Negative rebasing events occur during network slashing (stETH), liquidations (aTokens), or algorithmic adjustments, reducing actual token balances below recorded amounts.
Impact:
-
Negative rebase reduces contract's actual token balance below recorded amounts, creating phantom collateral where users can mint DSC against value that no longer exists, causing protocol insolvency.
-
Positive rebase increases contract balance above recorded amounts, with excess tokens becoming locked forever as accounting doesn't reflect gains, preventing users from claiming earned rewards.
Proof of Concept
with boa.env.prank(user):
steth.approve(dsce, 100e18)
dsce.deposit_collateral(steth, 100e18)
contract_balance = steth.balanceOf(dsce)
recorded = dsce.get_collateral_balance_of_user(user, steth)
contract_balance = steth.balanceOf(dsce)
recorded = dsce.get_collateral_balance_of_user(user, steth)
contract_balance = steth.balanceOf(dsce)
recorded = dsce.get_collateral_balance_of_user(user, steth)
dsce.redeem_collateral(steth, 100e18)
Recommended Mitigation
# Document rebasing tokens are not supported
+# IMPORTANT: Do not whitelist rebasing tokens
+# Incompatible tokens:
+# - stETH (Lido Staked ETH) - rebases for rewards/slashing
+# - aTokens (Aave) - rebases for interest
+# - sTSLA (Synthetix) - rebases algorithmically
+# - Any token with elastic supply mechanics
+#
+# Only whitelist fixed-balance tokens:
+# - WETH (Wrapped ETH)
+# - WBTC (Wrapped Bitcoin)
+# - Standard ERC20 tokens without rebase logic
# Alternative: Query actual balance instead of recorded amount
@internal
@view
def _get_account_collateral_value(user: address) -> uint256:
total_collateral_value_in_usd: uint256 = 0
for token: address in COLLATERAL_TOKENS:
- amount: uint256 = self.user_to_token_address_to_amount_deposited[user][token]
+ # Use actual contract balance proportional to user's share
+ # This requires tracking shares instead of amounts (major refactor)
total_collateral_value_in_usd += self._get_usd_value(token, amount)
return total_collateral_value_in_usd
Recommended Approach: Explicitly document that rebasing tokens must NOT be whitelisted as collateral.