Root + Impact
fundContest transfers contestToTotalRewards[pot] on every call with no funded-once guard and no post-transfer balance check. remainingRewards is fixed at construction, so any extra tokens are unaccounted and become permanently stranded (or drained by repeated closes).
Description
function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
IERC20 token = pot.getToken();
uint256 totalRewards = contestToTotalRewards[address(pot)];
if (token.balanceOf(msg.sender) < totalRewards) {
revert ContestManager__InsufficientFunds();
}
token.transferFrom(msg.sender, address(pot), totalRewards);
}
Risk
Likelihood: Calling fundContest twice on the same pot.
Impact: Excess tokens are stranded in the pot and never claimable.
Proof of Concept
function testRepeatedFundingStrandsTokens() public {
Pot pot = _createFundClose(100e18, 100e18, 200e18);
vm.prank(owner);
manager.fundContest(0);
assertEq(token.balanceOf(address(pot)), 400e18, "pot holds double the accounted total");
}
Recommended Mitigation
+ mapping(address => bool) public contestFunded;
function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
+ require(!contestFunded[address(pot)], "already funded");
+ contestFunded[address(pot)] = true;
...
+ uint256 before = token.balanceOf(address(pot));
token.transferFrom(msg.sender, address(pot), totalRewards);
+ require(token.balanceOf(address(pot)) - before == totalRewards, "unexpected received amount");
}