Dria

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

Denial of Service via Trivial Asset Listings: Disrupting Buyer Agents

Summary

The Swan contract contains a vulnerability within the list function that allows a malicious actor to Denial of Service (DoS) a BuyerAgent by listing assets with low or zero pricing. This approach navigates through the transaction process without executing meaningful token transfers, filling the buyer's asset count to its maximum (maxAssetCount) without a cost. This effectively prevents legitimate listings from being processed, disrupting the buyer's operations and the platform’s functionality.

Vulnerability Details

The key issue lies within the list and Relist functions and their integration with the transferRoyalties function of the Swan contract.

https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/Swan.sol#L157C2-L272C6

function list(string calldata _name, string calldata _symbol, bytes calldata _desc, uint256 _price, address _buyer)
external
{
BuyerAgent buyer = BuyerAgent(_buyer);
(uint256 round, BuyerAgent.Phase phase,) = buyer.getRoundPhase();
// buyer must be in the sell phase
if (phase != BuyerAgent.Phase.Sell) {
revert BuyerAgent.InvalidPhase(phase, BuyerAgent.Phase.Sell);
}
// asset count must not exceed `maxAssetCount`
@>> if (getCurrentMarketParameters().maxAssetCount == assetsPerBuyerRound[_buyer][round].length) {
revert AssetLimitExceeded(getCurrentMarketParameters().maxAssetCount);
}
// all is well, create the asset & its listing
address asset = address(swanAssetFactory.deploy(_name, _symbol, _desc, msg.sender));
listings[asset] = AssetListing({
createdAt: block.timestamp,
royaltyFee: buyer.royaltyFee(),
price: _price,
seller: msg.sender,
status: AssetStatus.Listed,
buyer: _buyer,
round: round
});
// add this to list of listings for the buyer for this round
@>> assetsPerBuyerRound[_buyer][round].push(_asset);
// transfer royaltiestransferRoyalties(listings[asset]);
emit AssetListed(msg.sender, asset, _price);
}
/// @notice Relist the asset for another round and/or another buyer and/or another price.
/// @param _asset address of the asset.
/// @param _buyer new buyerAgent for the asset.
/// @param _price new price of the token.
function relist(address _asset, address _buyer, uint256 _price) external {
AssetListing storage asset = listings[_asset];
// only the seller can relist the asset
if (asset.seller != msg.sender) {
revert Unauthorized(msg.sender);
}
// asset must be listed
if (asset.status != AssetStatus.Listed) {
revert InvalidStatus(asset.status, AssetStatus.Listed);
}
// relist can only happen after the round of its listing has ended
// we check this via the old buyer, that is the existing asset.buyer
//
// note that asset is unlisted here, but is not bought at all
//
// perhaps it suffices to check `==` here, since buyer round
// is changed incrementially
(uint256 oldRound,,) = BuyerAgent(asset.buyer).getRoundPhase();
if (oldRound <= asset.round) {
revert RoundNotFinished(_asset, asset.round);
}
// now we move on to the new buyer
BuyerAgent buyer = BuyerAgent(_buyer);
(uint256 round, BuyerAgent.Phase phase,) = buyer.getRoundPhase();
// buyer must be in sell phase
if (phase != BuyerAgent.Phase.Sell) {
revert BuyerAgent.InvalidPhase(phase, BuyerAgent.Phase.Sell);
}
// buyer must not have more than `maxAssetCount` many assets
uint256 count = assetsPerBuyerRound[_buyer][round].length;
@>> if (count >= getCurrentMarketParameters().maxAssetCount) {
revert AssetLimitExceeded(count);
}
// create listing
listings[_asset] = AssetListing({
createdAt: block.timestamp,
royaltyFee: buyer.royaltyFee(),
price: _price,
seller: msg.sender,
status: AssetStatus.Listed,
buyer: _buyer,
round: round
});
// add this to list of listings for the buyer for this round
@>> assetsPerBuyerRound[_buyer][round].push(_asset);
// transfer royalties
transferRoyalties(listings[_asset]);
emit AssetRelisted(msg.sender, _buyer, _asset, _price);
}
/// @notice Function to transfer the royalties to the seller & Dria.
function transferRoyalties(AssetListing storage asset) internal {
// calculate fees
uint256 buyerFee = (asset.price * asset.royaltyFee) / 100;
uint256 driaFee = (buyerFee * getCurrentMarketParameters().platformFee) / 100;
// first, Swan receives the entire fee from seller
// this allows only one approval from the seller's side
token.transferFrom(asset.seller, address(this), buyerFee);
// send the buyer's portion to them
token.transfer(asset.buyer, buyerFee - driaFee);
// then it sends the remaining to Swan owner
token.transfer(owner(), driaFee);
}
  • The list function allows users to list assets with any price, including very low or zero. The cost for the listing depends on the asset price, which influences the execution of transferRoyalties.

  • When the price is zero (or trivially low), transferRoyalties results in zero-value token transfers, bypassing effective financial commitment while still adding the asset to the target buyer's list for the round.

  • The check maxAssetCount becomes pivotal but can be abused by filling up an agent’s quota with these costless listings, denying access to legitimate sellers attempting to list assets for purchase.

Impact

  • Denial of Service: An attacker can flood a BuyerAgent's capacity by repeatedly executing zero-cost listings, effectively barring any legitimate listings from being added within the same round.

Tools Used

Foundry

Recommendations

  • Set a minimum price threshold within the list and relist functions. Listings should only proceed if the price exceeds a defined minimum, sufficient to ensure meaningful financial transactions and deter trivial listing behaviors.

contract Swan {
// Define a minimum price threshold for listings
++ uint256 public constant MIN_LISTING_PRICE = 1 ether; // Example value, adjust as required
// Other contract variables and mappings...
// The listing implementation with price check
function list(string calldata _name, string calldata _symbol, bytes calldata _desc, uint256 _price, address _buyer)
external
{
// Ensure the price exceeds the minimum threshold
++ require(_price >= MIN_LISTING_PRICE, "Listing price below minimum threshold");
BuyerAgent buyer = BuyerAgent(_buyer);
(uint256 round, BuyerAgent.Phase phase,) = buyer.getRoundPhase();
// Buyer must be in the Sell phase
if (phase != BuyerAgent.Phase.Sell) {
revert BuyerAgent.InvalidPhase(phase, BuyerAgent.Phase.Sell);
}
// Asset count must not exceed `maxAssetCount`
if (getCurrentMarketParameters().maxAssetCount == assetsPerBuyerRound[_buyer][round].length) {
revert AssetLimitExceeded(getCurrentMarketParameters().maxAssetCount);
}
// Create the asset & its listing
address asset = address(swanAssetFactory.deploy(_name, _symbol, _desc, msg.sender));
listings[asset] = AssetListing({
createdAt: block.timestamp,
royaltyFee: buyer.royaltyFee(),
price: _price,
seller: msg.sender,
status: AssetStatus.Listed,
buyer: _buyer,
round: round
});
// Add this to the list of listings for the buyer for this round
assetsPerBuyerRound[_buyer][round].push(asset);
// Transfer royalties
transferRoyalties(listings[asset]);
emit AssetListed(msg.sender, asset, _price);
}
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.