Dria

Swan
NFTHardhat
21,000 USDC
View results
Submission Details
Severity: medium
Valid

Validators and Responder can temporarily lose access to fees

Summary

The withdrawPlatformFees function allows the platform admin to withdraw accumulated platform fees. However, this function can unintentionally cause loss of access to pending fees for validators and responders. Validators and responders are approved by the contract to spend their fees independently. If they have not collected their fees before withdrawPlatformFees is called, their access to the currently accumulated fees is removed, and they have to wait for new fees to accumulate before withdrawing. This design flaw leads to potential losses for validators and responders, making it important to address the fee management logic.

Vulnerability Details

function respond(uint256 taskId, uint256 nonce, bytes calldata output, bytes calldata metadata)
public
onlyRegistered(LLMOracleKind.Generator)
onlyAtStatus(taskId, TaskStatus.PendingGeneration)
{
TaskRequest storage task = requests[taskId];
// ensure responder to be unique for this task
for (uint256 i = 0; i < responses[taskId].length; i++) {
if (responses[taskId][i].responder == msg.sender) {
revert AlreadyResponded(taskId, msg.sender);
}
}
// check nonce (proof-of-work)
assertValidNonce(taskId, task, nonce);
// push response
TaskResponse memory response =
TaskResponse({responder: msg.sender, nonce: nonce, output: output, metadata: metadata, score: 0});
responses[taskId].push(response);
// emit response events
emit Response(taskId, msg.sender);
// send rewards to the generator if there is no validation
if (task.parameters.numValidations == 0) {
@> _increaseAllowance(msg.sender, task.generatorFee);
}
// check if we have received enough responses & update task status
bool isCompleted = responses[taskId].length == uint256(task.parameters.numGenerations);
if (isCompleted) {
if (task.parameters.numValidations == 0) {
// no validations required, task is completed
task.status = TaskStatus.Completed;
emit StatusUpdate(taskId, task.protocol, TaskStatus.PendingGeneration, TaskStatus.Completed);
} else {
// now we are waiting for validations
task.status = TaskStatus.PendingValidation;
emit StatusUpdate(taskId, task.protocol, TaskStatus.PendingGeneration, TaskStatus.PendingValidation);
}
}
}
function finalizeValidation(uint256 taskId) private {
TaskRequest storage task = requests[taskId];
// compute score for each generation
for (uint256 g_i = 0; g_i < task.parameters.numGenerations; g_i++) {
// get the scores for this generation, i.e. the g_i-th element of each validation
uint256[] memory scores = new uint256[]();
for (uint256 v_i = 0; v_i < task.parameters.numValidations; v_i++) {
scores[v_i] = validations[taskId][v_i].scores[g_i];
}
// compute the mean and standard deviation
(uint256 _stddev, uint256 _mean) = Statistics.stddev(scores);
// compute the score for this generation as the "inner-mean"
// and send rewards to validators that are within the range
uint256 innerSum = 0;
uint256 innerCount = 0;
for (uint256 v_i = 0; v_i < task.parameters.numValidations; ++v_i) {
uint256 score = scores[v_i];
if ((score >= _mean - _stddev) && (score <= _mean + _stddev)) {
innerSum += score;
innerCount++;
// send validation fee to the validator
@> _increaseAllowance(validations[taskId][v_i].validator, task.validatorFee);
}
}
// set score for this generation as the average of inner scores
uint256 inner_score = innerCount == 0 ? 0 : innerSum / innerCount;
responses[taskId][g_i].score = inner_score;
}
// now, we have the scores for each generation
// compute stddev for these and pick the ones above a threshold
uint256[] memory generationScores = new uint256[]();
for (uint256 g_i = 0; g_i < task.parameters.numGenerations; g_i++) {
generationScores[g_i] = responses[taskId][g_i].score;
}
// compute the mean and standard deviation
(uint256 stddev, uint256 mean) = Statistics.stddev(generationScores);
for (uint256 g_i = 0; g_i < task.parameters.numGenerations; g_i++) {
// ignore lower outliers
if (generationScores[g_i] >= mean - generationDeviationFactor * stddev) {
_increaseAllowance(responses[taskId][g_i].responder, task.generatorFee);
}
}
}
function withdrawPlatformFees() public onlyOwner {
feeToken.transfer(owner(), feeToken.balanceOf(address(this)));
}

Loss of Access to Fees for Validators and Responders:

Validators and responders are granted approval to withdraw their portion of the fees from the contract directly. However, if they have not withdrawn these fees before the admin executes the withdrawPlatformFees function, their access to the existing funds is effectively removed until another round of fees is collected. This limitation leads to an inconsistent payout experience for validators and responders, who may lose their pending fees if the platform withdraws them first.

Timing Dependency:

This setup creates a dependency on timing for fee withdrawal, where validators and responders must claim their fees promptly to avoid missing out when withdrawPlatformFees is called. This process is not ideal in a decentralized environment where parties should be able to withdraw their funds independently without a forced timeline.

Impact

Loss of Funds for Validators and Responders: Validators and responders risk losing access to their fees if they miss the withdrawal window, impacting their earnings and participation.

Centralized Control: The platform admin’s ability to withdraw all fees at once introduces a degree of centralized control, which can undermine trust in a decentralized protocol.

Tools Used

Manual Review

Recommendations

Consider creating distinct tracking for platform fees and validator/responder fees. This separation allows validators and responders to withdraw their portion independently of the platform’s withdrawal, ensuring their funds remain accessible regardless of the admin’s actions.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`withdrawPlatformFees` withdraws the entire balance

Support

FAQs

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