Summary
The Distributor: :_distribute() function in Distributor.sol is designed in a way that the tokens distributed to it are send to winners by percentage and remaining tokens is transferred to the STADIUM_ADDRESS
but if Sponsor (anyone)
sends other whitelisted tokens to the contract, it will not be withdrawn and gets stuck inside the contract.
There is a known issue that non-whitelisted tokens can get stuck but whitelisted tokens can get stuck too!!!
Vulnerability Details
File:Distributor.sol
function _distribute(address token, address[] memory winners, uint256[] memory percentages, bytes memory data)
internal
{
if (token == address(0)) revert Distributor__NoZeroAddress();
if (!_isWhiteListed(token)) {
revert Distributor__InvalidTokenAddress();
}
............
............
............
IERC20 erc20 = IERC20(token);
uint256 totalAmount = erc20.balanceOf(address(this));
if (totalAmount == 0) revert Distributor__NoTokenToDistribute();
uint256 winnersLength = winners.length;
for (uint256 i; i < winnersLength;) {
uint256 amount = totalAmount * percentages[i] / BASIS_POINTS;
erc20.safeTransfer(winners[i], amount);
unchecked {
++i;
}
}
_commissionTransfer(erc20);
emit Distributed(token, winners, percentages, data);
}
Only the single token whose address that is passed to the function is transferred. Other tokens will be stuck.
Proof of Concept
Using a modified version of testIfAllConditionsMetThenJpycv2SendingCallShouldSuceed() so this PoC can be tested by just pasting inside test/integration/ProxyTest.t.sol.
function testIfDifferentTokensAreSendToProxy()
public
setUpContestForNameAndSentAmountToken("James", jpycv2Address, 10000 ether)
{
assertEq(MockERC20(jpycv1Address).balanceOf(address(user1)), 0);
assertEq(MockERC20(jpycv1Address).balanceOf(address(user2)), 0);
bytes32 randomId_ = keccak256(abi.encode("James", "001"));
bytes memory data = createDataToDistributeJpycv2();
vm.startPrank(organizer);
deployedProxy = proxyFactory.deployProxyAndDistribute(randomId_, address(distributor), data);
vm.stopPrank();
proxyWithDistributorLogic = Distributor(address(deployedProxy));
address[] memory winners = new address[](2);
winners[0] = user1;
winners[1] = user2;
uint256[] memory percentages_ = new uint256[](2);
percentages_[0] = 9000;
percentages_[1] = 500;
deal(address(jpycv1Address), sponsor, 200000 ether);
deal(address(jpycv2Address),sponsor, 100000 ether);
vm.startPrank(sponsor);
MockERC20(jpycv1Address).transfer(deployedProxy, 200000 ether);
MockERC20(jpycv2Address).transfer(deployedProxy, 100000 ether);
vm.stopPrank();
vm.startPrank(address(proxyFactory));
vm.expectEmit(true, false, false, false);
emit Distributed(jpycv1Address, winners, percentages_, "");
proxyWithDistributorLogic.distribute(jpycv1Address, winners, percentages_, "");
vm.stopPrank();
assertEq(MockERC20(jpycv2Address).balanceOf(deployedProxy), 100000 ether);
}
Impact
If other whitelisted tokens are accidentally send to the deployedProxy, they will be stuck and can lead to loss of funds. Impact is high and likelihood depends on who sends the tokens, if the protocol is handling it then low else if people are directly interacting, then likelihood is Medium/High , they did mention in the stream that target audience might necessarily not be crypto native people. Severity might be Medium to High depending on that.
Tools Used
Manual review, Foundry
Recommendations
Modify the functionality of the existing Distributor: :_commissionTransfer()
function or create another contract to facilitate unused whitelisted token transfer.