Algo Ssstablecoinsss

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

[H] If the token is deflationary, the deposit calculation should use the balance difference.

Root + Impact

Description

  • The user deposits a certain amount of tokens, and the contract will also record the amount of tokens deposited; the actual deposited amount must match the recorded data.

  • If the token is a deflationary token, the actual deposited amount must be reduced by the fee; if the contract still records the previous deposited amount, this will cause a discrepancy between the records and the actual amount.

// Root cause in the codebase with @> marks to highlight the relevant section
def _deposit_collateral(
token_collateral_address: address, amount_collateral: uint256
):
self.user_to_token_address_to_amount_deposited[msg.sender][
token_collateral_address
] += amount_collateral //@>The calculation here should use the difference in balances.

Risk

Likelihood:

  • As long as the token is a deflationary token, accounting records will be misstated.

Impact:

  • Inflated Health Factor → Undercollateralized Minting

  • Redemption Shortfall → Last-Withdrawer Insolvency

  • Liquidation Failure → Bad Debt Accumulation

Proof of Concept

  1. Create a mock of a deflationary token

  2. The user deposits 10 tokens, but the contract actually receives only 9.9 (1% fee deducted).

  3. But the protocol ledger records 10, and get_account_collateral_value returns an inflated collateral value.

  4. The health factor is calculated based on inflated values, allowing the protocol to permit users to mint DSC beyond what the actual collateral supports.

# pragma version 0.4.0
"""
@title mock_fee_token
@license MIT
@notice A mock ERC-20 that deducts a 1% fee on every transferFrom,
used to test fee-on-transfer (deflationary token) compatibility.
"""
from ethereum.ercs import IERC20
implements: IERC20
from ethereum.ercs import IERC20Detailed
implements: IERC20Detailed
from snekmate.auth import ownable as ow
initializes: ow
from snekmate.tokens import erc20
initializes: erc20[ownable := ow]
exports: (
erc20.totalSupply,
erc20.balanceOf,
erc20.allowance,
erc20.transfer,
erc20.approve,
erc20.permit,
erc20.DOMAIN_SEPARATOR,
erc20.nonces,
erc20.name,
erc20.symbol,
erc20.decimals,
)
NAME: constant(String[25]) = "Mock Fee Token"
SYMBOL: constant(String[5]) = "MFEE"
DECIMALS: constant(uint8) = 18
EIP712_VERSION: constant(String[20]) = "1"
FEE_BPS: constant(uint256) = 100 # 1% fee (100 basis points out of 10000)
@deploy
def __init__():
ow.__init__()
erc20.__init__(NAME, SYMBOL, DECIMALS, NAME, EIP712_VERSION)
@external
def mock_mint():
erc20._mint(msg.sender, 10 * 10**18)
@external
def mint_amount(amount: uint256):
erc20._mint(msg.sender, amount)
@external
def transferFrom(sender: address, receiver: address, amount: uint256) -> bool:
"""
@notice Transfer with 1% fee: fee is burned, receiver gets amount - fee.
"""
erc20._spend_allowance(sender, msg.sender, amount)
fee: uint256 = amount * FEE_BPS // 10000
net_amount: uint256 = amount - fee
erc20._burn(sender, fee)
erc20._transfer(sender, receiver, net_amount)
return True
def test_poc_deflationary_token(eth_usd, some_user):
"""
POC: Fee-on-transfer token causes ledger balance > real contract balance.
mock_fee_token charges 1% on every transferFrom.
The protocol records the nominal deposit amount, but only receives 99%.
This inflates the health factor and allows undercollateralized minting.
"""
from src import decentralized_stable_coin
from src.mocks import mock_fee_token
# Deploy a fresh fee-on-transfer collateral token and price feed
fee_token = mock_fee_token.deploy()
with boa.env.prank(some_user):
fee_token.mock_mint() # mints 10e18 to some_user
# Deploy DSC + engine using fee_token as collateral
poc_dsc = decentralized_stable_coin.deploy()
# Reuse eth_usd price feed: 1 fee_token = $2000 (same as ETH mock price)
from src.mocks import mock_token
dummy_token = mock_token.deploy() # second slot placeholder
from src.mocks import MockV3Aggregator
dummy_feed = MockV3Aggregator.deploy(8, 2000 * 10**8)
poc_dsce = dsc_engine.deploy([fee_token, dummy_token], [eth_usd, dummy_feed], poc_dsc)
poc_dsc.transfer_ownership(poc_dsce)
deposit_amount = to_wei(10, "ether") # user tries to deposit 10 tokens
with boa.env.prank(some_user):
fee_token.approve(poc_dsce, deposit_amount)
poc_dsce.deposit_collateral(fee_token, deposit_amount)
# Ledger says 10 tokens, but contract only received 9.9 (1% fee burned)
ledger_amount = poc_dsce.get_collateral_balance_of_user(some_user, fee_token)
actual_balance = fee_token.balanceOf(poc_dsce)
# Bug: ledger overstates holdings by the fee amount
assert ledger_amount > actual_balance, (
f"Expected ledger ({ledger_amount}) > actual ({actual_balance})"
)
assert actual_balance == deposit_amount * 99 // 100, (
f"Expected 1% fee deducted, got {actual_balance}"
)
# Impact: health factor is computed on the inflated ledger amount
reported_collateral_usd = poc_dsce.get_account_collateral_value(some_user)
real_collateral_usd = poc_dsce.get_usd_value(fee_token, actual_balance)
assert reported_collateral_usd > real_collateral_usd, (
"Reported collateral USD should exceed the real on-chain value"
)
# Max mintable DSC based on inflated vs real collateral (50% threshold)
max_mint_reported = reported_collateral_usd * 50 // 100
max_mint_real = real_collateral_usd * 50 // 100
assert max_mint_reported > max_mint_real, (
"Exploit confirmed: protocol allows minting more DSC than real collateral supports"
)

Recommended Mitigation

Apply the balance-difference pattern to record only the actual received amount

def _deposit_collateral(
token_collateral_address: address, amount_collateral: uint256
):
+ 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
+ self.user_to_token_address_to_amount_deposited[msg.sender][token_collateral_address] += actual_received
- 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)
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 8 days 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!