Dria

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

The Buy Phase of a round can be skipped due to an update in the marketplaces `SwanMarketParameters`

Summary

The Buy Phase of a round can be skipped due to an update in the marketplaces SwanMarketParameters which would lead to sellers having listed assets and never having a chance for them to be bought buy a buyerAgent in that round. Theses sellers would still have to pay the listing fees in the Swan:list or Swan:relist functions. Sellers would be expecting to have a chance at the buyerAgent buying their asset, but due to the update they never get this chance. It should be noted that assets can only be bought in the round they were listed and not in future rounds without that asset being relisted. This leads to a permanent loss of those fees with no value being given from them.

Vulnerability Details

SwanManager:setMarketParameters allows the Swan owner to update the market parameters of the marketplace post initialisation, and potentially in the middle of a buyerAgent round.
BuyerAgent:_computePhase is the function that calculates what Phase that specific buyerAgent is in.
When certain market parameters are set an entire Buy Phase can be skipped for a round. See the PoC below:

The following PoC was created using Foundry, insert the below functions into their own Test file for the test. These functions are direct copies from the codebase.

enum Phase {
Sell, //0
Buy, //1
Withdraw //2
}
function _computeCycleTime(SwanMarketParameters memory params) internal pure returns (uint256) {
return params.sellInterval + params.buyInterval + params.withdrawInterval;
}
function _computePhase(SwanMarketParameters memory params, uint256 elapsedTime)
internal
pure
returns (uint256, Phase, uint256)
{
uint256 cycleTime = _computeCycleTime(params);
uint256 round = elapsedTime / cycleTime;
uint256 roundTime = elapsedTime % cycleTime;
// example:
// |-------------> | (roundTime)
// |--Sell--|--Buy--|-Withdraw-| (cycleTime)
if (roundTime <= params.sellInterval) {
return (round, Phase.Sell, params.sellInterval - roundTime);
} else if (roundTime <= params.sellInterval + params.buyInterval) {
return (round, Phase.Buy, params.sellInterval + params.buyInterval - roundTime);
} else {
return (round, Phase.Withdraw, cycleTime - roundTime);
}
}

Please then insert the following test into the same Test file.

function testUpdatedMarketParameters() public pure {
//Standard, initial market parameter
SwanMarketParameters memory marketParams = SwanMarketParameters({
withdrawInterval: 100,
sellInterval: 100,
buyInterval: 100,
platformFee: 1,
maxAssetCount: 100,
timestamp: 0
});
uint256 elapsedTime = 80; //This elapsed time represents a buyerAgent in the sell phase according to the current market parameters
(, Phase computedPhase,) = _computePhase(marketParams, elapsedTime);
assertEq(uint256(computedPhase), uint256(Phase.Sell));
//New market parameters that will cause the skip of the Buy Phase for this buyerAgent
SwanMarketParameters memory newMarketParams = SwanMarketParameters({
withdrawInterval: 30,
sellInterval: 30,
buyInterval: 30,
platformFee: 1,
maxAssetCount: 100,
timestamp: 0
});
(, Phase computedPhaseNew,) = _computePhase(newMarketParams, elapsedTime);
assertEq(uint256(computedPhaseNew), uint256(Phase.Withdraw));
}

This PoC represents the issue occuring for a single buyerAgent. In the realistic scenario where there are many buyerAgents, this could very easily happen to multiple buyerAgents at the same time.

Impact: High

The impact of this issue is that sellers will be charged fees for assets that never had a chance to be bought buy a buyer. The protocol would reap the rewards of this buy collecting fees from the listings, and the buyer would also reap the rewards of this as they would collect fees for all the listings in their name, but would not buy anything.

It should be noted that in the event that market parameter updates were announced off-chain before they took place, a malicious actor could deliberately setup a buyerAgent that would exploit this vulnerability, and they could even set the description of this buyerAgent maliciously to something along the lines of, "buy everything thats listed", which would entice sellers to sell to the buyer as they would likely get a sale of their asset and in turn make a profit, however they could be unaware that the Buy Phase for that round may never occur. The malicious actor would then use the upcoming Withdraw phase to withdraw all the money made from the royalties and then drain the wallet, so that it does not buy any assets in future rounds.

Tools Used

Manual Review

Recommendations

The solution to this issue is non-trivial due to the fact that every individual buyerAgent is at their own Phase in their own Round. Meaning this issue will not happen for every single buyerAgent when it does occur.

Updates

Lead Judging Commences

inallhonesty Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Appeal created

virpysec Submitter
12 months ago
virpysec Submitter
12 months ago
inallhonesty Lead Judge
11 months ago
inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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