Dria

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

Market Parameter Changes Can Cause Loss of Listing Fees by Forcing Round Increments

Summary

When market parameters are updated via setMarketParameters, it causes a round increment for all existing buyers. This can cause sellers to lose their listing fees (royaltyFee) as their listings become invalid for the new round. This is a critical issue as it disrupts market operation and seller expectations.

Vulnerability Details

A seller pays a royalty fee to list an asset in the current round.

The owner can update market parameters at any time via setMarketParameters.

When market parameters are updated, all existing buyers increment their round count. This causes the asset to become stranded in the previous round, making it inaccessible to buyers in the new round. The seller loses the listing fee without the possibility of a sale. This is a critical issue as it disrupts market operation and seller expectations.

When new market parameters are added:

  1. All existing buyers increment their round count

  2. Assets listed for the original round become inaccessible

  3. Sellers have already paid royalty fees for these listings


According to the readme

Changing any of the intervals (withdrawInterval, sellInterval, buyInterval) is a disruptive action, it will automatically increase the round count of all existing buyers by 1; this is intended.
This known issue doesn't address the financial loss from listing fees that sellers incur when their assets become stranded in the previous round.

We can see this behavior in the following code snippet:
https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/swan/BuyerAgent.sol#L341-L373

// In BuyerAgent.sol
function getRoundPhase() public view returns (uint256, Phase, uint256) {
SwanMarketParameters[] memory marketParams = swan.getMarketParameters();
if (marketParams.length == marketParameterIdx + 1) {
return _computePhase(marketParams[marketParameterIdx], block.timestamp - createdAt);
@> } else {
uint256 idx = marketParameterIdx;
// First iteration
(uint256 round,,) = _computePhase(marketParams[idx], marketParams[idx + 1].timestamp - createdAt);
idx++;
while (idx < marketParams.length - 1) {
(uint256 innerRound,,) =
_computePhase(marketParams[idx], marketParams[idx + 1].timestamp - marketParams[idx].timestamp);
round += innerRound + 1;
idx++;
}
// ... rest of function
}
}

Then we can see that when the buyer calls oraclePurchaseRequest to request a llm purchase response, it will get a different round number than the seller

function oraclePurchaseRequest(bytes calldata _input, bytes calldata _models) external onlyAuthorized {
// check that we are in the Buy phase, and return round
@> (uint256 round,) = _checkRoundPhase(Phase.Buy);
oraclePurchaseRequests[round] =
swan.coordinator().request(SwanBuyerPurchaseOracleProtocol, _input, _models, swan.getOracleParameters());
}

Impact

HIGH - Multiple critical issues:

  • Sellers lose listing fees without possibility of sale

  • Assets become stranded in incorrect rounds

  • No refund mechanism for affected sellers

  • Protocol owner can force this state change at any time

  • Disrupts market operation and seller expectations

Proof of Concept

Example

  1. A seller lists an asset in round 5 and pays a listing fee

  2. The protocol owner updates market parameters, causing a round increment

  3. The asset becomes stranded in round 5 and is inaccessible to buyers in round 6 when it calls oraclePurchaseRequest

From the existing test file that confirms this behavior:

it("should be in round 6 (first buyer)", async function () {
const [round] = await buyerFirst.getRoundPhase();
expect(round).to.be.equal(6n); // 5 + 1 due to new update
});

Recommendation

Add parameter update delay and notification:

contract Swan {
struct PendingUpdate {
SwanMarketParameters params;
uint256 effectiveFromRound;
}
PendingUpdate public pendingUpdate;
function scheduleParameterUpdate(
SwanMarketParameters memory _params,
uint256 effectiveFromRound
) external onlyOwner {
require(effectiveFromRound > getCurrentRound(), "Invalid round");
pendingUpdate = PendingUpdate(_params, effectiveFromRound);
emit ParameterUpdateScheduled(_params, effectiveFromRound);
}
}
Updates

Lead Judging Commences

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

Support

FAQs

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