Sending Ether in Solidity: transfer, send, and call
A practical guide to the three methods of sending Ether in Solidity contracts, explaining their differences, gas limitations, and when to use each approach.
Understanding Ether Transfers in Solidity
When you write a smart contract that needs to send Ether to another address, Solidity gives you three ways to do it:
transfer, send, and call. Each method has different characteristics, and picking the wrong one can lead to failed transactions or security vulnerabilities.The Three Methods Explained
transfer()
The
transfer method is the simplest way to send Ether. It forwards exactly 2300 gas to the recipient and automatically reverts if the transfer fails.function sendViaTransfer(address payable _to) public payable { _to.transfer(msg.value); }
When you use
transfer, the recipient address gets a fixed 2300 gas stipend. This is enough to emit an event or perform simple operations, but not enough for complex logic. If something goes wrong, the entire transaction reverts automatically, and you don't need to write error handling code.The 2300 gas limit exists as a security feature. It prevents reentrancy attacks because the recipient can't make external calls with such limited gas.
send()
The
send method works similarly to transfer but handles failures differently. It also forwards 2300 gas but returns a boolean instead of reverting.function sendViaSend(address payable _to) public payable { bool sent = _to.send(msg.value); require(sent, "Failed to send Ether"); }
Notice how
send returns true or false. This means you must manually check if the transfer succeeded and handle failures yourself. If you forget to check the return value, your contract might think it sent Ether when it actually didn't.This makes
send more error-prone than transfer. The only reason to use it is if you want custom error handling logic instead of an automatic revert.call()
The
call method is the most flexible and currently recommended approach. It forwards all available gas by default and returns both a success boolean and any returned data.function sendViaCall(address payable _to) public payable { (bool sent, bytes memory data) = _to.call{value: msg.value}(""); require(sent, "Failed to send Ether"); }
The syntax looks different because
call is a low-level function. The curly braces {value: msg.value} specify how much Ether to send, and the empty string ("") means we're not calling any specific function, just sending Ether.With
call, the recipient gets all remaining gas, not just 2300. This allows them to execute complex logic if needed. You can also specify a custom gas amount:(bool sent, ) = _to.call{value: msg.value, gas: 10000}("");
Which Method Should You Use?
After the Istanbul hard fork in 2019, gas costs for certain operations changed, making the 2300 gas limit in
transfer and send problematic. A recipient contract that was safe before might now run out of gas with these methods.Current best practice is to use
call for sending Ether:contract SendEther { function sendEther(address payable _to) public payable { (bool success, ) = _to.call{value: msg.value}(""); require(success, "Transfer failed"); } }
To protect against reentrancy attacks (since
call forwards all gas), use the checks-effects-interactions pattern or a reentrancy guard:contract SafeSender { mapping(address => uint) public balances; function withdraw() public { uint amount = balances[msg.sender]; // Update state before external call balances[msg.sender] = 0; // Then send Ether (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); } }
Notice how we set the balance to zero before calling
call. This prevents reentrancy because even if the recipient calls back into our contract, their balance is already zero.Receiving Ether in Contracts
For a contract to receive Ether, it needs special functions. There are two options:
contract ReceiveEther { // Called when msg.data is empty receive() external payable {} // Called when no other function matches fallback() external payable {} }
The
receive function handles plain Ether transfers with no data. The fallback function is a catch-all for everything else. At minimum, you need one of these to accept Ether sent via call.Complete Working Example
Here's a full contract demonstrating all three methods:
contract EtherSender { function sendViaTransfer(address payable _to) public payable { _to.transfer(msg.value); } function sendViaSend(address payable _to) public payable { bool sent = _to.send(msg.value); require(sent, "Send failed"); } function sendViaCall(address payable _to) public payable { (bool sent, ) = _to.call{value: msg.value}(""); require(sent, "Call failed"); } } contract EtherReceiver { event Received(address sender, uint amount); receive() external payable { emit Received(msg.sender, msg.value); } }
When someone calls
sendViaCall on the EtherSender contract with 1 Ether, that Ether gets forwarded to the recipient. If the recipient is an EtherReceiver contract, its receive function executes and emits an event.Conclusion
Use
call for sending Ether in modern Solidity contracts as it provides the most flexibility and avoids gas limit issues. Always check the boolean return value and protect against reentrancy by updating contract state before making external calls.