Dria

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

A buyerAgent creation with Zero Purchase Limit will cause loss of funds for asset creators

Summary

Any user can create a buyerAgent by calling createBuyer in swan contract. The issue is _amountPerRound can be set 0 or very low amount by a malicious user to collect fees from asset creators but not buying any asset in exchange. This will lead to repeated loss of fees for asset creators.

Vulnerability Details

A user can create a BuyerAgent by the following function:

function createBuyer(
string calldata _name,
string calldata _description,
uint96 _feeRoyalty,
uint256 _amountPerRound
) external returns (BuyerAgent) {
BuyerAgent agent = buyerAgentFactory.deploy(_name, _description, _feeRoyalty, _amountPerRound, msg.sender);
emit BuyerCreated(msg.sender, address(agent));
return agent;
}

As it can be seen _amountPerRound can be set 0 or any trivial amount. On the other hand, an asset can be created by calling list:

function list(string calldata _name, string calldata _symbol, bytes calldata _desc, uint256 _price, address _buyer)
external
{
//SNIP
// 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 royalties
transferRoyalties(listings[asset]);
emit AssetListed(msg.sender, asset, _price);
}

There is a call made to transferRoyalties() method, which is defined as follow:

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);
}

We can see that Swan collects the royalty fee from the seller initially, then distributes a portion of it to the buyer, with the platform retaining the remainder. When there is a buy phase, no purchases will be made due to the immediate BuyLimitExceeded revert within purchase.

function purchase() external onlyAuthorized {
//SNIP
address[] memory assets = abi.decode(output, (address[]));
// we purchase each asset returned
for (uint256 i = 0; i < assets.length; i++) {
address asset = assets[i];
// must not exceed the roundly buy-limit
uint256 price = swan.getListingPrice(asset);
spendings[round] += price;
if (spendings[round] > amountPerRound) {
revert BuyLimitExceeded(spendings[round], amountPerRound);
}
//SNIP
}

The seller will pay the fee but gets nothing in return.If the seller lists assets repeatedly while the buyer’s amountPerRound remains zero or any trivial amount, they might incur royalty fees each time without actually closing any sales. This could lead to cumulative losses over time without any compensatory revenue.
It is also important to note that if the seller is not aware of the royaltyFee set by the buyer before listing, this may lead to a huge loss for sellers. Because the royaltyFee can be set 100 by the buyer, which means the listing price of asset will be equal to buyerFee(see transferRoyalties() above).

Impact

Financiall loss for asset creators

Tools Used

Manual Review

Recommendations

I suggest sending buyerFee only after a confirmed purchase.This will prevent the seller from paying fees on assets that remain unsold due to amountPerRound constraints.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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