The validation scoring system in LLMOracleCoordinator is vulnerable to order dependency where early validators can influence the scoring standard deviation, affecting reward distribution for subsequent validators.
https://github.com/Cyfrin/2024-10-swan-dria/blob/main/contracts/llm/LLMOracleCoordinator.sol
function finalizeValidation(uint256 taskId) private {
TaskRequest storage task = requests[taskId];
for (uint256 g_i = 0; g_i < task.parameters.numGenerations; g_i++) {
uint256[] memory scores = new uint256[]();
for (uint256 v_i = 0; v_i < task.parameters.numValidations; v_i++) {
scores[v_i] = validations[taskId][v_i].scores[g_i];
}
(uint256 _stddev, uint256 _mean) = Statistics.stddev(scores);
uint256 innerSum = 0;
uint256 innerCount = 0;
for (uint256 v_i = 0; v_i < task.parameters.numValidations; ++v_i) {
uint256 score = scores[v_i];
if ((score >= _mean - _stddev) && (score <= _mean + _stddev)) {
innerSum += score;
innerCount++;
_increaseAllowance(validations[taskId][v_i].validator, task.validatorFee);
}
}
}
}
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {LLMOracleCoordinator} from "../contracts/llm/LLMOracleCoordinator.sol";
import {MockERC20} from "./mocks/MockERC20.sol";
contract ValidationOrderTest is Test {
LLMOracleCoordinator coordinator;
MockERC20 token;
address owner;
address[] validators;
uint256 constant NUM_VALIDATORS = 5;
uint256 constant VALIDATION_FEE = 1 ether;
function setUp() public {
owner = makeAddr("owner");
token = new MockERC20();
validators = new address[](NUM_VALIDATORS);
for(uint i = 0; i < NUM_VALIDATORS; i++) {
validators[i] = makeAddr(string.concat("validator", vm.toString(i)));
token.mint(validators[i], 100 ether);
}
coordinator = new LLMOracleCoordinator();
coordinator.initialize(
address(0),
address(token),
0,
0,
VALIDATION_FEE
);
}
function testValidationOrderManipulation() public {
uint256 taskId = createTestTask();
coordinator = new LLMOracleCoordinator();
uint256[] memory normalScores = new uint256[]();
normalScores[0] = 70;
normalScores[1] = 75;
normalScores[2] = 80;
for(uint i = 0; i < 3; i++) {
vm.prank(validators[i]);
coordinator.validate(taskId, 123, new uint256[](1){normalScores[i]}, "");
}
vm.prank(validators[3]);
coordinator.validate(taskId, 123, new uint256[](1){74}, "");
vm.prank(validators[4]);
coordinator.validate(taskId, 123, new uint256[](1){76}, "");
for(uint i = 0; i < NUM_VALIDATORS; i++) {
assertEq(
coordinator.validatorRewards(validators[i]),
VALIDATION_FEE,
"Validator should receive reward"
);
}
coordinator = new LLMOracleCoordinator();
taskId = createTestTask();
vm.prank(validators[0]);
coordinator.validate(taskId, 123, new uint256[](1){10}, "");
vm.prank(validators[1]);
coordinator.validate(taskId, 123, new uint256[](1){90}, "");
vm.prank(validators[2]);
coordinator.validate(taskId, 123, new uint256[](1){50}, "");
vm.prank(validators[3]);
coordinator.validate(taskId, 123, new uint256[](1){85}, "");
vm.prank(validators[4]);
coordinator.validate(taskId, 123, new uint256[](1){15}, "");
for(uint i = 0; i < NUM_VALIDATORS; i++) {
assertEq(
coordinator.validatorRewards(validators[i]),
VALIDATION_FEE,
"Validator should receive reward"
);
}
}
function testTargetedExclusion() public {
uint256 taskId = createTestTask();
for(uint i = 0; i < 4; i++) {
vm.prank(validators[i]);
coordinator.validate(taskId, 123, new uint256[](1){50}, "");
}
vm.prank(validators[4]);
coordinator.validate(taskId, 123, new uint256[](1){75}, "");
for(uint i = 0; i < NUM_VALIDATORS - 1; i++) {
assertEq(
coordinator.validatorRewards(validators[i]),
VALIDATION_FEE,
"Colluding validator should receive reward"
);
}
assertEq(
coordinator.validatorRewards(validators[4]),
0,
"Honest validator should be excluded"
);
}
function createTestTask() internal returns (uint256) {
bytes memory input = "test input";
bytes memory models = "test models";
LLMOracleTaskParameters memory params = LLMOracleTaskParameters({
difficulty: 1,
numGenerations: 1,
numValidations: NUM_VALIDATORS
});
return coordinator.request(bytes32("test"), input, models, params);
}
}
contract LLMOracleCoordinator {
struct ValidationCommitment {
bytes32 commitment;
uint256 timestamp;
bool revealed;
}
mapping(uint256 => mapping(address => ValidationCommitment)) public commitments;
function commitValidation(uint256 taskId, bytes32 commitment) external {
commitments[taskId][msg.sender] = ValidationCommitment({
commitment: commitment,
timestamp: block.timestamp,
revealed: false
});
}
function revealValidation(
uint256 taskId,
uint256[] calldata scores,
bytes32 salt
) external {
ValidationCommitment storage commitment = commitments[taskId][msg.sender];
require(!commitment.revealed, "Already revealed");
require(
keccak256(abi.encodePacked(scores, salt)) == commitment.commitment,
"Invalid revelation"
);
commitment.revealed = true;
_processValidation(taskId, scores);
}
}