Some tokens (e.g. USDC, USDT) have a contract level admin controlled address blocklist. If an address is blocked, then transfers to and from that address are forbidden.
Open
This is the test written by protocol team inside test/integration/ProxyFactoryTest.t.sol file.
I am going to modify it to show the effect when any user will be blacklist.
here user1 is the winner and we are going to blacklist this address to see the effect.
function testSucceedsWhenConditionsAreMet()
public
setUpContestForJasonAndSentJpycv2Token(organizer)
{
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 0 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 0 ether);
bytes32 randomId_ = keccak256(abi.encode("Jason", "001"));
bytes memory data = createData();
vm.warp(9 days);
vm.startPrank(organizer);
proxyFactory.deployProxyAndDistribute(
randomId_,
address(distributor),
data
);
vm.stopPrank();
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 9500 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 500 ether);
}
Instead of creating the whole MockERC20 token with the blacklist feature, I have added the blacklisted (user1) address inside the require.
uint256 winnersLength = winners.length;
for (uint256 i; i < winnersLength; ) {
uint256 amount = (totalAmount * percentages[i]) / BASIS_POINTS;
require(winners[i] != 0x000000000000000000000000000000000000000E, "User Blocked");
erc20.safeTransfer(winners[i], amount);
unchecked {
++i;
}
}
run the test using command forge test --mt testSucceedsWhenConditionsAreMet -vvv
├─ [72143] ProxyFactory::deployProxyAndDistribute(0xad40762e8c031b7ef93b899573e7257f7221bc68688c4c73bbfb58ce9c2e102d, Distributor: [0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0], 0x3cc83ebe00000000000000000000000090193c961a926261b756d1e5bb255e67ff9498a1000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000251c0000000000000000000000000000000000000000000000000000000000000000)
│ ├─ [28306] → new <Unknown>@0x717a630f36795235Aa9Bd5bf6DF42c431d2875FD
│ │ └─ ← 140 bytes of code
│ ├─ [9507] 0x717a630f36795235Aa9Bd5bf6DF42c431d2875FD::distribute(MockERC20: [0x90193C961A926261B756D1E5bb255e67ff9498A1], [0x000000000000000000000000000000000000000E], [9500], 0x)
│ │ ├─ [6708] Distributor::distribute(MockERC20: [0x90193C961A926261B756D1E5bb255e67ff9498A1], [0x000000000000000000000000000000000000000E], [9500], 0x) [delegatecall]
│ │ │ ├─ [2575] ProxyFactory::whitelistedTokens(MockERC20: [0x90193C961A926261B756D1E5bb255e67ff9498A1]) [staticcall]
│ │ │ │ └─ ← true
│ │ │ ├─ [563] MockERC20::balanceOf(0x717a630f36795235Aa9Bd5bf6DF42c431d2875FD) [staticcall]
│ │ │ │ └─ ← 10000000000000000000000 [1e22]
│ │ │ └─ ← "User Blocked"
│ │ └─ ← "User Blocked"
│ └─ ← "ProxyFactory__DelegateCallFailed()"
└─ ← "ProxyFactory__DelegateCallFailed()"
Due to few blacklisted winners' addresses other winners will not be able to receive the rewards.
View Mitigation
Distributor.sol
Add two state variables, book slot 0 for implementation to prevent storage collision, and use second mapping which tracks users token rewards.
+ address _implementation; // 0 slot booked for proxy implementation address
+ mapping(address token => mapping(address user => uint256 reward)) public usersTokenReward;
uint256 winnersLength = winners.length; // cache length
+ // add the local variable to calculate reward
+ uint256 rewardAmount = 0;
for (uint256 i; i < winnersLength; ) {
uint256 amount = (totalAmount * percentages[i]) / BASIS_POINTS;
// * @audit DOS - if any winner blocks the payment from receiving then others will not be able to get the reward.
+ // instead of transfering, store the data in a mapping
- erc20.safeTransfer(winners[i], amount);
+ usersTokenReward[token][winners[i]] = amount;
+ rewardAmount += amount;
unchecked {
++i;
}
}
// send commission fee as well as all the remaining tokens to STADIUM_ADDRESS to avoid dust remaining
+ uint256 commisionFee = erc20.balanceOf(address(this)) - rewardAmount;
- _commissionTransfer(erc20);
+ _commissionTransfer(erc20, commisionFee);
- function _commissionTransfer(IERC20 token) internal {
+ function _commissionTransfer(IERC20 token, uint256 amount) internal {
- token.safeTransfer(STADIUM_ADDRESS, token.balanceOf(address(this)));
+ token.safeTransfer(STADIUM_ADDRESS, amount);
}
Add the external getReward function which winners should call from the Proxy Factory for a contest (Make sure to validate there using salt).
function getReward(IERC20 token) external {
uint256 reward = usersTokenReward[address(token)][msg.sender];
if (reward == 0) {
revert Distributor__DidNotWinReward();
}
token.safeTransfer(msg.sender, reward);
}