Types of reentrancy:
The reentrancy vulnerability is divided into four different types. They are,
- Single function reentrancy
- Cross-function reentrancy
- Cross-contract Reentrancy
- Read-only Reentrancy
Single function reentrancy:
- In single-function reentrancy, the vulnerability occurs within the same function of a contract. This typically happens when the contract performs external calls to other contracts or accounts before completing its internal state changes.
- An attacker exploits this vulnerability by recursively calling the same function of the contract before the previous call completes, allowing them to manipulate the contract's state unpredictably.
- Single function reentrancy vulnerability is often the result of improper sequencing of state changes and external calls within a function.
- This type of attack is the simplest and easiest to prevent. It occurs when the vulnerable function is the same function the attacker is trying to recursively call. For example:
contract Vulnerable {
mapping (address => uint) private balances;
function withdraw() public {
// Effects
uint amount = balances[msg.sender];
// Interactions
(bool success, ) = msg.sender.call.value(amount)("");
require(success);
// Additional Effects (this line is vulnerable to reentrancy)
balances[msg.sender] = 0;
}
}
In the above code, the withdraw()
function is vulnerable to a single function reentrancy attack. An attacker can repeatedly call the withdraw()
function before updating the balance
, resulting in multiple withdrawals.
Cross-function reentrancy:
- Cross-function reentrancy involves multiple functions within the same contract. The vulnerability arises when one function makes an external call to another function before completing its own state changes.
- An attacker can exploit this vulnerability by calling into the same contract from within the externally called function, potentially reentering the original function or other functions within the contract.
- Cross-function reentrancy vulnerability can be challenging to detect as it involves interactions between different functions within the same contract.
- This type of attack is more complex and harder to prevent. It occurs when the vulnerable function calls another function that is also vulnerable to reentrancy. For example:
contract Vulnerable {
mapping (address => uint) private balances;
function transfer(address to, uint amount) public {
// Checks
if (balances[msg.sender] >= amount) {
// Effects
balances[to] += amount;
balances[msg.sender] -= amount;
}
}
function withdraw() public {
// Checks
uint amount = balances[msg.sender];
// Interactions
(bool success, ) = msg.sender.call.value(amount)("");
require(success);
// Effects
balances[msg.sender] = 0;
}
}
- The
withdraw
function initiates atransfer
of funds to the caller using a low-levelcall
. - Before the function completes and sets the
balance
to zero, it does not prevent other functions (such astransfer
) from being called. - An attacker can exploit this by calling the
withdraw
function and, during thecall
, reentering the contract through another function liketransfer
, taking advantage of the fact that thebalance
has not yet been set to zero.
Explanation to above Vulnerable contract:
The Vulnerable
contract does not strictly adhere to the Checks-Effects-Interactions pattern.
-
Transfer Function:
-
Checks: The
transfer
function checks if the sender (msg.sender
) has a sufficientbalance
to perform thetransfer
. This check ensures that thetransfer
is only executed when the sender has enough funds. -
Effects: If the sender has a sufficient
balance
, the function updates the balances of the sender and the recipient accordingly. -
Interactions: There is no interactions in
transfer
function.
-
Checks: The
-
Withdraw Function:
-
Checks: The
withdraw
function retrieves the balance of the caller (msg.sender
) and uses it for the withdrawal. However, it does not explicitly check if thebalance
is greater than zero before initiating the withdrawal, which could lead to unnecessary gas expenditure if the balance is zero.(i.e,amount = 0
) -
Effects: After retrieving the
balance
, the function initiates anexternal call
usingmsg.sender.call.value(amount)("")
to transfer funds to the caller. Then, it immediately sets the caller'sbalance
to zero. - Interactions: The external call to transfer funds is performed before the state change of setting the balance to zero, violating the sequencing principle of the Checks-Effects-Interactions pattern.
-
Checks: The
In summary, while the transfer
function in the Vulnerable
contract follows the Checks-Effects-Interactions pattern by performing state changes before interactions, the withdraw
function violates this pattern by initiating an external call before completing its internal state changes. This violation exposes the contract to reentrancy vulnerabilities. To mitigate this vulnerability, developers should ensure that all state changes are completed before interacting with external contracts or accounts, following the Checks-Effects-Interactions pattern consistently across all contract functions.
Cross-contract Reentrancy:
- Cross-contract reentrancy occurs when a contract interacts with external contracts, and the vulnerability lies in the interaction between different contracts.
- An attacker exploits this vulnerability by recursively calling back into the vulnerable contract from within an external contract before the original call completes, potentially causing unexpected behavior or manipulating the state of the contracts involved.
- This type of reentrancy vulnerability often arises when contracts trust external contracts to perform certain actions without properly validating or handling the consequences of the interactions.
contract Vulnerable {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
// Checks: Whether the `user` has neccessary amount
require(balances[msg.sender] >= amount, "Insufficient balance");
// Effects: Update sender's balance
balances[msg.sender] -= amount;
// Interactions: External contract call
ExternalContract externalContract = ExternalContract(msg.sender);
externalContract.withdraw(amount);
// Additional effects (this line is vulnerable to reentrancy)
balances[msg.sender] += amount;
}
}
contract ExternalContract {
function withdraw(uint256 amount) external {
// Some logic
// Vulnerable to reentrancy attack
msg.sender.call("");
}
}
In this example, the withdraw
function of the ExternalContract
is vulnerable to reentrancy. When called by the Vulnerable
contract, an attacker can exploit this vulnerability to reenter the Vulnerable
contract and manipulate the sender's balance
.
Read-only Reentrancy:
- Read-only reentrancy is a specific type of reentrancy vulnerability where the attacker is limited to reading contract state during the reentrant call, without the ability to modify state directly.
- While read-only reentrancy may seem less severe than other types, it can still lead to information leakage or manipulation of contract behavior based on the observed state.
- This type of vulnerability may occur when contracts allow external calls to view certain state variables without adequate protection against reentrancy attacks.
In the example below, a banking contract is presented that permits users to deposit and withdraw:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Bank is ReentrancyGuard {
mapping (address => uint) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() public nonReentrant {
// Checks
uint amount = balances[msg.sender];
// Interactions
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success);
// Effects
balances[msg.sender] = 0;
}
}
The Bank
contract protects itself against reentrancy issues by using OpenZeppelin's ReentrancyGuard
. Additionally, a third-party smart contract exists that uses the bank's publicly available balances
mapping for its own business logic, e.g. managing shares based on a user's investment in the bank:
contract BankConsumer {
Bank private bank;
constructor(address _bank) {
bank = Bank(_bank);
}
function getBalance(address account) public view returns (uint256) {
return bank.balances(account);
}
}
The bank's withdraw
function is safeguarded against reentrancy. Still, this guard only applies to the contract itself and does not extend to other systems. The execution flow of the external call in withdraw
can be taken over by an attacker, who can then craft a smart contract that presents a misleading balance while interacting with other projects.
In this case, BankConsumer
is such a project, merely exposing a view
function for illustration purposes. Such a view
function could be internally utilized elsewhere, e.g. for assigning shares and a check preventing users to liquidate more shares than they own. An outdated balance
exposed by the Bank
contract can be exploited by designing a malicious receive function:
contract Attacker {
event Checkpoint(uint256 balance);
Bank private bank;
BankConsumer private consumer;
constructor(address _bank, address _consumer) payable {
bank = Bank(_bank);
consumer = BankConsumer(_consumer);
}
function attack() public {
emit Checkpoint(consumer.getBalance(address(this)));
bank.deposit{value: 1 ether}();
bank.withdraw();
emit Checkpoint(consumer.getBalance(address(this)));
}
receive() external payable {
emit Checkpoint(consumer.getBalance(address(this)));
// more malicious code here
}
}
The Attacker contract records Checkpoint events, illustrating that the balances visible to the BankConsumer
contract are outdated. When the system is set up in Remix
, and the attack function is executed, the following events are logged by the Attacker contract:
[
{
"from": "0xE3Ca443c9fd7AF40A2B5a95d43207E763e56005F",
"topic": "0xde5ae8a37da230f7df39b8ea385fa1ab48e7caa55f1c25eaaef1ed8690f36998",
"event": "Checkpoint",
"args": {
"0": "0",
"balance": "0"
}
},
{
"from": "0xE3Ca443c9fd7AF40A2B5a95d43207E763e56005F",
"topic": "0xde5ae8a37da230f7df39b8ea385fa1ab48e7caa55f1c25eaaef1ed8690f36998",
"event": "Checkpoint",
"args": {
"0": "1000000000000000000",
"balance": "1000000000000000000"
}
},
{
"from": "0xE3Ca443c9fd7AF40A2B5a95d43207E763e56005F",
"topic": "0xde5ae8a37da230f7df39b8ea385fa1ab48e7caa55f1c25eaaef1ed8690f36998",
"event": "Checkpoint",
"args": {
"0": "0",
"balance": "0"
}
}
]
The second Checkpoint event indicates that, during the receive
function, the Bank
contract still presents a balance
of 1 ETH for the Attacker contract address despite the funds having been transferred to the attacker. This outdated information can be manipulated to mislead third-party infrastructure building on the Bank contract, such as BankConsumer
.
Another Example:
contract Vulnerable {
mapping(address => uint256) public balances;
function viewBalance(address account) external view returns (uint256) {
// Vulnerable to read-only reentrancy
return balances[account];
}
}
contract Attacker {
Vulnerable vulnerableContract;
constructor(address vulnerableAddress) public {
vulnerableContract = Vulnerable(vulnerableAddress);
}
function viewAndManipulateBalance(address account) external {
// Read the balance
uint256 balance = vulnerableContract.viewBalance(account);
// Manipulate contract behavior based on the observed balance
if (balance > 100) {
// Do something
}
}
}
In this example, the viewBalance
function of the Vulnerable contract is vulnerable to read-only reentrancy. An attacker can call this function recursively, observing the balance
of multiple accounts, and manipulate the behavior of another contract (Attacker) based on the observed balances.
Conclusion :
Each type of reentrancy vulnerability presents unique challenges and risks, but they all share the common characteristic of allowing attackers to exploit the sequential execution of contract code to their advantage. Solidity developers should be aware of these vulnerabilities and follow best practices, such as using mutex patterns or reentrancy guards, to prevent and mitigate their impact.
To prevent reentrancy attacks, you should avoid making external calls in critical sections of your code, and use the nonReentrant modifier provided by the OpenZeppelin library or implement the Checks-Effects-Interactions pattern.
For more visit my Github: https://github.com/logesh21n/Solidity-Common-Vulnerabilities
Top comments (0)