Missing Re-entrancy Protection
Description
Re-entrancy is a vulnerability that occurs when a contract makes an external call to an untrusted contract, which then calls back into the original contract before the first call has completed. This can lead to unexpected behavior, including re-entrancy attacks where the attacker repeatedly re-enters the original contract, exploiting it to extract more funds or perform unauthorized actions.
One common way to prevent re-entrancy attacks is to use a mutex lock. A mutex lock is a simple mechanism that allows a contract to ensure that only one execution path can access critical code at any given time. By using a mutex lock, a contract can prevent any external call from interrupting its execution and ensure that all state changes are committed before any new external call can be made.
Example Code
Consider the following code, which allows users to withdraw funds from their account:
pragma solidity ^0.8.0;
contract MyContract {
mapping (address => uint256) balances;
function withdraw(uint256 amount) public {
require(amount <= balances[msg.sender], "Insufficient balance");
// send funds to user
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Failed to send funds");
// update balance
balances[msg.sender] -= amount;
}
}
This code is vulnerable to re-entrancy attacks because the withdraw
function sends funds to an external contract before updating the account balance. An attacker could exploit this vulnerability by creating a malicious contract that repeatedly calls the withdraw function, extracting more funds than the user is entitled to.
Recommendation
To prevent re-entrancy attacks, it is recommended to use a mutex lock to ensure that only one execution path can access critical code at any given time. One common pattern is to use the ReentrancyGuard
contract from OpenZeppelin, which provides a nonReentrant
modifier that can be applied to any function that requires protection.
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract MyContract is ReentrancyGuard {
mapping (address => uint256) balances;
function withdraw(uint256 amount) public nonReentrant {
require(amount <= balances[msg.sender], "Insufficient balance");
// send funds to user
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Failed to send funds");
// update balance
balances[msg.sender] -= amount;
}
}
By using the nonReentrant
modifier, this code ensures that the withdraw
function can only be called once at any given time, preventing any re-entrancy attacks.