Summary
The recoverToken
function in your Solidity contract deletes the current auction epoch without adjusting the currentEpochId
. This can lead to inconsistencies and unintended behavior, particularly affecting the startAuction
function, which relies on the correct management of epochs. This oversight allows bypassing the previous auction data which will be zero, potentially disrupting the auction process and protocol functionality.
Vulnerability Details
as DAI auction is simple don't have much complexity as spice, so i will go with the flow owner will call setAuctionconfig then startAuction and suppose i have already performed set of auctions and now the currentEpochId = 10, as daiAuction contract don't want us to set cool down period as 0, so suppose i have called startAuction 11th time and after it executes Current epochid == 11, now suppose i want to cancel this auction so i will call recover tokens function which checks whether the auction is started or ended basically to clarify whether its in cool down period or not after that if it didn't revert it will delete epoch , but one they forgot is to subtract the epoch id from total, the issue occur when the start Auction wll be called and currentEpochid will be 11 so the if statment which check the preAuction.endtime which will be 0, so block.timestamp should be just need to greater than config.auctionsTimeDiff
, and the epoch Data will behave like auction is called for first time which create issue while making new auctions
Incomplete Epoch Deletion: The recoverToken
function deletes the current epoch (delete epochs[epochId];
) but does not adjust the currentEpochId
. This leaves the currentEpochId
in an inconsistent state, which can lead to issues in subsequent function calls that rely on epoch management.
Incorrect Epoch State: With the incorrect handling of currentEpochId
, the contract logic can enter a state where auctions are started without respecting the intended cooldown period. This result in unintended auction behaviors.
https://github.com/Cyfrin/2024-07-templegold/blob/57a3e597e9199f9e9e0c26aab2123332eb19cc28/protocol/contracts/templegold/DaiGoldAuction.sol#L110
https://github.com/Cyfrin/2024-07-templegold/blob/57a3e597e9199f9e9e0c26aab2123332eb19cc28/protocol/contracts/templegold/DaiGoldAuction.sol#L260
function startAuction() external override {
if (auctionStarter != address(0) && msg.sender != auctionStarter) { revert CommonEventsAndErrors.InvalidAccess(); }
EpochInfo storage prevAuctionInfo = epochs[_currentEpochId];
if (!prevAuctionInfo.hasEnded()) { revert CannotStartAuction(); }
AuctionConfig storage config = auctionConfig;
if (_currentEpochId > 0 && (prevAuctionInfo.endTime + config.auctionsTimeDiff > block.timestamp)) {
revert CannotStartAuction();
}
_distributeGold();
uint256 totalGoldAmount = nextAuctionGoldAmount;
nextAuctionGoldAmount = 0;
uint256 epochId = _currentEpochId = _currentEpochId + 1;
if (totalGoldAmount < config.auctionMinimumDistributedGold) { revert LowGoldDistributed(totalGoldAmount); }
EpochInfo storage info = epochs[epochId];
info.totalAuctionTokenAmount = totalGoldAmount;
uint128 startTime = info.startTime = uint128(block.timestamp) + config.auctionStartCooldown;
uint128 endTime = info.endTime = startTime + AUCTION_DURATION;
emit AuctionStarted(epochId, msg.sender, startTime, endTime, totalGoldAmount);
}
function recoverToken(
address token,
address to,
uint256 amount
) external override onlyElevatedAccess {
if (to == address(0)) { revert CommonEventsAndErrors.InvalidAddress(); }
if (amount == 0) { revert CommonEventsAndErrors.ExpectedNonZero(); }
if (token != address(templeGold)) {
emit CommonEventsAndErrors.TokenRecovered(to, token, amount);
IERC20(token).safeTransfer(to, amount);
return;
}
uint256 epochId = _currentEpochId;
EpochInfo storage info = epochs[epochId];
if (info.startTime == 0) { revert InvalidOperation(); }
if (info.isActive()) { revert AuctionActive(); }
if (info.hasEnded()) { revert AuctionEnded(); }
uint256 _totalAuctionTokenAmount = info.totalAuctionTokenAmount;
if (amount > _totalAuctionTokenAmount) { revert CommonEventsAndErrors.InvalidAmount(token, amount); }
delete epochs[epochId];
unchecked {
nextAuctionGoldAmount += _totalAuctionTokenAmount - amount;
}
emit CommonEventsAndErrors.TokenRecovered(to, token, amount);
templeGold.safeTransfer(to, amount);
}
Impact
The impact of this vulnerability is significant and includes:
Protocol Disruption: The ability to bypass the cooldown period can disrupt the normal operation of creating new Auction and other checks performed while creating New auction and intended pacing of auctions, leading to potential protocol instability or abuse.
Inconsistent State: The contract may enter an inconsistent state where the currentEpochId
does not accurately reflect the actual state of auctions, leading to further logical errors or vulnerabilities.
Tools Used
Manual Review
Recommendations
To mitigate this issue, consider implementing the deletion operation *in *removetoken function
Adjust currentEpochId
on Deletion: Ensure that when an epoch is deleted via recoverToken
, the currentEpochId
is appropriately adjusted to maintain consistency. This could involve decrementing the currentEpochId
or implementing a more robust epoch management system.
//RECOVER TOKENS
function recoverToken(address token,
address to,
uint256 amount
) external override onlyElevatedAccess {
if (to == address(0)) { revert CommonEventsAndErrors.InvalidAddress(); }
if (amount == 0) { revert CommonEventsAndErrors.ExpectedNonZero(); }
if (token != address(templeGold)) {
emit CommonEventsAndErrors.TokenRecovered(to, token, amount);
IERC20(token).safeTransfer(to, amount);
return;
}
// auction started but cooldown pending
uint256 epochId = _currentEpochId;
EpochInfo storage info = epochs[epochId];
if (info.startTime == 0) { revert InvalidOperation(); }
if (info.isActive()) { revert AuctionActive(); }
if (info.hasEnded()) { revert AuctionEnded(); }
uint256 _totalAuctionTokenAmount = info.totalAuctionTokenAmount;
if (amount > _totalAuctionTokenAmount) { revert CommonEventsAndErrors.InvalidAmount(token, amount); }
/// @dev Epoch data is deleted and leftover amount is addedd to nextAuctionGoldAmount.
/// so admin should recover total auction amount for epoch if that's the requirement
delete epochs[epochId];
/// @dev nextAuctionGoldAmount is set to 0 in startAuction.
/// nextAuctionGoldAmount > 0 if there was a distribution after auctionStart called
/// epoch is deleted. so if amount < totalAuctionTokenAmount for epoch, add leftover to next auction amount
+ _currentEpochId = _currentEpochId - 1;
unchecked {
nextAuctionGoldAmount += _totalAuctionTokenAmount - amount;
}
emit CommonEventsAndErrors.TokenRecovered(to, token, amount);
templeGold.safeTransfer(to, amount);
}