Lesson 13 / 25
Sending and Receiving Ether
Receive ether with receive and fallback, send it safely and use pull payments.
Moving ether safely
A contract receives plain ether through a receive() external payable function, called when calldata is empty, and a fallback() function, called when no other function matches (it must be payable to accept ether). Without either, plain transfers to the contract revert. To send ether, use the low-level call: (bool ok, ) = recipient.call{value: amount}(""); and check ok. The older transfer and send forward only 2,300 gas, which breaks recipients that are smart contract wallets or need more gas after gas cost changes, so they are no longer recommended. Because call forwards gas and hands control to the recipient, it creates reentrancy risk (section 6), so update state before sending. Prefer the pull payment pattern: instead of pushing ether to many recipients in a loop (where one failing recipient can block everyone), record what each is owed and let them withdraw. Note that a contract can receive ether without any function running (from validator rewards or a forced send), so never rely on address(this).balance equalling the sum of your internal accounting.
Push versus pull payments
Pushing to many recipients can fail as a whole; pulling lets each withdraw independently.
A split-payment contract using pull payments
Record balances, let recipients withdraw, check the call result.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract RevenueSplitter {
address[] public payees;
mapping(address => uint256) public sharesBps; // basis points, total 10,000
mapping(address => uint256) public owed;
event Received(address indexed from, uint256 amount);
event Withdrawn(address indexed payee, uint256 amount);
error NothingOwed();
error TransferFailed();
constructor(address[] memory payees_, uint256[] memory shares_) {
require(payees_.length == shares_.length && payees_.length > 0, "bad config");
uint256 total;
for (uint256 i = 0; i < payees_.length; i++) {
payees.push(payees_[i]);
sharesBps[payees_[i]] = shares_[i];
total += shares_[i];
}
require(total == 10_000, "shares must total 100%");
}
// senders must forward enough gas (use call, not transfer): this loop needs more than 2,300 gas
receive() external payable {
uint256 distributed;
for (uint256 i = 0; i < payees.length; i++) {
uint256 part = (msg.value * sharesBps[payees[i]]) / 10_000;
owed[payees[i]] += part;
distributed += part;
}
owed[payees[0]] += msg.value - distributed; // rounding dust to the first payee
emit Received(msg.sender, msg.value);
}
function withdraw() external {
uint256 amount = owed[msg.sender];
if (amount == 0) revert NothingOwed();
owed[msg.sender] = 0; // effect before interaction
(bool ok, ) = msg.sender.call{value: amount}("");
if (!ok) revert TransferFailed();
emit Withdrawn(msg.sender, amount);
}
}Bound your loops
A loop over an array that users can grow without limit may eventually exceed the block gas limit, making the function permanently unusable. Keep loops bounded (like a fixed payee list) or process work in batches.
Quick check: Why are transfer and send no longer recommended for sending ether?
- They forward only 2,300 gas, which can make transfers to contract wallets fail
- They are too expensive
- They are not available in Solidity 0.8
- They do not move ether
Answer
They forward only 2,300 gas, which can make transfers to contract wallets fail — The fixed gas stipend breaks recipients that need more gas.