The Swan contract contains a serious issue where sellers are charged royalty fees multiple times when relisting an asset. This happens because the relist function incorrectly calls transferRoyalties, causing sellers to pay royalties each time they relist an asset, rather than only on the initial listing.
Take a look at the POC test, it can be seen that clearly that sellers pay duplicate royalties, leading to unintended financial losses.
describe("POC: Royalty Double-Spending in Relist", () => {
let pocSeller: HardhatEthersSigner;
let pocBuyer: HardhatEthersSigner;
let pocBuyerAgent: BuyerAgent;
let assetAddress: string;
const ROYALTY_FEE = 1;
const PRICE = parseEther("1.0");
it("should deploy contracts first", async function() {
const supply = parseEther("1000");
token = await deployTokenFixture(dria, supply);
expect(await token.balanceOf(dria.address)).to.eq(supply);
const currentTime = (await ethers.provider.getBlock("latest").then((block) => block?.timestamp)) as bigint;
MARKET_PARAMETERS.timestamp = currentTime;
({ swan, registry, coordinator } = await deploySwanFixture(
dria,
token,
STAKES,
FEES,
MARKET_PARAMETERS,
ORACLE_PARAMETERS
));
expect(await swan.owner()).to.eq(dria.address);
expect(await swan.isOperator(dria.address)).to.be.true;
});
it("demonstrates double royalty payment on relist", async function() {
[, , , , pocSeller, pocBuyer] = await ethers.getSigners();
await token.connect(dria).transfer(pocSeller.address, parseEther("10.0"));
await token.connect(pocSeller).approve(await swan.getAddress(), parseEther("10.0"));
const tx = await swan.connect(pocBuyer).createBuyer(
"Test Buyer",
"Test Description",
ROYALTY_FEE,
parseEther("10.0")
);
const receipt = await tx.wait();
const event = receipt?.logs.find(
log => log.topics[0] === ethers.id("BuyerCreated(address,address)")
);
if (!event) throw new Error("BuyerCreated event not found");
const buyerAgentAddress = ethers.AbiCoder.defaultAbiCoder().decode(
['address'],
event.topics[2]
)[0];
pocBuyerAgent = await ethers.getContractAt("BuyerAgent", buyerAgentAddress) as BuyerAgent;
const royaltyAmount = (PRICE * BigInt(ROYALTY_FEE)) / 100n;
const platformAmount = (royaltyAmount * BigInt(MARKET_PARAMETERS.platformFee)) / 100n;
const buyerAmount = royaltyAmount - platformAmount;
const initialSellerBalance = await token.balanceOf(pocSeller.address);
const initialBuyerBalance = await token.balanceOf(await pocBuyerAgent.getAddress());
const initialPlatformBalance = await token.balanceOf(await swan.owner());
await swan.connect(pocSeller).list(
"Test Asset",
"TEST",
ethers.encodeBytes32String("test"),
PRICE,
await pocBuyerAgent.getAddress()
);
const afterListSellerBalance = await token.balanceOf(pocSeller.address);
const afterListBuyerBalance = await token.balanceOf(await pocBuyerAgent.getAddress());
const afterListPlatformBalance = await token.balanceOf(await swan.owner());
expect(initialSellerBalance - afterListSellerBalance).to.equal(royaltyAmount);
expect(afterListBuyerBalance - initialBuyerBalance).to.equal(buyerAmount);
expect(afterListPlatformBalance - initialPlatformBalance).to.equal(platformAmount);
const listedAssets = await swan.getListedAssets(await pocBuyerAgent.getAddress(), 0);
assetAddress = listedAssets[0];
await time.increase(
MARKET_PARAMETERS.withdrawInterval +
MARKET_PARAMETERS.buyInterval +
MARKET_PARAMETERS.sellInterval
);
await swan.connect(pocSeller).relist(
assetAddress,
await pocBuyerAgent.getAddress(),
PRICE
);
const finalSellerBalance = await token.balanceOf(pocSeller.address);
const finalBuyerBalance = await token.balanceOf(await pocBuyerAgent.getAddress());
const finalPlatformBalance = await token.balanceOf(await swan.owner());
expect(initialSellerBalance - finalSellerBalance).to.equal(royaltyAmount * 2n);
expect(finalBuyerBalance - initialBuyerBalance).to.equal(buyerAmount * 2n);
expect(finalPlatformBalance - initialPlatformBalance).to.equal(platformAmount * 2n);
console.log("Financial Impact of Double Royalty Payment:");
console.log("Seller lost extra tokens:", ethers.formatEther(royaltyAmount));
console.log("Buyer gained extra tokens:", ethers.formatEther(buyerAmount));
console.log("Platform gained extra tokens:", ethers.formatEther(platformAmount));
});
});
Alternative: If royalties on relisting are intended, implement a clear documentation and warning system to inform sellers about this cost.
Consider implementing a tracking mechanism to ensure royalties are only paid once per asset if that's the intended behavior.