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];
for (uint256 i = 0; i < responses[taskId].length; i++) {
if (responses[taskId][i].responder == msg.sender) {
revert AlreadyResponded(taskId, msg.sender);
}
}
assertValidNonce(taskId, task, nonce);
TaskResponse memory response =
TaskResponse({responder: msg.sender, nonce: nonce, output: output, metadata: metadata, score: 0});
responses[taskId].push(response);
emit Response(taskId, msg.sender);
if (task.parameters.numValidations == 0) {
@> _increaseAllowance(msg.sender, task.generatorFee);
}
bool isCompleted = responses[taskId].length == uint256(task.parameters.numGenerations);
if (isCompleted) {
if (task.parameters.numValidations == 0) {
task.status = TaskStatus.Completed;
emit StatusUpdate(taskId, task.protocol, TaskStatus.PendingGeneration, TaskStatus.Completed);
} else {
task.status = TaskStatus.PendingValidation;
emit StatusUpdate(taskId, task.protocol, TaskStatus.PendingGeneration, TaskStatus.PendingValidation);
}
}
}
function finalizeValidation(uint256 taskId) private {
TaskRequest storage task = requests[taskId];
for (uint256 g_i = 0; g_i < task.parameters.numGenerations; g_i++) {
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];
}
(uint256 _stddev, uint256 _mean) = Statistics.stddev(scores);
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++;
@> _increaseAllowance(validations[taskId][v_i].validator, task.validatorFee);
}
}
uint256 inner_score = innerCount == 0 ? 0 : innerSum / innerCount;
responses[taskId][g_i].score = inner_score;
}
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;
}
(uint256 stddev, uint256 mean) = Statistics.stddev(generationScores);
for (uint256 g_i = 0; g_i < task.parameters.numGenerations; g_i++) {
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.