Skip to content

Unexpected Ether Transfers (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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 deterministically. The address generation can be looked up in any EVM implementation, such as the py-evm reference implementation by the Ethereum Foundation:

1
2
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.

Mitigation

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.

Generally, using the contract's balance as a guard is not advisable.