Description Weird ERC20 tokens charge a cut on every other transaction they process. The fee cut is either sent to the zero address or to the DAO of that particular token contract.
When a player claims reward by calling the claimCut::Pot.sol
function, the contract calls the _transferReward
internal function which transfers the assigned reward share to player's address.
The amount that is sent out is not what is received by the player. FeeOnTransfer
tokens take a cut of the amount while the rest is transfered to the player.
Impact There is a mismatch in the amount sent out as reward to the amount received. The player does not receive his intended cut.
Proof of Concept
WeirdERC20.sol
FeeOnTransfer contract is a deflationary token that charges 10 percent cut on every 4th transaction.
pragma solidity 0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract WeirdERC20 is ERC20 {
uint256 public constant INITIAL_SUPPLY = 1_000_000e18;
uint256 public count = 0;
uint256 public constant FEE = 10;
uint256 public constant USER_AMOUNT = 90;
uint256 public constant PRECISION = 100;
constructor() ERC20("WeirdERC20", "W20") {
_mint(msg.sender,INITIAL_SUPPLY);
}
function mint(address to,uint256 amount)public{
_mint(to,amount);
}
function _update(address from, address to, uint256 value) internal virtual override {
if (count >= 3) {
uint256 userAmount = value * USER_AMOUNT / PRECISION;
uint256 feeAmount = value * FEE / PRECISION;
count = 0;
super._update(from, to, userAmount);
super._update(from, address(0), feeAmount);
} else {
count++;
super._update(from, to, value);
}
}
}
Place the following test in TestMyCut.t.sol
:
function test_FeeOnTransferTokens()public{
vm.startPrank(user);
WeirdERC20 w20 = new WeirdERC20();
address conMan2 = address(new ContestManager());
address pot = ContestManager(conMan2).createContest(players,rewards,w20,totalRewards);
w20.approve(conMan2,type(uint256).max);
ContestManager(conMan2).fundContest(0);
vm.stopPrank();
vm.prank(player1);
Pot(pot).claimCut();
console.log("Player2 expected reward:",Pot(pot).checkCut(player2));
vm.prank(player2);
Pot(pot).claimCut();
assertEq(Pot(pot).getToken().balanceOf(player1),3e18);
console.log("Player2 actual reward:",Pot(pot).getToken().balanceOf(player2));
assertEq(Pot(pot).getToken().balanceOf(player2),0.9e18);
}
Player2 expected reward: 1000000000000000000 = 1 token
Player2 actual reward: 900000000000000000 = 0.9 token