Dria

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

LLMOracleCoordinator::finalizeValidation - function will revert due to underflow if _stddev > _mean

Summary

Link: https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/llm/LLMOracleCoordinator.sol#L343

In the finalizeValidation function, if _stddev > _mean, the following line of code will generate a panic error due to underflow:

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

Vulnerability Details

Proof of Concept:

Add the following test file (eg: FinalizeValidation.test.ts) to the test folder and run the test by executing:

npx hardhat test --network hardhat test/FinalizeValidation.test.ts

import { expect } from "chai";
import { ethers } from "hardhat";
import type { ERC20, LLMOracleCoordinator, LLMOracleRegistry } from "../typechain-types";
import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
import { parseEther } from "ethers";
import { deployLLMFixture, deployTokenFixture } from "./fixtures/deploy";
import { registerOracles, safeRequest, safeRespond, safeValidate } from "./helpers";
import { transferTokens } from "./helpers";
import { PANIC_CODES } from "@nomicfoundation/hardhat-chai-matchers/panic";
describe("Statistics", function () {
let dria: HardhatEthersSigner;
let requester: HardhatEthersSigner;
let generators: HardhatEthersSigner[];
let validators: HardhatEthersSigner[];
let coordinator: LLMOracleCoordinator;
let registry: LLMOracleRegistry;
let token: ERC20;
let taskId = 1n;
const input = "0x" + Buffer.from("What is 2 + 2?").toString("hex");
const output = "0x" + Buffer.from("2 + 2 equals 4.").toString("hex");
const models = "0x" + Buffer.from("gpt-4o-mini").toString("hex");
const metadata = "0x";
const difficulty = 2;
const SUPPLY = parseEther("1000");
const STAKES = {
generatorStakeAmount: parseEther("0.01"),
validatorStakeAmount: parseEther("0.01"),
};
const FEES = {
platformFee: parseEther("0.001"),
generationFee: parseEther("0.002"),
validationFee: parseEther("0.0003"),
};
const [numGenerations, numValidations] = [2, 5];
this.beforeAll(async function () {
const [deployer, req1, gen1, gen2, gen3, val1, val2, val3, val4, val5] = await ethers.getSigners();
dria = deployer;
requester = req1;
generators = [gen1, gen2, gen3];
validators = [val1, val2, val3, val4, val5];
token = await deployTokenFixture(deployer, SUPPLY);
({ registry, coordinator } = await deployLLMFixture(dria, token, STAKES, FEES));
const requesterFunds = parseEther("1");
await transferTokens(token, [
[requester.address, requesterFunds],
...generators.map<[string, bigint]>((oracle) => [oracle.address, STAKES.generatorStakeAmount]),
...validators.map<[string, bigint]>((oracle) => [oracle.address, STAKES.validatorStakeAmount]),
]);
});
it("finalizeValidation underflows", async function () {
await registerOracles(token, registry, generators, validators, STAKES);
//make a request
await safeRequest(coordinator, token, requester, taskId, input, models, {
difficulty,
numGenerations,
numValidations,
});
//respond
for (let i = 0; i < numGenerations; i++) {
await safeRespond(coordinator, generators[i], output, metadata, taskId, BigInt(i));
}
//first validation
let scores = new Array(0n, 0n)
await safeValidate(coordinator, validators[0], scores, metadata, taskId, 0n);
//second validation => calls finalizeValidation => calls Statistics.stddev(generationScores)
scores = new Array(0n, 0n)
await safeValidate(coordinator, validators[1], scores, metadata, taskId, 1n);
//third validation
scores = new Array(0n, 0n)
await safeValidate(coordinator, validators[2], scores, metadata, taskId, 2n);
//fourth validation
scores = new Array(0n, 0n)
await safeValidate(coordinator, validators[3], scores, metadata, taskId, 3n);
//fifth validation
scores = new Array(10n, 10n)
//this will generate an underflow at: score >= _mean - _stddev; in finalizeValidation() =>
//_mean - _stdde == 2 - 4 => underflow !
await expect(safeValidate(coordinator, validators[4], scores, metadata, taskId, 4n))
.to.be.revertedWithPanic(PANIC_CODES.ARITHMETIC_OVERFLOW);
});
});

Remark: In order for this test to work, the issue described in: "Statistics::variance - function will revert due to underflow if mean > data[i]" needs to be corrected first.

Impact

The faulty LLMOracleCoordinator::finalizeValidation function is called by LLMOracleCoordinator::validate

This function is required to validate requests for a given taskId. Because of the vulnerability, some validations (depending on provided score values) will fail, which prevents the protocol from functioning properly.

Tools Used

Manual Review

Recommendations

Modify the for-loop in the finalizeValidation function:

for (uint256 v_i = 0; v_i < task.parameters.numValidations; ++v_i) {
uint256 score = scores[v_i];
+ uint256 lowerBound = (_stddev <= _mean) ? _mean - _stddev : 0;
+ uint256 upperBound = _mean + _stddev;
- if ((score >= _mean - _stddev) && (score <= _mean + _stddev)) {
+ if ((score >= lowerBound) && (score <= upperBound)) {
innerSum += score;
innerCount++;
// send validation fee to the validator
_increaseAllowance(validations[taskId][v_i].validator, task.validatorFee);
}
}
Updates

Lead Judging Commences

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.