Reentrancy, DoS, Withdrawal Pattern and Fallback Functions on Ethereum Blockchain
Table of Contents
- Introduction
- Vulnerable Contract Implementation
- Denial of Service Attack
- Reentrancy Attack
- Conclusions
- References
1. Introduction
In this article, we simulate 2 attacks on Ethereum blockchain’s smart contracts, Denial of Service and Reentrancy attacks. We begin with background about Ethereum Mainnet and Testnet networks, smart contracts and wallets, and Solidity programming language. Then, we continue with implementing and deploying vulnerable smart contracts on Ropsten, an Ethereum test network. Finally, we exploit the vulnerabilities exactly as they happened in real contracts in the history of Ethereum. We explain the full flow of the attacks including the vulnerabilities themselves, why they happen, how to exploit them, and finally, how to mitigate and avoid these risks.
General Terms:
- Ethereum Network [1] — A blockchain (like Bitcoin) that allows trustless and decentralized storage and transfer of value from peer to peer. In addition, it supports code execution and data persistence using the Turing complete Ethereum Virtual Machine [2] (EVM).
- Mainnet — The main network of Ethereum (real value).
- Ropsten — The test network of Ethereum.
- Transaction — an action triggered by a wallet to transfer value or execute the code of a smart contract.
- Ether — The native currency used in the Ethereum network.
- Gas — A unit of measure that determines the fee to be paid by a transaction sender.
Accounts:
A participant on the Ethereum network with a hexadecimal address of 20 bytes length. There are 2 types of accounts on Ethereum.
1. EOA (Wallet) — A pair of private and public keys generated using the Elliptic Curve Digital Signature Algorithm (ECDSA) [3]. This pair represents a wallet on the Ethereum network. It is called Externally Owned Account as it is owned by a human user using a private key.
- Private Key (256 bit) — Should be kept as a secret and represents the ownership of a user over a wallet and the funds in it. This key is used to sign transactions sent by this wallet.
- Public Key — is used to derive the public address of the wallet which is exposed to the Ethereum network for interactions like receiving funds, sending funds, access control, and other different on-chain interactions.
2. Smart Contract — An account that has also a runnable code and state that are publicly open to read and use by any Ethereum user. The code is written in bytecodes like assembly or Java bytecodes, and it is executed using the EVM. In contrary to EOA, theoretically smart contracts are not owned by anyone (practically usually they are), and their addresses are not derived from a private-public keys pair.
- Vulnerabilities — As in every code written by humans, there might intentional and non-intentional bugs and flaws in the program. As the code on Ethereum is public and immutable after deployment, in the short term it increases the exploitability of smart contracts. However, it improves the overall ecosystem code quality in the long term as it is audited massively.
Development:
- Solidity [4] — A Programming language like Java that is used to program Ethereum smart contracts. The code is compiled before the deployment of the EVM bytecodes using ‘.solc’ compilers.
- Remix IDE [5] — A simple browser-based IDE for Solidity and Ethereum.
- Etherscan.io [6] — A Block explorer that allows exploring the blocks, transactions, accounts, and any other information of the Ethereum network using a web UI and an API.
- MetaMask [7] — An extension for browser providing wallet features for Ethereum users like keys creation, management, and backup, sending transactions, viewing wallet information, interacting with Ethereum node, etc.
- DeFi — Decentralized Finance, which is an aggregative name for all the financial applications deployed on Ethereum and other smart blockchains using smart contracts.
2. Vulnerable Contract Implementation
We implement one smart contract with 2 vulnerable functions, one for DoS and the second for Reentrancy. The contracts code is designed to allow an employer to send salaries to his employees using the Ethereum network. The implementation is a simple as possible, so it is easier to focus and emphasize the important parts of it. There are 2 types of entities: the owner
of the contract (employer) and the employees
of his company. There are 2 more functions that are simple and self-explained.
All smart contracts are developed on Remix using Solidity language and compiled to EVM bytecodes using the solc compiler. In addition, the smart contracts are deployed to the Ropsten network using MetaMask wallet, and the source code of the contracts is verified on Etherscan.io block explorer (otherwise only the binary bytecodes are visible). For simplicity, all transactions and deployments are initiated using the same EOA wallet: https://ropsten.etherscan.io/address/0x8020cc47e35cd4e291385c16fb587e270bda39e9
Using this link, you can explore all transactions and contracts involved in this article.
2.1. Desirable Functionality
After developing, compiling, deploying, funding with 5 Ether and verifying the smart contract code named Company, we can explore it on etherscan.io at address: https://ropsten.etherscan.io/address/0xef801ac273c1e42556d16a948f3926eed97481df#code
We begin by executing the logic that the contract was developed to do. First, the owner uses registerEmployee(address employee)
to register 2 EOAs as employees. Then, we send these employees their 0.1 Ether salaries using sendSalaries()
. All transactions succeed as expected and everything is great so far.
sendSalaries()
transaction: https://ropsten.etherscan.io/tx/0x5e87b6cefeb73f71eb23eb004362d70320117498f8001d1d59d0d90c9bc834b6
3. Denial of Service Attack
3.1. Background
As the code of a smart contract on Ethereum is publicly open and immutable, it makes it easier for attackers to exploit the vulnerability and harder (if not impossible at all) for developers to fix the issues. Denial of Service [8] is a name for attacks that make the attacked resources unable to provide their services. As the code is immutable, one can use the state of a contract to exploit a DoS vulnerability and attack the contract. Similar attacks to what we present in this article have been executed on real contracts.
Taking a closer look at the sendSalaries()
function, we can see that it iterates over the registered employees and send them 0.1 Ether using transfer(0.1 ether)
. For a code-less destination accounts like EOA, transfer()
will simply transfer the value. However, for a smart contract account, it will execute thefallback()
function or thereceive()
function in newer compiler versions [9]. These functions are optional and have a default implementation that simply receives the incoming funds.
However, these functions can be overridden, letting the smart contract code decide what to do with the received funds. This can be used by malicious contracts to disrupt the functionality of the sender contract.
3.2. Exploitation
We deploy the malicious contract AttackerDos and register it as a contract of an employee: https://ropsten.etherscan.io/address/0x4080c2acd8939e6b8db4f3dcc977aba22dd6a682#code
Now, when the owner of the Company contract sends the sendSalaries()
transaction, it sends the salaries to all 3 employees. 2 employees are the previous EOAs and the 3rd is our malicious contract. However, when the Company contract tries to transfer(0.1 ether)
to our malicious AttackerDos contract, it will call its’ receive()
function.
Our receive()
implementation just reverts the execution and refuses to get the incoming funds. This results in the transfer()
function throwing an execution exception and reverting the entire transaction, which means canceling the transaction. This results in all 3 employees not getting their salaries just because of one malicious employee. Given that the code is immutable and there is no removal functionality for employees, the sendSalaries()
function will always revert. Therefore, it creates a Denial of Service on the Company contract making it useless, and the funds locked in the contract forever.
Failed sendSalaries()
transaction: https://ropsten.etherscan.io/tx/0x6677bd867765a30f2ae8bb1c6263e33b89154066da74e0ee3818112b0369435a
3.3. Mitigation
- send() — one option to mitigate the DoS risk is by changing
transfer()
tosend()
. In contrary totransfer()
,send()
does not throw an exception but rather returns true or false as a success indication. However, it creates a new risk of failure to transfer the funds to the user due to an unexpected error, resulting in funds locked forever in the contract. Thus, in fact, many ofsend()
uses actually used asrequire(send())
. This behaves like transfer in terms of exception throwing. Another issue is thattransfer()
andsend()
consume a constant 2,300 amount of gas. This is problematic as instructions gas costs tend to change and it might affect the code execution efficiency or even failure due to ‘out of gas’ exception. - call{value: amount}(‘’) — This is the preferred solution for transferring Ether [10] [11].
call()
is used to call any function of a contract and not only the fallbacks. On the one hand, this way the amount of gas used is not constant as opposed tosend()
. On the other hand, it opens a new attack vector named ‘Reentrancy’. - Withdrawal Pattern — suggests a lazy instead of eager implementation. The vulnerability of one malicious user able to affect all other users involved in a transaction can be solved using the withdrawal pattern. Instead of sending Ether to all employees, the contract will store how much every employee is eligible to get. Then, every user that wants to get his salary, will need to initialize a transaction by himself and withdraw only his salary. This way, the responsibility of getting the salary is on the employee, and if one decides to be malicious and revert the transaction, the only affected user will be himself. All other employees can withdraw their salaries independently. We implement this pattern in
withdrawSalary()
.
4. Reentrancy Attack
4.1. Description
There are risks in contracts calling externally to functions of other contracts, as they cant take over the control flow. Reentrancy [12] is when a called contract calls back into the calling contract, before the first invocation of the calling function is finished. For example, contract A function a()
calls to contract B function b()
, and b()
calls back to a()
before the first invocation is finished.
Malicious contracts can use this technique to change the control flow in an undesirable way making negative impacts like theft of tokens. This vulnerability occurs in the solution of the previous section solution to DoS using call()
and withdrawal pattern:
On a regular naïve flow, an employee initializes a withdrawSalary()
transaction to get his salary. We first check that the initiating address is indeed an employee and that he has not already withdrawn his salary. Then, we transfer him the salary and mark it as paid, so he won’t be able to withdraw it multiple times.
4.2. Exploitation
We deploy the malicious contract AttackerReentrancy and register it as a contract of an employee: https://ropsten.etherscan.io/address/0x88c3c78e47840826363a933ea74048920ebc6146#code
Attack flow:
- We send a small fraction of ETH to AttackerReentrancy contract to call its’
receive()
function. - The
receive()
function of the malicious contract callswithdrawSalary()
of the victim Company contract. - The
withdrawSalary()
function performs 2 safety checks, succeeds, and transfers the 0.1 ETH:
3.1.isEmployee[msg.sender]
- The calling address must be an employee.
3.2.!hasWithdrawnSalary[msg.sender]
- The employee has not yet withdrawn his salary.
3.3.payable(msg.sender).call{value: 0.1 ether}('')
- Transfer the salary to the employee. - However, before marking the employee as he has withdrawn his salary using
hasWithdrawnSalary[msg.sender] = true
, The calledreceive()
function of the malicious contract calls back towithdrawSalary()
. Thus, results in executing step 2 again. - Steps 2–4 are executed again, withdrawing more and more ETH. This vicious cycle is executed 5 times according to our code, withdrawing 0.5 ETH in total. This could be implemented to withdraw all of the victim’s contract funds (if we provide enough gas for the transaction execution).
- Finally, after the last iteration,
hasWithdrawnSalary[msg.sender] = true
is executed.
Reentrant withdrawSalary()
transaction: https://ropsten.etherscan.io/tx/0x1f8e47f3bcf734a3c2a551d9ba405acd3417b05f49cf325519837ac3453e1bb5
4.3. Mitigation
- No Writes After Call — If we take a closer look at the vulnerability, we can observe that the malicious contract was able to withdraw his salary multiple times. This is because the victim contract marks an employee as
hasWithdrawnSalary[msg.sender] = true
after making the external call to the malicious contract. This way, it is possible to reenter the function before the state is updated. Writing all state changes before the function call externally to other contracts will mitigate this risk. Thus, in the second iteration, thehasWithdrawnSalary[msg.sender]
flag will be alreadytrue
, and the execution will get reverted. - Reentrancy Guard — Reentrant functions expose a high risk to smart contracts, as this vulnerability might be more sophisticated. For example, cross-function reentrancy [13] involves multiple functions invocations to exploit reentrancy. Thus, the best way to mitigate reentrancy is by blocking it using a lock like OpenZeppelin’s
nonreentrant
modifier [14].
5. Conclusions
In this article, we covered two Ethereum smart contracts vulnerabilities. The first is Denial of Service and the second is Reentrancy. We have developed and deployed simple victim and attacker contracts on Ropsten. Finally, we have exploited the vulnerabilities and discussed different options for mitigating these risks.
6. References
- https://ethereum.org/en/
- https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf
- https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
- https://docs.soliditylang.org/en/latest/index.html
- https://remix.ethereum.org/
- https://etherscan.io/
- https://metamask.io/
- https://swcregistry.io/docs/SWC-113
- https://blog.soliditylang.org/2020/03/26/fallback-receive-split/
- https://ethereum.stackexchange.com/questions/19341/address-send-vs-address-transfer-best-practice-usage/38642
- https://ethereum.stackexchange.com/questions/78124/is-transfer-still-safe-after-the-istanbul-update
- https://swcregistry.io/docs/SWC-107
- https://consensys.github.io/smart-contract-best-practices/known_attacks/#cross-function-reentrancy
- https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard
Addresses List
- Deployer EOA: https://ropsten.etherscan.io/address/0x8020cc47e35cd4e291385c16fb587e270bda39e9
- Company Vulnerable Contract: https://ropsten.etherscan.io/address/0xef801ac273c1e42556d16a948f3926eed97481df#code
- Employee 1 EOA: https://ropsten.etherscan.io/address/0xc1387017d4ae2cf3cc7da19f977fa74d85df0cdd
- Employee 2 EOA: https://ropsten.etherscan.io/address/0x7dab537acb832738f020192a9cdb2b531fa1c599
- DoS Attack Contract: https://ropsten.etherscan.io/address/0x4080c2acd8939e6b8db4f3dcc977aba22dd6a682#code
- Reentrancy Attack Contract: https://ropsten.etherscan.io/address/0x88c3c78e47840826363a933ea74048920ebc6146#code