Back to Lessons
Lesson 10

Fee-on-Transfer & Rebasing Tokens

+20EFFORT
7 sections
transfer()
01
text

Most smart-contract code rests on two hidden assumptions: that the amount you send equals the amount received, and that a balance only changes when a transfer happens. Two families of tokens break these assumptions and have drained real protocols: fee-on-transfer tokens and rebasing (elastic supply) tokens.

02
code
solidity
1// A fee-on-transfer token skims a fee on every move:
2function _transfer(address from, address to, uint256 amount) internal {
3 uint256 fee = amount * 2 / 100; // 2% fee
4 uint256 sent = amount - fee;
5 _balances[from] -= amount;
6 _balances[to] += sent; // recipient gets LESS than 'amount'
7 _balances[feeWallet] += fee;
8 emit Transfer(from, to, sent);
9 emit Transfer(from, feeWallet, fee);
10}
03
text

Now imagine a vault that calls token.transferFrom(user, vault, 100) and then credits the user 100 shares. With a 2% fee the vault only actually received 98 tokens, but it minted shares worth 100. Repeat across many deposits and the vault owes more than it holds — the last users to withdraw find the cupboard bare. The fix is to never trust the requested amount; measure what actually arrived.

04
code
solidity
1// Balance-delta pattern: trust the scale, not the label.
2function deposit(IERC20 token, uint256 amount) external {
3 uint256 balBefore = token.balanceOf(address(this));
4 token.safeTransferFrom(msg.sender, address(this), amount);
5 uint256 received = token.balanceOf(address(this)) - balBefore;
6
7 // Credit shares based on 'received', NOT 'amount'.
8 _mintShares(msg.sender, received);
9}
05
note
Key Insight

Rebasing tokens are even sneakier. With stETH or AMPL, your balanceOf() can go UP or down over time on its own — no transfer by you, and often no per-holder Transfer event. A balance you cached in a state variable last week is now wrong, and any logic comparing a stored snapshot against the live balanceOf() will misbehave.

06
text

Rebasing tokens work by tracking internal 'shares' plus a global scaling factor: balanceOf() = shares * scalingFactor. When the protocol rebases, it changes the scaling factor for everyone at once, so individual balances move without any Transfer event. This is why integrators either refuse rebasing tokens, read the live balance every single time (never cache), or use a wrapped non-rebasing version such as wstETH whose balance is fixed and whose value instead accrues through an exchange rate.

07
text

Integration takeaways: (1) For deposits, always compute the received amount via the balance delta. (2) Never cache a token balance you do not control. (3) Decide explicitly whether your protocol supports fee-on-transfer or rebasing tokens, and enforce that choice — many top protocols simply blocklist them, because supporting every exotic token safely is genuinely hard.

Complete

Connect your wallet to mark this lesson as complete

Earn 20 EFFORT tokens