Dria

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

Accessing New Market Parameters Increases Gas Costs Over Time, Causing Potential DoS Vulnerability

Summary

In SwanManager.sol, the setMarketParameters function has onlyOwner access control, allowing the contract owner to add new market parameters to the marketParameters array. While this function serves its purpose well, over time, the accumulation of market parameters increases gas consumption, particularly when buyerAgent.sol calls getRoundPhase. This function retrieves all market parameters to calculate the current round, phase, and roundTime.

Initially, retrieving only the first market parameter is efficient. However, as the marketParameters array grows, calculating these values requires accessing each entry, significantly increasing gas costs. Over time, this escalating gas usage could lead to a Denial of Service (DoS) scenario.

SwanManager.sol::setMarketParameters Function:

function setMarketParameters(SwanMarketParameters memory _marketParameters) external onlyOwner {
require(_marketParameters.platformFee <= 100, "Platform fee cannot exceed 100%");
_marketParameters.timestamp = block.timestamp;
// @info: missing minimum intervals threshold
@> marketParameters.push(_marketParameters);
}

buyerAgent.sol::getRoundPhase Function:

function getRoundPhase() public view returns (uint256, Phase, uint256) {
@> SwanMarketParameters[] memory marketParams = swan.getMarketParameters();
if (marketParams.length == marketParameterIdx + 1) {
// if our index is the last market parameter, we can simply treat it as a single instance,
// and compute the phase according to the elapsed time from the beginning of the contract.
return _computePhase(marketParams[marketParameterIdx], block.timestamp - createdAt);
} else {
// we will accumulate the round from each phase, starting from the first one.
uint256 idx = marketParameterIdx;
//
// first iteration, we need to compute elapsed time from createdAt:
// createdAt -|- VVV | ... | ... | block.timestamp
(uint256 round,,) = _computePhase(marketParams[idx], marketParams[idx + 1].timestamp - createdAt);
idx++;
// start looking at all the intervals beginning from the respective market parameters index
// except for the last element, because we will compute the current phase and timeRemaining for it.
// @info: gas consumption if marketParams array increases
@> while (idx < marketParams.length - 1) {
// for the intermediate elements we need the difference between their timestamps:
// createdAt | ... -|- VVV -|- ... | block.timestamp
(uint256 innerRound,,) =
_computePhase(marketParams[idx], marketParams[idx + 1].timestamp - marketParams[idx].timestamp);
// accumulate rounds from each intermediate phase, along with a single offset round
round += innerRound + 1;
idx++;
}
// for last element we need to compute current phase and timeRemaining according
// to the elapsedTime at the last iteration, where we need to compute from the block.timestamp:
// createdAt | ... | ... | VVV -|- block.timestamp
(uint256 lastRound, Phase phase, uint256 timeRemaining) =
_computePhase(marketParams[idx], block.timestamp - marketParams[idx].timestamp);
// accumulate the last round as well, along with a single offset round
round += lastRound + 1;
return (round, phase, timeRemaining);
}
}

Vulnerability Details

As the protocol runs, the marketParameters array continuously expands. Since rounds and phases rely on historical intervals, the getRoundPhase function must repeatedly reference every parameter in the array. Each added parameter increases the execution cost, measured in gas, leading to escalating gas usage over time. Eventually, users may exhaust their gas limits, making transactions unfeasible, thus creating a DoS vulnerability.

Below is a proof-of-code to demonstrate this vulnerability:

Proof of Code

  1. Add the following test code to the Swan.test.ts file within the Swan test suite:

describe("Swan Attack Mode", async () => {
const currRound = 0n;
it("should list 5 assets for the first round", async function () {
await listAssets(
swan,
buyerAgent,
[
[seller, PRICE1],
[seller, PRICE2],
[seller, PRICE3],
[sellerToRelist, PRICE2],
[sellerToRelist, PRICE1],
],
NAME,
SYMBOL,
DESC,
0n
);
[assetToBuy, assetToRelist, assetToFail, ,] = await swan.getListedAssets(
await buyerAgent.getAddress(),
currRound
);
expect(await token.balanceOf(seller)).to.be.equal(FEE_AMOUNT1 + FEE_AMOUNT2);
expect(await token.balanceOf(sellerToRelist)).to.be.equal(0);
});
it("should NOT allow to list an asset more than max asset count", async function () {
// try to list an asset more than max asset count
await expect(swan.connect(sellerToRelist).list(NAME, SYMBOL, DESC, PRICE1, await buyerAgent.getAddress()))
.to.be.revertedWithCustomError(swan, "AssetLimitExceeded")
.withArgs(MARKET_PARAMETERS.maxAssetCount);
});
it("new market parameter consumes more Gas than prior ones!", async () => {
let NEW_MARKET_PARAMETERS = {
withdrawInterval: minutes(10),
sellInterval: minutes(20),
buyInterval: minutes(30),
platformFee: 2n,
maxAssetCount: 2n,
timestamp: (await ethers.provider.getBlock("latest").then((block) => block?.timestamp)) as bigint,
};
await swan.connect(dria).setMarketParameters(NEW_MARKET_PARAMETERS);
const tx = await swan.connect(seller).list(NAME, SYMBOL, DESC, PRICE1, await buyerAgent.getAddress());
const receipt = await tx.wait();
console.log("GAS Used: ", receipt.gasUsed);
for(let i = 0; i < 1000; i++) {
await swan.connect(dria).setMarketParameters(NEW_MARKET_PARAMETERS);
}
const tx2 = await swan.connect(seller).list(NAME, SYMBOL, DESC, PRICE1, await buyerAgent.getAddress());
const receipt2 = await tx2.wait();
console.log("GAS2 Used: ", receipt2.gasUsed);
for(let i = 0; i < 1000; i++) {
await swan.connect(dria).setMarketParameters(NEW_MARKET_PARAMETERS);
}
await expect(swan.connect(seller).list(NAME, SYMBOL, DESC, PRICE1, await buyerAgent.getAddress())).to.be.rejectedWith("contract call run out of gas and made the transaction revert");
});
});
  1. Comment out all test cases after Sell phase #1: listing inclusively.

  2. Execute the following command in the terminal:

yarn test --grep "Swan"
  1. Review the logs for gas usage results.

    GAS Used: 1422906n
    GAS2 Used: 16998207n
    Swan
    should deploy swan (479ms)
    should create buyers (72ms)
    should fund buyers & sellers
    ✔ should register oracles
    Swan Attack Mode
    ✔ should list 5 assets for the first round
    ✔ should NOT allow to list an asset more than max asset count
    new market parameter consumes more Gas than prior ones! (9316ms)
    ✔ Owner can make a DoS if he doesn't like the earned fee!

As shown, initial gas consumption is around 1,422,906 gas, but after adding 1000 parameters, gas usage jumps to 16,998,207 gas. Adding further parameters leads to an eventual out-of-gas error, proving the DoS vulnerability.

Impact

  • Main Logic Affected: As users interact with the protocol over time, they risk facing prohibitive gas costs, limiting their ability to complete transactions.

  • DoS Risk: The protocol may become unusable for asset creators trying to list or relist due to excessive gas usage.

Recommendations

To mitigate this issue, consider using mappings with an identifier (e.g., 1, 2, 3...) and store accumulated elapsed times for each stage. This approach can reduce gas consumption by removing older, irrelevant parameters and focusing on essential, recent values.

Updates

Lead Judging Commences

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

Appeal created

theirrationalone Submitter
9 months ago
inallhonesty Lead Judge
9 months ago
inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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