TempleGold

TempleDAO
Foundry
25,000 USDC
View results
Submission Details
Severity: low
Valid

DaiGoldAuction doesn't have TLGD recovery for zero bid auction

Summary

DaiGoldAuction lacks a mechanism to recover TGLD tokens if no bids are placed during an epoch, resulting in stuck tokens. In contrast, SpiceAuction has a function to recover tokens in zero bid situations.

Vulnearbility Details

DaiGoldAuction is used to hold an auction for TLGD tokens. Each auction operates in an epoch, with each epoch lasting one week.

At the start of an epoch, users can place bids for TGLD tokens using DAI. Once the epoch ends, TGLD tokens are distributed based on the total amount of DAI pledged. Users can then claim their TGLD tokens.

The only source of TGLD token is through distribution. Whenever TempleGold#mint is called, new tokens are minted directly to DaiGoldAuction. DaiGoldAuction#notifyDistribution updates nextAuctionGoldAmount, the current availble amount for next auction.

Privileged actor can call recoverToken function during the cooldown period (before bidding starts), to recover TGLD tokens to a designated target address and basically cancel the upcoming auction and also shifting the leftover amount to the next epoch.

However, DaiGoldAuction does not address the situation where no bids are placed for TGLD tokens during an epoch. If no bid are made, the allocated TGLD token for that epoch become stuck as there is no mechanism to claim or recover them. Calling recoverToken function in this scenario result in a revert transaction since the zero bid auction has already ended.

Following is an illustration of how the token would get stuck in a zero bid auction.
Supposed that an upcoming epoch has this initial state, and the next epochId is X:

Initial state

nextAuctionGoldAmount = 1_000
epoch[X].totalAuctionTokenAmount = 0

startAuction() is called (epoch X starts)

nextAuctionGoldAmount = 0
epoch[X].totalAuctionTokenAmount = 1_000

epoch X ends, no bids are made

nextAuctionGoldAmount = 1_000 @< newly mint from distribution
epoch[X].totalAuctionTokenAmount = 1_000
epoch[X+1].totalAuctionTokenAmount = 0

startAuction() is called (epoch X+1 starts)

nextAuctionGoldAmount = 0
epoch[X].totalAuctionTokenAmount = 1_000
epoch[X+1].totalAuctionTokenAmount = 1_000

An amount of 1_000 of TGLD tokens are stuck in epoch X.

To illustrate more regarding this issue, the mirror of the same situation actually does exist in SpiceAuction and there exists a mechanism to recover auction token in a zero bid auction.
See: SpiceAuction.sol#L275-L292

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);
}

Proof-of-Concept

The following test demonstrates the exact situation described in description section.
TGLD tokens are stuck in zero bid auction with no mechanism to recover.

Steps

  • Add a new test in 2024-07-templegold/protocol/test/forge/templegold/DaiGoldAuction.t.sol using below git diff

  • Run forge test --mc DaiGoldAuctionTest --match-test test_tokenStuckInZeroBid -vv

diff --git a/protocol/test/forge/templegold/DaiGoldAuction.t.sol b/protocol/test/forge/templegold/DaiGoldAuction.t.sol
index dc2274c..c56365d 100644
--- a/protocol/test/forge/templegold/DaiGoldAuction.t.sol
+++ b/protocol/test/forge/templegold/DaiGoldAuction.t.sol
@@ -12,6 +12,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { CommonEventsAndErrors } from "contracts/common/CommonEventsAndErrors.sol";
import { ITempleERC20Token } from "contracts/interfaces/core/ITempleERC20Token.sol";
import { TempleGoldStaking } from "contracts/templegold/TempleGoldStaking.sol";
+import {console} from "forge-std/Test.sol";
contract DaiGoldAuctionTestBase is TempleGoldCommon {
event AuctionStarted(uint256 epochId, address indexed starter, uint128 startTime, uint128 endTime, uint256 auctionTokenAmount);
@@ -40,7 +41,7 @@ contract DaiGoldAuctionTestBase is TempleGoldCommon {
function setUp() public {
/// @dev forking for layerzero endpoint to execute code
- fork("arbitrum_one", 204026954);
+ fork("arbitrum_one", 230257791);
ITempleGold.InitArgs memory initArgs;
initArgs.executor = executor;
@@ -291,6 +292,46 @@ contract DaiGoldAuctionTestView is DaiGoldAuctionTestBase {
contract DaiGoldAuctionTest is DaiGoldAuctionTestBase {
+ function test_tokenStuckInZeroBid() public{
+ _setVestingFactor(templeGold);
+ uint256 currentEpoch = daiGoldAuction.currentEpoch();
+ if (currentEpoch == 0) {
+ vm.warp(block.timestamp + 1 weeks);
+ } else {
+ IAuctionBase.EpochInfo memory info = daiGoldAuction.getEpochInfo(currentEpoch);
+ vm.warp(info.endTime + 1 weeks);
+ }
+
+ templeGold.mint();
+ vm.prank(executor);
+ daiGoldAuction.setAuctionStarter(address(0));
+
+ console.log("@> TGLD availble for next auction: %e", daiGoldAuction.nextAuctionGoldAmount());
+ console.log("@> Starting auction");
+ daiGoldAuction.startAuction();
+ uint epochX = daiGoldAuction.currentEpoch();
+ IAuctionBase.EpochInfo memory epochX_info = daiGoldAuction.getEpochInfo(epochX);
+ console.log("@> TGLD availble: %e", daiGoldAuction.nextAuctionGoldAmount());
+ console.log("@> epoch X's totalAuctionTokenAmount: %e", epochX_info.totalAuctionTokenAmount);
+ console.log("@> Simulate epochX ends");
+
+ vm.warp( epochX_info.endTime + 1 weeks );
+ templeGold.mint();
+
+ console.log("@> TGLD availble for epoch_X+1: %e", daiGoldAuction.nextAuctionGoldAmount());
+ console.log("@> Starting auction");
+ daiGoldAuction.startAuction();
+ uint epochX_plus_1 = daiGoldAuction.currentEpoch();
+ IAuctionBase.EpochInfo memory epochX_plus_1_info = daiGoldAuction.getEpochInfo(epochX_plus_1);
+ console.log("@> TGLD availble: %e", daiGoldAuction.nextAuctionGoldAmount());
+ console.log("@> epoch X+1's totalAuctionTokenAmount: %e", epochX_plus_1_info.totalAuctionTokenAmount);
+ epochX_info = daiGoldAuction.getEpochInfo(epochX);
+ console.log("@> epoch X's totalAuctionTokenAmount: %e", epochX_info.totalAuctionTokenAmount);
+ console.log("@> TGLD balance in auction contract: %e", templeGold.balanceOf(address(daiGoldAuction)));
+ assertEq( epochX_plus_1_info.totalAuctionTokenAmount+epochX_info.totalAuctionTokenAmount, templeGold.balanceOf(address(daiGoldAuction)));
+
+ }
+
function test_startAuction() public {
_setVestingFactor(templeGold);
vm.startPrank(executor);

Impact

  • minted TGLD in the zero bid auction is locked in DaiGoldAuction

Rationale for severity

  • A material amount of TGLD can get stuck forever but also only in a certain situation, hence Medium

Recommended Mitigations

  • Implement the same recover mechanism for zero bid auction in SpiceAuction

Updates

Lead Judging Commences

inallhonesty Lead Judge
11 months ago
inallhonesty Lead Judge 11 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Auctioned tokens cannot be recovered for epochs with empty bids in DaiGoldAuction

Support

FAQs

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