Sparkn

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

Organizer is unable to distribute all funds if Contest is funded by more than one whitelisted asset

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

  1. Sponsor funds a Contest Proxy with 10 JPYC.

  2. Sponsor funds a Contest Proxy with 1000 USDC.

  3. Contest ends.

  4. Organizer deploys Proxy and Distributes JPYC.

  5. Organizer tries to deploy and distribute USDC. Transaction Reverts.

  6. 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));
// Create contest
vm.startPrank(factoryAdmin);
proxyFactory.setContest(organizer, randomId, block.timestamp + 8 days, address(distributor));
vm.stopPrank();
// Fund contest with usdc and jpyc, both whitelisted tokens
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);
// Reverts because the Proxy already exists but the organizer has no other way to distribute the funds in the Contest.
vm.expectRevert();
proxyFactory.deployProxyAndDistribute(randomId, address(distributor), usdcDistributionCalldata);
vm.stopPrank();
// After deployProxyAndDistribute
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 95 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 5 ether);
// These two SHOULD have 9500 and 500 units of usdc respectively. They are stuck in the contract.
assertEq(MockERC20(usdcAddress).balanceOf(user1), 0 ether);
assertEq(MockERC20(usdcAddress).balanceOf(stadiumAddress), 0 ether);
// This proxy SHOULD have 0 usdc in it. The funds are locked in the proxy until after the expiration date.
// When the ProxyFactory owner can withdraw them.
assertEq(MockERC20(usdcAddress).balanceOf(proxyAddress), 10000 ether);
// The ProxyFactory owner can distribute the remaining funds after the expiration date.
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:

  1. 'Organizer: The person who creates the contest and he is responsible for distributing the prizes to the winners...'

  2. '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

  1. Create a distributeByOrganizer and distributeByOrganizerSignature function.

// ProxyFactory.sol
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;
}

Support

FAQs

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