Dria

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

Potential underflow vulnerability in score range calculation of `LLMOracleCoordinator::finalizeValidation`, leading to DoS.

Summary

The LLMOracleCoordinator::finalizeValidation function calculates the range for valid scores depending on the result of the expression score >= _mean - _stddev. If _mean is less than _stddev, this calculation leads to an underflow error, causing a revert that will fail the transaction. This behavior prevents successful validation completion and rewards distribution, disrupting normal contract operations and usability.

Description

In the LLMOracleCoordinator::finalizeValidation function, scores are evaluated within a standard deviation range around the mean, using the criteria (_mean - _stddev) and (_mean + _stddev), see [https://github.com/Cyfrin/2024-10-swan-dria/blob/main/contracts/llm/LLMOracleCoordinator.sol#L343]:

if ((score >= _mean - _stddev) && (score <= _mean + _stddev))

However, in cases where _mean < _stddev, such as some valid edge case where for example scores[] = [0,1,0,1,2], the calculation of _mean - _stddev attempts to produce a negative value.

Since Solidity’s uint256 type does not support negative numbers, this results in an underflow, triggering an automatic revert and causing the transaction to fail. The edge case described results in _stddev = 1 and _mean = 0, which causes the check score >= _mean - _stddev to revert, as _mean - _stddev evaluates to a negative result.

The same issue exists also in [https://github.com/Cyfrin/2024-10-swan-dria/blob/main/contracts/llm/LLMOracleCoordinator.sol#L368]:

Exploit Scenario

For the input scores[] = [0, 1, 0, 1, 2], the standard deviation and mean calculated by Statistics.stddev(scores) in LLMOracleCoordinator::finalizeValidation are _stddev = 1 and _mean = 0. Note that the calculation using the Statistics.sol library would be successful in this case. That can be quickly checked in Remix:

decoded input {
"uint256[] data": [
"0",
"1",
"0",
"1",
"2"
]}
decoded output {
"0": "uint256: ans 1",
"1": "uint256: mean 0"
}

When performing the range check in (score >= _mean - _stddev) (see [https://github.com/Cyfrin/2024-10-swan-dria/blob/main/contracts/llm/LLMOracleCoordinator.sol#L343]), the _mean - _stddev calculation attempts to compute 0 - 1, which underflows as it is not representable within the unsigned integer type, triggering a revert. The same problem was also found at [https://github.com/Cyfrin/2024-10-swan-dria/blob/main/contracts/llm/LLMOracleCoordinator.sol#L368].

PoC

For the proof of concept, we will stick to the same score values as shown in the section above. Here is the test case and setup, please paste it into the LLMOracleCoordinator.test.ts file and adjust the this.beforeAll(async function ()) section with additional validators, like this:

// Add validators to the setUp
this.beforeAll(async function () {
// assign roles, full = oracle that can do both generation & validation
const [deployer, dum, req1, gen1, gen2, gen3, gen4, gen5, val1, val2, val3, val4, val5] = await ethers.getSigners();
dria = deployer;
requester = req1;
dummy = dum;
generators = [gen1, gen2, gen3, gen4, gen5];
validators = [val1, val2, val3, val4, val5];
...........
...........
...........
describe("underflow in score range calculation", function () {
const [numGenerations, numValidations] = [1, 5];
const scores = [
parseEther("0"),
parseEther("0.000000000000000001"),
parseEther("0"),
parseEther("0.000000000000000001"),
parseEther("0.000000000000000002")
];
let generatorAllowancesBefore: bigint[];
let validatorAllowancesBefore: bigint[];
this.beforeAll(async () => {
taskId++;
generatorAllowancesBefore = await Promise.all(
generators.map((g) => token.allowance(coordinatorAddress, g.address))
);
validatorAllowancesBefore = await Promise.all(
validators.map((v) => token.allowance(coordinatorAddress, v.address))
);
});
it("should make a request", async function () {
await safeRequest(coordinator, token, requester, taskId, input, models, {
difficulty,
numGenerations,
numValidations,
});
});
it("should respond to each generation", async function () {
const availableGenerators = generators.length;
const generationsToRespond = Math.min(numGenerations, availableGenerators);
expect(availableGenerators).to.be.at.least(generationsToRespond);
for (let i = 0; i < generationsToRespond; i++) {
await safeRespond(coordinator, generators[i], output, metadata, taskId, BigInt(i));
}
});
// UNDERFLOW TEST
it("it should underflow calculating score ranges for inner mean", async function () {
const requestBefore = await coordinator.requests(taskId);
console.log(`Request status before validation: ${requestBefore.status}`);
for (let i = 0; i < numValidations; i++) {
console.log(`Validating with validator at index ${i} with address: ${validators[i].address}`);
console.log(`Score being used: ${scores[i].toString()}, Task ID: ${taskId}`);
try {
if (i < numValidations - 1) {
await safeValidate(coordinator, validators[i], [scores[i]], metadata, taskId, BigInt(i));
console.log(`Validation succeeded for validator at index ${i}`);
} else {
// For the last validator, expect a revert without a specific error
await safeValidate(coordinator, validators[i], [scores[i]], metadata, taskId, BigInt(i));
console.log(`Validation succeeded for validator at index ${i}`); // This should not run if it reverts
}
} catch (error:any) {
if (i < numValidations - 1) {
console.error(`Validation failed for validator at index ${i} with error: ${error.message}`);
} else {
if (error instanceof Error) {
console.error(`Validation failed for validator at index ${i}: ${error.message}`);
} else {
console.error(`Validation failed for validator at index ${i}: ${JSON.stringify(error)}`);
}
}
}
}
// Confirm the tasks status
const finalRequest = await coordinator.requests(taskId);
console.log(`Request status after all validations: ${finalRequest.status}`);
// Confirm the status is still 'PendingValidation'
expect(finalRequest.status).to.equal(TaskStatus.PendingValidation);
});
});

The test can be run with yarn test ./test/LLMOracleCoordinator.test.ts --verbose .
Here are the logs:

Request status before validation: 2
Validating with validator at index 0 with address: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f
Score being used: 0, Task ID: 3
Validation succeeded for validator at index 0
Validating with validator at index 1 with address: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
Score being used: 1, Task ID: 3
Validation succeeded for validator at index 1
Validating with validator at index 2 with address: 0xBcd4042DE499D14e55001CcbB24a551F3b954096
Score being used: 0, Task ID: 3
Validation succeeded for validator at index 2
Validating with validator at index 3 with address: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788
Score being used: 1, Task ID: 3
Validation succeeded for validator at index 3
Validating with validator at index 4 with address: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a
Score being used: 2, Task ID: 3
Validation failed for validator at index 4: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block)
Request status after all validations: 2

As you can see from the error logs, the transaction failed with an error message: panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block). And the problem was not the use of a buggy Statistics.sol library, but the logic behind the score range calculation in LLMOracleCoordinator::finalizeValidation.

Impact

Tasks with scores causing _mean < _stddev cannot complete, leading to a halt in the validation process and blocking reward distribution. Since scores submitted by prior validators to the TaskValidation struct array are immutable, tasks may become permanently locked in PendingValidation status, posing a potential DoS vulnerability.

Tools Used

Manual Code Review, Remix, Hardhat

Recommendations

LLMOracleCoordinator::finalizeValidation needs to be refactored. You could think about rewriting both underflow-prone conditions in finalizeValidation using only addition operations, adjusting the logic to avoid subtraction:

for (uint256 v_i = 0; v_i < task.parameters.numValidations; ++v_i) {
uint256 score = scores[v_i];
- if ((score >= _mean - _stddev) && (score <= _mean + _stddev)) {
+ if ((score + _stddev >= _mean) && (score <= _mean + _stddev))
innerSum += score;
innerCount++;
_increaseAllowance(validations[taskId][v_i].validator, task.validatorFee);
}
}
........
........
for (uint256 g_i = 0; g_i < task.parameters.numGenerations; g_i++) {
// ignore lower outliers
- if (generationScores[g_i] >= mean - generationDeviationFactor * stddev) {
+ if (generationScores[g_i] + generationDeviationFactor * stddev >= mean)
_increaseAllowance(responses[taskId][g_i].responder, task.generatorFee);
}
}

After applying the suggested fixes, the issues are mitigated. Adjust the last line in the PoC to expect(finalRequest.status).to.equal(TaskStatus.Completed); and run the test with yarn test ./test/LLMOracleCoordinator.test.ts, it will pass.

Updates

Lead Judging Commences

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

Underflow in `LLMOracleCoordinator::validate`

Appeal created

robertodf99 Auditor
12 months ago
jsondoge Auditor
12 months ago
0xw3 Auditor
12 months ago
corn Auditor
12 months ago
inallhonesty Lead Judge
12 months ago
inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Underflow in `LLMOracleCoordinator::validate`

Support

FAQs

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