Sparkn

CodeFox Inc.
DeFiFoundryProxy
15,000 USDC
View results
Submission Details
Severity: medium

Token prizes can not be fully distributed if sponsors fund contest with multiple token types

Summary

If sponsors fund contest with multiple token types i.e. USDC and DAI both are used then the organizer can distribute only one of them to the winners as Distributor.sol::distribute doesn't handle the distribution of multiple tokens.

// You can pass only one token address to this function.
function distribute(address token, address[] memory winners, uint256[] memory percentages, bytes memory data)
external
{
if (msg.sender != FACTORY_ADDRESS) {
revert Distributor__OnlyFactoryAddressIsAllowed();
}
_distribute(token, winners, percentages, data);
}

Vulnerability Details

Let's say sponsor A funds the contest xyz with USDC and sponsor B with DAI, then organizer can only distribute DAI or USDC but not both.

Let's say organizer distributes USDC by calling ProxyFactory.sol::deployProxyAndDistribute, but trying to distribute DAI again by ProxyFactory.sol::deployProxyAndDistribute or ProxyFactory.sol::deployProxyAndDistributeBySignature will cause these functions to revert because of the below line in these functions as the proxy has already been deployed when organizer distributed USDC.

address proxy = _deployProxy(msg.sender, contestId, implementation);

Note: Same is the case with ProxyFactory.sol::deployProxyAndDistributeByOwner if owner tries to do the same.

Note 2: ProxyFactory.sol::distributeByOwner cannot be used here as its clearly mentioned in the comments at line
ProxyFactory.sol#L196 @notice Owner can rescue funds if token is stuck after the deployment and contest is over for a while
ProxyFactory.sol#L197 @dev only owner can call this function and it is supposed not to be called often

POC

Add the following modifier, function and test to OnlyProxyTest.t.sol

modifier setUpContestForJasonAndSendJpycv1TokenAndJpycv2Token(address _organizer) {
vm.startPrank(factoryAdmin);
bytes32 randomId = keccak256(abi.encode("Jason", "001"));
// Setting up contest
proxyFactory.setContest(_organizer, randomId, block.timestamp + 8 days, address(distributor));
vm.stopPrank();
bytes32 salt = keccak256(abi.encode(_organizer, randomId, address(distributor)));
address proxyAddress = proxyFactory.getProxyAddress(salt, address(distributor));
vm.startPrank(sponsor);
// Sending 1st token
MockERC20(jpycv2Address).transfer(proxyAddress, 10000 ether);
// Sending 2nd token
MockERC20(jpycv1Address).transfer(proxyAddress, 10000 ether);
vm.stopPrank();
assertEq(MockERC20(jpycv2Address).balanceOf(proxyAddress), 10000 ether);
assertEq(MockERC20(jpycv1Address).balanceOf(proxyAddress), 10000 ether);
_;
}
function createDataForSecondToken() public view returns (bytes memory data) {
address[] memory tokens_ = new address[](1);
tokens_[0] = jpycv1Address;
address[] memory winners = new address[](1);
winners[0] = user1;
uint256[] memory percentages_ = new uint256[](1);
percentages_[0] = 9500;
data = abi.encodeWithSelector(Distributor.distribute.selector, jpycv1Address, winners, percentages_, "");
}
function testIfOrganizerDistributesAgainThenRevert() public setUpContestForJasonAndSendJpycv1TokenAndJpycv2Token(organizer) {
// before
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 0 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 0 ether);
assertEq(MockERC20(jpycv1Address).balanceOf(user1), 0 ether);
assertEq(MockERC20(jpycv1Address).balanceOf(stadiumAddress), 0 ether);
bytes32 randomId_ = keccak256(abi.encode("Jason", "001"));
bytes memory data = createData();
vm.warp(9 days); // 9 days later deploy and didtribute
vm.startPrank(organizer);
address proxyAddress = proxyFactory.deployProxyAndDistribute(randomId_, address(distributor), data);
vm.stopPrank();
// after
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 9500 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 500 ether);
// Balance of user1 and stadiumAddress of jpycv1Address should still be zero as its haven’t been distributed yet.
assertEq(MockERC20(jpycv1Address).balanceOf(user1), 0 ether);
assertEq(MockERC20(jpycv1Address).balanceOf(stadiumAddress), 0 ether);
// Proxy address should still have 10,000 jpycv1Address
assertEq(MockERC20(jpycv1Address).balanceOf(proxyAddress), 10000 ether);
// Expect revert because the proxy is already deployed
vm.startPrank(organizer);
vm.expectRevert();
proxyFactory.deployProxyAndDistribute(randomId_, address(distributor), data);
vm.stopPrank();
}

Impact

Only 1 out of multiple tokens funded to the contest can be distributed by organizer and the rest will stay stuck until owner uses ProxyFactory.sol::distributeByOwner to distribute the tokens which is reportedly can only be used to recover the funds sent after the deployment of proxy and this function supposed not to be called often.

Tools Used

Manual Analysis

Recommendations

Modify Distributor.sol to distribute multiple tokens to winners, below is the modified distribute function.

function distribute(address[] memory token, address[] memory winners, uint256[] memory percentages, bytes memory data)
external
{
if (msg.sender != FACTORY_ADDRESS) {
revert Distributor__OnlyFactoryAddressIsAllowed();
}
if(token.length == 0) revert Distributor__NoZeroAddress();
for(uint i; i < token.length;){
_distribute(token[i], winners, percentages, data);
unchecked{
++i;
}
}
}

Support

FAQs

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