TempleGold

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

Incorrect token allocation due to no-bid auction recovery mechanism

Summary

The _totalAuctionTokenAllocation and auction allocation token balances can be incorrectly updated when the same no-bid auction is recovered multiple times.

This causes subsequent auctions to use incorrect values for _totalAuctionTokenAllocation and epochAuctionTokenAmount.

Additionally, the SpiceAuction.recoverAuctionTokenForZeroBidAuction() function does not record on-chain the epoch auction that was recovered, resulting in potential mistakes and causing the issue.

Vulnerability Details

When the same no-bid auction is recovered multiple times, the _totalAuctionTokenAllocation and auction allocation token balances are updated incorrectly.

This affects the allocation of tokens for subsequent auctions, leading to inability to start the subsequence auction or incorrect values being used in the auction token allocation for subsequent auctions.

The lack of on-chain recording in the SpiceAuction.recoverAuctionTokenForZeroBidAuction() function increase the potentially occurs of this issue, as it allows for the possibility of repeated recoveries of the same auction epoch.

// Location: SpiceAuction.sol
//@audit no tracking of the recovered epochId
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; // incorrectly update -> affected the other audtions
emit CommonEventsAndErrors.TokenRecovered(to, auctionToken, amount); //@audit not emit the epochId that be recovered
@> IERC20(auctionToken).safeTransfer(to, amount); // incorrectly transfer
}

// Location: SpiceAuction.sol
// Affected points
function startAuction() external override {
uint256 epochId = _currentEpochId;
// snipped
(,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(); }
// epoch start settings
// now update currentEpochId
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;
// Keep track of total allocation auction tokens per epoch
@> _totalAuctionTokenAllocation[auctionToken] = totalAuctionTokenAllocation + epochAuctionTokenAmount;
emit AuctionStarted(epochId, msg.sender, startTime, endTime, epochAuctionTokenAmount);
}

Impact

  • Inability to start subsequence auctions.

  • The incorrect updating of _totalAuctionTokenAllocation can lead to improper token distribution in subsequent auctions.

Proof of Concept

Actors:

  • onlyDAOExecutor: TempleDAO executor that can executes the SpiceAuction.recoverAuctionTokenForZeroBidAuction()

  • Alice: The auction starter (or any if config to zero address).

  • SpiceAuction: The SpiceAuction contract

Scenario:

  • Initial State:

    • auctionToken = TempleGold token

    • epochId: 1 auction has ended without bidding and can be recover

    • epochInfo(1).totalBidTokenAmount = 0

    • epochInfo(1).totalAuctionTokenAmount = 100e18

    • _totalAuctionTokenAllocation = 100e18

    • TempleGold.balanceOf(SpiceAuction) = 100e18

  • Step 1: 100e18 TGLD tokens is funded to the SpiceAuction contract for the next auction.

  • Step 2: Alice starts new auction (epochId: 2)

    • epochInfo(2).totalAuctionTokenAmount = 100e18

    • _totalAuctionTokenAllocation = 100e18 + 100e18 = 200e18

    • TempleGold.balanceOf(SpiceAuction) = 200e18

  • Step 3: onlyDAOExecutor recovers the epochId: 1 via the SpiceAuction.recoverAuctionTokenForZeroBidAuction()

    • _totalAuctionTokenAllocation = 200e18 - epochInfo(1).totalAuctionTokenAmount(100e18) = 100e18

    • TempleGold.balanceOf(SpiceAuction) = 200e18 - epochInfo(1).totalAuctionTokenAmount(100e18) = 100e18

  • Step 4: onlyDAOExecutor recovers the epochId: 1 again via the SpiceAuction.recoverAuctionTokenForZeroBidAuction()

    • _totalAuctionTokenAllocation = 100e18 - epochInfo(1).totalAuctionTokenAmount(100e18) = 0

    • TempleGold.balanceOf(SpiceAuction) = 100e18 - epochInfo(1).totalAuctionTokenAmount(100e18) = 0

// Location: SpiceAuction.sol
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);
}
  • Step 5: As the mistake in the Step 4, The protocol team attempt to transfer the TGLD back to the SpiceAuction contract

    • TempleGold.balanceOf(SpiceAuction) = 0 + 100e18 = 100e18

  • Step 6: epochId: 2 auction ended with fully claiming 100e18 claimed auction tokens

    • _claimedAuctionTokens[auctionToken] = 100e18

    • TempleGold.balanceOf(SpiceAuction) = 100 - 100e18 = 0

  • Step 7: Alice attempts to start new auction (epochId: 3) (no TGLD token funds in this auction)

    • As the _totalAuctionTokenAllocation has incorrectly updated to 0 at Step 4. _totalAuctionTokenAllocation = 0

    • The epochAuctionTokenAmount calculation will cause the revert.

// Location: SpiceAuction.sol
function startAuction() external override {
uint256 epochId = _currentEpochId;
// snipped
(,address auctionToken) = _getBidAndAuctionTokens(config);
@> uint256 totalAuctionTokenAllocation = _totalAuctionTokenAllocation[auctionToken]; // 0
uint256 balance = IERC20(auctionToken).balanceOf(address(this)); // 80e18
@> uint256 epochAuctionTokenAmount = balance - (totalAuctionTokenAllocation - _claimedAuctionTokens[auctionToken]);
// epochAuctionTokenAmount = 80e18 - (0 - 100e18) -> REVERT
if (config.activationMode == ActivationMode.AUCTION_TOKEN_BALANCE) {
if (config.minimumDistributedAuctionToken == 0) { revert MissingAuctionTokenConfig(); }
}
if (epochAuctionTokenAmount < config.minimumDistributedAuctionToken) { revert NotEnoughAuctionTokens(); }
// epoch start settings
// now update currentEpochId
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;
// Keep track of total allocation auction tokens per epoch
_totalAuctionTokenAllocation[auctionToken] = totalAuctionTokenAllocation + epochAuctionTokenAmount;
emit AuctionStarted(epochId, msg.sender, startTime, endTime, epochAuctionTokenAmount);
}
  • Outcome: The epochId: 3 (subsequence auction) is unable to be started.

  • Implications:

    This scenario describes the revert case as it drains the _totalAuctionTokenAllocation to 0.

    Let's consider the case where the _totalAuctionTokenAllocation is not fully drained. The epochInfo for subsequent auctions will use incorrect funds that should have been allocated for the previous epoch, as the allocation for that epoch was incorrectly updated.

Tools Used

  • Foundry

  • Manual Review

Recommendations

Modify the SpiceAuction.recoverAuctionTokenForZeroBidAuction() function to include on-chain recording of recovered epoch auctions.

This ensures that each auction recovery is tracked and prevents multiple recoveries of the same epoch.

// Location: SpiceAuction.sol
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(); }
+ // epoch has already recovered
+ if (recoverZeroBidAuction[epochId]) { revert AlreadyRecover(); }
+ recoverZeroBidAuction[epochId] = true;
SpiceAuctionConfig storage config = auctionConfigs[epochId];
(, address auctionToken) = _getBidAndAuctionTokens(config);
uint256 amount = epochInfo.totalAuctionTokenAmount;
_totalAuctionTokenAllocation[auctionToken] -= amount; //@audit 007 - if same-epoch invoke multiple time will distrubt the protocol -> _totalAuctionTokenAllocation incorrectly subtract and thereis no onchain tracking of the flag that the epoch is recovered
- emit CommonEventsAndErrors.TokenRecovered(to, auctionToken, amount);
+ emit CommonEventsAndErrors.TokenRecovered(epochId, to, auctionToken, amount);
IERC20(auctionToken).safeTransfer(to, amount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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