Algo Ssstablecoinsss

AI First Flight #2
Beginner FriendlyDeFi
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Fee-on-transfer token accounting mismatch

Root + Impact

The protocol records the requested deposit amount before transferring tokens, but fee-on-transfer tokens (e.g., USDT with enabled fees) deduct fees during transfer, causing the contract to receive less than recorded, creating undercollateralization.

Description

  • The protocol allows users to deposit whitelisted ERC20 collateral tokens by recording the deposit amount in state, then calling transferFrom() to pull tokens from the user.

  • The accounting records the full requested amount before transfer, but fee-on-transfer tokens deduct a percentage during the transfer operation, resulting in the contract receiving fewer tokens than recorded in state.

# dsc_engine.vy lines 215-230
@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 # @> Records full amount BEFORE transfer
log CollateralDeposited(msg.sender, amount_collateral)
success: bool = extcall IERC20(token_collateral_address).transferFrom(
msg.sender, self, amount_collateral
) # @> Transfer deducts fee, contract receives less
assert success, "DSCEngine_TransferFailed"
# @> No verification that received amount matches recorded amount

Risk

Likelihood:

  • Protocol whitelists a token with transfer fees (e.g., USDT enables 0.1% fee, or wrapped tokens with management fees), making every deposit create accounting mismatches.

  • Users deposit such tokens repeatedly over time, accumulating discrepancies between recorded collateral and actual token balance held by contract.

Impact:

  • Protocol records more collateral than actually held, allowing users to mint more DSC than their real collateral supports, creating systemic undercollateralization.

  • Withdrawal operations fail when actual balance is insufficient to cover recorded amounts, causing user funds to become locked or protocol insolvency during mass withdrawals.

Proof of Concept

# Assume USDT-like token with 1% transfer fee
# User deposits 100 USDT
user_balance_before = fee_token.balanceOf(user) # 1000 USDT
contract_balance_before = fee_token.balanceOf(dsce) # 0 USDT
with boa.env.prank(user):
fee_token.approve(dsce, 100e18)
dsce.deposit_collateral(fee_token, 100e18)
# State after deposit:
user_balance_after = fee_token.balanceOf(user) # 900 USDT (1000 - 100)
contract_balance_after = fee_token.balanceOf(dsce) # 99 USDT (100 - 1% fee)
recorded_collateral = dsce.get_collateral_balance_of_user(user, fee_token) # 100 USDT
# MISMATCH:
# Recorded: 100 USDT
# Actual: 99 USDT
# Diff: 1 USDT lost
# Scale this across 1000 users:
# Recorded: 100,000 USDT
# Actual: 99,000 USDT
# Protocol is 1,000 USDT undercollateralized
# Attack: User mints DSC based on 100 USDT recorded
# But can only be liquidated for 99 USDT actual
# 1% of collateral is phantom value

Recommended Mitigation

# 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"
+ # Check balance before transfer
+ balance_before: uint256 = staticcall IERC20(token_collateral_address).balanceOf(self)
- 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"
+ # Check balance after transfer and record actual amount received
+ balance_after: uint256 = staticcall IERC20(token_collateral_address).balanceOf(self)
+ actual_amount: uint256 = balance_after - balance_before
+
+ # Record only what was actually received
+ self.user_to_token_address_to_amount_deposited[msg.sender][
+ token_collateral_address
+ ] += actual_amount
+ log CollateralDeposited(msg.sender, actual_amount)

Alternative: Document that fee-on-transfer tokens are not supported and should not be whitelisted.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!