I do not understand how transaction are processed sequentially and reentrancy attacks can still happen. Can someone explain this?
Solidity – How Reentrancy Works
reentrant-attacksremixsolidity
Related Solutions
A computer program or subroutine is called reentrant if it can be interrupted in the middle of its execution and then safely be called again before its previous invocations complete execution. The interruption could be caused by an internal action such as a jump or call, or by an external action such as an interrupt or signal. Once the reentered invocation completes, the previous invocations will resume correct execution. And doing this with some piece of code will causes an Reentrancy Attack.
Now let's see this line of code:
bank.Withdraw.value(0)(balance);
This line of code means, call the function Withdraw
with argument balance such as Withdraw(balance)
but also set the value of this function call as 0 which is done by value(0)
. So the in simple english we can say this line of code as:
There is a function in
bank
contract, named asWithdraw
, set thevalue
as0
, and pass thebalance
as function argument.
Let say the above line of code is as follows:
bank.Withdraw.value(0);
that mean, only set the value
of function Withdraw
as 0
but did not call.
Before going further to actual attack here, let me explain you a little bit about fallback functions in contract.
- In Solidity, a contract may have precisely one unnamed function, which cannot have arguments, nor return anything.
- Fallback functions are executed if a contract is called and no other function matches the specified function identifier, or if no data is supplied.
- These functions are also executed whenever a contract would receive plain Ether, without any data, in this case it must be payable.
Now come to the actual attack. Your Malicious contract is actually an attacker contract, because it has a malicious fallback function.
function (){
bank.Withdraw.value(0)(balance);
}
this function is actually calling again to bank contract by withdraw
function which is actually a line of attack.
Now see the MyBank
contract which is innocent baby contract and does not know that outside world is so cruel and called the external function from withdraw
function.
function Withdraw(uint amount) {
if(balances[msg.sender] >= amount) {
msg.sender.send(amount);
balances[msg.sender] -= amount;
}
}
msg.sender.send(amount);
this line is actually an external call to Malicious contract's fallback function before the execution of withdraw
function completes. It becomes like this.
And this function calling will continue unless MyBank contract drain out of all it's ether (via gas or value). The recursive tree will look like this
I hope this answer will help you.
First of all, let's create a basic re-entrancy vulnerable contract:
contract Safe {
mapping(address => uint) public userBalances;
function deposit() public payable {
userBalances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(userBalances[msg.sender] >= _amount, "low balance");
payable(msg.sender).call({value: _amount});
userBalances[msg.sender] -= _amount;
}
}
As you can see we didn't set the max gas that call
can use. What does it mean? You can restrict the gas amount that external function calls can use. Take a look at here. What if you did not set this? It can use all over of your gas (nearly 1m gas). Is it bad? It can be. Fallback functions are for what should contract do when it receives ether
(not just that but it is enough for now to now that).
We are a popular DeFi project. So, there will be thousands of ether in our contract.
This attacker creates a contract for attack to our Safe contract. And this contract has this function:
function attack() public onlyOwner {
Safe.deposit({value: 1 ether});
Safe.withdraw(1 ether);
}
This function is basically to deposit and withdraw 1 ether for the attacker's smart contract. And our attacker's smart contract also has a fallback function:
fallback() external payable {
Safe.withdraw(1 ether);
}
Okay, let's take a deep breath and look step by step what are we doing:
- Create an attacker contract
- Call attack function - Which means deposit and withdraw 1 ether.
- When we try to withdraw, the Safe contract tries to call our fallback function. And that function also tries to call withdraw function.
Can you see that? There is a loop. This loop will continue until all Safe contract funds are over. Because there will be an error. After then we will receive all the funds. Because our funds didn't decrease (because of our loop).
But if you'll change the call function line to this:
payable(msg.sender).call({value: _amount, gas: 30000});
Probably it will be okay (depending the gas calculation). But the best solution is first decrease userBalances, and after then send his money.
Best Answer
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
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 :
Let's run through what happens step by step when calling pwn() :
Hope that's clear enough, don't hesitate to ask questions :)