Solidity Advanced: Sending and Receiving ETH with Payable, Receive, and Fallback Functions

·

Smart contracts on the Ethereum blockchain often need to handle the native cryptocurrency, ETH. Mastering how to securely and efficiently send and receive ETH is a critical skill for any Solidity developer. This guide dives deep into the core mechanisms—payable, receive, and fallback functions—and explores best practices for sending ETH using transfer, send, and call.

Whether you're building decentralized finance (DeFi) applications, NFT marketplaces, or wallet systems, understanding these foundational concepts ensures your contracts interact safely with ETH transactions.


Receiving ETH in Solidity

Ethereum smart contracts can receive ETH through special functions designed to respond to incoming transactions. These functions are triggered automatically when someone sends ETH to the contract address. The two primary functions used for this purpose are receive() and fallback().

Using Payable Functions

The simplest way for a contract to accept ETH is by marking a function as payable. When a function has the payable modifier, it allows users to send ETH along with their function call.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

contract Payable {
    // Accepts ETH during function call
    function deposit() external payable {}

    // Returns the current contract balance
    function getBalance() external view returns (uint) {
        return address(this).balance;
    }
}

This pattern is commonly used in crowdfunding contracts or user deposit systems where ETH is sent during specific actions.

👉 Discover how to test payable functions in a secure environment


The receive() Function

The receive() function is specifically designed to handle plain ETH transfers—such as those made via wallet apps like MetaMask when no data is included in the transaction.

Key characteristics:

receive() external payable {}

Use this function when you want to log or react to direct Ether deposits.


The fallback() Function

While receive() handles simple transfers, the fallback() function serves a dual role:

  1. Handling ETH sent with additional data (calldata).
  2. Acting as a catch-all for calls to non-existent functions.

It’s also essential in proxy patterns and contract forwarding logic.

fallback() external payable {}

Like receive(), it must be external. Adding payable allows it to accept ETH; otherwise, sending ETH will cause a revert.


Key Differences Between receive() and fallback()

ScenarioTriggered Function
ETH sent with no datareceive() (if exists), else fallback()
ETH sent with datafallback()
Call to non-existent functionfallback()

Here’s a practical example demonstrating both:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

contract FallBack {
    event Log(string func, address sender, uint value, bytes data);

    fallback() external payable {
        emit Log("fallback", msg.sender, msg.value, msg.data);
    }

    receive() external payable {
        emit Log("receive", msg.sender, msg.value, "");
    }

    function getBalance() external view returns (uint) {
        return address(this).balance;
    }
}

In this setup:

Understanding these triggers prevents unexpected behavior and potential loss of funds.


Sending ETH from a Contract

Contracts can also send ETH to other addresses. Solidity provides three methods: transfer(), send(), and call(). Each has distinct safety and flexibility trade-offs.

1. transfer() – Safe but Limited

function sendByTransfer(address payable _to, uint amount) external payable {
    _to.transfer(amount);
}

✅ Pros: Safe default; automatic rollback on error
❌ Cons: Rigid gas limit; not suitable for complex receiving logic

Use only when you’re certain the recipient doesn’t require more than 2300 gas.


2. send() – Low-level with Manual Error Handling

function sendBySend(address payable _to, uint amount) external payable {
    bool ok = _to.send(amount);
    require(ok, "send failed");
}

✅ Pros: More control than transfer()
❌ Cons: Still gas-limited; requires explicit error checking

Avoid unless maintaining legacy code.


3. call() – Recommended for Modern Contracts

function sendByCall(address payable _to, uint amount) external payable {
    (bool ok,) = _to.call{value: amount}("");
    require(ok, "call failed");
}

✅ Pros: Flexible, future-proof, supports complex logic
✅ Industry standard in modern Solidity (post-Istanbul upgrade)
❌ Requires careful handling of reentrancy risks

👉 Learn secure coding practices before deploying send functions


Frequently Asked Questions (FAQ)

Q1: Can a contract receive ETH without any special functions?

Yes—but only if it has a payable constructor or at least one of the following:

Otherwise, sending ETH will result in a transaction failure.

Q2: Why was transfer() deprecated in newer Solidity versions?

After the Istanbul hard fork, the gas cost of certain operations increased. Since transfer() relies on a strict 2300 gas stipend, there's a risk it may fail unexpectedly. Thus, the community now recommends call() for reliability.

Q3: What is reentrancy, and why does it matter when sending ETH?

Reentrancy occurs when a malicious contract calls back into your contract during an ETH transfer (e.g., within a call). This can drain funds if state changes aren't properly ordered. Always use the Checks-Effects-Interactions pattern to mitigate this risk.

Q4: How do I withdraw ETH from a contract?

You typically combine a modifier (like onlyOwner) with a withdrawal function using call:

function withdraw() external onlyOwner {
    (bool ok,) = msg.sender.call{value: address(this).balance}("");
    require(ok, "withdraw failed");
}

Never use .transfer() here due to gas concerns.

Q5: Can both receive and fallback exist in the same contract?

Yes—and they often should. Use receive for pure Ether deposits and fallback for everything else (function dispatch errors, data-bearing transfers).


Core Keywords for SEO

These keywords naturally appear throughout the content to enhance search visibility while maintaining readability and technical accuracy.


By mastering how to send and receive ETH using modern Solidity patterns, developers can build more robust and secure blockchain applications. Always prefer call() over outdated methods, validate inputs, and follow security best practices.

👉 Start building and testing your own ETH-handling contracts today