Effected Contract: ProxyFactory.sol, Distributor.sol
Summary
When a Contest Proxy is funded by two or more assets that are on the whitelist, the organizer is only able to distribute one of them. The other assets are stuck until after the Context expiration is reached, in which the Factory Owner can distribute other assets.
Vulnerability Details
Steps
Sponsor funds a Contest Proxy with 10 JPYC.
Sponsor funds a Contest Proxy with 1000 USDC.
Contest ends.
Organizer deploys Proxy and Distributes JPYC.
Organizer tries to deploy and distribute USDC. Transaction Reverts.
Supporters do not receive funds when contest completes.
Reproduction Testcase:
function testDistributingMultipleTokensResultsInStuckFunds() public {
bytes32 randomId = keccak256(abi.encode("Jason", "001"));
bytes32 salt_ = keccak256(abi.encode(organizer, randomId, address(distributor)));
address proxyAddress = proxyFactory.getProxyAddress(salt_, address(distributor));
vm.startPrank(factoryAdmin);
proxyFactory.setContest(organizer, randomId, block.timestamp + 8 days, address(distributor));
vm.stopPrank();
vm.startPrank(sponsor);
MockERC20(usdcAddress).transfer(proxyAddress, 10000 ether);
MockERC20(jpycv2Address).transfer(proxyAddress, 100 ether);
vm.stopPrank();
bytes memory jpyDistributionCalldata = createData();
bytes memory usdcDistributionCalldata = createUsdcDistributionData();
vm.warp(9 days);
vm.startPrank(organizer);
proxyFactory.deployProxyAndDistribute(randomId, address(distributor), jpyDistributionCalldata);
vm.expectRevert();
proxyFactory.deployProxyAndDistribute(randomId, address(distributor), usdcDistributionCalldata);
vm.stopPrank();
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 95 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 5 ether);
assertEq(MockERC20(usdcAddress).balanceOf(user1), 0 ether);
assertEq(MockERC20(usdcAddress).balanceOf(stadiumAddress), 0 ether);
assertEq(MockERC20(usdcAddress).balanceOf(proxyAddress), 10000 ether);
vm.warp(16 days);
vm.startPrank(factoryAdmin);
proxyFactory.distributeByOwner(
proxyAddress, organizer, randomId, address(distributor), usdcDistributionCalldata
);
assertEq(MockERC20(usdcAddress).balanceOf(user1), 9500 ether);
assertEq(MockERC20(usdcAddress).balanceOf(stadiumAddress), 500 ether);
}
Impact
Any extra assets other than the first distributed one will be stuck in the Contest Proxy until after the expiration date, in which the owner can then distribute the stuck funds themselves.
A couple details from the README:
'Organizer: The person who creates the contest and he is responsible for distributing the prizes to the winners...'
'The owner can deploy proxy and distribute prizes to winners if organizer did not call the function in time.'
With this behavior, Organizers are not able to distribute all the prizes to supporters as soon as the Contest ends.
With this behavior, even if an Organizer calls Distribution in time, they may still be forced to trust the Factory Owner.
Tools Used
Manual Review
Recommendations
Create a distributeByOrganizer
and distributeByOrganizerSignature
function.
function distributeByOrganizer(
address proxy,
bytes32 contestId,
address implementation,
bytes calldata data
) public {
bytes32 salt = _calculateSalt(msg.sender, contestId, implementation);
if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
if (saltToCloseTime[salt] > block.timestamp) revert ProxyFactory__ContestIsNotClosed();
address proxy = getProxyAddress(salt, implementation);
_distribute(proxy, data);
return proxy;
}
function distributeByOrganizerSignature(
address organizer,
bytes32 contestId,
address implementation,
bytes calldata signature,
bytes calldata data
) public returns (address) {
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, data)));
if (ECDSA.recover(digest, signature) != organizer) revert ProxyFactory__InvalidSignature();
bytes32 salt = _calculateSalt(organizer, contestId, implementation);
if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
if (saltToCloseTime[salt] > block.timestamp) revert ProxyFactory__ContestIsNotClosed();
address proxy = getProxyAddress(salt, implementation);
_distribute(proxy, data);
return proxy;
}