MyCut

AI First Flight #8
Beginner FriendlyFoundry
EXP
View results
Submission Details
Impact: high
Likelihood: medium
Invalid

Blocklisted address can cause a DoS when distributing remaining rewards

External calls to transfer tokens are executed within a loop, where a single transfer failure causes the entire transaction to revert, blocking all distributions

Description

  • Normal behavior - When closing the pot if there are remainign rewards they should be distributed to the users who claimed on time

  • Issue- The rewards distribution pattern is going to fail if one of the users is having his address blocklisted and the token supports blocklisting.

@> for (uint256 i = 0; i < claimants.length; i++) {
//@audit blocklisted users can DoS the distribution
_transferReward(claimants[i], claimantCut);
}

Risk

Likelihood:

  • Likelihood : Medium - Not all of the tokens support blocklisting functionality but some that are really widely used such as USDC do support it. The issue will happen when the owner creates a pot with a token that has a blocklisting functionality

Impact:

  • Denial of Service that prevents eligible users from claiming their additional rewards due to transaction reversion.


Proof of Concept

  1. Owner creates a contest with an erc20 token that supports blocklisting

  2. Before the deadline of 90 days some of the participans claim their cut

  3. At some point an address of the claimers becomes a blocklisted address from the token that is used in the conetst for reward distribution.

  4. After the deadline period owner closes the pot and additional rewards should be transfered to the eligible claimers since one of them is blocklisted, no one will be able to receive additional rewards.


// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract ERC20Weird is ERC20 {
// Fee configuration (in basis points, e.g., 100 = 1%)
uint256 public transferFeePercent = 100; // 1% fee
address public feeCollector;
// Blocklist
mapping(address => bool) public isBlocked;
constructor(string memory name, string memory symbol, address initialAccount, uint256 initialBalance)
payable
ERC20(name, symbol)
{
_mint(initialAccount, initialBalance);
feeCollector = initialAccount;
}
// Override to use 6 decimals like USDC
function decimals() public pure override returns (uint8) {
return 6;
}
function mint(address account, uint256 amount) public {
_mint(account, amount);
}
function burn(address account, uint256 amount) public {
_burn(account, amount);
}
function transferInternal(address from, address to, uint256 value) public {
_transfer(from, to, value);
}
function approveInternal(address owner, address spender, uint256 value) public {
_approve(owner, spender, value);
}
function addToBlocklist(address account) public {
isBlocked[account] = true;
}
function removeFromBlocklist(address account) public {
isBlocked[account] = false;
}
function setTransferFee(uint256 _feePercent) public {
require(_feePercent <= 1000, "Fee too high");
transferFeePercent = _feePercent;
}
function setFeeCollector(address _feeCollector) public {
feeCollector = _feeCollector;
}
// Override transfer
function transfer(address to, uint256 amount) public override returns (bool) {
address owner = _msgSender();
require(!isBlocked[owner], "Sender is blocked");
require(!isBlocked[to], "Recipient is blocked");
if (transferFeePercent > 0 && owner != feeCollector && to != feeCollector) {
uint256 fee = (amount * transferFeePercent) / 10000;
uint256 amountAfterFee = amount - fee;
_transfer(owner, feeCollector, fee);
_transfer(owner, to, amountAfterFee);
} else {
_transfer(owner, to, amount);
}
return true;
}
// Override transferFrom
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
require(!isBlocked[from], "Sender is blocked");
require(!isBlocked[to], "Recipient is blocked");
if (transferFeePercent > 0 && from != feeCollector && to != feeCollector) {
uint256 fee = (amount * transferFeePercent) / 10000;
uint256 amountAfterFee = amount - fee;
_transfer(from, feeCollector, fee);
_transfer(from, to, amountAfterFee);
} else {
_transfer(from, to, amount);
}
return true;
}
function balanceOf(address account) public view override returns (uint256) {
return super.balanceOf(account);
}
}
contract TestMyCut is Test {
address conMan;
address player1 = makeAddr("player1");
address player2 = makeAddr("player2");
address[] players = [player1, player2];
uint256 public constant STARTING_USER_BALANCE = 1000 ether;
ERC20Mock weth;
ERC20Weird tokenX;
address contest;
address[] totalContests;
uint256[] rewards = [3, 1];
address user = makeAddr("user");
uint256 totalRewards = 4;
uint256[] rewardsArr = [3, 3];
uint256[] rewardsArr1 = [50, 50];
function setUp() public {
vm.startPrank(user);
// DeployContestManager deploy = new DeployContestManager();
conMan = address(new ContestManager());
weth = new ERC20Mock("WETH", "WETH", msg.sender, 1000e8);
tokenX = new ERC20Weird("USDC", "USDC", msg.sender, 10_000e6);
// console.log("WETH Address: ", address(weth));
// console.log("Test Address: ", address(this));
console.log("User Address: ", user);
// (conMan) = deploy.run();
vm.stopPrank();
}
modifier mintAndApproveTokens() {
console.log("Minting tokens to: ", user);
vm.startPrank(user);
ERC20Mock(weth).mint(user, STARTING_USER_BALANCE);
ERC20Mock(weth).approve(conMan, STARTING_USER_BALANCE);
ERC20Weird(tokenX).mint(user, 10_000e6);
ERC20Weird(tokenX).approve(conMan, 10_000e6);
console.log("Approved tokens to: ", address(conMan));
vm.stopPrank();
_;
}
function test_blocklistedUserDoS() public mintAndApproveTokens {
address[] memory addresses = new address[](10);
uint256[] memory nums = new uint256[](10);
for (uint256 i = 0; i < 10; i++) {
addresses[i] = address(uint160(i + 1));
nums[i] = 50;
}
vm.startPrank(user);
contest = ContestManager(conMan).createContest(addresses, nums, IERC20(ERC20Weird(tokenX)), 10_000e6);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
assertEq(9900e6, ERC20Weird(tokenX).balanceOf(contest));
for (uint256 i = 0; i < 7; i++) {
vm.prank(addresses[i]);
Pot(contest).claimCut();
}
ERC20Weird(tokenX).addToBlocklist(addresses[0]);
vm.warp(91 days);
vm.prank(user);
vm.expectRevert();
ContestManager(conMan).closeContest(contest);
}
}

Recommended Mitigation

Introduce a mapping in the Pot contract where that mapping is going to store address -> bool -> uint256 amount respectively -> user address -> isClaimingAdditionalRewards -> amount . And then introduce a new function that is going to be called by the users and if they are in the mapping and have additional rewards to be able to pull them from the contract

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!