Root + Impact
Description
-
The protocol should credit users with the actual amount of collateral received by the engine.
-
_deposit_collateral records amount_collateral before calling transferFrom and never compares the engine's balance before and after the transfer. For fee-on-transfer collateral, the engine receives less than amount_collateral but credits the user with the full amount. The user can then mint DSC against collateral that the protocol does not actually hold.
# src/dsc_engine.vy
@internal
def _deposit_collateral(
token_collateral_address: address, amount_collateral: uint256
):
assert amount_collateral > 0, "DSCEngine_NeedsMoreThanZero"
assert self.token_address_to_price_feed[token_collateral_address] != empty(
address
), "DSCEngine__TokenNotAllowed"
@> self.user_to_token_address_to_amount_deposited[msg.sender][
@> token_collateral_address
@> ] += amount_collateral
log CollateralDeposited(msg.sender, amount_collateral)
@> success: bool = extcall IERC20(token_collateral_address).transferFrom(
@> msg.sender, self, amount_collateral
@> )
assert success, "DSCEngine_TransferFailed"
Risk
Likelihood:
-
This occurs when the codebase is forked with a supported collateral token that charges a transfer fee.
-
The README states that the codebase should work after swapping WETH and WBTC for any basket of assets.
Impact:
-
The engine overstates user collateral balances and lets users mint more DSC than the protocol's real collateral supports.
-
Protocol solvency can be reduced because recorded collateral exceeds actual token balances held by the engine.
Proof of Concept
def test_fee_on_transfer_collateral_is_overcredited():
user_requested_deposit = 100 * 10**18
transfer_fee = 10 * 10**18
actual_received_by_engine = user_requested_deposit - transfer_fee
credited_to_user = user_requested_deposit
assert credited_to_user > actual_received_by_engine
assert credited_to_user == 100 * 10**18
assert actual_received_by_engine == 90 * 10**18
With a $1 collateral token, the engine values the user at $100 of collateral while holding only $90. At a 50% threshold, the user can mint against $50 of effective collateral, while the protocol only has $45 of threshold-adjusted real collateral.
Recommended Mitigation
- self.user_to_token_address_to_amount_deposited[msg.sender][
- token_collateral_address
- ] += amount_collateral
- success: bool = extcall IERC20(token_collateral_address).transferFrom(
- msg.sender, self, amount_collateral
- )
- assert success, "DSCEngine_TransferFailed"
+ balance_before: uint256 = staticcall IERC20(token_collateral_address).balanceOf(self)
+ success: bool = extcall IERC20(token_collateral_address).transferFrom(
+ msg.sender, self, amount_collateral
+ )
+ assert success, "DSCEngine_TransferFailed"
+ balance_after: uint256 = staticcall IERC20(token_collateral_address).balanceOf(self)
+ actual_received: uint256 = balance_after - balance_before
+ assert actual_received > 0, "DSCEngine_NeedsMoreThanZero"
+ self.user_to_token_address_to_amount_deposited[msg.sender][
+ token_collateral_address
+ ] += actual_received
Credit the user based on the actual token balance increase, or explicitly reject fee-on-transfer and rebasing collateral tokens.