Dria

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

Asset Lock & Payment Vulnerability in Purchase Flow

Summary

Non-atomic operations in Swan's purchase mechanism create asset lockup and payment vulnerabilities through improper state management.

function purchase(address _asset) external {
// @audit-info No validation of asset existence
AssetListing storage listing = listings[_asset];
// @audit-info State change before asset transfer creates lockup risk
listing.status = AssetStatus.Sold;
// @audit-info Non-atomic transfers can trap NFT in contract
SwanAsset(_asset).transferFrom(listing.seller, address(this), 1);
SwanAsset(_asset).transferFrom(address(this), listing.buyer, 1);
// @audit-info Payment after asset transfer enables value extraction
token.transferFrom(listing.buyer, address(this), listing.price);
token.transfer(listing.seller, listing.price);
}

Vulnerability Details

There's a potential ownership violation in the two-step transfer process. The contract assumes both transfers will succeed, but if the second transfer fails after the first one succeeds, the asset could become stuck in the Swan contract.

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

function purchase(address _asset) external {
AssetListing storage listing = listings[_asset];
// @audit-info No check if asset exists in listings mapping
// @audit-info Missing zero address validation for _asset
// @audit-info Status check happens before asset ownership verification
if (listing.status != AssetStatus.Listed) {
revert InvalidStatus(listing.status, AssetStatus.Listed);
}
// @audit-info Race condition possible between status check and transfer
if (listing.buyer != msg.sender) {
revert Unauthorized(msg.sender);
}
// @audit-info Status updated before transfers complete - state inconsistency risk
listing.status = AssetStatus.Sold;
// @audit-info Non-atomic transfer operation creates locked funds risk
SwanAsset(_asset).transferFrom(listing.seller, address(this), 1);
SwanAsset(_asset).transferFrom(address(this), listing.buyer, 1);
// @audit-info Payment happens after asset transfers - potential value extraction
token.transferFrom(listing.buyer, address(this), listing.price);
token.transfer(listing.seller, listing.price);
emit AssetSold(listing.seller, msg.sender, _asset, listing.price);
}
  1. First transferFrom moves the asset from seller to Swan

  2. If second transferFrom fails (e.g., due to revert), asset remains in Swan

  3. The listing status is already updated to Sold

  4. No mechanism exists to recover from this state

See this example

1. Initial State:
- Seller lists NFT
- Buyer initiates purchase
2. Attack Sequence:
- purchase() called
- listing.status set to Sold
- First transfer succeeds (seller -> Swan)
- Second transfer fails
- NFT locked in Swan contract
- Status incorrectly marked as Sold
3. Result:
- Asset trapped
- Buyer lost funds
- Seller lost NFT
- Market state corrupted

Impact

  • Asset can become permanently locked in the Swan contract

  • Neither buyer nor seller can recover the asset

  • Breaks NFT ownership invariant

Tools Used

Vs

Recommendations

function purchase(address _asset) external {
AssetListing storage listing = listings[_asset];
+ require(_asset != address(0), "Invalid asset");
+ require(listings[_asset].seller != address(0), "Asset not listed");
+ // Verify payment first
+ token.transferFrom(listing.buyer, address(this), listing.price);
try {
+ // Atomic transfer
+ SwanAsset(_asset).transferFrom(listing.seller, listing.buyer, 1);
+ listing.status = AssetStatus.Sold;
+ token.transfer(listing.seller, listing.price);
} catch {
+ // Rollback payment
+ token.transfer(listing.buyer, listing.price);
revert("Transfer failed");
}
emit AssetSold(listing.seller, msg.sender, _asset, listing.price);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

[KNOWN] - Low-35 Unsafe use of transfer()/transferFrom() with IERC20

Support

FAQs

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