Summary
In this protocol, intervals regulate transitions in round
, roundTime
, and phases
, controlling crucial functions such as selling, withdrawing, etc. However, the current implementation lacks validation for these interval values, exposing the protocol to potential Denial of Service (DoS) vulnerabilities. The following code snippet demonstrates this issue, where intervals can be set to zero without restriction:
function setMarketParameters(SwanMarketParameters memory _marketParameters) external onlyOwner {
require(_marketParameters.platformFee <= 100, "Platform fee cannot exceed 100%");
_marketParameters.timestamp = block.timestamp;
@>
marketParameters.push(_marketParameters);
}
If intervals are set to zero, a DoS vulnerability is triggered as the protocol attempts to compute the phase, round, and cycleTime
, which all depend on valid interval values.
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;
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);
}
}
Vulnerability Details
The following test code provides both proof of code and proof of concept, demonstrating the protocol's susceptibility to this vulnerability. Add this code to Swan.test.ts
under 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 listing of assets beyond maximum asset count", async function () {
await expect(swan.connect(sellerToRelist).list(NAME, SYMBOL, DESC, PRICE1, await buyerAgent.getAddress()))
.to.be.revertedWithCustomError(swan, "AssetLimitExceeded")
.withArgs(MARKET_PARAMETERS.maxAssetCount);
});
it("0 intervals trigger DoS due to undefined cycleTime and round computation", async () => {
let NEW_MARKET_PARAMETERS = {
withdrawInterval: minutes(0),
sellInterval: minutes(0),
buyInterval: minutes(0),
platformFee: 2n,
maxAssetCount: 2n,
timestamp: (await ethers.provider.getBlock("latest")).timestamp,
};
await swan.connect(dria).setMarketParameters(NEW_MARKET_PARAMETERS);
await expect(swan.connect(seller).list(NAME, SYMBOL, DESC, PRICE1, await buyerAgent.getAddress())).to.be.reverted;
});
});
Steps to Run Test
-
Comment out all subsequent tests in Swan
after Sell phase #1: listing
inclusively.
-
Execute the test with the following command:
-
Review the output logs, which should indicate the successful identification of the vulnerability.
Swan
✔ should deploy swan (709ms)
✔ should create buyers (122ms)
✔ should fund buyers & sellers (61ms)
✔ should register oracles
Swan Attack Mode
✔ should list 5 assets for the first round (55ms)
✔ should NOT allow listing beyond max asset count (54ms)
✔ 0 intervals trigger DoS due to undefined round computation! (49ms)
As seen, the test succeeds in exposing the vulnerability.
Impact
Disrupts core protocol functionality.
Restricts users from listing and re-listing assets.
Zero intervals alter protocol behavior, potentially leading to malicious operation.
Recommendations
To mitigate this vulnerability, validate interval values to prevent them from being set to zero. The following modification ensures these values are properly constrained:
function setMarketParameters(SwanMarketParameters memory _marketParameters) external onlyOwner {
require(_marketParameters.platformFee <= 100, "Platform fee cannot exceed 100%");
_marketParameters.timestamp = block.timestamp;
+ require(_marketParameters.buyInterval > 0 && _marketParameters.sellInterval > 0 && _marketParameters withdrawInterval > 0, "Interval values cannot be zero");
marketParameters.push(_marketParameters);
}