
Solidity Fallback and Receive Functions: Building Robust Contract Entry Points
What fallback and receive actually do
Solidity provides two special external functions:
receive() external payablefallback() 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 markedpayable
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
| Situation | receive() | fallback() |
|---|---|---|
| Empty calldata, ETH sent | preferred if present and payable | used if receive() is absent |
| Non-empty calldata, no matching function | not used | used |
| Matching function exists | not used | not used |
| ETH sent to non-payable handler | transaction reverts | transaction 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.valueviadelegatecall. - 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()orsend()- 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.
| Pattern | Behavior | Typical use | Risk level |
|---|---|---|---|
| Strict reject | Revert on unknown calls | Simple vaults, payment receivers | Low |
| ETH accept only | receive() accepts ETH, fallback() reverts | Donation or treasury contracts | Low |
| Proxy forwarder | fallback() delegates to implementation | Upgradeable systems | High |
| Compatibility shim | Translate old calls to new logic | Migration layers | Medium |
| Metrics sink | Log and reject unknown calls | Monitoring and debugging | Low 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 eventfallback()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:
- Implement
receive()only if the contract should accept plain ETH. - Keep
receive()minimal and side-effect light. - Revert in
fallback()unless you are intentionally forwarding or translating calls. - If you are building a proxy, make the fallback logic small, deterministic, and thoroughly tested.
- 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.
