Solidity Basics: Paying and Sending ETH

·

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:

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:

  1. receive() – Triggered when Ether is sent without calldata (i.e., simple transfers).
  2. fallback() – Called when either no data matches a function or no receive() 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:

MethodGas ForwardedReturn ValueError Handling
transfer2300 gasReverts on failureThrows automatically
send2300 gasbool success/failManual check required
callAll 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:

❌ Cons:

⚠️ Note: As of recent best practices, transfer() is discouraged in favor of call() 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:

❌ Cons:


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:

❌ Cons:

🔐 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:

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:

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.