Solidity Reentrancy Guard
A reentrancy guard is a lock that blocks a second entry into a function. The lock is set before the outside call and cleared after it.
The lock clears after a normal withdrawal
A normal withdrawal sets the lock and clears it. A second withdrawal after the first finishes works fine.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract GuardedVault {
mapping(address => uint256) public balances;
bool public locked;
modifier nonReentrant() {
require(!locked, "reentrant call");
locked = true;
_;
locked = false;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public nonReentrant {
uint256 bal = balances[msg.sender];
require(bal > 0, "nothing to withdraw");
(bool ok, ) = msg.sender.call{value: bal}("");
require(ok, "payout failed");
balances[msg.sender] = 0;
}
}A re-entering call reverts while the lock is set
The attacker swallows the failed re-entry instead of failing with it. The first withdrawal still pays out exactly once.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract GuardedVault {
mapping(address => uint256) public balances;
bool public locked;
modifier nonReentrant() {
require(!locked, "reentrant call");
locked = true;
_;
locked = false;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public nonReentrant {
uint256 bal = balances[msg.sender];
require(bal > 0, "nothing to withdraw");
(bool ok, ) = msg.sender.call{value: bal}("");
require(ok, "payout failed");
balances[msg.sender] = 0;
}
}
contract Attacker {
GuardedVault public v;
bool public blocked;
constructor(address vault) {
v = GuardedVault(vault);
}
function attack() public {
v.deposit{value: 1 ether}();
v.withdraw();
}
receive() external payable {
(bool ok, ) = address(v).call(abi.encodeWithSignature("withdraw()"));
blocked = !ok;
}
}The guard blocks a second guarded function too
Both payout functions carry the same lock. A re-entry into either one reverts while the first call runs.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract GuardedVault {
mapping(address => uint256) public balances;
bool public locked;
modifier nonReentrant() {
require(!locked, "reentrant call");
locked = true;
_;
locked = false;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public nonReentrant {
uint256 bal = balances[msg.sender];
require(bal > 0, "nothing to withdraw");
(bool ok, ) = msg.sender.call{value: bal}("");
require(ok, "payout failed");
balances[msg.sender] = 0;
}
function emergencyWithdraw() public nonReentrant {
uint256 bal = balances[msg.sender];
require(bal > 0, "nothing to withdraw");
(bool ok, ) = msg.sender.call{value: bal}("");
require(ok, "payout failed");
balances[msg.sender] = 0;
}
}
contract Attacker {
GuardedVault public v;
bool public blocked;
constructor(address vault) {
v = GuardedVault(vault);
}
function attack() public {
v.deposit{value: 1 ether}();
v.withdraw();
}
receive() external payable {
(bool ok, ) = address(v).call(abi.encodeWithSignature("emergencyWithdraw()"));
blocked = !ok;
}
}Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract GuardedVault {
mapping(address => uint256) public balances;
bool public locked;
modifier nonReentrant() {
require(!locked, "reentrant call");
locked = true;
_;
locked = false;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public nonReentrant {
uint256 bal = balances[msg.sender];
require(bal > 0, "nothing to withdraw");
(bool ok, ) = msg.sender.call{value: bal}("");
require(ok, "payout failed");
balances[msg.sender] = 0;
}
}
contract Attacker {
GuardedVault public v;
bool public blocked;
constructor(address vault) {
v = GuardedVault(vault);
}
function attack() public {
v.deposit{value: 1 ether}();
v.withdraw();
}
receive() external payable {
(bool ok, ) = address(v).call(abi.encodeWithSignature("withdraw()"));
blocked = !ok;
}
}Key points
- Lock: a boolean flag that is true while a guarded function runs.
- Modifier: a wrapper that sets the lock, runs the function, then clears the lock.
- Second entry: a call that arrives while the lock is set reverts at once.
- First entry: the original call still finishes and pays out normally.
- Clearing: the lock is cleared after the function finishes, so later calls work.
In this example, the proof funds the vault with 3 ether and gives the attacker 1 ether. The attacker deposits and withdraws, and its receive function tries to enter withdraw again. The lock is set, so the second entry reverts and the flag records the block. The attacker keeps only its own ether, the vault keeps the victim funds, and a second attack after the lock clears behaves the same way.
Note
the guard is written by hand here. It is a boolean flag and a modifier, with no outside code.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
What does the guard lock do during a guarded call?