Dria

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

A malicious user with different wallet addresses can repeatedly respond in `LLMOracleCoordinator::respond` for a task with 0 validations, collecting the generatorFee multiple times.

Summary

A single malicious user can use multiple wallet addresses to call the LLMOracleCoordinator::respond function and collect the generatorFee multiple times for a task requiring 0 validations.

Vulnerability Details

In the respond function, a user can register as Generator with multiple wallet addresses to call the function repeatedly until isCompleted is true, changing the task state from TaskStatus.PendingGeneration to TaskStatus.Completed and then unregister stealing the generatorfee multiple times.

Since the task requires no validation and all responses have a score of 0, the first responder will always occupy the bestResponse position, regardless of the number of responses submitted.

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 getBestResponse(uint256 taskId) external view returns (TaskResponse memory) {
TaskResponse[] storage taskResponses = responses[taskId];
// ensure that task is completed
if (requests[taskId].status != LLMOracleTask.TaskStatus.Completed) {
revert InvalidTaskStatus(taskId, requests[taskId].status, LLMOracleTask.TaskStatus.Completed);
}
// pick the result with the highest validation score
@> TaskResponse storage result = taskResponses[0];
@> uint256 highestScore = result.score;
@> for (uint256 i = 1; i < taskResponses.length; i++) {
@> if (taskResponses[i].score > highestScore) {
@> highestScore = taskResponses[i].score;
@> result = taskResponses[i];
}
}
return result;
}

Impact

The user can collect the generatorFee multiple times until the task status is set to TaskStatus.Completed.

Regardless of the number of responses in a task with 0 validation required, since each response has a score of 0, the first user will always be the bestResponse.

Tools Used

Manual review

Recommendations

Since only the first response for a task with 0 validations becomes the bestResponse (as the score is 0), allow user to respond only once to tasks with 0 validations and mark it as completed. This prevents them from collecting the generatorFee multiple times.

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);
+ task.status = TaskStatus.Completed;
+ emit StatusUpdate(taskId, task.protocol, TaskStatus.PendingGeneration, TaskStatus.Completed);
}
+ // check if we have received enough responses & update task status
+ bool isCompleted = responses[taskId].length == uint256(task.parameters.numGenerations);
+ if(isCompleted) {
+ // now we are waiting for validations
+ task.status = TaskStatus.PendingValidation;
+ emit StatusUpdate(taskId, task.protocol, TaskStatus.PendingGeneration, TaskStatus.PendingValidation);
+ }
+ }
- // 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);
- }
- }
- }
Updates

Lead Judging Commences

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

There is no oracle whitelisting

Support

FAQs

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