Dria

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

Purchased assets will be locked in the buyer agent contract

Summary

This protocol draws inspiration from Story's "Proof-of-Creativity," enabling each item to accumulate lore and intrinsic value as buyers achieve in-game goals. In this world, every asset has a story: imagine a wizard who vanquishes a formidable dragon with an enchanted staff, painstakingly crafted by a legendary artisan. When the time comes for the wizard to pass this heirloom to an apprentice, the lack of a mechanism to grant approvals or transfer ownership from the buyer agent contract can create a barrier, potentially locking assets permanently. This limitation risks anchoring items within the buyer agent contract, preventing their stories from evolving further.

Vulnerability Details

Users can buy the tokens by calling BuyerAgent::purchase, this will in turn call Swan::purchase which will perform the following transfer at the end of the function:

SwanAsset(_asset).transferFrom(listing.seller, address(this), 1);
SwanAsset(_asset).transferFrom(address(this), listing.buyer, 1);

Here, listing.buyer represents the buyer agent contract itself. If there is no mechanism within the buyer agent contract to transfer the token to a different address, the actual owner (the entity on behalf of whom the buyer agent is deployed) will be unable to access the purchased asset, essentially locking it in the contract.

Impact

The issue can be tested using the following PoC in Foundry:

See PoC
// SPDX-License-Identifier: MIT
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.001 ether;
uint256 _generationFee = 0.001 ether;
uint256 _validationFee = 0.001 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(1),
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;
address _operator = address(swan);
address _owner = buyer;
buyerAgent = new BuyerAgent(
_name,
_description,
_royaltyFee,
_amountPerRound,
_operator,
_owner
);
}
// Mocks listing and buyer purchase request
function mockTokenPurchase() public returns (bytes32) {
string memory _name = "asset";
string memory _symbol = "asset";
bytes memory _desc = "description";
uint256 _price = 100;
address _buyer = address(buyerAgent);
mocktoken.approve(address(swan), type(uint256).max);
vm.recordLogs();
// This contract lists any asset targeting buyer
swan.list(_name, _symbol, _desc, _price, _buyer);
Vm.Log[] memory entries = vm.getRecordedLogs();
bytes32 assetAddr = entries[6].topics[2];
// This should be new bytes32[](1); for whatever reason it does not display it
bytes32[] memory assetAddrArr = new bytes32[]();
assetAddrArr[0] = assetAddr;
vm.warp(block.timestamp + 1 weeks + 1);
bytes memory input = "input";
bytes memory models = "models";
LLMOracleTaskParameters memory oracleParams = LLMOracleTaskParameters({
difficulty: 1,
numGenerations: 1,
numValidations: 0
});
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 = abi.encode(assetAddrArr);
// Oracle response contains our previously listed asset
coordinator.respond(taskId, nonce, output, models); // Reuse models for metadata to avoid stack too deep error
vm.stopPrank();
vm.prank(buyer);
// Buyer agent acquires the asset
buyerAgent.purchase();
return assetAddr;
}
function testAssetLocked() public {
// Buyer agent owns the recently bought asset
bytes32 addressBytes = mockTokenPurchase();
address assetAddr = abi.decode(
abi.encodePacked(addressBytes),
(address)
);
assertEq(SwanAsset(assetAddr).ownerOf(1), address(buyerAgent));
// We would like to transfer the asset to our EOA, but...
vm.expectRevert();
vm.prank(buyer);
// ERC721InsufficientApproval
SwanAsset(assetAddr).transferFrom(address(buyerAgent), buyer, 1);
}
}

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

Add a function so that users can approve themselves in the buyer agent to transfer the tokens.

Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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