Algo Ssstablecoinsss

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

Deposit accounting credits the requested amount, not the amount received, so fee-on-transfer collateral creates phantom balances and insolvency

Deposit accounting credits the requested amount, not the amount received, so fee-on-transfer collateral creates phantom balances and insolvency

Severity: Medium · Impact: Medium · Likelihood: Medium

Description

  • _deposit_collateral records the user's collateral by the amount_collateral argument and then pulls the tokens with transferFrom.

  • It never measures how many tokens actually arrived. For a fee-on-transfer (or rebasing) token the contract receives less than amount_collateral, but the books credit the full amount — so the engine believes it holds collateral it does not.

self.user_to_token_address_to_amount_deposited[msg.sender][
token_collateral_address
@> ] += amount_collateral # credits the requested amount...
log CollateralDeposited(msg.sender, amount_collateral)
success: bool = extcall IERC20(token_collateral_address).transferFrom(
@> msg.sender, self, amount_collateral # ...but may receive less than this
)
assert success, "DSCEngine_TransferFailed"

Risk

Likelihood:

  • Occurs whenever a supported collateral takes a transfer fee or rebases — a first-class scenario because the protocol is explicitly designed to be forked with "any basket of assets."

Impact:

  • The engine's recorded collateral exceeds its real token balance, so DSC is over-minted against collateral that isn't there, pushing the system toward undercollateralization.

  • Redemptions/liquidations draw against the phantom balance until the real tokens run out; the last users to withdraw cannot (their transfer out reverts), stranding funds.

Proof of Concept

Save the block below as tests/poc_m1.py inside the cloned repo and run mox test tests/poc_m1.py. The fee-on-transfer collateral is compiled inline via boa.loads; everything else uses the contest's own files. A 10-token deposit credits 10 but the engine holds only 9, and the user can no longer redeem the booked balance.

import boa
from eth_utils import to_wei
from src import dsc_engine, decentralized_stable_coin
from src.mocks import mock_token, MockV3Aggregator
# A minimal 18-decimal ERC20 that charges a 10% fee on transferFrom, compiled inline.
FEE_TOKEN_SRC = """
# pragma version 0.4.0
from ethereum.ercs import IERC20
implements: IERC20
balanceOf: public(HashMap[address, uint256])
allowance: public(HashMap[address, HashMap[address, uint256]])
totalSupply: public(uint256)
@external
def transfer(_to: address, _v: uint256) -> bool:
self.balanceOf[msg.sender] -= _v
self.balanceOf[_to] += _v
return True
@external
def transferFrom(_f: address, _t: address, _v: uint256) -> bool:
self.allowance[_f][msg.sender] -= _v
fee: uint256 = (_v * 1000) // 10000 # 10% fee
self.balanceOf[_f] -= _v
self.balanceOf[_t] += (_v - fee) # recipient gets less; fee is burned
self.totalSupply -= fee
return True
@external
def approve(_s: address, _v: uint256) -> bool:
self.allowance[msg.sender][_s] = _v
return True
@external
@view
def decimals() -> uint8:
return 18
@external
def mint_amount(_v: uint256):
self.balanceOf[msg.sender] += _v
self.totalSupply += _v
"""
def test_engine_credits_more_than_received():
dsc = decentralized_stable_coin.deploy()
fee_tok = boa.loads(FEE_TOKEN_SRC) # 10% fee-on-transfer collateral
weth = mock_token.deploy()
price = MockV3Aggregator.deploy(8, 2_000 * 10**8)
eth_usd = MockV3Aggregator.deploy(8, 2_000 * 10**8)
engine = dsc_engine.deploy(
[fee_tok.address, weth.address], [price.address, eth_usd.address], dsc
)
dsc.set_minter(engine.address, True)
dsc.transfer_ownership(engine.address)
user = boa.env.generate_address("fee_user")
deposit = to_wei(10, "ether")
with boa.env.prank(user):
fee_tok.mint_amount(deposit)
fee_tok.approve(engine.address, deposit)
engine.deposit_collateral(fee_tok.address, deposit)
# Engine credited the full 10 tokens...
assert engine.get_collateral_balance_of_user(user, fee_tok.address) == deposit
# ...but only actually received 9 (10% fee burned in transfer).
assert fee_tok.balanceOf(engine.address) == to_wei(9, "ether")
# The 1-token gap is phantom collateral. The user cannot redeem the booked balance:
with boa.env.prank(user):
with boa.reverts():
engine.redeem_collateral(fee_tok.address, deposit)

Recommended Mitigation

Credit the balance actually received by measuring the engine's token balance before and after the transfer.

+ 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"
+ received: uint256 = staticcall IERC20(token_collateral_address).balanceOf(self) - balance_before
self.user_to_token_address_to_amount_deposited[msg.sender][
token_collateral_address
- ] += amount_collateral
+ ] += received
- log CollateralDeposited(msg.sender, amount_collateral)
+ log CollateralDeposited(msg.sender, token_collateral_address, received)

Alternatively, maintain an explicit allow-list of standard, non-fee, 18-decimal tokens and document the restriction (which also narrows the fork claim).

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!