What fallback and receive actually do

Solidity provides two special external functions:

  • receive() external payable
  • fallback() external [payable]

They are not called directly by name in normal usage. Instead, the EVM dispatches to them when no matching function selector exists, or when Ether is sent without calldata.

receive()

receive() is executed when:

  • the contract receives Ether
  • the calldata is empty
  • receive() exists and is marked payable

This is the cleanest way to accept plain ETH transfers.

fallback()

fallback() is executed when:

  • the calldata does not match any function selector
  • or receive() is absent and Ether is sent with empty calldata

It is also commonly used in proxy contracts to forward unknown calls to an implementation contract.

Dispatch behavior summary

Situationreceive()fallback()
Empty calldata, ETH sentpreferred if present and payableused if receive() is absent
Non-empty calldata, no matching functionnot usedused
Matching function existsnot usednot used
ETH sent to non-payable handlertransaction revertstransaction reverts

Understanding this dispatch order is essential when designing payment receivers, proxies, and compatibility layers.


When to use receive() versus fallback()

A good rule is to keep the two functions separate in purpose.

Use receive() for plain Ether reception

If your contract should accept direct ETH transfers, implement receive() and keep it minimal. Typical use cases include:

  • donation contracts
  • escrow contracts
  • payment vaults
  • contracts that must accept refunds or forced transfers from other contracts

Use fallback() for unknown function handling

Use fallback() when you need to respond to arbitrary calldata. Common cases include:

  • proxy forwarding
  • compatibility shims for older interfaces
  • metering or logging unknown calls
  • controlled rejection with custom logic

Avoid mixing responsibilities

A contract that both receives ETH and forwards unknown calls should still keep those paths conceptually separate. If the same fallback logic handles both, the code becomes harder to audit and easier to break during upgrades.


A practical example: accepting donations safely

The simplest valid receive() function is one that accepts ETH and emits an event.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract DonationVault {
    address public immutable owner;

    event DonationReceived(address indexed sender, uint256 amount, bytes data);

    constructor() {
        owner = msg.sender;
    }

    receive() external payable {
        emit DonationReceived(msg.sender, msg.value, msg.data);
    }

    fallback() external payable {
        revert("Unsupported call");
    }

    function withdraw(uint256 amount) external {
        require(msg.sender == owner, "Not owner");
        require(address(this).balance >= amount, "Insufficient balance");

        (bool ok, ) = payable(owner).call{value: amount}("");
        require(ok, "Withdrawal failed");
    }
}

Why this design works

  • receive() accepts direct ETH transfers.
  • The event records sender, amount, and calldata for observability.
  • fallback() explicitly rejects unexpected calls instead of silently accepting them.
  • withdraw() keeps the contract’s purpose clear and avoids accidental fund loss.

Best practice: keep receive() simple

Do not perform complex logic in receive(). It should not:

  • call untrusted external contracts
  • depend on fragile state transitions
  • perform expensive loops
  • assume a specific gas stipend

A minimal receive() reduces the chance of unexpected reverts during transfers.


How fallback() is used in proxy contracts

The most important real-world use of fallback() is proxy forwarding. A proxy contract stores state, while an implementation contract contains logic. When users call the proxy with a function selector the proxy does not recognize, fallback() forwards the call to the implementation using delegatecall.

This pattern is powerful, but it must be implemented carefully.

Example: minimal delegate proxy

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract SimpleProxy {
    address public implementation;

    constructor(address _implementation) {
        implementation = _implementation;
    }

    fallback() external payable {
        address impl = implementation;
        require(impl != address(0), "No implementation");

        assembly {
            calldatacopy(0, 0, calldatasize())

            let result := delegatecall(
                gas(),
                impl,
                0,
                calldatasize(),
                0,
                0
            )

            returndatacopy(0, 0, returndatasize())

            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    receive() external payable {}
}

What this proxy does

  • It forwards unknown calls to implementation.
  • It preserves the original caller and msg.value via delegatecall.
  • It returns or reverts with the implementation’s exact returndata.

Important caveat

This example is intentionally minimal. In production, you must carefully manage storage layout, access control, upgrade authorization, and initialization. The fallback itself is only one part of a safe proxy architecture.


Security considerations

Fallback and receive functions are small, but they sit on critical paths. Mistakes here can cause lost funds, broken integrations, or upgrade vulnerabilities.

1. Do not assume direct ETH transfers are always intentional

Contracts can receive ETH through:

  • transfer() or send()
  • low-level call{value: ...}("")
  • self-destructed contracts
  • miner/validator-related edge cases in older systems

Your receive() should accept or reject transfers based on explicit policy, not assumptions about the sender.

2. Be careful with gas limits

Historically, transfer() and send() forwarded only 2300 gas, which made complex fallback logic unreliable. Modern Solidity development generally prefers low-level call, but that means your receive() may be invoked with more gas than you expect.

Design receive() to be cheap and deterministic.

3. Never rely on fallback for authorization

A fallback function should not become a hidden admin interface. If a contract uses fallback to route special commands, the dispatch logic must be explicit and auditable. Otherwise, you risk creating undocumented behavior that bypasses normal access controls.

4. Avoid silent acceptance of unknown calls

If a contract is not intended to act as a proxy, unknown function selectors should usually revert. Silent acceptance can mask integration bugs and make debugging difficult.

5. Be aware of reentrancy in forwarding logic

A fallback that performs call or delegatecall can expose reentrancy surfaces. If the contract also updates state before or after forwarding, ensure the sequence is safe and well understood.


Design patterns for fallback behavior

Different applications need different fallback strategies. The table below summarizes common choices.

PatternBehaviorTypical useRisk level
Strict rejectRevert on unknown callsSimple vaults, payment receiversLow
ETH accept onlyreceive() accepts ETH, fallback() revertsDonation or treasury contractsLow
Proxy forwarderfallback() delegates to implementationUpgradeable systemsHigh
Compatibility shimTranslate old calls to new logicMigration layersMedium
Metrics sinkLog and reject unknown callsMonitoring and debuggingLow to medium

For most non-proxy contracts, the safest default is strict rejection in fallback() and a minimal receive().


Common mistakes to avoid

Forgetting to mark receive() as payable

If receive() is not payable, plain ETH transfers will revert. This is one of the most common mistakes when building payment-receiving contracts.

Making fallback() payable without a reason

If the contract does not need to accept ETH with unknown calldata, do not mark fallback() payable. Accepting value unintentionally can complicate accounting and increase attack surface.

Using fallback as a catch-all business logic router

A fallback that parses arbitrary calldata and dispatches internal behavior can become brittle and opaque. Prefer explicit functions unless you are implementing a proxy or a carefully designed compatibility layer.

Ignoring returndata in proxies

A proxy must return the exact returndata from the implementation. If it discards or alters returndata, many integrations will fail in subtle ways.

Writing state changes before delegatecall

In a proxy, state mutations in the proxy itself can be dangerous if the implementation expects a different storage layout. Keep the proxy minimal and reserve state changes for well-defined admin functions.


Testing fallback and receive paths

These functions deserve dedicated tests because they are easy to overlook.

Test cases to include

  • sending plain ETH with empty calldata
  • calling an unknown function selector
  • sending ETH to a non-payable contract
  • forwarding a call through a proxy
  • verifying returndata propagation
  • checking that unexpected calls revert with the right reason

Example test scenarios

A robust test suite should confirm that:

  • receive() emits the expected event
  • fallback() rejects unknown calls in non-proxy contracts
  • proxy forwarding preserves msg.sender, msg.value, and returndata
  • admin-only upgrade functions are not reachable through fallback

If you use Foundry or Hardhat, write tests that send raw calldata rather than only calling typed contract methods. That is the only way to exercise these special entry points realistically.


Practical guidelines

Use these rules of thumb when designing a contract entry strategy:

  1. Implement receive() only if the contract should accept plain ETH.
  2. Keep receive() minimal and side-effect light.
  3. Revert in fallback() unless you are intentionally forwarding or translating calls.
  4. If you are building a proxy, make the fallback logic small, deterministic, and thoroughly tested.
  5. Document the contract’s external behavior so integrators know whether ETH transfers and unknown calls are supported.

A well-designed fallback strategy improves reliability without adding unnecessary complexity.


Learn more with useful resources