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(); }
if (epochId > _currentEpochId) { revert InvalidEpoch(); }
EpochInfo storage epochInfo = epochs[epochId];
if (!epochInfo.hasEnded()) { revert AuctionActive(); }
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
@@ -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
Rationale for severity
Recommended Mitigations