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.