Dria

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

Full `platformFee` in `Swan` deprives Buyer from getting royalty fees.

Vulnerability Details

The protocol allows sellers & buyers to earn tokens through the following,

  1. Selling NFTs: Sellers tailor NFTs that align with AI agents, sell it to them & earn tokens.

  2. RoyaltyFee: BuyerAgents receive token as a royaltyFee paid by the sellers to them when asset gets created in Swan::list.

The issue lies in the royalty amount that is actually sent. The royaltyFee is specified by BuyerAgent owner and when Swan::list() is called by seller to create an asset, a percentage of the price of this asset is set aside in transferRoyalties. This is known as the buyerFee. The driaFee (fee sent to Swan owner) is taken from the buyerFee

uint256 buyerFee = (asset.price * asset.royaltyFee) / 100;
@> uint256 driaFee = (buyerFee * getCurrentMarketParameters().platformFee) / 100; // @note dria taken from royalty

and the rest (buyerFee - driaFee) is sent to BuyerAgent,

token.transfer(asset.buyer, buyerFee - driaFee);

This means, the fee to be sent to owner is taken from the royalty that is meant to be sent to Buyer and that is the problem. If
platformFee == 100 (the protocol allows this), then buyerFee == driaFee, and nothing would be sent to buyer! This means 100% of royalty would go to Swan Owner, leaving no royalty for buyer since the amount sent to Swan Owner is directly taken from the ROYALTY AMOUNT and so, if the platformFee is 100%, then
buyerFee - driaFee = 0. It is possible for the owner to set platformFee to 100%. In blockchain, the ultimate truth & the only trustworthy entity is not the owner, but the logic of the protocol and if the logic allows it, it's bound to happen.

Impact

Buyer doesn't necessarily get the royalty when platformFee is 100%. They get a LEFT-OVER AMOUNT which is

(buyerFee - driaFee) making it appear
as if platformFee is actually an EXPLOITATIVE AMOUNT set by protocol, leading to reduced buyer participation and loss of royalty for the Buyer.

Proof-Of-Code

Add the test in Swan.test.ts,

it("buyer gets no royalty", async() => {
const supply = parseEther("1000");
token = await deployTokenFixture(dria, supply);
expect(await token.balanceOf(dria.address)).to.eq(supply);
const MARKET_PARAMETERS2 = {
withdrawInterval: minutes(30),
sellInterval: minutes(60),
buyInterval: minutes(10),
platformFee: 100n, // ALERT!!!!!!!!!!!!! 100% fee being charged by the platform.
maxAssetCount: 5n,
timestamp: 0n,
} satisfies SwanMarketParametersStruct;
const currentTime = (await ethers.provider.getBlock("latest").then((block) => block?.timestamp)) as bigint;
MARKET_PARAMETERS2.timestamp = currentTime;
({ swan, registry, coordinator } = await deploySwanFixture(
dria,
token,
STAKES,
FEES,
MARKET_PARAMETERS2,
ORACLE_PARAMETERS
));
const ROYALTY_FEE2 = 50;
const buyerAgentParams = [
{
name: "BuyerAgent#1",
description: "Description of BuyerAgent 1",
royaltyFee: ROYALTY_FEE2,
amountPerRound: AMOUNT_PER_ROUND,
owner: buyer,
},
];
// get deployed buyer agents
[buyerAgent, buyerAgentToFail] = await createBuyers(swan, buyerAgentParams);
await transferTokens(token, [
[buyer.address, parseEther("120")],
[buyerToFail.address, parseEther("3")],
[seller.address, FEE_AMOUNT1 + FEE_AMOUNT2 + FEE_AMOUNT3 + FEE_AMOUNT1 + FEE_AMOUNT2],
[sellerToRelist.address, FEE_AMOUNT2 + FEE_AMOUNT1],
[generator.address, STAKE_AMOUNT],
[validator.address, STAKE_AMOUNT],
])
const buyerAgentAddress = await buyerAgent.getAddress();
const tokenBalanceBefore = await token.balanceOf(buyerAgentAddress);
// BuyerAgent currently has no tokens. They should get royalty from the list transaction.
expect(tokenBalanceBefore).to.equal(0);
const customPrice = parseEther("120");
await token.connect(buyer).approve(await swan.getAddress(), parseEther("120"));
await swan.connect(buyer).list(NAME, SYMBOL, DESC, customPrice, await buyerAgent.getAddress())
const tokenBalanceAfter = await token.balanceOf(buyerAgentAddress);
// Agent receives no royalty when platformFee = 100%
expect(tokenBalanceAfter).to.equal(0);
});

Tools Used

Manual Review & Hardhat Testing

Recommendations

Consider one of the following changes,

  1. Add a min threshold for the royalty that should be sent to the Buyer on each listing to ensure that buyers get at least "min" amount of royalty, encouraing users to participate, create agents and so on,

+ uint256 public minRoyalty = x;
function transferRoyalties(AssetListing storage asset) internal {
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);
+ if (buyerFee - driaFee < minRoyalty) revert();
// 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);
}

2.Make driaFee a small, fixed amount provided by Swan Owner that should get deducted when list() is called.

+ uint256 public protocolFee = x;
function transferRoyalties(AssetListing storage asset) internal {
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);
+ token.transferFrom(asset.seller, address(this), buyerFee + protocolFee);
// send the buyer's portion to them
- token.transfer(asset.buyer, buyerFee - driaFee);
+ token.transfer(asset.buyer, buyerFee);
// then it sends the remaining to Swan owner
- token.transfer(owner(), driaFee);
+ token.transfer(owner(), protocolFee);
}

3.Ensure that platformFee is NEVER 100%. An acceptable percentage is 90% so Buyer gets a decent royalty amount.

function initialize(
SwanMarketParameters calldata _marketParameters,
LLMOracleTaskParameters calldata _oracleParameters,
// contracts
address _coordinator,
address _token,
address _buyerAgentFactory,
address _swanAssetFactory
) public initializer {
__Ownable_init(msg.sender);
- require(_marketParameters.platformFee <= 100, "Platform fee cannot exceed 100%");
+ require(_marketParameters.platformFee <= 90, "Platform fee cannot exceed 90%");
...
...
}
function setMarketParameters(SwanMarketParameters memory _marketParameters) external onlyOwner {
- require(_marketParameters.platformFee <= 100, "Platform fee cannot exceed 100%");
+ require(_marketParameters.platformFee <= 90, "Platform fee cannot exceed 90%");
_marketParameters.timestamp = block.timestamp;
marketParameters.push(_marketParameters);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

falsegenius Submitter
12 months ago
inallhonesty Lead Judge
12 months ago
inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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