Smart contract dev Solidity & Vyper Security researcher
Smart contract dev Solidity & Vyper Security researcher

Subscribe to ArefXV

Subscribe to ArefXV
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
Smart contracts on Ethereum can receive ETH directly. But what actually happens when someone sends ETH to a contract? And why do some transactions fail silently while others go through?
Let’s dive deep into how Ether is handled in Solidity, the differences between receive() and fallback(), and some key developer mistakes you should avoid.
When you send ETH to a contract, one of two functions will be triggered based on the context:
receive() external payable
fallback() external payable
These are special functions in Solidity that aren’t called directly in code. They get triggered under very specific circumstances.
The receive() function runs when:
A contract receives ETH with empty calldata
The receive() function is marked payable
Example:
receive() external payable {
emit Received(msg.sender, msg.value);
}
If someone calls the contract with just ETH (e.g., using .transfer() or sending ETH directly via MetaMask), this function will run.
Use case: Simple ETH deposits with no logic or function calls.
The fallback() function is more general. It runs when:
A function is called that doesn’t exist
OR when calldata is not empty
AND receive() is either not present or doesn’t match
fallback() external payable { emit FallbackCalled(msg.sender, msg.value, msg.data); }
You can also define a non-payable fallback if you want to reject ETH:
fallback() external { revert("ETH not accepted"); }
Not defining receive() or fallback() If your contract is meant to accept ETH but has neither, any direct ETH transfer will revert.
✅ Fix: Add at least one payable function (receive() or fallback()).
Heavy Logic in Fallbacks Since fallback functions can get triggered unintentionally, avoid writing logic-heavy operations that might increase gas use or create vulnerabilities.
Not Handling msg.data Sometimes, users (or attackers) send ETH with extra calldata. If you're only using receive(), those transactions will revert. Always consider a fallback for maximum safety.
Real-World Example: Accepting ETH in a Treasury Contract solidity Copy Edit
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract Treasury {
address public owner;
event Received(address sender, uint256 amount);
event FallbackCalled(address sender, uint256 amount, bytes data);
constructor() {
owner = msg.sender;
}
receive() external payable {
emit Received(msg.sender, msg.value);
}
fallback() external payable {
emit FallbackCalled(msg.sender, msg.value, msg.data);
}
function withdraw() external {
require(msg.sender == owner, "Only owner");
payable(owner).transfer(address(this).balance);
}
}
Try sending ETH from your wallet or via Remix — both receive and fallback will log different events based on how the ETH was sent.
Vyper handles this similarly but doesn’t use receive() or fallback() keywords directly. It uses:
@external @payable def __default__(): # Code here runs on unknown function call or direct ETH
Everything is handled in __default__, so you must manually manage what to accept and reject.
Always define receive() if your contract is meant to receive ETH directly.
Use a fallback() to catch unexpected calls or ETH sent with data.
Don’t rely on these functions for business logic.
Always emit logs for transparency (emit Received(...)) for off-chain monitoring.
Many developers forget to test how their contracts behave with unexpected inputs or ETH sent manually. A missing receive() function can easily break dApps like fundraising contracts, treasuries, or NFT mints.
When building production-grade contracts, always include proper ETH handling, and test them via .call, .transfer, or even raw transactions.
Want me to cover gas optimizations, reentrancy, or cross-chain bridging next? Stay tuned.
Smart contracts on Ethereum can receive ETH directly. But what actually happens when someone sends ETH to a contract? And why do some transactions fail silently while others go through?
Let’s dive deep into how Ether is handled in Solidity, the differences between receive() and fallback(), and some key developer mistakes you should avoid.
When you send ETH to a contract, one of two functions will be triggered based on the context:
receive() external payable
fallback() external payable
These are special functions in Solidity that aren’t called directly in code. They get triggered under very specific circumstances.
The receive() function runs when:
A contract receives ETH with empty calldata
The receive() function is marked payable
Example:
receive() external payable {
emit Received(msg.sender, msg.value);
}
If someone calls the contract with just ETH (e.g., using .transfer() or sending ETH directly via MetaMask), this function will run.
Use case: Simple ETH deposits with no logic or function calls.
The fallback() function is more general. It runs when:
A function is called that doesn’t exist
OR when calldata is not empty
AND receive() is either not present or doesn’t match
fallback() external payable { emit FallbackCalled(msg.sender, msg.value, msg.data); }
You can also define a non-payable fallback if you want to reject ETH:
fallback() external { revert("ETH not accepted"); }
Not defining receive() or fallback() If your contract is meant to accept ETH but has neither, any direct ETH transfer will revert.
✅ Fix: Add at least one payable function (receive() or fallback()).
Heavy Logic in Fallbacks Since fallback functions can get triggered unintentionally, avoid writing logic-heavy operations that might increase gas use or create vulnerabilities.
Not Handling msg.data Sometimes, users (or attackers) send ETH with extra calldata. If you're only using receive(), those transactions will revert. Always consider a fallback for maximum safety.
Real-World Example: Accepting ETH in a Treasury Contract solidity Copy Edit
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract Treasury {
address public owner;
event Received(address sender, uint256 amount);
event FallbackCalled(address sender, uint256 amount, bytes data);
constructor() {
owner = msg.sender;
}
receive() external payable {
emit Received(msg.sender, msg.value);
}
fallback() external payable {
emit FallbackCalled(msg.sender, msg.value, msg.data);
}
function withdraw() external {
require(msg.sender == owner, "Only owner");
payable(owner).transfer(address(this).balance);
}
}
Try sending ETH from your wallet or via Remix — both receive and fallback will log different events based on how the ETH was sent.
Vyper handles this similarly but doesn’t use receive() or fallback() keywords directly. It uses:
@external @payable def __default__(): # Code here runs on unknown function call or direct ETH
Everything is handled in __default__, so you must manually manage what to accept and reject.
Always define receive() if your contract is meant to receive ETH directly.
Use a fallback() to catch unexpected calls or ETH sent with data.
Don’t rely on these functions for business logic.
Always emit logs for transparency (emit Received(...)) for off-chain monitoring.
Many developers forget to test how their contracts behave with unexpected inputs or ETH sent manually. A missing receive() function can easily break dApps like fundraising contracts, treasuries, or NFT mints.
When building production-grade contracts, always include proper ETH handling, and test them via .call, .transfer, or even raw transactions.
Want me to cover gas optimizations, reentrancy, or cross-chain bridging next? Stay tuned.
No activity yet