Starknet Auction

First Flight #26
Beginner FriendlyNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Reentrancy vulnerability in `withdraw` function allow attacker to drain funds from the contract

Summary

The withdraw function in the provided StarkNet auction contract is susceptible to a reentrancy attack. This issue arises because the contract allows external token transfers (via ERC20) before updating its internal state. As a result, an attacker could exploit the vulnerability by re-entering the contract and calling certain functions multiple times before the state is properly updated, potentially draining funds from the contract.

Vulnerability Details

Affected function:

withdraw(ref self: ContractState)

In the withdraw function, the contract makes external calls to transfer ERC20 tokens before updating critical state variables. Specifically, after initiating a transfer of tokens to the caller, the state change (like resetting bid_values) occurs. This opens the door for a reentrancy attack, where an attacker can trigger the withdraw function repeatedly, before the contract updates its internal state, leading to multiple withdrawals of funds.

Steps for reentrancy attack:

Step 1: An attacker places a bid on the auction and calls the withdraw function to initiate a withdrawal.
Step 2: During the token transfer (before the contract state is updated), the attacker makes another call to the withdraw function, using the first call to reenter.
Step 3: Since the contract's internal state has not been updated yet, the attacker is able to withdraw multiple times, draining more funds than they originally deposited.

Here’s a Proof of Concept (PoC) to demonstrate how a reentrancy attack could be exploited in the withdraw function. This PoC simulates an attacker exploiting the vulnerability by repeatedly calling withdraw before the contract updates its internal state.

// Attacker contract exploiting reentrancy vulnerability in withdraw function
contract Attacker {
VulnerableAuction vulnerableAuction;
bool reentered = false; // To prevent infinite reentrancy loop
constructor(address _vulnerableAuction) public {
vulnerableAuction = VulnerableAuction(_vulnerableAuction);
}
// Function to initiate the attack
function attack() public {
// Step 1: Bid first to participate in the auction
vulnerableAuction.bid{value: 1 ether}();
// Step 2: Withdraw to trigger the reentrancy attack
vulnerableAuction.withdraw();
}
// This fallback function is called during the reentrant attack
fallback() external payable {
if (!reentered) {
reentered = true;
// Step 3: Reenter the withdraw function to exploit the vulnerability
vulnerableAuction.withdraw();
}
}
}

Step 1: Bid: The attacker first bids on the auction by calling vulnerableAuction.bid(). This ensures that the attacker has some funds stored in the auction contract.

Step 2: Withdraw: After placing the bid, the attacker calls withdraw() on the vulnerable auction contract. This triggers the first withdrawal of funds from the contract.

Step 3: Reentrancy (Fallback Function): During the external token transfer in the withdraw function of the auction contract, control is passed back to the attacker’s fallback function. At this point, the attacker re-enters the withdraw function and calls it again before the auction contract has a chance to update its internal state.

Since the vulnerable contract does not update its state before performing external calls, the attacker is able to withdraw funds multiple times, draining the contract’s balance beyond their original bid.

Impact

  1. An attacker withdraw more funds than allowed, potentially draining the contract's ERC20 token balance.

  2. It cause incorrect final balances for legitimate bidders and disrupt the fair functioning of the auction.

  3. The contract will likely have to handle multiple calls, leading to gas inefficiencies.

Tools Used

Manual review.

Recommendations

  • Always update the contract's state before making external calls. For instance, the contract should update the bidder's balance or highest_bid before calling the transfer_from function to avoid the contract being reentered during the external call.

// Update the state first
self.bid_values.entry(caller).write(0); // Clear the bid value to prevent reentrancy
self.highest_bid.write(0);
// Then perform the transfer
erc20_dispatcher.transfer_from(sender, caller, amount.into());
  • Add a reentrancy guard to prevent the function from being called recursively. This guard can be implemented by setting a flag when the function is first called and resetting it after the execution completes.

assert(!self.reentrancy_guard.read(), 'Reentrant call detected');
self.reentrancy_guard.write(true);
// Transfer tokens and handle logic
self.reentrancy_guard.write(false);
  • Adhere to the "Checks-Effects-Interactions" design pattern to prevent reentrancy. Perform checks first, then update internal state, and only then make external calls.

  • Utilize well-established libraries such as OpenZeppelin’s ReentrancyGuard to simplify the implementation and ensure that reentrancy is properly handled throughout the contract.

Updates

Lead Judging Commences

bube Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Reentrancy in `withdraw` function

The `withdraw` function doesn't reset the `bid_values` to 0 after the withdraw. That means the bidder can call multiple time the `withdraw` function and receive the whole balance of the protocol.

Support

FAQs

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