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.
TL;DR
payableis 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
payablefunction, cannot be sent ETH by an ordinary transfer. The attempt reverts. msg.valueis 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.
Our worked contract uses it twice, in both of its forms:
receive() external payable {}
function smash() external {
require(msg.sender == owner, "only the owner");
require(address(this).balance >= goal, "goal not reached");
payable(owner).transfer(address(this).balance);
}
Reading those two lines in file order is the third pass of how to read a smart contract: for each function, who can call it, does it change anything, and can it receive ETH. Line 13 marks a function payable, so ETH may arrive. The last line of smash converts a plain address into a payable one, so ETH may leave. Money in, money out, and the same word gates both directions.
Which word actually lets the ETH in?
Look at line 13 again, because it has three words on it and only one of them does the permitting:
receive() external payable {}
receiveis the name, and in Solidity that particular name decides when this function runs: whenever plain ETH arrives with no data attached.externalanswers who may call it: anyone from outside the contract. It says nothing about money.payableis 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. Ana can still call receive with zero value, pointlessly, but any actual ETH transfer to this contract fails. That is the cleanest example of the "delete the word and ask what breaks" test in the whole contract: one word, one consequence, no ambiguity.
The body is empty, {}, and that is deliberate. Accepting silently is all this function does. It does not record who sent the ETH, because the contract has no state to record it in, as covered in state variables in Solidity.
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.
| Function | Runs when | Must be payable? |
|---|---|---|
receive() | Plain ETH arrives, no calldata | Yes |
fallback() | A call arrives whose data matches no function | Only 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 at the end of smash, 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 buy() external payable {
require(msg.value >= price, "not enough ETH");
}
That is the shape of nearly every mint, deposit, and purchase function in existence. 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 progress() external view returns (uint256) {
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. The pig weighing itself.
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
payablefromreceive()and the contract can no longer be sent ETH at all. The pig has no coin slot. - Delete the whole
receiveline and the same thing happens, unless a payablefallbackexists to catch it. - Delete
payable(...)aroundowneron the transfer line and the code does not compile, because Solidity will not send ETH to a plain address. - Add
payabletosmashand the owner could send ETH along while emptying the pig, which is harmless here and nonsensical, but it 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