Smart contracts on the Ethereum blockchain often need to handle the transfer of Ether (ETH), the network’s native cryptocurrency. To enable this functionality securely and efficiently, Solidity provides special functions and modifiers that govern how ETH is received and sent. This guide dives deep into the receive() and fallback() functions, ETH transfer methods like transfer, send, and call, the role of the payable modifier, gas considerations, and best practices for error handling.
Whether you're building decentralized applications (dApps) or learning smart contract development, understanding these core mechanisms is essential for creating robust, secure, and functional contracts.
Receiving Ether: The receive() and fallback() Functions
In Solidity, contracts cannot receive Ether by default. To accept ETH, a contract must explicitly allow it using special functions: receive() and fallback(). These are low-level functions designed to respond to incoming Ether transfers under different conditions.
The receive() Function
The receive() function is specifically designed to handle plain Ether transfers—transactions that send ETH without any associated data or function calls.
Key characteristics:
- A contract can have at most one
receive()function. - It must be declared as both
externalandpayable. - It has no name, no parameters, and cannot return values.
- It executes only when a transaction sends ETH with empty calldata (i.e., no function call).
- It should contain minimal logic to stay within the 2300 gas limit imposed by
.send()and.transfer().
👉 Learn how to securely manage Ethereum transactions with advanced tools
Here's an example:
event Received(address sender, uint256 value);
receive() external payable {
emit Received(msg.sender, msg.value);
}This function logs the sender and amount whenever the contract receives ETH.
The fallback() Function
The fallback() function acts as a catch-all for transactions that don't match any defined function. It also handles ETH transfers that include data.
Characteristics:
- Can be
externalandpayable. Executes when:
- A function call doesn’t match any existing function.
- ETH is sent with non-empty calldata.
- No
receive()function exists and ETH is sent without data.
- Can access full call data via
msg.data.
Example:
event FallbackCalled(address sender, uint256 value, bytes data);
fallback() external payable {
emit FallbackCalled(msg.sender, msg.value, msg.data);
}This logs not only the sender and value but also the raw data sent with the transaction.
Key Differences Between receive() and fallback()
| Condition | Triggered Function |
|---|---|
| ETH sent with no data | receive() (if exists), otherwise fallback() |
| ETH sent with data | fallback() |
| Function call doesn’t exist | fallback() |
This distinction ensures flexibility in handling various types of interactions with your contract.
Understanding msg.data in Depth
msg.data contains the full calldata of a transaction, including the function selector and encoded parameters.
For example, calling exampleFunction(12345) generates calldata like:
0x7c600ae6000000000000000000000000000000000000000000000000000000003039Where:
0x7c600ae6is the function selector (first 4 bytes ofkeccak256("exampleFunction(uint256)")).- The rest is the zero-padded 32-byte encoding of
12345.
This allows contracts to parse incoming data dynamically—a key feature used in proxy patterns and meta-transactions.
Sending Ether: Three Methods Compared
Contracts can send ETH using three primary methods: transfer, send, and call.
1. .transfer()
- Gas limit: 2300 (safe for simple operations).
- Behavior: Automatically reverts on failure.
- Use case: Safe for sending ETH when you expect minimal execution in the recipient.
function transferEth(address payable _to, uint256 _value) external payable {
_to.transfer(_value);
}2. .send()
- Gas limit: 2300.
- Behavior: Returns a boolean; does not revert automatically.
- Requires manual error handling.
error FailedSend();
function sendEth(address payable _to, uint256 _value) external payable {
bool success = _to.send(_value);
if (!success) {
revert FailedSend();
}
}3. .call{value: ...}("")
- Gas: Full gas available (no 2300 limit).
- Returns:
(bool success, bytes memory data) - Most flexible—can trigger complex logic in recipient contracts.
error CallFailed();
function callEth(address payable _to, uint256 _value) external payable {
(bool success,) = _to.call{value: _value}("");
if (!success) {
revert CallFailed();
}
}👉 Discover secure ways to interact with Ethereum smart contracts
The Role of the payable Modifier
The payable keyword is crucial for enabling Ether handling:
On functions: Allows a function to accept ETH.
function deposit() public payable { // Accepts ETH }On addresses: Declares an address as capable of receiving Ether (
address payable).address payable public owner;
Only address payable can use .transfer() or .send(). Regular address types must be cast or declared appropriately.
Why Is the 2300 Gas Limit Important?
When ETH is sent via .transfer() or .send(), only 2300 gas is forwarded. This is enough to emit an event but insufficient for complex logic.
If a contract’s receive() or fallback() function exceeds this limit, the transaction fails. Therefore:
- Keep fallback logic minimal.
- Use
.callwhen sending ETH to contracts that may perform complex operations.
Error Handling: Modern Patterns in Solidity
Solidity 0.8+ introduced custom errors for efficient revert messages:
error NotOwner();
function restrictedAction() public {
if (msg.sender != owner) {
revert NotOwner();
}
// Proceed with logic
}Compared to require("Not owner"), custom errors reduce gas costs by replacing expensive string literals with identifiers.
Complete Example: A Functional ETH Receiver
Here’s a fully working contract demonstrating core concepts:
pragma solidity ^0.8.0;
contract ReceiveETH {
event ReceiveEth(uint256 value, uint256 gas);
function getBalance() public view returns (uint256) {
return address(this).balance;
}
receive() external payable {
emit ReceiveEth(msg.value, gasleft());
}
}It:
- Emits an event on receipt.
- Tracks remaining gas using
gasleft(). - Allows balance checks.
Using gasleft() to Monitor Execution
gasleft() returns the amount of gas remaining in the current transaction. Useful for:
- Avoiding out-of-gas errors in loops.
- Conditional logic based on available resources.
- Debugging gas usage during development.
👉 Explore Ethereum development tools that enhance security and efficiency
Frequently Asked Questions
Q: Can a contract receive ETH without a receive() or fallback() function?
A: No. Without either function, any attempt to send ETH will fail.
Q: What happens if receive() uses more than 2300 gas?
A: The transaction reverts due to out-of-gas, especially when triggered by .send() or .transfer().
Q: When should I use call instead of transfer?
A: Use .call when interacting with contracts that require more than 2300 gas to process incoming ETH.
Q: Is fallback() payable required to receive ETH?
A: Yes. If marked as non-payable, it cannot receive Ether and will revert on value-bearing calls.
Q: Can I have both receive() and fallback() in one contract?
A: Yes—and it's common. They serve different purposes based on calldata presence.
Q: How do I check how much ETH a contract holds?
A: Use address(this).balance inside the contract to query its current balance.
Core Keywords: Ethereum, Solidity, receive function, fallback function, payable modifier, gas limit, smart contract security, send ETH