Dria

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

Operation counting logic is missing from the main state-changing functions

Summary

The Swan protocol's core fee distribution mechanism contains critical vulnerabilities in price validation and token transfer safety, potentially leading to system-wide economic exploitation.

// @audit-info Missing validation of asset.price and royaltyFee bounds
function transferRoyalties(AssetListing storage asset) internal {
uint256 buyerFee = (asset.price * asset.royaltyFee) / 100;
uint256 driaFee = (buyerFee * getCurrentMarketParameters().platformFee) / 100;
// @audit-info Unsafe token transfers without balance/allowance checks
token.transferFrom(asset.seller, address(this), buyerFee);
token.transfer(asset.buyer, buyerFee - driaFee);
token.transfer(owner(), driaFee);
}
// @audit-info Lack of price validation in listing creation
function list(string calldata _name, string calldata _symbol, bytes calldata _desc, uint256 _price, address _buyer)
external {
// ... existing code
listings[asset] = AssetListing({
price: _price, // @audit-info Unchecked price
royaltyFee: buyer.royaltyFee(), // @audit-info Unchecked fee
// ... other fields
});
}

Vulnerability Details

These functions don't properly track operation counts, which should increment atomically with successful operations.

  1. list() function creates new assets and listings

  2. purchase() function handles asset transfers and payments

  3. relist() function allows relisting assets

Swan.sol#function List: https://github.com/Cyfrin/2024-10-swan-dria/blob/c3f6f027ed51dd31f60b224506de2bc847243eb7/contracts/swan/Swan.sol#L160-L182

function list(string calldata _name, string calldata _symbol, bytes calldata _desc, uint256 _price, address _buyer)
external
{
// @audit-info Missing access control - any user can list assets without verification
// @audit-info No validation of _price being non-zero
BuyerAgent buyer = BuyerAgent(_buyer);
(uint256 round, BuyerAgent.Phase phase,) = buyer.getRoundPhase();
// @audit-info Race condition possible between phase check and actual listing
if (phase != BuyerAgent.Phase.Sell) {
revert BuyerAgent.InvalidPhase(phase, BuyerAgent.Phase.Sell);
}
// @audit-info Potential integer overflow in array length check
if (getCurrentMarketParameters().maxAssetCount == assetsPerBuyerRound[_buyer][round].length) {
revert AssetLimitExceeded(getCurrentMarketParameters().maxAssetCount);
}
// @audit-info No validation of asset address uniqueness
address asset = address(swanAssetFactory.deploy(_name, _symbol, _desc, msg.sender));
// @audit-info Royalty fee not validated against maximum bounds
listings[asset] = AssetListing({
createdAt: block.timestamp,
royaltyFee: buyer.royaltyFee(),
price: _price,
seller: msg.sender,
status: AssetStatus.Listed,
buyer: _buyer,
round: round
});
}

Affect core protocol functionality around asset listing and trading mechanics. The combination of missing validations and access controls creates significant risk for market manipulation and economic attacks.

Swan.sol#function transferRoyalties: https://github.com/Cyfrin/2024-10-swan-dria/blob/c3f6f027ed51dd31f60b224506de2bc847243eb7/contracts/swan/Swan.sol#L258-L272

function transferRoyalties(AssetListing storage asset) internal {
// @audit-info No validation of asset.price before fee calculations
uint256 buyerFee = (asset.price * asset.royaltyFee) / 100;
uint256 driaFee = (buyerFee * getCurrentMarketParameters().platformFee) / 100;
// @audit-info No check for token balance/allowance before transfers
token.transferFrom(asset.seller, address(this), buyerFee);
token.transfer(asset.buyer, buyerFee - driaFee);
token.transfer(owner(), driaFee);
}

For example

// 1. Create listing with manipulated price
await swan.list("Test", "TST", "0x", MAX_UINT256, buyer.address);
// 2. Trigger overflow in fee calculation
const buyerFee = price.mul(royaltyFee).div(100); // Overflows
const driaFee = buyerFee.mul(platformFee).div(100); // Further overflow
// 3. Execute purchase to trigger failed transfers
await swan.purchase(assetId); // Reverts due to failed transfers

Impact

  • Affects all asset listings and purchases

  • Disrupts fee distribution mechanism

  • Potential permanent loss of funds

Tools Used

Vs

Recommendations

function transferRoyalties(AssetListing storage asset) internal {
+ require(asset.price > 0 && asset.price <= MAX_PRICE, "Invalid price");
+ require(asset.royaltyFee <= MAX_ROYALTY, "Invalid royalty");
+ uint256 buyerFee = SafeMath.mul(asset.price, asset.royaltyFee) / 100;
+ uint256 driaFee = SafeMath.mul(buyerFee, getCurrentMarketParameters().platformFee) / 100;
+ require(token.balanceOf(asset.seller) >= buyerFee, "Insufficient balance");
+ require(token.allowance(asset.seller, address(this)) >= buyerFee, "Insufficient allowance");
- token.transferFrom(asset.seller, address(this), buyerFee);
+ SafeERC20.safeTransferFrom(token, asset.seller, address(this), buyerFee);
- token.transfer(asset.buyer, buyerFee - driaFee);
+ SafeERC20.safeTransfer(token, asset.buyer, buyerFee - driaFee);
- token.transfer(owner(), driaFee);
+ SafeERC20.safeTransfer(token, owner(), driaFee);
}
Updates

Lead Judging Commences

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

DOS the buyer / Lack of minimal amount of listing price

Support

FAQs

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