Understanding how to properly send and receive Ether (ETH) in Solidity is essential for building secure and functional smart contracts on the Ethereum blockchain. This guide dives deep into the core mechanisms—payable, receive(), fallback(), and the three primary methods of sending ETH: transfer(), send(), and call()—with practical examples and best practices.
Whether you're developing decentralized applications (dApps), crowdfunding platforms, or NFT marketplaces, mastering ETH handling ensures your contracts behave as expected under real-world conditions.
Receiving Ether in Solidity
Smart contracts can receive Ether through special functions designed to handle incoming transactions. These functions are triggered automatically when someone sends ETH to the contract address.
The payable Function Modifier
To allow a function to accept Ether, it must be marked with the payable modifier. Without this, any attempt to send ETH will result in a transaction revert.
// 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;
}
}In this example, calling deposit() with ETH attached increases the contract’s balance. The getBalance() function allows anyone to check how much ETH the contract holds.
👉 Learn how to securely manage cryptocurrency transactions with advanced tools
Using the receive() Function
The receive() function is specifically designed to handle plain Ether transfers (e.g., via wallet send). It executes when a transaction includes value but no data (i.e., no function call).
- Must be declared as
external payable - Cannot accept parameters or return values
- Only one
receive()function allowed per contract
receive() external payable {}This function is ideal for simple ETH deposits. If a transaction sends ETH with no calldata, receive() is triggered—if present.
Leveraging the fallback() Function
The fallback() function acts as a catch-all for two scenarios:
- Receiving ETH with additional data
- Calling a non-existent function in the contract
Like receive(), it must be external. Adding payable allows it to accept Ether.
fallback() external payable {}If both receive() and fallback() exist:
- A plain ETH transfer triggers
receive() - A transfer with data or an invalid function call triggers
fallback()
Key Differences Between receive() and fallback()
| Scenario | Triggered Function |
|---|---|
| Plain ETH transfer (no data) | receive() |
| ETH transfer with data | fallback() |
| Call to undefined function | fallback() |
Here’s a complete example illustrating 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;
}
}This contract logs which function was called, who sent ETH, how much was sent, and any accompanying data—perfect for debugging or monitoring incoming transactions.
Sending Ether from Smart Contracts
Contracts can also send ETH to other addresses using three built-in methods: transfer(), send(), and call().
Each has distinct behavior regarding gas limits, error handling, and safety.
1. address.transfer(amount)
- Gas forwarded: 2300 (enough for basic logging)
- Reverts automatically on failure
- Safe and simple, but inflexible
function sendByTransfer(address payable _to, uint amount) external payable {
_to.transfer(amount);
}Because it reverts on failure, there's no need to manually check success—but complex recipient logic may fail due to insufficient gas.
2. address.send(amount)
- Gas forwarded: 2300
- Returns a boolean (
true/false) - Does not revert on failure; requires manual handling
function sendBySend(address payable _to, uint amount) external payable {
bool ok = _to.send(amount);
require(ok, "send failed");
}More flexible than transfer(), but still limited by gas constraints.
3. address.call{value: amount}("") ✅ Recommended
- No gas limit (full remaining gas)
- Returns
(bool success, bytes memory data) - Most flexible and future-proof
function sendByCall(address payable _to, uint amount) external payable {
(bool ok,) = _to.call{value: amount}("");
require(ok, "call failed");
}This method is recommended by the Solidity team because it supports complex logic in recipient contracts and avoids common pitfalls like gas exhaustion.
👉 Discover secure ways to interact with blockchain networks using trusted platforms
Best Practices for Handling ETH in Smart Contracts
- Always use
call()when sending ETH unless you have a specific reason not to. - Mark functions that accept ETH as
payable. - Prefer having both
receive()andfallback()for full control over incoming transactions. - Never assume transfers succeed—especially with
send()andcall(). - Use
require()statements to enforce expected behavior. - Limit exposure by implementing withdrawal patterns instead of direct transfers.
Frequently Asked Questions (FAQ)
Q: Can a contract receive ETH without any special functions?
A: Yes, but only if it has a payable function (including receive() or fallback()). Otherwise, sending ETH will cause a revert.
Q: Why was transfer() deprecated in newer Solidity versions?
A: Due to its fixed 2300 gas stipend, which can lead to unexpected failures when interacting with complex contracts. It's now discouraged in favor of call().
Q: What happens if I send ETH to a contract that doesn’t accept it?
A: The transaction will revert, and the ETH will be returned to the sender.
Q: Is it safe to use send() in production?
A: While functional, it's risky due to lack of automatic reversion. Always validate its return value with require().
Q: How do I check a contract’s ETH balance?
A: Use address(this).balance inside the contract or query the address externally via web3.js or ethers.js.
Q: Can I have both receive() and fallback() in the same contract?
A: Yes—and it’s often recommended. They serve different purposes and work together seamlessly.
👉 Explore powerful blockchain development tools that support Ethereum and Solidity integration
Conclusion
Mastering ETH handling in Solidity is foundational for robust dApp development. By understanding how payable, receive(), and fallback() functions manage incoming funds—and choosing the right method (call() preferred) for outgoing transfers—you ensure your contracts are secure, efficient, and compatible with modern Ethereum standards.
As blockchain applications grow more complex, leveraging best practices around Ether management becomes increasingly critical—not just for functionality, but for user trust and long-term maintainability.