We can see that standard deviation, can greatly exceed mean, even with no min/max elements like 0 or 1e18. The calculation was also confirmed using wolframalpha - population standard deviation {1e14, 1e15, 1e16}
and average {1e14, 1e15, 1e16}
.
Because standard deviation can exceed mean it will cause a panic and halt the execution.
When this happens the last oracle will be unable to finalizeValidation() - loss of gas and the buyerAgent will not be able to get a getBestResponse
due the task never reaching "Completed" status - loss of fees. And can result in listed assets being lost.
Manual review + hardhat tests.
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 { OracleKind, TaskStatus } from "./../types/enums";
import { transferTokens } from "./../helpers";
* Test scenario:
*
* There are 3 generators, only 2 responses required in total.
*
* - Generator #1 responds to the request
* - Generator #1 fails to respond again because it already responded
* - Generator #2 responds to the request
* - Generator #3 tries to respond (should fail because generation phase has ended)
*/
describe("LLMOracleCoordinator", function () {
let dria: HardhatEthersSigner;
let requester: HardhatEthersSigner;
let generators: HardhatEthersSigner[];
let validators: HardhatEthersSigner[];
let dummy: HardhatEthersSigner;
let coordinator: LLMOracleCoordinator;
let registry: LLMOracleRegistry;
let token: ERC20;
let coordinatorAddress: string;
let registryAddress: string;
let taskId = 0n;
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"),
};
this.beforeAll(async function () {
const [deployer, dum, req1, gen1, gen2, gen3, val1, val2, val3] = await ethers.getSigners();
dria = deployer;
requester = req1;
dummy = dum;
generators = [gen1, gen2, gen3];
validators = [val1, val2, val3];
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]),
]);
registryAddress = await registry.getAddress();
coordinatorAddress = await coordinator.getAddress();
});
it("should register oracles", async function () {
await registerOracles(token, registry, generators, validators, STAKES);
});
(BigInt.prototype as any).toJSON = function () {
return this.toString();
};
describe("with validation", function () {
const [numGenerations, numValidations] = [2, 3];
this.beforeAll(async () => {
taskId++;
});
it("should make a request", async function () {
await safeRequest(coordinator, token, requester, taskId, input, models, {
difficulty,
numGenerations,
numValidations,
});
});
it("should respond (1/2 & 2/2)", async function () {
for (let i = 0; i < numGenerations; i++) {
await safeRespond(coordinator, generators[i], output, metadata, taskId, BigInt(i));
}
});
it("should validate (1/3) a generation only once", async function () {
const validator = validators[0];
await safeValidate(coordinator, validator, [parseEther("0.01"), parseEther("0.01")], metadata, taskId, 0n);
});
it("should validate (2/3)", async function () {
const validator = validators[1];
await safeValidate(coordinator, validator, [parseEther("0.001"), parseEther("0.001")], metadata, taskId, 1n);
const request = await coordinator.requests(taskId);
expect(request.status).to.equal(TaskStatus.Completed);
});
it("should validate (3/3)", async function () {
const validator = validators[2];
await safeValidate(coordinator, validator, [parseEther("0.0001"), parseEther("0.0001")], metadata, taskId, 2n);
const request = await coordinator.requests(taskId);
expect(request.status).to.equal(TaskStatus.Completed);
});
});
});
Either add an explicit limit to not go below zero or switch to using an alternative method to calculating average sample distance.