What are variables in solidity and what is a renterancy bug - Fri, Aug 30, 2024
Guide to solidity variables usage in Solidity and how to prevent renterancy
What are variables in solidity
In Solidity, a variable is a name given to a location in memory or storage that holds data. You declare a variable with any of the following data types such as uint
, int
, address
, bool
, string
, and bytes
among others. Below is a simple example declaring variables in Solidity:
pragma solidity ^0.8.0;
contract MyContract {
// Declare a state variable of type `uint256`
uint256 public myNumber;
// This is the declaration of a state variable of type `address`
address public owner;
// Below is the declaration of a state variable of type `bool`
bool public isComplete;
// Declaration of another state variable of type `string`
string public myString;
// Now below is the declaration of a constructor function
constructor() {
// Initialize the state variables
myNumber = 2;
owner = msg.sender;
isComplete = false;
myString = "Hello, World";
}
}
In the example below, some state variables of different data types-uint256, address, bool, string - are declared and initialized by the constructor function. Note that Solidity is a statically typed language, meaning that the data type of a variable is declared at compile time and not at runtime. Also, Solidity fully supports state variables stored on the blockchain and local variables stored in memory.
What is renterancy
A reentrancy vulnerability, in the context of smart contracts, occurs if at any instance a called contract in a contract changes the state of a calling contract in such a way that it can call this very contract again even before finishing the first call. That really puts into place a really bad sequence of events and could allow anyone to empty the contract, among other things.
Here’s how a reentrancy may happen, for example: Some function of Contract A calls some function of Contract B and forwards some ether to it.
The function of Contract B calls the function of Contract A, which changes the latter’s state. The change of the state of Contract A triggers an event that calls Contract B’s function again, forwarding more ether. Contract B’s function calls the function of Contract A again and changes its state; this just repeats in a cycle. You can prevent reentrancy attacks by employing various techniques. One of them is the “checks-effects-interactions” pattern: you would make all state changes before making any external call. This ensures one thing-that the state of the contract isn’t changed until after the external call has returned to its normal execution and has not allowed the attacker to manipulate the contract state within the call.
Another one is to implement a “withdrawal pattern,” i.e., the contract maintains an amount of balance for every user and allows withdrawal in several transactions sums, not one. That way, the state of the contract does not change until the user already has money in his account, while the attacker is not able to drain all funds at once.
pragma solidity ^0.8.0;
contract VulnerableBank {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount, "Insufficient balance");
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent, "Failed to send Ether");
balances[msg.sender] -= _amount;
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
the example above shows reentrancy vulnerable code
This VulnerableBank has one critical reentrancy bug in the contract of its withdraw function. Now, I will explain how this vulnerability works:
The program allows users to deposit Ether through the function Deposit and, simultaneously, withdraw this Ether using the function Withdraw.
It happens in the following function.
Firstly, it checks if the user has enough balance.
Secondly, it sends the Ether to the user using msg.sender.call{value: _amount}()
.
Thirdly, it will update the balance of the user.
But the thing is, this contract pays out the Ether before updating the balance. So if the receiving address is a malicious contract, it’ll call back into the withdraw function before the balance is updated.
A malicious contract does this by:
Sending some amount of Ether to the target contract. Calling withdraw. Its fallback function calls withdraw again before the previous one had finished. Repeat until contract is drained of Ether.
This can occur because the balance check passes every time it’s executed-after all, its logic hasn’t been updated yet-and because Ether is being sent out before the balance actually drops.
This bug can be fixed by having the contract follow the “Checks-Effects-Interactions” pattern:
Conditions Checking State Changes External Interaction
This is how the fixed withdraw function could look like:
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount, "Insufficient balance");
balances[msg.sender] -= _amount; // Update the balance first
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
By updating the balance before sending the Ether we prevent reentrancy: any subsequent call to withdraw
would fail now, since its balance would have been already reduced.
Note that contracts with known vulnerabilities should only be developed for educational purposes and in an isolated environment. When it comes to the deployment of real applications, ensure that your contracts are secure and follow best practices.