Dria

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

Validators cannot receive their earned fees

Summary

When calculating differences between data points and mean values, the current implementation using unsigned integers (uint256) can lead to arithmetic underflow when a data point is smaller than the mean, causing the transaction to revert and potentially resulting in a Denial of Service (DoS) condition.

Vulnerability Details

In LLMOracleCoordinator.sol the contract validates requests for a given taskId using the function validate. The final line of this function is:

finalizeValidation(taskId);

This function finalizeValidation computes the validation scores for a given taskId. After computing scores for each generation it computes the standard deviation and the mean using Statistics::stddev.

Statistics::stddev uses Statistics::variance to calculate the variance.

The vulnerability exists in the following code section of the variance function:

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; // Vulnerable line
uint256 diffSquared = diff * diff;
sum += diffSquared;
}
ans = sum / data.length;
}

The issue occurs in the line uint256 diff = data[i] - mean when data[i] < mean. Since uint256 cannot represent negative numbers, this operation will cause an arithmetic underflow and the transaction will revert due to Solidity's built-in overflow/underflow protection (introduced in version 0.8.0).

Proof of Concept

I wrote a simple POC using fuzz testing with Foundry to demonstrate the issue:

Note: This is a simplified version of the project.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../contracts/llm/LLMOracleCoordinator.sol";
import "../contracts/llm/LLMOracleRegistry.sol";
import "../contracts/llm/LLMOracleTask.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "forge-std/console.sol";
contract TestToken is ERC20 {
constructor() ERC20("TestToken", "TTK") {
_mint(msg.sender, 1_000_000 ether);
}
}
contract LLMOracleCoordinatorTest2 is Test {
LLMOracleCoordinator coordinator;
LLMOracleRegistry registry;
TestToken feeToken;
address owner = address(0x1);
address[] generators;
address[] validators;
function setUp() public {
// Deploy the fee token
feeToken = new TestToken();
// Deploy and initialize the registry
registry = new LLMOracleRegistry(0, 0, address(feeToken));
// Deploy and initialize the coordinator
coordinator = new LLMOracleCoordinator(
address(registry),
address(feeToken),
1 ether, // platform fee
1 ether, // generation fee
1 ether // validation fee
);
// Set up owner
vm.label(owner, "Owner");
vm.deal(owner, 100 ether); // Provide Ether for gas costs
// Transfer tokens from the test contract to the owner
feeToken.transfer(owner, 1_000_000 ether); // Transfer tokens to owner
}
// Helper function to log arrays
function logUint256Array(string memory name, uint256[] memory array) internal view {
console.log(string(abi.encodePacked(name, " length: ", vm.toString(array.length))));
for (uint256 i = 0; i < array.length; i++) {
console.log(string(abi.encodePacked(name, "[", vm.toString(i + 1), "] = ")), array[i]);
}
}
function testFuzzFinalizeValidation(uint256 numGenerations, uint256 numValidations) public {
// Ensure numGenerations and numValidations are within reasonable bounds
numGenerations = bound(numGenerations, 1, 10); // between 1 and 10
numValidations = bound(numValidations, 1, 10); // between 1 and 10
console.log("numGenerations: ", numGenerations);
console.log("numValidations: ", numValidations);
// Owner approves the coordinator to spend tokens
vm.startPrank(owner);
feeToken.approve(address(coordinator), 1_000_000 ether);
vm.stopPrank();
// Create and register generators
for (uint256 i = 0; i < numGenerations; i++) {
address generator = address(uint160(uint256(keccak256(abi.encodePacked("Generator", i)))));
vm.label(generator, string(abi.encodePacked("Generator", vm.toString(i))));
generators.push(generator);
vm.deal(generator, 10 ether); // Provide Ether for gas costs
// Owner transfers tokens to the generator
vm.startPrank(owner);
feeToken.transfer(generator, 100 ether);
vm.stopPrank();
vm.startPrank(generator);
feeToken.approve(address(registry), 100 ether);
registry.register(LLMOracleKind.Generator);
vm.stopPrank();
}
// Create and register validators
for (uint256 i = 0; i < numValidations; i++) {
address validator = address(uint160(uint256(keccak256(abi.encodePacked("Validator", i)))));
vm.label(validator, string(abi.encodePacked("Validator", vm.toString(i))));
validators.push(validator);
vm.deal(validator, 10 ether); // Provide Ether for gas costs
// Owner transfers tokens to the validator
vm.startPrank(owner);
feeToken.transfer(validator, 10 ether);
vm.stopPrank();
vm.startPrank(validator);
feeToken.approve(address(registry), 10 ether);
registry.register(LLMOracleKind.Validator);
vm.stopPrank();
}
// Set up task parameters
LLMOracleTaskParameters memory parameters = LLMOracleTaskParameters({
numGenerations: uint40(numGenerations),
numValidations: uint40(numValidations),
difficulty: 1
});
// Owner requests a task
vm.startPrank(owner);
uint256 taskId = coordinator.request("protocol", "input", "models", parameters);
vm.stopPrank();
// Generators respond
for (uint256 i = 0; i < numGenerations; i++) {
address generator = generators[i];
vm.startPrank(generator);
coordinator.respond(taskId, i + 1, "output", "metadata");
vm.stopPrank();
}
// Validators validate
for (uint256 i = 0; i < numValidations; i++) {
address validator = validators[i];
vm.startPrank(validator);
uint256[] memory scores = new uint256[]();
for (uint256 j = 0; j < numGenerations; j++) {
// Generate random scores between 1 and 10
scores[j] = (uint256(keccak256(abi.encodePacked(i, j, block.timestamp))) % 10) + 1;
}
coordinator.validate(taskId, i + 1, scores, "metadata");
logUint256Array("Total Scores", scores);
vm.stopPrank();
}
}
}

Impact

Validators cannot receive their earned fees.

Tools Used

Manual review.

Foundry.

Recommendations

Setting diff to int256, casting data[i] and mean to handle possible negative values when calculating `variance`.

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++) {
int256 diff = int256(data[i]) - int256(mean);
int256 diffSquared = diff * diff; // This will always be positive or zero
sum += uint256(diffSquared);
}
ans = sum / data.length;
}
Updates

Lead Judging Commences

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

Underflow in computing variance

Support

FAQs

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