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:
- Only one
receive()function per contract. - Must be declared as
external payable. - Cannot accept parameters or return values.
- Triggered only when ETH is sent without calldata (i.e., no function call).
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:
- Handling ETH sent with additional data (calldata).
- 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()
| Scenario | Triggered Function |
|---|---|
| ETH sent with no data | receive() (if exists), else fallback() |
| ETH sent with data | fallback() |
| Call to non-existent function | fallback() |
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:
- A simple transfer triggers
receive. - Sending ETH with extra data (e.g., via
call) triggersfallback. - Calling an undefined function also triggers
fallback.
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
- Syntax:
_to.transfer(amount) - Sends a fixed amount of wei.
- Automatically reverts on failure.
- Limits gas to 2300, sufficient only for basic logging in the recipient.
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
- Syntax:
_to.send(amount) - Also limited to 2300 gas.
- Does not revert automatically on failure.
- Returns a boolean indicating success or failure.
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
- Syntax:
_to.call{value: amount}("") - No gas restrictions (uses all available if needed).
- Returns
(bool success, bytes memory data)—must check success manually. - Supports advanced interactions like calling specific functions on other 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:
- A
payablefunction - A
receive()function - A
payable fallback()function
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
- Solidity receive function
- Solidity fallback function
- Sending ETH in Solidity
- Payable modifier
- Smart contract ETH transfer
- Solidity call value
- Receiving Ether in smart contracts
- Secure ETH transfer Solidity
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