All articles
Web3 FoundationsSeptember 14, 20268 min read

payable, receive, and fallback: How a Contract Accepts ETH

One word decides whether a contract can hold money. What payable does, the difference between receive and fallback, and what happens when ETH arrives with nowhere to land.

By Carlos (Bloqarl)

TL;DR

  • payable is the word that permits ETH to arrive with a call. Without it, a function refuses the transfer and the whole call fails.
  • receive() runs when plain ETH arrives with no instructions attached. It is the coin slot.
  • fallback() runs when a call arrives that matches no function at all. It is the catch-all.
  • A contract with neither, and no payable function, cannot be sent ETH by an ordinary transfer. The attempt reverts.
  • msg.value is how much ETH came with the call, in wei, and it is already credited to the contract by the time your code runs.

What does payable mean in Solidity?

payable is a marker you put on a function or an address that permits ETH to move with it. A function without payable rejects any call that carries value, and an address without it cannot be sent ETH.

Here is the whole of WETH9's money path, in and out:

    function() external payable {
        deposit();
    }
    function deposit() public payable {
        balanceOf[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value);
    }
    function withdraw(uint wad) public {
        require(balanceOf[msg.sender] >= wad);
        balanceOf[msg.sender] -= wad;
        msg.sender.transfer(wad);
        emit Withdrawal(msg.sender, wad);
    }

Reading those functions in file order is the third pass of how to read a smart contract: for each one, who can call it, does it change anything, and can it receive ETH. Two of the three are marked payable, so ETH may arrive by either door. The third sends it back out. One word gates the way in.

Which word actually lets the ETH in?

Look at that first function again, because it carries three signals and only one of them does the permitting:

    function() external payable {
  • The function has no name at all, and in this older dialect that is what decides when it runs: whenever plain ETH arrives with no data attached. Modern Solidity gives that job a name, receive(), and you will meet both.
  • external answers who may call it: anyone from outside the contract. It says nothing about money.
  • payable is the permission. It is the only word on this line that allows ETH to accompany the call.

Delete payable and the contract refuses the coin at the door. Nobody could ever wrap ether again. That is the cleanest example of the "delete the word and ask what breaks" test there is: one word, one consequence, no ambiguity.

Its body simply calls deposit, so sending ETH straight to the address does the same thing as calling the function by name. Here is the same idea in current syntax, from Solmate's modern implementation of the same contract:

    receive() external payable virtual {
        deposit();
    }

Same job, same permission word, a name instead of a blank. See state variables in Solidity for where the deposited amount is recorded.

What is the difference between receive and fallback?

Both are special functions that handle calls which do not name a specific function. The difference is whether data came along.

FunctionRuns whenMust be payable?
receive()Plain ETH arrives, no calldataYes
fallback()A call arrives whose data matches no functionOnly if it should accept ETH

Put concretely: if someone sends ETH from a wallet with no extra data, receive handles it. If someone calls a function name this contract does not have, fallback handles it. If a contract has only fallback and no receive, then fallback handles plain ETH too.

This is why a contract can appear to have a function it does not have. Proxy contracts are built on exactly this: their fallback catches every unrecognised call and forwards it to an implementation contract elsewhere. When you open a contract and find a fallback full of assembly, you are almost certainly looking at a proxy, and the logic you actually want to read lives at another address.

What happens if a contract cannot accept ETH?

The transfer fails, and so does everything else in that transaction.

This is the consequence that surprises people most. Sending ETH to a contract is not like sending it to a wallet. A wallet always accepts. A contract accepts only if it has a payable way to do so: a receive, a payable fallback, or a payable function the sender explicitly calls.

If none of those exist, the ETH does not sit in limbo and does not get quietly kept. The call reverts, which means the entire transaction is undone as if it never happened. The sender keeps their ETH and pays the gas for the failed attempt. See require and revert for what reverting actually does.

There is a matching subtlety on the way out. transfer, used on the payout line of withdraw, forwards only a small, fixed amount of gas to the recipient. If the recipient is a contract whose receive function needs more gas than that to run, the transfer fails. This is why a great deal of modern Solidity uses a lower-level call to send ETH instead of transfer, and it is one of the clearest cases where reading old and new code teaches you the same job done two different ways.

What is msg.value?

msg.value is how much ETH arrived with the current call, denominated in wei.

Two things about it are worth knowing before you read any contract that handles money.

It is already credited. By the time your Solidity runs, the contract's balance already includes msg.value. So inside a payable function, address(this).balance counts the ETH that just arrived. Code that forgets this double-counts.

It is zero unless someone sent something. Every non-payable function has msg.value of zero by definition, because a non-zero value would have been rejected before the function ran.

You will see it used to price things:

    function deposit() public payable {
        balanceOf[msg.sender] += msg.value;

That is the shape of nearly every mint, deposit, and purchase function in existence: read what arrived, credit it to whoever sent it. Notice the contract does not ask the caller how much they sent. It reads it, the same way it reads msg.sender rather than being told who is calling.

How does a contract check its own balance?

    function totalSupply() public view returns (uint) {
        return address(this).balance;
    }

this refers to the contract itself, address(this) is its own address, and .balance is how much ETH sits at that address. WETH9 uses it for something neat: the amount of WETH in existence is simply the ETH the contract is holding, so it never has to count anything.

Every address has a .balance, not just the contract's own, so a contract can read anyone's ETH balance this way. And because a balance can be changed by ETH arriving from outside any of the contract's functions, a contract's balance is not always a number the contract itself chose. That is a real property of the type, and it is the sort of thing worth noticing when you read code that treats its own balance as authoritative.

Delete the word and ask what breaks

  • Delete payable from the nameless function and plain ETH transfers to the contract fail.
  • Delete the whole nameless function and the same thing happens, unless some other payable path exists to catch it.
  • Move the payout line into modern Solidity without wrapping the recipient in payable(...) and it will not compile, because current versions refuse to send ETH to a plain address.
  • Add payable to withdraw and a caller could send ETH along while taking ETH out, which is nonsensical but compiles. Not every legal combination is a meaningful one.

Related questions

What does payable mean in Solidity? It marks a function or address as permitted to handle ETH. A function without it rejects any call carrying value, and an address must be cast to payable before ETH can be sent to it.

What is the difference between receive and fallback? receive runs when plain ETH arrives with no calldata and must be payable. fallback runs when a call's data matches no function, and only needs to be payable if it should also accept ETH. With no receive present, a payable fallback handles plain transfers too.

Can you send ETH to a contract without a payable function? Not with an ordinary transfer, which will revert. There are edge cases in the protocol that can increase a contract's balance without executing its code, which is why well-written contracts avoid assuming their balance only changes through their own functions.

What is msg.value? The amount of ETH sent with the current call, in wei. It is already added to the contract's balance by the time the function body runs, and it is zero for any function not marked payable.

Why is transfer discouraged in modern Solidity? It forwards a fixed, small gas allowance to the recipient. Contracts whose receiving logic needs more than that will fail to be paid, so most current code uses a low-level call instead and checks the result.

What is wei? The smallest unit of ETH. One ETH is 10 to the power of 18 wei. All ETH amounts on chain, including msg.value and .balance, are counted in wei, and the decimal point you see in a wallet is applied afterwards for display.

Where to go next

One word decides whether a contract can hold money, and its absence is not a silent no but a loud one: the transfer reverts and the transaction dies. That asymmetry, where refusing costs the sender gas and changes nothing, is a recurring shape in Solidity and worth carrying forward.

The next two words in our contract are promises rather than permissions: view vs pure in Solidity, the two things a function can swear it will not do.

Tagged

SoliditySmart ContractsLearn to Code