Dria

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

Inconsistent Best Response Selection Due to Missing Tiebreak Mechanism

Summary

The getBestResponse function in LLMOracleCoordinator lacks a tiebreak mechanism when multiple responses have the same highest validation score.

This can lead to inconsistent results and potential manipulation of which response is selected as "best".

Vulnerability Details

Current implementation simply takes the first response with the highest score:

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) { // Note: only strictly greater than
highestScore = taskResponses[i].score;
result = taskResponses[i];
}
}
return result;
}

Issues:

No tiebreaker for equal scores

First response has advantage in ties

Order-dependent results

Impact

Early responders have advantage in ties

Inconsistent selection among equally-scored responses

Tools Used

Manual Review

Recommendations

Implement deterministic tiebreak using multiple factors:

function getBestResponse(uint256 taskId) external view returns (TaskResponse memory) {
TaskResponse[] storage taskResponses = responses[taskId];
require(requests[taskId].status == LLMOracleTask.TaskStatus.Completed, "Task not completed");
TaskResponse storage bestResponse = taskResponses[0];
uint256 bestScore = bestResponse.score;
bytes32 bestHash = keccak256(abi.encodePacked(
bestResponse.output,
bestResponse.responder,
bestResponse.nonce
));
for (uint256 i = 1; i < taskResponses.length; i++) {
TaskResponse storage currentResponse = taskResponses[i];
uint256 currentScore = currentResponse.score;
// If strictly better score, always choose it
if (currentScore > bestScore) {
bestResponse = currentResponse;
bestScore = currentScore;
bestHash = keccak256(abi.encodePacked(
currentResponse.output,
currentResponse.responder,
currentResponse.nonce
));
}
// If tied score, use deterministic tiebreak
else if (currentScore == bestScore) {
bytes32 currentHash = keccak256(abi.encodePacked(
currentResponse.output,
currentResponse.responder,
currentResponse.nonce
));
// Use hash comparison as tiebreaker
if (currentHash < bestHash) {
bestResponse = currentResponse;
bestHash = currentHash;
}
}
}
return bestResponse;
}
Updates

Lead Judging Commences

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

wrong implementation of "getBestResponse" when there are more than 1 responses with highestScore

Support

FAQs

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