Dria

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

Asset State Manipulation in Swan Protocol

Summary

The issue is that there's no check preventing a sold asset from being relisted. This could lead to the following attack scenario:

  1. Asset is listed (status = 1)

  2. Asset is purchased (status = 2)

  3. Asset is relisted (status = 1)

  4. Asset could be purchased again

This violates the intended state machine where an asset should not be transferable after being sold.

Vulnerability Details

The contract fails to enforce proper state transitions for assets. Once an asset is sold, it can be relisted and sold again due to missing validation checks.

https://github.com/Cyfrin/2024-10-swan-dria/blob/c3f6f027ed51dd31f60b224506de2bc847243eb7/contracts/swan/Swan.sol#L66-L70

// @bug detected: AssetStatus enum allows invalid state transitions
// Assets can move between states without proper validation
enum AssetStatus {
Unlisted,
Listed,
Sold
}

https://github.com/Cyfrin/2024-10-swan-dria/blob/c3f6f027ed51dd31f60b224506de2bc847243eb7/contracts/swan/Swan.sol#L276-L290

// @bug detected: No state transition validation in purchase()
// Assets can be purchased multiple times due to lack of state checks
function purchase(address _asset) external {
AssetListing storage listing = listings[_asset];
listing.status = AssetStatus.Sold;
// ...transfers happen here
}

https://github.com/Cyfrin/2024-10-swan-dria/blob/c3f6f027ed51dd31f60b224506de2bc847243eb7/contracts/swan/Swan.sol#L197-L243

// @bug detected: Improper state validation in relist()
// Sold assets can be relisted and resold due to missing status checks
function relist(address _asset, address _buyer, uint256 _price) external {
AssetListing storage asset = listings[_asset];
// Missing check for Sold status
listings[_asset] = AssetListing({
status: AssetStatus.Listed,
// ...
});
}

This is dangerous for the protocol because:

  1. Assets can transition between states in unintended ways, breaking the core invariant that assets should follow Unlisted -> Listed -> Sold progression.

  2. The same asset can be purchased multiple times since there's no permanent "Sold" state.

  3. Multiple sales of the same asset would create artificial supply and potentially drain funds from buyers who purchase already-sold assets.

  4. The ability to resell already sold assets breaks the fundamental NFT ownership guarantees that users expect.

The core issue stems from missing state transition validations, allowing assets to be recycled through the system multiple times when they should be permanently marked as sold.

The vulnerability connects directly to Swan's core asset management system, affecting:

  • Asset listing (list())

  • Asset purchases (purchase())

  • Asset relisting (relist())

  • State management (AssetStatus)

This creates a circular path where assets can infinitely cycle through states that should be terminal.

PoC

// 1. Seller lists asset
swan.list("Asset", "AST", "desc", 100, buyer);
// 2. Buyer purchases asset
swan.purchase(assetAddress); // Asset is now "Sold"
// 3. Seller relists the same asset
swan.relist(assetAddress, newBuyer, 100); // Asset becomes "Listed" again
// 4. New buyer can purchase the already-sold asset
swan.purchase(assetAddress); // Double-spend achieved

Impact

  1. Assets can be sold multiple times

  2. Buyers can lose funds by purchasing already-sold assets

  3. Market manipulation through artificial supply

  4. Token transfers can occur for the same asset multiple times

Tools Used

Manual Review

Recommendations

function relist(address _asset, address _buyer, uint256 _price) external {
AssetListing storage asset = listings[_asset];
+ if (asset.status == AssetStatus.Sold) {
+ revert InvalidStatus(asset.status, AssetStatus.Listed);
+ }
listings[_asset] = AssetListing({
status: AssetStatus.Listed,
// ...
});
}
+ modifier notSold(address asset) {
+ if (listings[asset].status == AssetStatus.Sold) {
+ revert AssetAlreadySold(asset);
+ }
+ _;
+ }
Updates

Lead Judging Commences

inallhonesty Lead Judge
7 months ago
inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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