Vulnerability Details
For tasks with at least one required validation, the finalizeValidation
function is invoked during the last validate
call. This function calculates the standard deviation of the scores using Statistics::stddev
, which in turn relies on Statistics::variance
. However, a vulnerability in Statistics::variance
causes an underflow in most cases, leading to a revert that fails the final validate call, preventing generators and validators from receiving their fees.
File: llm/LLMOracleCoordinator.sol
function validate(...) public {
if (isCompleted) {
@> finalizeValidation(taskId);
}
}
function finalizeValidation(uint256 taskId) private {
(uint256 _stddev, uint256 _mean) = Statistics.stddev(scores);
(uint256 stddev, uint256 mean) = Statistics.stddev(generationScores);
}
File: libraries/Statistics.sol
function variance(uint256[] memory data) internal pure returns (uint256 ans, uint256 mean) {
mean = avg(data);
uint256 sum = 0;
for (uint256 i = 0; i < data.length; i++) {
@> uint256 diff = data[i] - mean;
sum += diff * diff;
}
ans = sum / data.length;
}
https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/llm/LLMOracleCoordinator.sol#L304
https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/libraries/Statistics.sol#L22
This is due to the fact that even if one value in the data array is different than the rest, the mean of the data will be greater than atleast one value (ignoring the soidity rounding down), and the difference, data[i] - mean
will underflow.
Proof of Concept
Add this to LLMOracleCoordinator.test.ts
describe("final validation fails when scores are not equal", function () {
const [numGenerations, numValidations] = [3, 2];
const dummyScore = parseEther("0.9");
const dummyScore2 = parseEther("0.5");
const scores = [dummyScore, dummyScore2, dummyScore];
this.beforeAll(async () => {
taskId++;
});
it("should revert the final validation", async function () {
await safeRequest(coordinator, token, requester, taskId, input, models, {
difficulty,
numGenerations,
numValidations,
});
for (let i = 0; i < numGenerations; i++) {
await safeRespond(coordinator, generators[i], output, metadata, taskId, BigInt(i));
}
await safeValidate(coordinator, validators[0], scores, metadata, taskId, 0n);
await expect(safeValidate(coordinator, validators[1], scores, metadata, taskId, 1n)).to.be.revertedWithPanic(0x11);
});
});
Impact
The generators and validators will not receive their fee and the task will not be completed.
Tools Used
Manual Review, Foundry
Recommendations
Modify the Statistics::variance
function as:
File: libraries/Statistis.sol
function variance(uint256[] memory data) internal pure returns (uint256 ans, uint256 mean) {
mean = avg(data);
uint256 sum = 0;
for (uint256 i = 0; i < data.length; i++) {
@> uint256 diff = data[i] - mean;
sum += diff * diff;
}
ans = sum / data.length;
}