Smart contracts on the Ethereum blockchain often need to handle native cryptocurrency transactions—specifically, receiving and sending Ether (ETH). Understanding how to manage these operations securely and efficiently is fundamental for any developer working with Solidity, Ethereum’s primary smart contract programming language.
In this guide, we’ll explore the core mechanics of payable functions, receiving ETH, and sending ETH from a contract using three distinct methods: transfer, send, and call. Each method has unique characteristics in terms of gas usage, error handling, and security implications.
Whether you're building decentralized applications (dApps), token contracts, or automated payment systems, mastering ETH transfer logic is essential.
👉 Learn how to test smart contract transactions securely on a leading blockchain platform.
What Makes a Function Payable?
By default, Solidity functions cannot accept Ether. To allow a function to receive ETH, it must be marked with the payable keyword.
Similarly, if a contract needs to store or forward ETH, certain addresses within the contract should also be declared as address payable.
Here's a basic example:
pragma solidity >=0.7.0 <0.9.0;
contract Payable {
address payable public owner;
constructor() {
owner = payable(msg.sender);
}
// Allows users to send ETH to the contract
function deposit() external payable {}
// View the current balance of the contract
function getBalance() external view returns (uint) {
return address(this).balance;
}
}Key Concepts:
- The
payablemodifier on thedeposit()function allows it to accept incoming Ether. - The
ownervariable is declared asaddress payableso that later it can receive funds from the contract. address(this).balancereturns the total amount of ETH stored in the contract.
Without payable, any attempt to send ETH to the function will result in a transaction revert.
How to Receive Ether in a Contract
Starting from Solidity 0.6.0, there are two special functions designed to handle incoming Ether:
receive()– Triggered when Ether is sent without calldata (i.e., simple transfers).fallback()– Called when either no data matches a function or noreceive()exists.
For receiving ETH safely, use the receive() function:
receive() external payable {}This empty implementation accepts ETH and emits no events by default. You can enhance it by logging data or enforcing conditions.
Example with event logging:
contract EthReceiver {
event Log(uint amount, uint gas);
receive() external payable {
emit Log(msg.value, gasleft());
}
}This logs both the amount received and the remaining gas at execution time—useful for debugging gas consumption during transactions.
👉 Explore tools that support real-time monitoring of Ethereum transactions.
Sending ETH from a Contract: 3 Methods Compared
There are three primary ways for a contract to send Ether to another address:
| Method | Gas Forwarded | Return Value | Error Handling |
|---|---|---|---|
transfer | 2300 gas | Reverts on failure | Throws automatically |
send | 2300 gas | bool success/fail | Manual check required |
call | All available | (bool, data) | Flexible, manual check |
Let’s examine each one in detail.
1. Using transfer() – Safe but Limited
The transfer method sends a fixed amount of 2300 gas, which is enough for basic operations like logging but not sufficient for complex contract logic.
If the recipient fails (e.g., due to running out of gas or throwing an error), the entire transaction reverts.
function sendViaTransfer(address payable _to) external payable {
_to.transfer(123);
}✅ Pros:
- Automatically reverts on failure (safe default behavior).
- Prevents reentrancy attacks due to limited gas.
❌ Cons:
- Rigid gas stipend may prevent legitimate recipient contracts from processing funds.
- Can cause unexpected reverts in upgradable or complex recipient contracts.
⚠️ Note: As of recent best practices,transfer()is discouraged in favor ofcall()due to inflexibility.
2. Using send() – Manual Failure Handling
Similar to transfer(), send() forwards only 2300 gas but returns a boolean indicating success or failure instead of reverting automatically.
You must manually check the result:
function sendViaSend(address payable _to) external payable {
bool sent = _to.send(123);
require(sent, "Send failed");
}✅ Pros:
- Slight control over flow (non-reverting by default).
❌ Cons:
- Still limited to 2300 gas.
- Easy to forget checking the return value—security risk.
- Obsolete in modern development standards.
3. Using call() – Modern and Flexible
The most recommended method today is .call{value: amount}(""), which forwards all remaining gas by default and allows customization via gas limits.
It returns a tuple: (bool success, bytes memory data).
function sendViaCall(address payable _to) external payable {
(bool success,) = _to.call{value: 123}("");
require(success, "Call failed");
}You can also limit forwarded gas:
(bool success,) = _to.call{value: 123, gas: 5000}("");✅ Pros:
- Full control over gas and execution.
- Compatible with modern contracts and upgradeable patterns.
- Safer when combined with checks and proper access control.
❌ Cons:
- Requires careful handling to avoid reentrancy vulnerabilities.
- Developers must explicitly check return values.
🔐 Best Practice: Always use call() for sending ETH in new projects, paired with reentrancy guards if needed.Core Keywords for SEO & Developer Search Intent
To align with common search queries and improve visibility, here are key terms naturally integrated throughout this article:
- Solidity payable function
- Send ETH from contract
- Transfer ETH in Solidity
- Receive Ether in smart contract
- Solidity call vs transfer vs send
- Payable address in Solidity
- Ethereum smart contract transactions
- How to send ETH using Solidity
These reflect high-intent phrases developers use when learning or troubleshooting ETH transfers in decentralized applications.
Frequently Asked Questions (FAQ)
Q: What happens if I don’t mark a function as payable but try to send ETH?
A: The transaction will revert with an error. Only functions marked payable can accept Ether. This includes constructors and external functions intended for funding.
Q: Why is transfer() no longer recommended?
A: Because it forwards only 2300 gas, which may not be enough for modern recipient contracts (especially those with complex fallback logic). This can lead to failed transactions even when intended. The community now favors .call() for flexibility.
Q: Can a contract reject incoming Ether?
A: Yes. A contract can omit receive() or fallback() functions, making it non-payable. Alternatively, it can include logic inside these functions to reject funds based on conditions (e.g., minimum amount, whitelisting).
Q: Is it safe to use .send()?
A: Not recommended. Like .transfer(), it’s limited to 2300 gas and requires manual success checks. Forgetting to validate the return value introduces silent failure risks.
Q: How do I prevent reentrancy when using .call()?
A: Use the Checks-Effects-Interactions pattern and consider implementing OpenZeppelin’s ReentrancyGuard. Always update state before making external calls.
Q: Can I send ETH to any address?
A: Technically yes—but only externally owned accounts (EOAs) and payable contracts can accept it. Sending to non-payable contracts without proper setup will fail unless they have a receive() function.
👉 Access developer resources for secure Ethereum transaction handling.
Conclusion
Handling ETH payments correctly is crucial for secure and functional smart contracts. From marking functions as payable to choosing the right method for sending Ether (call being the current best practice), every decision impacts safety, compatibility, and user experience.
Remember:
- Use
receive()to accept simple ETH transfers. - Prefer
.call{value: amount}("")overtransfer()orsend(). - Always validate return values and protect against reentrancy.
- Test thoroughly on testnets before deployment.
With these principles, you're well-equipped to build robust financial logic in your dApps—whether it's crowdfunding platforms, NFT marketplaces, or automated payout systems.
Stay updated with evolving best practices and prioritize security-first design patterns in all your Solidity projects.