Dria

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

Lack of settings validation risks unnecessary generator payouts and user fund losses

Summary

If there is no validations, the generators directly earn the fees in LLMOracleCoordinator::respond:

...
if (task.parameters.numValidations == 0) {
_increaseAllowance(msg.sender, task.generatorFee);
}
...

With zero validations, the best response defaults to the first oracle submission, as no scores will be assigned to other responses:

function getBestResponse(
uint256 taskId
) external view returns (TaskResponse memory) {
...
// pick the result with the highest validation score
TaskResponse storage result = taskResponses[0];
uint256 highestScore = result.score;
for (uint256 i = 1; i < taskResponses.length; i++) {
if (taskResponses[i].score > highestScore) {
highestScore = taskResponses[i].score;
result = taskResponses[i];
}
}
return result;
}

If the protocol admin neglects to set the number of generations to 1 when validations are disabled, all subsequent generator fees paid by users are essentially wasted, as only the first response is ever considered.

Vulnerability Details

Although the protocol admin is trusted, small configuration errors, like incorrect parameter settings, could unintentionally lead to user fund losses.

Impact

Refer to the PoC below for more details:

See PoC
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;
import {Vm, Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {Swan} from "../src/swan/Swan.sol";
import {BuyerAgent} from "../src/swan/BuyerAgent.sol";
import {LLMOracleCoordinator} from "../src/llm/LLMOracleCoordinator.sol";
import {LLMOracleRegistry, LLMOracleKind} from "../src/llm/LLMOracleRegistry.sol";
import {MockToken} from "./mock/MockToken.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {LLMOracleTaskParameters, LLMOracleTask} from "../src/llm/LLMOracleTask.sol";
import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
import {SwanMarketParameters} from "../src/swan/SwanManager.sol";
import {SwanAssetFactory, SwanAsset} from "../src/swan/SwanAsset.sol";
contract BaseTest is Test {
LLMOracleRegistry registry;
LLMOracleCoordinator coordinator;
MockToken mocktoken;
Swan swan;
BuyerAgent buyerAgent;
address requester = makeAddr("requester");
// Mocks the oracle LLM
address responder = makeAddr("responder");
// Address whose buyer agent will acquire listed assets
address buyer = makeAddr("buyer");
// Proxies deployment part 1
function setUp() public {
LLMOracleRegistry registryImpl = new LLMOracleRegistry();
mocktoken = new MockToken("Mock Token", "MT");
uint256 generatorStake = 1;
bytes memory _data = abi.encodeWithSignature(
"initialize(uint256,uint256,address)",
generatorStake,
0,
address(mocktoken)
);
ERC1967Proxy registryProxy = new ERC1967Proxy(
address(registryImpl),
_data
);
registry = LLMOracleRegistry(address(registryProxy));
LLMOracleCoordinator coordinatorImpl = new LLMOracleCoordinator();
uint256 _platformFee = 0.1 ether;
uint256 _generationFee = 0.1 ether;
uint256 _validationFee = 0.1 ether;
_data = abi.encodeWithSignature(
"initialize(address,address,uint256,uint256,uint256)",
address(registry),
address(mocktoken),
_platformFee,
_generationFee,
_validationFee
);
ERC1967Proxy coordinatorProxy = new ERC1967Proxy(
address(coordinatorImpl),
_data
);
coordinator = LLMOracleCoordinator(address(coordinatorProxy));
deploySwanAndAgent();
}
// Deployments function part 2 to avoid stack to deep error
function deploySwanAndAgent() public {
SwanAssetFactory assetFactory = new SwanAssetFactory();
Swan swanImpl = new Swan();
SwanMarketParameters memory marketParams = SwanMarketParameters({
withdrawInterval: uint256(1 weeks),
sellInterval: uint256(1 weeks),
buyInterval: uint256(1 weeks),
platformFee: uint256(1),
maxAssetCount: uint256(100),
timestamp: uint256(block.timestamp)
});
LLMOracleTaskParameters memory oracleParams = LLMOracleTaskParameters({
difficulty: uint8(1),
numGenerations: uint40(2),
numValidations: uint40(0)
});
bytes memory _data = abi.encodeWithSelector(
Swan(address(swanImpl)).initialize.selector,
marketParams,
oracleParams,
address(coordinator),
address(mocktoken),
address(1),
assetFactory
);
ERC1967Proxy swanProxy = new ERC1967Proxy(address(swanImpl), _data);
swan = Swan(address(swanProxy));
string memory _name = "buyer agent";
string memory _description = "buyer agent";
uint96 _royaltyFee = 1;
// uint256 _amountPerRound = type(uint256).max;
uint256 _amountPerRound = 1 ether;
address _operator = address(swan);
address _owner = buyer;
buyerAgent = new BuyerAgent(
_name,
_description,
_royaltyFee,
_amountPerRound,
_operator,
_owner
);
}
function testOnlyFirstGenerationUsed() public {
// Move to buy phase
vm.warp(block.timestamp + 1 weeks + 1);
bytes memory input = "input";
bytes memory models = "models";
deal(address(mocktoken), address(buyerAgent), 1 ether);
vm.startPrank(buyer);
// Submit purchase request
buyerAgent.oraclePurchaseRequest(input, models);
vm.stopPrank();
deal(address(mocktoken), responder, 1 ether);
vm.startPrank(responder);
mocktoken.approve(address(registry), type(uint256).max);
LLMOracleKind oracleKind = LLMOracleKind.Generator;
registry.register(oracleKind);
uint256 taskId = 1;
uint256 nonce = 123;
bytes memory output = "output_best";
// First generator response
coordinator.respond(taskId, nonce, output, models);
vm.stopPrank();
address responder2 = makeAddr("responder2");
deal(address(mocktoken), responder2, 1 ether);
vm.startPrank(responder2);
mocktoken.approve(address(registry), type(uint256).max);
registry.register(oracleKind);
bytes memory output2 = "output2";
uint256 nonce2 = 12345;
// Second generator response (ignored), buyer spent fees for an answer that was not needed
coordinator.respond(taskId, nonce2, output2, models);
vm.stopPrank();
// Chosen response is the first one
assertEq(buyerAgent.oracleResult(1), output);
}
}

Here you can also find the code for the mock token:

Mock Token
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockToken is ERC20 {
constructor(
string memory name_,
string memory symbol_
) ERC20(name_, symbol_) {
_mint(msg.sender, 100 ether);
}
}

Tools Used

Manual review.

Recommendations

Extend valid parameters verification in LLMOracleManager::onlyValidParameters, this way the owner will be aware of an incorrect parameters setting:

modifier onlyValidParameters(LLMOracleTaskParameters calldata parameters) {
if (
parameters.difficulty < minimumParameters.difficulty ||
parameters.difficulty > maximumParameters.difficulty
) {
revert InvalidParameterRange(
parameters.difficulty,
minimumParameters.difficulty,
maximumParameters.difficulty
);
}
if (
parameters.numGenerations < minimumParameters.numGenerations ||
parameters.numGenerations > maximumParameters.numGenerations
) {
revert InvalidParameterRange(
parameters.numGenerations,
minimumParameters.numGenerations,
maximumParameters.numGenerations
);
}
if (
parameters.numValidations < minimumParameters.numValidations ||
parameters.numValidations > maximumParameters.numValidations
) {
revert InvalidParameterRange(
parameters.numValidations,
minimumParameters.numValidations,
maximumParameters.numValidations
);
}
+ if (parameters.numValidations == 0 && parameters.numGenerations > 1) {
+ revert InvalidParameters(
+ parameters.numValidations,
+ parameters.numGenerations
+ );
}
_;
}
Updates

Lead Judging Commences

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

Return value of `getBestResponse` when no validators

Support

FAQs

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