Sparkn

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

Reward distribution mechanism can be abused to underpay supporters

Summary

When distributing token to winners, the organizer may promise payment in one token but choose another, less valuable token, to send rewards as.

Organizer may post an offer as: "Completing this task will reward $100,000 in protocol tokens", leaving supporter to interpret the payment as 100,000 USD fiat equivalent but actually paying 100,000 of a less valuable token, such as JPY Coin.

Note that this is not a case of direct refusal to pay, which is possible but out of scope, but the attempt to manipulate supporters to believe they were mistaken while pleading innocence to still remain participants in the Sparkan protocol, receive the work and pay less then promised.

Vulnerability Details

Reward distribution, regardless of which function from ProxyFactory is called, reaches ProxyFactory::_distribute

function _distribute(address proxy, bytes calldata data) internal {
(bool success,) = proxy.call(data);
if (!success) revert ProxyFactory__DelegateCallFailed();
emit Distributed(proxy, data);
}

The calldata data is directly provided as input without any checks in the ProxyFactory in all cases.
From here, it reaches the Proxy->Distributor::distribute->_distribute function where the input is checked so that the token is a whitelisted one

// token address input check
if (token == address(0)) revert Distributor__NoZeroAddress();
if (!_isWhiteListed(token)) {
revert Distributor__InvalidTokenAddress();
}

Thus, the only only requirements for a valid payment are:

  • token to be whitelisted

  • distribution contract proxy to have the tokens

An abuse scenario would be:

  • organizer and sponsor launches a contest with the message "Completing this task will reward $100,000 in protocol tokens"

  • they send 100,000 USDC to the precalculated proxy address and initiate the contest

  • supporters start working on the issue

  • sponsor, without announcing, also sends 100,000 JPY Coin tokens (worth $0.006847 at the time of this report)

  • contest is finished and organizer distributes 100,000 JPY Coin tokens to supporters

  • supporters complain but with the ambiguity owner can't firmly asses that they are correct

  • organizer gets the initial 100,000 USDC from the protocol via owner help

  • supporters are left underpaid for their efforts

Impact

Organizer can abuse reward distribution oversight to underpay for services

Tools Used

Manual reviews

Recommendations

Modify protocol logic to include in the payment token in the salt and, when distributing the rewards, take the payment token from the calldata in order to compute the salt. This way, proxy address deployment is tied to reward token and organizers are forced to transparently show exactly what reward token will be used, leaving no room for any interpretation or abuse situation.

Example implementation for setContest:

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

and implementation for deployProxyAndDistribute:

function deployProxyAndDistribute(bytes32 contestId, address implementation, bytes calldata data)
public
returns (address)
{
address paymentToken = abi.decode(data[4:36], (address));
bytes32 salt = _calculateSalt(msg.sender, contestId, implementation, paymentToken);
if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered();
// can set close time to current time and end it immediately if organizer wish
if (saltToCloseTime[salt] > block.timestamp) revert ProxyFactory__ContestIsNotClosed();
address proxy = _deployProxy(msg.sender, contestId, implementation, paymentToken);
_distribute(proxy, data);
return proxy;
}

Support

FAQs

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