0

I do not understand how transaction are processed sequentially and reentrancy attacks can still happen. Can someone explain this?

1 Answer 1

5

Reentrancy can happen when a contracts critical variable is updated after a call to an external contract that depends on this critical variable to happen. Here's a classic example

contract BadBank {
  mapping(address => uint) balances;

  receive() external payable {
    balances[msg.sender] += msg.value
  }
  // This function is vulnerable to a reentrancy attack
  function withdrawAll() external {
    require(balances[msg.sender] != 0);
    (bool success, ) = payable(msg.sender).call{value: balances[msg.sender]}("");
    require(success);
    balances[msg.sender] = 0;
  }
}

This simple contract lets you send ETH to it, and withdraw it whenever you want through the withdrawAll function.

Now, that withdrawnAll function is vulnerable to reentrancy. Let me explain. When this function is called, execution goes something like this : balance check => transfer msg.sender's balance (which calls msg.sender's receive or fallback function if it's a contract, allowing it to execute code on its end, without restrictions, including calling other contracts) => balance is set to 0.

Now let's imagine badBank currently holds 1 ETH and msg.sender is this contract :

interface IBadBank {
  function withdrawAll() external;
}
contract PwnBadBank {
  function pwn(address badBank) external {
      (bool success, ) = payable(badBank).call{value: 1 ether}("");
      require(success);
      IBadBank(badBank).withdrawAll();
  }
  receive() external payable {
      if(msg.sender.balance >= 1 ether) {
          IBadBank(msg.sender).withdrawAll();
      }
  }
}

Let's run through what happens step by step when calling pwn() :

  • The contract deposits 1 ether to BadBank, making its balance 1 ether (BadBank now holds 2 ETH).
  • The contract calls withdrawAll()
  • BadBank checks that the attackers contracts balance is > 0 (which is true for now, since its balance is 1 ether).
  • BadBank calls back to our PwnBadBank contract's receive() function, with a value of 1 ether (a new call frame is opened, whatever BadBank is supposed to do after the call will be "put on hold" until PwnBadBank's receive() function is done running) (BadBank now holds 1 ETH)
  • PwnBadBank's receive function calls back to withdrawAll (hence the name, it literally "reenters" the function before it is done running), since the balance of the BadBank is 1 ETH.
  • BadBank checks that PwnBadBank's balance is > 0 (which is still true, since the previous call didnt update the balance yet, it's gonna do it when PwnBadBank returns execution to it), and sends 1 ETH to it again (new call frame again) (BadBank's balance is now 0 ETH)
  • PwnBadBank's receive function finds that BadBank's balance is now 0 ETH, and returns immediately.
  • BadBanks regains control, and sets PwnBadBank's balance to 0, but it's already too late since PwnBadBank was able to withdraw more than what it has deposited.

Hope that's clear enough, don't hesitate to ask questions :)

7
  • Great answer! What does using the IBadBank do, couldn't you just use badBank.withdrawAll(); ? Also, why check that the initial deposit is sucessful. Does (bool success, ) = payable(badBank).call{value: 1 ether}(""); prevent reverse honeypots with, for example, fake logs contracts?
    – Danny B
    Feb 17, 2023 at 8:04
  • 1
    IBadBank is just the interface to the BadBank contract here. Just doing badBank.withdrawAll(); wouldnt compile. (badBank is an address, not a contract instance, in the context of the pwn function in PwnBadBank)
    – Foxxxey
    Feb 17, 2023 at 10:43
  • 1
    And i'm not sure i understand your other question. (bool success, ) = payable(badBank).call{value: 1 ether}(""); is just a way of sending ETH to an address while allowing it to execute logic after the call ( other methods, such as payable(address).transfer(amount); only forward a small amount of gas (2300) to the called contract, so it wouldnt work here (using .transfer instead of .call is a way (arguably a bad one) to defend against reentrancy, btw))
    – Foxxxey
    Feb 17, 2023 at 10:46
  • 1
    Can reentrancy be solved by changing variables order of updating instead of using a reentrancy guard? Feb 17, 2023 at 12:24
  • @shitblockchaindev Yes, and that's how you should do it, actually. ReentrancyGuard is just a safeguard for sloppy programmers, and it wastes gas. In my BadBank example, the reentrancy issue in withrawAll() can be solved simply by setting balances[msg.sender] = 0; before sending the ETH to msg.sender.
    – Foxxxey
    Feb 17, 2023 at 13:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.