pragma solidity ^0.8.20;
import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract TestToken is ERC20 {
constructor() ERC20("TestToken", "TEST") {
_mint(msg.sender, 1_000_000 * 1e18);
}
}
contract VulnerablePot is Ownable(msg.sender) {
address[] private i_players;
address[] private claimants;
uint256 private immutable i_totalRewards;
uint256 private immutable i_deployedAt;
TestToken private immutable i_token;
mapping(address => uint256) private playersToRewards;
uint256 public remainingRewards;
uint256 private constant managerCutPercent = 10;
constructor(
address[] memory players,
uint256[] memory rewards,
uint256 totalRewards
) {
i_players = players;
i_token = new TestToken();
i_totalRewards = totalRewards;
remainingRewards = totalRewards;
i_deployedAt = block.timestamp - 100 days;
for (uint256 i = 0; i < players.length; i++) {
playersToRewards[players[i]] = rewards[i];
}
}
function claimCut() public {
address player = msg.sender;
uint256 reward = playersToRewards[player];
if (reward <= 0) revert("No reward");
playersToRewards[player] = 0;
remainingRewards -= reward;
claimants.push(player);
i_token.transfer(player, reward);
}
function closePot() external onlyOwner {
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
for (uint256 i = 0; i < claimants.length; i++) {
i_token.transfer(claimants[i], claimantCut);
}
}
}
function fund() external onlyOwner {
i_token.transfer(address(this), i_totalRewards);
}
}
contract POCDemo {
VulnerablePot public pot;
function deployPot() public returns (VulnerablePot) {
address[] memory players = new address[](10);
uint256[] memory rewards = new uint256[](10);
for (uint256 i = 0; i < 10; i++) {
players[i] = address(uint160(0x100 + i));
rewards[i] = 100;
}
pot = new VulnerablePot(players, rewards, 1000);
pot.fund();
return pot;
}
function demonstrateBug() public {
pot.claimCut();
pot.claimCut()
pot.claimCut();
uint256 beforeCloseBalanceOwner = TestToken(pot.i_token()).balanceOf(address(this));
uint256 remainingBefore = pot.remainingRewards();
pot.closePot();
uint256 managerCut = 700 / 10;
uint256 expectedLeftoverPerClaimant = (700 - 70) / 3; claimant (630 total)
uint256 buggyLeftoverPerClaimant = (700 - 70) / 10;
uint256 tokensProcessed = 70 + (63 * 3);
uint256 tokensLost = 700 - tokensProcessed;
console.log("Remaining before close:", remainingBefore);
console.log("Manager cut (10%):", managerCut);
console.log("Buggy cut per claimant (intended /10 players):", 63);
console.log("Total distributed to claimants:", 63 * 3);
console.log("TOKENS LOST FOREVER:", tokensLost);
}
}