Dria

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

A Malicious Seller Can DoS `BuyerAgent.purchase()` By Transferring The Intended Asset To Be Purchased to Another Address

## Summary
The buyer agent purchase of a listed asset can be DoS'd by the malicious owner of the listed asset (seller) if that seller transferred the asset before the purchase function is called.
## Vulnerability Details
- The process of buying a listed swan asset is as follows:
1. The seller of the asset calls `Swan.list()` to list his asset to a buyer agent, where this asset NFT is going to be minted to the seller.
2. When the buyer agent decides to buy that listed asset and the phase is in the `buy` phase : the buyer agent calls `BuyerAgent.oraclePurchaseRequest()` with the required input (listed assets to be purchased) wherea call is made to the `LLMOracleCoordinator.request()` to make a purchase request **after paying the request fees**, and a taskId is generated for that request and saved in `BuyerAgent.oraclePurchaseRequests[round]`.
3. The oracle request is processed first by the generators adding their responses (via `LLMOracleCoordinator.respond()`) --> then after the responses generation is done, the task is moved to the validation phase to be validated and finalized after getting the required validations -if the task requires a validation- (via `LLMOracleCoordinator.validate()`).
4. Then the buyerAgent can call `BuyerAgent.purchase()` to move on with the purchase process, where the result of the oracle request is extracted via `BuyerAgent.oracleResult()` that gives the task result given by the generator that got the highest validation score, then the purchase process is done via `swan.purchase(asset)`:
```javascript
function purchase() external onlyAuthorized {
//...
// read oracle result using the latest task id for this round
bytes memory output = oracleResult(taskId);
address[] memory assets = abi.decode(output, (address[]));
// we purchase each asset returned
for (uint256 i = 0; i < assets.length; i++) {
//...
swan.purchase(asset);
}
// update taskId as completed
isOracleRequestProcessed[taskId] = true;
}
```
where `swan.purchase(asset)` is going to check the buyer and the state of the listed asset and then transfer the asset (ERC721 NFT) from the seller to the Swan contract and from the Swan contract to the BuyerAgent, and when the transfer of the asset and the price is done successfully; the task is marked as processed `isOracleRequestProcessed[taskId] = true`:
```javascript
function purchase(address _asset) external {
AssetListing storage listing = listings[_asset];
// asset must be listed to be purchased
if (listing.status != AssetStatus.Listed) {
revert InvalidStatus(listing.status, AssetStatus.Listed);
}
// can only the buyer can purchase the asset
if (listing.buyer != msg.sender) {
revert Unauthorized(msg.sender);
}
// update asset status to be sold
listing.status = AssetStatus.Sold;
// transfer asset from seller to Swan, and then from Swan to buyer
// this ensure that only approval to Swan is enough for the sellers
SwanAsset(_asset).transferFrom(listing.seller, address(this), 1);
SwanAsset(_asset).transferFrom(address(this), listing.buyer, 1);
// transfer money
token.transferFrom(listing.buyer, address(this), listing.price);
token.transfer(listing.seller, listing.price);
emit AssetSold(listing.seller, msg.sender, _asset, listing.price);
}
```
- But as can be noticed from the `Swan.purchase()`; the asset is transferred from the `listing.seller` to the swan contract and then to the `listing.buyer` , so if the seller is malicious; he can easily DoS the purchase operation of the buyer agent for that round by transferring his listed asset to another address (an address different from the `listing.seller`), resulting in reverting the `BuyerAgent.purchase()` txn.
## Impact
DoSing the `BuyerAgent.purchase()` function will result in not marking the task of that round as processed (`isOracleRequestProcessed[taskId] = false`) as it can't be processed, so this would result in temporarly freezing the purchase function until the buyer agent recalls the `BuyerAgent.oraclePurchaseRequest()` again to overwrite the previous purchase task, and this will incur the buyer agent to pay fees again to make the new purchase request that overwrites the previous one.
## Proof of Concept
[BuyerAgent.purchase() function/ L251](https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L251C13-L251C34)
```javascript
function purchase() external onlyAuthorized {
//...
// we purchase each asset returned
for (uint256 i = 0; i < assets.length; i++) {
//...
swan.purchase(asset);
}
//...
}
```
[Swan.purchase() function/ L294](https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/Swan.sol#L294C9-L294C74)
```javascript
function purchase(address _asset) external {
//...
// transfer asset from seller to Swan, and then from Swan to buyer
// this ensure that only approval to Swan is enough for the sellers
SwanAsset(_asset).transferFrom(listing.seller, address(this), 1);
SwanAsset(_asset).transferFrom(address(this), listing.buyer, 1);
//...
}
```
## Tools Used
Manual Review.
## Recommendations
Update `Swan.list()` function to transfer the listed asset to the swan contract, and add another mechanism to enable the swan asset creator to get back his asset if not purchased by the old buyer agent and not intended to be relisted again by the original owner.
Updates

Lead Judging Commences

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

DoS in BuyerAgent::purchase

Support

FAQs

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