Summary
The SpiceAuction contract fails to update the _totalAuctionTokenAllocation
when removing an auction during its cooldown period, leading to an inflated and inaccurate token allocation record.
Vulnerability Details
In the startAuction
function, the contract updates the total auction token allocation:
* @notice Start auction. Checks caller is set config starter. Address zero for anyone to call
*/
function startAuction() external override {
uint256 epochId = _currentEpochId;
SpiceAuctionConfig storage config = auctionConfigs[epochId+1];
if (config.duration == 0) { revert CannotStartAuction(); }
if (config.starter != address(0) && msg.sender != config.starter) { revert CommonEventsAndErrors.InvalidAccess(); }
if (epochId > 0) {
EpochInfo memory lastEpochInfo = epochs[epochId];
uint64 _waitPeriod = auctionConfigs[epochId].waitPeriod;
if (lastEpochInfo.endTime + _waitPeriod > block.timestamp) { revert CannotStartAuction(); }
} else {
if (_deployTimestamp + config.waitPeriod > block.timestamp) { revert CannotStartAuction(); }
}
(,address auctionToken) = _getBidAndAuctionTokens(config);
uint256 totalAuctionTokenAllocation = _totalAuctionTokenAllocation[auctionToken];
uint256 balance = IERC20(auctionToken).balanceOf(address(this));
uint256 epochAuctionTokenAmount = balance - (totalAuctionTokenAllocation - _claimedAuctionTokens[auctionToken]);
if (config.activationMode == ActivationMode.AUCTION_TOKEN_BALANCE) {
if (config.minimumDistributedAuctionToken == 0) { revert MissingAuctionTokenConfig(); }
}
if (epochAuctionTokenAmount < config.minimumDistributedAuctionToken) { revert NotEnoughAuctionTokens(); }
epochId = _currentEpochId = _currentEpochId + 1;
EpochInfo storage info = epochs[epochId];
uint128 startTime = info.startTime = uint128(block.timestamp) + config.startCooldown;
uint128 endTime = info.endTime = startTime + config.duration;
info.totalAuctionTokenAmount = epochAuctionTokenAmount;
❌ _totalAuctionTokenAllocation[auctionToken] = totalAuctionTokenAllocation + epochAuctionTokenAmount;
emit AuctionStarted(epochId, msg.sender, startTime, endTime, epochAuctionTokenAmount);
}
However, the removeAuctionConfig
function, which can be called during an auction's cooldown period, does not reverse this allocation:
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);
}
}
This oversight results in the _totalAuctionTokenAllocation
remaining inflated with the allocation of the removed auction.
Importantly, the existing recovery functions recoverToken
and recoverAuctionTokenForZeroBidAuction
cannot be used to recover these tokens:
recoverToken
calculates the recoverable amount as:
uint256 maxRecoverAmount = balance - (totalAuctionTokenAllocation - _claimedAuctionTokens[token]);
The inflated totalAuctionTokenAllocation
prevents recovery of the locked tokens.
recoverAuctionTokenForZeroBidAuction
only works for ended auctions with zero bids, not for cancelled auctions in the cooldown period.
This leaves no mechanism within the contract to correct the inflated _totalAuctionTokenAllocation
, exacerbating the issue over time.
Scenario
Admin sets up Auction 1 with 1000 TGLD tokens
startAuction
is called, setting _totalAuctionTokenAllocation[TGLD] = 1000
The auction enters cooldown period
Admin decides to cancel Auction 1 and calls removeAuctionConfig
_totalAuctionTokenAllocation[TGLD]
remains at 1000, despite the auction being cancelled
Admin sets up Auction 2
startAuction
for Auction 2 fails, as the contract believes all 1000 TGLD are still allocated to the non-existent Auction 1
Previous 1000 TGLD are locked in the contract
Impact
Token Lockup: Future auctions will have access to fewer tokens than actually available, as the system believes more tokens are allocated than reality.
uint256 epochAuctionTokenAmount = balance - (totalAuctionTokenAllocation - _claimedAuctionTokens[auctionToken]);
This calculation in startAuction
will result in a lower epochAuctionTokenAmount
than it should be.
DoS of next auctions: If the next auction is started with the same balance as the removed one, epochAuctionTokenAmount
will be 0, which would deny the starting of new auction because of this check:
if (epochAuctionTokenAmount < config.minimumDistributedAuctionToken) { revert NotEnoughAuctionTokens(); }
Recommendations
Implement a proper deallocation in the removeAuctionConfig
function:
if (!configSetButAuctionStartNotCalled) {
if (info.hasEnded()) { revert AuctionEnded(); }
SpiceAuctionConfig storage config = auctionConfigs[id];
(, address auctionToken) = _getBidAndAuctionTokens(config);
_totalAuctionTokenAllocation[auctionToken] -= info.totalAuctionTokenAmount;
delete auctionConfigs[id];
delete epochs[id];
_currentEpochId = id - 1;
emit AuctionConfigRemoved(id, id);
}