Summary:
The SpiceAuction contract's setAuctionConfig function fails to update the epochs mapping with EpochInfo, causing the removeAuctionConfig function to always revert. This creates a Denial of Service (DoS) vulnerability that prevents the removal of auction configurations.
Vulnerability Details:
The setAuctionConfig function does not store EpochInfo in the epochs mapping, leading to a mismatch in expected state. Consequently, the removeAuctionConfig function cannot find the necessary EpochInfo, causing it to revert due to the first if condition always failing.
Code Snippet:
function setAuctionConfig(SpiceAuctionConfig calldata _config) external onlyDAOExecutor {
uint256 currentEpochIdCache = _currentEpochId;
if (currentEpochIdCache > 0) {
EpochInfo storage info = epochs[currentEpochIdCache];
if (info.isActive()) revert InvalidConfigOperation();
}
if (
_config.duration < MINIMUM_AUCTION_PERIOD || _config.duration > MAXIMUM_AUCTION_DURATION
|| _config.waitPeriod > MAXIMUM_AUCTION_WAIT_PERIOD
) revert CommonEventsAndErrors.InvalidParam();
if (_config.waitPeriod == 0 || _config.minimumDistributedAuctionToken == 0) {
revert CommonEventsAndErrors.ExpectedNonZero();
}
if (_config.recipient == address(0)) revert CommonEventsAndErrors.InvalidAddress();
currentEpochIdCache += 1;
@>
auctionConfigs[currentEpochIdCache] = _config;
emit AuctionConfigSet(currentEpochIdCache, _config);
}
function removeAuctionConfig() external override onlyDAOExecutor {
uint256 id = _currentEpochId;
@> EpochInfo storage info = epochs[id];
if (info.startTime == 0) revert InvalidConfigOperation();
if (info.isActive()) revert InvalidConfigOperation();
bool configSetButAuctionStartNotCalled = auctionConfigs[id + 1].duration > 0;
if (!configSetButAuctionStartNotCalled) {
if (info.hasEnded()) revert AuctionEnded();
delete auctionConfigs[id];
delete epochs[id];
_currentEpochId = id - 1;
emit AuctionConfigRemoved(id, id);
} else {
id += 1;
delete auctionConfigs[id];
emit AuctionConfigRemoved(id, 0);
}
}
Impact:
This vulnerability can significantly disrupt the normal operations of the SpiceAuction contract. By preventing the removal of outdated or unwanted auction configurations. Users and stakeholders are forced to participate in that auction configuration, and the contract administrators cannot update auction parameters. This breakdown in functionality undermines the trust and reliability of the auction platform, potentially causing financial loss and reputational damage to the managing DAO and the broader ecosystem.
Proof Of Concept:
DAO administrators call the setAuctionConfig function to set a new auction configuration.
The setAuctionConfig function increments the currentEpochIdCache and stores the configuration in the auctionConfigs mapping but fails to update the epochs mapping with EpochInfo.
The removeAuctionConfig function is called to remove the auction configuration.
The removeAuctionConfig function attempts to retrieve the EpochInfo from the epochs mapping using the _currentEpochId but fails to find the necessary information, causing the function to revert.
The removeAuctionConfig function reverts, preventing the removal of the auction configuration and causing a Denial of Service (DoS) vulnerability.
Proof Of Code:
Place the following code in the SpiceAuction.t.sol contract:
To run the test, follow the commands below:
forge test --mt test_FortisAudits_removeAuctionConfig_Reverts -vvvv
function test_FortisAudits_removeAuctionConfig_Reverts() public {
ISpiceAuction.SpiceAuctionConfig memory config = _getAuctionConfig();
vm.startPrank(daoExecutor);
spice.setAuctionConfig(config);
vm.stopPrank();
uint256 id = spice.currentEpoch();
spice.getAuctionConfig(id);
assertEq(id, 0);
spice.getEpochInfo(id);
vm.startPrank(daoExecutor);
vm.expectRevert(abi.encodeWithSelector(ISpiceAuction.InvalidConfigOperation.selector));
spice.removeAuctionConfig();
vm.stopPrank();
vm.startPrank(alice);
vm.warp(block.timestamp + config.waitPeriod);
IERC20 auctionToken = IERC20(_getAuctionToken(config.isTempleGoldAuctionToken, daiToken));
dealAdditional(auctionToken, address(spice), 100 ether);
spice.startAuction();
vm.warp(block.timestamp + config.duration + config.startCooldown + 1);
vm.stopPrank();
vm.startPrank(daoExecutor);
vm.expectRevert(abi.encodeWithSelector(IAuctionBase.AuctionEnded.selector));
spice.removeAuctionConfig();
vm.stopPrank();
}
Tools Used:
Manual code review
Foundry
Recommendation:
To mitigate this issue, the setAuctionConfig function should be updated to store EpochInfo in the epochs mapping. This update will allow the removeAuctionConfig function to retrieve the necessary information and remove auction configurations as intended.
Here is the recommended mitigation:
function setAuctionConfig(SpiceAuctionConfig calldata _config) external onlyDAOExecutor {
uint256 currentEpochIdCache = _currentEpochId;
if (currentEpochIdCache > 0) {
EpochInfo storage info = epochs[currentEpochIdCache];
if (info.isActive()) revert InvalidConfigOperation();
}
if (
_config.duration < MINIMUM_AUCTION_PERIOD || _config.duration > MAXIMUM_AUCTION_DURATION
|| _config.waitPeriod > MAXIMUM_AUCTION_WAIT_PERIOD
) revert CommonEventsAndErrors.InvalidParam();
if (_config.waitPeriod == 0 || _config.minimumDistributedAuctionToken == 0) {
revert CommonEventsAndErrors.ExpectedNonZero();
}
if (_config.recipient == address(0)) revert CommonEventsAndErrors.InvalidAddress();
currentEpochIdCache += 1;
+ EpochInfo storage info = epochs[currentEpochIdCache]; // Add this line
+ uint128 startTime = info.startTime = uint128(block.timestamp) + _config.startCooldown;
+ info.endTime = startTime + _config.duration;
+ info.totalAuctionTokenAmount = _config.minimumDistributedAuctionToken;
+ auctionConfigs[currentEpochIdCache] = _config;
emit AuctionConfigSet(currentEpochIdCache, _config);
}
function removeAuctionConfig() external override onlyDAOExecutor {
/// only delete latest epoch if auction is not started
uint256 id = _currentEpochId;
- EpochInfo storage info = epochs[id];
+ EpochInfo storage info = epochs[id + 1];
.
.
.
.
}