Dria

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

Reentrancy in Core Trading Functions Leading to Double Spending

Summary

Critical reentrancy vulnerabilities in Swan's core trading functions allow double spending of assets and manipulation of listing states due to state updates occurring after external calls.

Vulnerability Details

Looking at the Swan.sol contract, there are several potential reentrancy vectors.

In the purchase() function: https://github.com/Cyfrin/2024-10-swan-dria/blob/c3f6f027ed51dd31f60b224506de2bc847243eb7/contracts/swan/Swan.sol#L276-L290

function purchase(address _asset) external {
AssetListing storage listing = listings[_asset];
// Checks
if (listing.status != AssetStatus.Listed) {
revert InvalidStatus(listing.status, AssetStatus.Listed);
}
if (listing.buyer != msg.sender) {
revert Unauthorized(msg.sender);
}
// @bug detected: State update happens after external calls, enabling reentrancy
// Effects should come before Interactions
// Transfer asset first (Interactions)
SwanAsset(_asset).transferFrom(listing.seller, address(this), 1);
SwanAsset(_asset).transferFrom(address(this), listing.buyer, 1);
// Transfer money (Interactions)
token.transferFrom(listing.buyer, address(this), listing.price);
token.transfer(listing.seller, listing.price);
// Effects come last
listing.status = AssetStatus.Sold;
}

A malicious buyer could:

  1. Call purchase()

  2. During the first transferFrom, reenter the contract

  3. The listing status is still Listed since it hasn't been updated yet

  4. This allows buying the same asset multiple times

In the list function: https://github.com/Cyfrin/2024-10-swan-dria/blob/c3f6f027ed51dd31f60b224506de2bc847243eb7/contracts/swan/Swan.sol#L157-L182

function list(...) external {
// @bug detected: Royalty transfers happen before state updates
transferRoyalties(listings[asset]);
// State updates should happen before external transfers
listings[asset] = AssetListing({...});
}

In the relist() function: https://github.com/Cyfrin/2024-10-swan-dria/blob/c3f6f027ed51dd31f60b224506de2bc847243eb7/contracts/swan/Swan.sol#L197-L246

function relist(...) external {
// @bug detected: Same issue as list() - external calls before state updates
transferRoyalties(listings[_asset]);
// State updates should happen after external calls
listings[_asset] = AssetListing({...});
}

These vulnerabilities allow reentrancy attacks because they violate the Checks-Effects-Interactions pattern. The key issue is that external calls (transferFrom/transfer) occur before state updates.

Root Cause:

  • The contract performs external calls (transferFrom/transfer) before updating critical state variables

  • This violates the Checks-Effects-Interactions pattern

  • The listing status remains "Listed" during external calls, allowing reentrant purchases

Why It's Dangerous:

  • Breaks core invariants around asset ownership and listing status

  • Allows economic attacks through double spending

  • Impacts all users interacting with listings

  • Connected to core purchase flow affecting all asset transfers

  • No protection against reentrancy in place

The vulnerability exists in the main trading functions (purchase, list, relist) which are central to the protocol's functionality. This makes it particularly severe as it affects the core economic mechanisms of the system.

Impact

  1. Assets can be purchased multiple times before status updates

  2. Royalty payments can be manipulated through reentrancy

  3. Listing states can be exploited during reentrant calls

  4. Potential loss of funds through double-spending attacks

The attack vectors exist because a malicious contract could reenter during the external calls and execute additional transactions while the state is still unchanged. This allows exploitation of the interim state before updates are finalized.

Tools Used

Manual code review

Recommendations

function purchase(address _asset) external {
AssetListing storage listing = listings[_asset];
+ // Update state first
+ listing.status = AssetStatus.Sold;
// External calls after state updates
SwanAsset(_asset).transferFrom(listing.seller, address(this), 1);
SwanAsset(_asset).transferFrom(address(this), listing.buyer, 1);
token.transferFrom(listing.buyer, address(this), listing.price);
token.transfer(listing.seller, listing.price);
- listing.status = AssetStatus.Sold;
}

We can apply a similar pattern to list() and relist() - update state before making external calls. Consider adding reentrancy guards to critical functions.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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