Anatomy of a Hack: How `sol-sentry` Prevents Reentrancy Attacks
The reentrancy attack is one of the most devastating and infamous vulnerabilities in smart contract history, responsible for hundreds of millions of dollars in losses. It's a subtle bug where an attacker's contract can call back into your contract and drain funds before your code has finished its initial execution.
In this post, we'll dissect a classic example of this vulnerability and show how sol-sentry provides an instant, automated defense against it.
The Vulnerable Code: A Simple Vault
Let's look at a simplified contract that holds user balances. It has a deposit function and a withdraw function. Can you spot the flaw?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableVault {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint userBalance = balances[msg.sender];
require(userBalance > 0, "No balance to withdraw");
// THE FLAW: External call is made BEFORE the state update
(bool success, ) = msg.sender.call{value: userBalance}("");
require(success, "Transfer failed.");
// State update happens last, which is too late
balances[msg.sender] = 0;
}
}
The vulnerability lies in the order of operations. The contract sends ETH to the user before it sets their balance to zero. This tiny mistake opens the door for a catastrophic exploit.
The Attack Explained
An attacker wouldn't call withdraw() from a regular wallet. They would write another smart contract to perform the attack in a few simple steps:
- The attacker deploys a malicious contract (`Attacker.sol`).
- Inside `Attacker.sol`, they write a special `receive()` function. This function's code will automatically run anytime the contract receives ETH.
- The attacker programs the `receive()` function to call
VulnerableVault.withdraw()again if there's still gas available. - The attacker calls their own contract, which deposits 1 ETH into the vault and then calls
withdraw()for the first time.
The result is a vicious loop: The vault sends 1 ETH, triggering the attacker's `receive()` function, which calls `withdraw()` again. Since the vault hasn't updated the balance yet, it happily sends another 1 ETH, and the cycle repeats until the vault is completely drained.
The Automated Defense: sol-sentry
Manually spotting this requires a trained eye and disciplined adherence to security patterns. Forgetting the correct order just once can be a fatal mistake. This is where automated analysis becomes your most critical line of defense. Running sol-sentry on this contract provides an immediate, unambiguous warning:
$ sol-sentry scan ./VulnerableVault.sol
🚨 Found 1 issues in contracts/VulnerableVault.sol
[CRITICAL] Potential Reentrancy Vulnerability
- Function: withdraw()
- Line: 16
- Details: An external call is made to `msg.sender` before the state variable `balances` is updated. This violates the Checks-Effects-Interactions pattern and can be exploited.
❌ This project is NOT safe to deploy.
Without needing to understand the complex attack vector, `sol-sentry` instantly identifies the root cause: an interaction (the external call) happens before an effect (the state change). This simple, automated check would have prevented the hack before it ever had a chance.
The Fix: Checks-Effects-Interactions
The solution is to always follow the Checks-Effects-Interactions (CEI) pattern. First, check conditions. Second, update state (the effect). Third, and only as the very last step, interact with other contracts.
function safeWithdraw() public {
// 1. Checks
uint userBalance = balances[msg.sender];
require(userBalance > 0, "No balance to withdraw");
// 2. Effects (Update state FIRST)
balances[msg.sender] = 0;
// 3. Interactions (External call is LAST)
(bool success, ) = msg.sender.call{value: userBalance}("");
require(success, "Transfer failed.");
}
Conclusion
While reentrancy is a complex attack, preventing it comes down to a simple, repeatable pattern. But developers are human, and mistakes happen. Automated tools like sol-sentry aren't just convenient; they are an essential part of a professional security workflow, acting as a tireless sentry that never forgets to check for the patterns that lead to exploits.