Sparkn

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

Insufficient validation leads to locking up prize tokens forever

Summary

The ProxyFactory::setContest() lacks validating the case of the implementation param pointing to a target address with no contract. As a result, all tokens sent to the Proxy address will be stuck forever.

Vulnerability Details

The Proxy contract was designed as an escrow for distributing tokens to contest winners. By design, a contest organizer or sponsor must send tokens to an address of the contest's Proxy before the Proxy gets deployed. The tokens can be permanently stuck in the Proxy address if a mistake occurs.

Every contest will be initiated by an owner via the setContest(). The function will verify the implementation param against the address(0). However, the verification cannot guarantee that the Proxy will function properly.

Specifically, the setContest() lacks checking that the implementation param must point to an address with a contract bytecode. If an incorrect address is inputted by mistake, such as address(1), the Proxy will be bricked, locking away all tokens.

This mistake could not be undone since the inputted implementation param will be used to compute a salt for the Proxy. The implementation param will no longer be updated after executing the setContest().

function setContest(address organizer, bytes32 contestId, uint256 closeTime, address implementation)
public
onlyOwner
{
@> if (organizer == address(0) || implementation == address(0)) revert ProxyFactory__NoZeroAddress();
if (closeTime > block.timestamp + MAX_CONTEST_PERIOD || closeTime < block.timestamp) {
revert ProxyFactory__CloseTimeNotInRange();
}
@> bytes32 salt = _calculateSalt(organizer, contestId, implementation);
if (saltToCloseTime[salt] != 0) revert ProxyFactory__ContestIsAlreadyRegistered();
saltToCloseTime[salt] = closeTime;
emit SetContest(organizer, contestId, closeTime, implementation);
}

Further Explanation

To explain further the vulnerability.

The Proxy contract will be deployed and triggered by calling the following functions after sending prize tokens.

  1. deployProxyAndDistribute()

  2. deployProxyAndDistributeBySignature()

  3. deployProxyAndDistributeByOwner()

Below is the Proxy contract's snippet. The inputted implementation param will be assigned to the immutable variable _implementation in the constructor(). When the Proxy is invoked to distribute tokens, the fallback() will be triggered to execute the delegatecall() to the target implementation address.

If an incorrect address was inputted by mistake, the delegatecall() will execute nothing and return a success status. In other words, all tokens will be stuck forever in the Proxy contract.

...
@> address private immutable _implementation;
...
constructor(address implementation) {
@> _implementation = implementation;
}
...
fallback() external {
@> address implementation = _implementation;
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
@> let result := delegatecall(gas(), implementation, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}

Impact

The setContest() lacks validating the case of the implementation param pointing to a target address with no contract. As a result, all tokens sent to the Proxy address will be stuck forever.

To clarify the vulnerability, although only an owner can execute the setContest() and the owner is trusted, the incident can occur by mistake (i.e., this vulnerability is not about any centralization or trust risks; it is about the risks of input mistakes only).

The likelihood is considered LOW (since the owner is expected to do due diligence). The impact is considered HIGH. Therefore, the severity is considered MEDIUM.

Tools Used

Manual Review

Recommendations

Further validating that the implementation param must point to an address with a contract bytecode, as follows.

+ import {Address} from "openzeppelin/utils/Address.sol";
...
function setContest(address organizer, bytes32 contestId, uint256 closeTime, address implementation)
public
onlyOwner
{
if (organizer == address(0) || implementation == address(0)) revert ProxyFactory__NoZeroAddress();
+ if (!Address.isContract(implementation)) revert ProxyFactory__NoImplementationContract();
if (closeTime > block.timestamp + MAX_CONTEST_PERIOD || closeTime < block.timestamp) {
revert ProxyFactory__CloseTimeNotInRange();
}
bytes32 salt = _calculateSalt(organizer, contestId, implementation);
if (saltToCloseTime[salt] != 0) revert ProxyFactory__ContestIsAlreadyRegistered();
saltToCloseTime[salt] = closeTime;
emit SetContest(organizer, contestId, closeTime, implementation);
}

Support

FAQs

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