TempleGold

TempleDAO
Foundry
25,000 USDC
View results
Submission Details
Severity: medium
Invalid

`recoverAuctionTokenForZeroBidAuction` Also Updates the Epoch Auction Amount to Prevent Multiple Recoveries

Summary

The TempleDAO allows the DAO executor to withdraw tokens from auctions that have no bids and update the necessary states. However, it does not mark the auction as withdrawn, which can cause a DoS, if multiple withdrawal calls are executed on a single auction.

Vulnerability Details

The Temple DAO with withdraw the auction tokens which were transferred for auction biding and the auction did not receive any bid from users. So in this case it will allow the DAOExecutor to withdraw these tokens from spiceAuction contract via calling recoverAuctionTokenForZeroBidAuction:

function recoverAuctionTokenForZeroBidAuction(uint256 epochId, address to) external override onlyDAOExecutor {
if (to == address(0)) { revert CommonEventsAndErrors.InvalidAddress(); }
// has to be valid epoch
if (epochId > _currentEpochId) { revert InvalidEpoch(); }
// epoch has to be ended
EpochInfo storage epochInfo = epochs[epochId];
if (!epochInfo.hasEnded()) { revert AuctionActive(); }
// bid token amount for epoch has to be 0
if (epochInfo.totalBidTokenAmount > 0 ) { revert InvalidOperation(); }
SpiceAuctionConfig storage config = auctionConfigs[epochId];
(, address auctionToken) = _getBidAndAuctionTokens(config);
uint256 amount = epochInfo.totalAuctionTokenAmount;
_totalAuctionTokenAllocation[auctionToken] -= amount;
emit CommonEventsAndErrors.TokenRecovered(to, auctionToken, amount);
IERC20(auctionToken).safeTransfer(to, amount);
}

POC:

add import import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; inside SpiceAuction.t.sol file.

function test_recoverAuctionTokenForZeroBidAuction() public {
vm.startPrank(daoExecutor);
// revert, zero address
vm.expectRevert(abi.encodeWithSelector(CommonEventsAndErrors.InvalidAddress.selector));
spice.recoverAuctionTokenForZeroBidAuction(0, address(0));
// invalid epoch
vm.expectRevert(abi.encodeWithSelector(IAuctionBase.InvalidEpoch.selector));
spice.recoverAuctionTokenForZeroBidAuction(1, alice);
_startAuction(true, true);
IAuctionBase.EpochInfo memory info = spice.getEpochInfo(1);
vm.startPrank(daoExecutor);
vm.warp(info.startTime);
vm.expectRevert(abi.encodeWithSelector(IAuctionBase.AuctionActive.selector));
spice.recoverAuctionTokenForZeroBidAuction(1, alice);
vm.startPrank(bob);
uint256 bidAmount = 10 ether;
deal(daiToken, bob, bidAmount);
IERC20(daiToken).approve(address(spice), type(uint).max);
spice.bid(bidAmount);
uint256 epochOneTotalAuctionTokenAmount = info.totalAuctionTokenAmount;
ISpiceAuction.SpiceAuctionConfig memory _config = spice.getAuctionConfig(1);
vm.warp(info.endTime + _config.waitPeriod);
// fail for epoch with bid
vm.startPrank(daoExecutor);
vm.expectRevert(abi.encodeWithSelector(IAuctionBase.InvalidOperation.selector));
spice.recoverAuctionTokenForZeroBidAuction(1, alice);
_startAuction(true, true);
info = spice.getEpochInfo(2);
_config = spice.getAuctionConfig(2);
vm.warp(info.endTime + _config.waitPeriod);
address auctionToken = spice.getAuctionTokenForCurrentEpoch();
uint256 auctionTokenBalance = IERC20(auctionToken).balanceOf(address(spice));
uint256 auctionTokenAmount = info.totalAuctionTokenAmount;
uint256 aliceBalance = IERC20(auctionToken).balanceOf(alice);
vm.startPrank(daoExecutor);
vm.expectEmit(address(spice));
emit TokenRecovered(alice, auctionToken, auctionTokenAmount);
spice.recoverAuctionTokenForZeroBidAuction(2, alice);
spice.recoverAuctionTokenForZeroBidAuction(2, alice);
assertEq(IERC20(auctionToken).balanceOf(alice), aliceBalance + (auctionTokenAmount*2));
// // bidders from previous auction can claim
vm.startPrank(bob);
uint256 bobBalance = IERC20(auctionToken).balanceOf(bob);
console.logAddress(address(auctionToken));
vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, address(0xffD4505B3452Dc22f8473616d50503bA9E1710Ac), 0, 100000000000000000000));
spice.claim(1); // @audit : the user who is eligible to claim is not able to claim auction tokens
assertEq(IERC20(auctionToken).balanceOf(bob), bobBalance + 0);
}

run with command : forge test --mt test_recoverAuctionTokenForZeroBidAuction

Impact

If the recoverAuctionTokenForZeroBidAuction called more than once it will create a DoS for other user and Users of other auctions with the same auction tokens will be unable to claim their tokens.

Tools Used

Manual Review

Recommendation

Add boolean state variable to EpochInfo which will be set to true if recoverAuctionTokenForZeroBidAuction has called first time , also add condition which will check for if auction tokens have already recovered.

struct EpochInfo {
/// @notice Start time for epoch
uint128 startTime;
/// @notice End time for epoch
uint128 endTime;
/// @notice Total amount of bid token deposited
uint256 totalBidTokenAmount;
/// @notice Total amount of auction tokens to distribute. Constant value
uint256 totalAuctionTokenAmount;
++ bool isRecovered;
}
// inside spiceAuction::recoverAuctionTokenForZeroBidAuction function add following changes and also add `AlreadyRecovered` inside `ISpiceAuction` interface.
EpochInfo storage epochInfo = epochs[epochId];
if (!epochInfo.hasEnded()) {
revert AuctionActive();
}
++ if (epochInfo.isRecovered) {
++ revert AlreadyRecovered();
}
// bid token amount for epoch has to be 0
if (epochInfo.totalBidTokenAmount > 0) {
revert InvalidOperation();
}
SpiceAuctionConfig storage config = auctionConfigs[epochId];
(, address auctionToken) = _getBidAndAuctionTokens(config);
uint256 amount = epochInfo.totalAuctionTokenAmount;
_totalAuctionTokenAllocation[auctionToken] -= amount;
++ epochInfo.isRecovered = true;
Updates

Lead Judging Commences

inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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