Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back.
In our last lesson, we focused on getting Ether into your smart contracts by making functions payable. Now, we'll tackle the other side of the equation: getting Ether back out. This is a critical function for almost any financial application on the blockchain, from processing withdrawals and refunds to distributing funds from a token sale.
Your learning goal for today is to send Ether from a contract securely using the call method. The word "securely" is key here, as sending value is one of the most high-stakes operations in smart contract development.
1. The Three Ways to Send Ether
In modern Solidity, there are three main low-level functions for sending Ether from a contract: transfer, send, and call.
Let's start with a short video that introduces and compares these three methods.
This video from Smart Contract Programmer gives a quick overview of transfer, send, and call, highlighting their main differences.
Pay close attention to these parts: Introduction (00:00 - 00:36): Get a quick definition of each method. The call method (03:01 - 04:02): Focus on the syntax for call and the fact that it returns a success boolean. Recap and Recommendation (05:44 - 06:10): Listen for the final recommendation on which method to use.
As the video explained:
transfer()andsend(): These methods are simple to use, but they forward a fixed, small amount of gas (2300 gas) to the recipient. If the recipient is a contract, this is only enough for very basic operations, like emitting an event.call(): This method forwards all available gas by default. It's more flexible but requires more careful handling.
Why is call() the recommended method?
The fixed 2300 gas limit of transfer and send was initially seen as a security feature to prevent a class of attacks called reentrancy. However, it makes contracts brittle. Gas costs in Ethereum can change with network upgrades. A recipient contract's logic might become more complex. If the recipient's receive function ever requires more than 2300 gas, any contract trying to pay it with transfer or send will fail.
Because of this, the current best practice is to always use call for sending Ether, as it is more robust and future-proof.
2. The Syntax and Mechanics of call
Using call to send Ether has a unique syntax. It's a low-level function, which gives you more control.
(bool success, bytes memory data) = payable(recipient_address).call{value: amount}("");
Let's break this down:
payable(recipient_address): The address you are sending Ether to. It must be explicitly cast to apayableaddress type..call{...}: Invokes thecallfunction.value: amount: This is how you specify the amount of Ether to send with the call.amountshould be auint256value in wei.(""): This is the "calldata." An empty string means you aren't trying to execute any specific function on the recipient contract; you are just sending Ether. This will trigger the recipient'sreceive()function (orfallback()ifreceive()doesn't exist).(bool success, bytes memory data):callreturns two values.success: A boolean that istrueif the call succeeded andfalseotherwise. Crucially,calldoes not automatically revert the transaction on failure.data: Any data returned by the recipient function. For simple Ether transfers, this is usually empty.
Since call does not revert on failure, you must handle the outcome yourself.
Steps to Send Value from Within a Smart Contract
This article from Alchemy provides a clear, step-by-step guide to using call. We will focus on the final implementation step.
Read 'Step 4: Use call Function to Send Value'. Pay close attention to the code example. Notice how the boolean success is captured and then checked using a require statement. This is a non-negotiable part of using call securely.
As the article shows, the correct pattern is:
(bool success, ) = recipient.call{value: amount}("");
require(success, "Payment failed."); // ALWAYS do this!
3. The Reentrancy Attack: The Greatest Danger
The flexibility of call (forwarding all gas) opens the door to a major security vulnerability if not handled correctly: the reentrancy attack.
Here’s how it works:
- A victim contract has a
withdrawfunction that sends Ether to the caller. - An attacker creates a malicious contract.
- The attacker's contract calls the victim's
withdrawfunction. - The victim contract uses
callto send Ether back to the attacker's contract. - When the attacker's contract receives the Ether, its
receive()orfallback()function is triggered. This function immediately calls the victim'swithdrawfunction again, before the first withdrawal has finished executing. - If the victim contract hasn't updated the attacker's balance before sending the Ether, it will think the attacker still has funds and will send them again.
- This loop continues until the victim contract is drained of its funds.

4. The Defense: The Checks-Effects-Interactions Pattern
To prevent reentrancy attacks, the Solidity community has established a critical design pattern: Checks-Effects-Interactions (CEI).
This is arguably the most important security pattern to learn.
The official Solidity documentation provides the definitive explanation of reentrancy and the CEI pattern. This is essential reading.
Please read two sections from this document: Start at the heading 'Reentrancy' and read until the heading 'Gas Limit and Loops'. This section explains the attack with code examples showing vulnerable and corrected contracts. Read the section titled 'Use the Checks-Effects-Interactions Pattern'. This section formally defines the pattern. Focus on understanding the order of operations in the secure example.
The CEI pattern dictates a strict order of operations within your function:
-
Checks: First, perform all validation checks.
- Is
msg.senderauthorized? (require(msg.sender == owner)) - Does the user have a sufficient balance? (
require(balances[msg.sender] > 0))
- Is
-
Effects: Second, update the contract's state variables. This is the most important step for preventing reentrancy.
- Set the user's balance to zero (
balances[msg.sender] = 0;) or decrease it before you send any Ether. If the attacker calls back, their balance will already be zero, and the "Check" will fail.
- Set the user's balance to zero (
-
Interactions: Last, interact with any external contracts.
- This is where you make the external
callto send the Ether.
- This is where you make the external
Incorrect (Vulnerable) Order:
// DO NOT DO THIS
function withdraw(uint amount) public {
// Check
require(balances[msg.sender] >= amount, "Insufficient balance.");
// Interaction (!!! DANGER !!!)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed.");
// Effect (Too late!)
balances[msg.sender] -= amount;
}
Correct (Secure) Order - Applying CEI:
// THIS IS THE SECURE PATTERN
function withdraw(uint amount) public {
// 1. Checks
require(balances[msg.sender] >= amount, "Insufficient balance.");
// 2. Effects (Update state BEFORE the external call)
balances[msg.sender] -= amount;
// 3. Interactions
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed."); // Still need to check for failure
}
Test your understanding!
You have a simple "PiggyBank" contract where users can deposit Ether. Your task is to implement the smashPiggyBank() function to allow a user to withdraw their entire deposited amount securely.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract PiggyBank {
mapping(address => uint) public balances;
// Users can deposit Ether
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// Users can withdraw their entire balance
function smashPiggyBank() public {
// Your code goes here.
// Implement the Checks-Effects-Interactions pattern.
}
}
Your Task:
Write the code for smashPiggyBank() following all the security best practices we've discussed.
Show answer
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract PiggyBank {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function smashPiggyBank() public {
// 1. Checks: Get the user's balance and ensure it's greater than 0.
uint userBalance = balances[msg.sender];
require(userBalance > 0, "You have no Ether in the piggy bank.");
// 2. Effects: Set the user's balance to 0 BEFORE sending the Ether.
// This is the critical step to prevent reentrancy.
balances[msg.sender] = 0;
// 3. Interactions: Send the Ether and check for success.
(bool success, ) = msg.sender.call{value: userBalance}("");
require(success, "Withdrawal failed.");
}
}
This solution correctly applies the CEI pattern. It first reads the balance, then updates the state (sets balance to 0), and only then performs the external call to send the Ether.
Conclusion
You've now learned the modern, secure way to send Ether from a smart contract. This skill is fundamental to building robust and trustworthy decentralized applications.
Key Takeaways:
- Use
call: It is the industry-standard method for sending Ether due to its flexibility with gas. - Check the Return Value:
callreturns aboolindicating success. You must always userequire()to check this value and revert the transaction on failure. - Beware of Reentrancy: The flexibility of
callcreates a risk of reentrancy attacks if used carelessly. - Master Checks-Effects-Interactions: This pattern is your primary defense against reentrancy. Always update your contract's state before you make an external call (like sending Ether).
Preview of the Next Lesson:
We've been using require() extensively to guard our functions. In the next lesson, we will dive deeper into error handling. You will learn to handle contract errors and revert transactions using require, assert, and revert, giving you a complete toolkit for managing state and control flow in your smart contracts.