Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to the next step in your journey to becoming a blockchain developer.
In our last lesson, we focused on the "data" part of a smart contract by learning about state variables and the primitive data types uint, bool, and address. We also saw how the public keyword automatically creates a "getter" function, giving us a first glimpse into how we can interact with contract data.
Today, we shift our focus to the "logic" or "behavior" of a contract. The goal of this lesson is to learn how to define and call functions with different visibility modifiers (public, private, external, internal). Mastering function visibility is non-negotiable for writing secure, efficient, and well-structured smart contracts.
Given your background in languages like Java and TypeScript, the concept of access control (like public or private keywords) will be familiar. Solidity has its own unique set of rules, which we'll explore in detail.
Why Visibility Matters in Smart Contracts
In any software system, controlling access to functions and data is crucial for security and design. In blockchain, where logic is immutable and transactions are final, it's even more critical. Visibility modifiers are your first line of defense. They allow you to enforce who can call your functions and change your contract's state.
A core principle in security, which you may have encountered in backend development, is the Principle of Least Privilege. This means you should only grant the minimum level of access necessary for a function to do its job. We'll keep this principle in mind as we explore the different modifiers.
Solidity Functions - Everything You Need To Know About Visibility
Let's start by framing the concept of visibility in Solidity. This article provides a great introduction, comparing it to access modifiers in other languages.
Read the introductory section under the heading 'The best place to start is with Visibility.' Notice the comparison to access modifiers, which should resonate with your programming background.
The Four Visibility Modifiers
Solidity provides four visibility modifiers. Let's get a quick overview of what they are before diving into the details.
This short video provides a concise, high-level overview of all four visibility modifiers. It will give you a good mental map before we examine each one.
Watch the introduction (00:00 - 00:39) and the summary at the end (05:35 - 06:00). Just focus on getting familiar with the names: public, private, internal, and external.
Now, let's break them down into two logical groups: those for external interaction and those for internal logic.
1. External-Facing Functions: public and external
These modifiers expose your functions to the outside world, allowing users and other contracts to interact with them.
public
- Accessibility: Can be called from anywhere: externally (by users or other contracts) and internally (by other functions within the same contract or derived contracts).
- Analogy: A company's main phone number. Anyone can call it.
external
- Accessibility: Can only be called from outside the contract. You cannot call an
externalfunction from another function within the same contract (without a special, gas-inefficient trick that should be avoided). - Analogy: A doorbell. It's designed to be used only by people outside the house.
The following video demonstrates this key difference in practice.
Solidity Tutorial | Visibility Modifiers - Public, External, Internal Private
Let's see a practical comparison between public and external. This video uses the Remix IDE to clearly show how they behave differently.
Watch from 03:28 to 06:22. Pay close attention when the compiler throws an error as the developer tries to call an external function from another function within the same contract. This highlights their core difference.
So, why use external if public is more flexible?
The answer is gas efficiency. External calls to public functions are more expensive than calls to external functions.
Solidity Functions - Everything You Need To Know About Visibility
This performance difference is a crucial optimization in Solidity. This article explains the technical reason why external is cheaper.
Read the section titled 'public Uses More Gas Than external'. The key takeaway is that external functions read arguments directly from calldata, which is cheaper than public functions copying them to memory.
Rule of thumb: If a function is only meant to be called by users or other contracts, always prefer external over public.
2. Internal-Facing Functions: private and internal
These modifiers restrict access, hiding functions from the outside world. They are used for helper functions and implementing internal logic.
private
- Accessibility: Can only be called from other functions within the same contract where it is defined. It is not accessible to derived (child) contracts.
- Analogy: This is identical to a
privatemethod in a Java or TypeScript class.
internal
- Accessibility: Can be called from other functions within the same contract and by functions in derived (child) contracts.
- Analogy: This is very similar to a
protectedmethod in Java. It allows for creating base contracts with functionality that can be extended by child contracts.
The difference between private and internal only becomes apparent when using contract inheritance.
Solidity Tutorial | Visibility Modifiers - Public, External, Internal Private
This part of the video demonstrates the concept of inheritance to show the difference between private and internal.
Watch from 06:22 to 10:24. Notice how the derived (child) contract can access the internal function from the parent, but receives an error when trying to access the private one.
Consolidating Your Knowledge
Let's bring all four modifiers together. This table provides a clear summary of the access rules.
Test your understanding!
You are designing a token contract and need to decide on the visibility for the following functions. Choose the most appropriate and gas-efficient modifier for each (public, private, external, internal).
- A
transfer(address to, uint amount)function that allows users to send their tokens to others. - A
_updateBalances(address from, address to, uint amount)helper function that is only ever called bytransfer()and other functions within your contract to adjust the token balances. It should not be accessible to any future child contracts. - A
_logTransfer(address from, address to, uint amount)function that emits an event. You want this function to be available to any contract that inherits from your token contract, but not to the outside world.
Show answer
external: Thetransferfunction is meant to be called by users (from outside the contract).externalis more gas-efficient thanpublicfor this use case.private: This is a classic helper function. It's only used by the contract itself and doesn't need to be exposed, soprivateis the most restrictive and secure choice.internal: The key is that it needs to be available to child contracts but not to the outside world. This is the exact use case forinternal.
Practical Application in Hardhat
Let's create a new contract in your project to see all these modifiers in action.
- In VS Code, create a new file in your
contractsfolder namedVisibility.sol. - Add the following code. Read the comments carefully to understand the purpose of each function and its visibility.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "hardhat/console.sol";
contract Visibility {
// A state variable to demonstrate state changes.
uint private counter = 0;
// PRIVATE function.
// Common convention is to prefix private/internal function names with an underscore.
// Can only be called by other functions inside THIS contract.
function _increment() private {
counter++;
console.log("Counter is now:", counter);
}
// INTERNAL function.
// Can be called by this contract and any child contracts that inherit from it.
function _resetCounter() internal {
counter = 0;
console.log("Counter has been reset.");
}
// EXTERNAL function.
// Can ONLY be called from outside the contract. Gas-efficient for user interactions.
function performExternalAction() external {
// This would cause a compile error: "Undeclared identifier. Did you mean \"this.performExternalAction\"?"
// performExternalAction();
// This is a valid call to a private function from an external one.
_increment();
}
// PUBLIC function.
// Can be called from anywhere: outside, inside, and by child contracts.
function performPublicAction() public {
// We can call our internal helper function here.
_resetCounter();
}
}
- Compile the contract by running
npx hardhat compilein your terminal to ensure there are no syntax errors.
Feel free to experiment! Try making _increment public and see what happens. Or try calling _resetCounter from performExternalAction. Breaking things is a great way to learn.
Conclusion
Excellent work! You've just mastered one of the most fundamental security features of Solidity. Properly scoping your functions is a hallmark of a professional smart contract developer.
Key Takeaways:
- Function visibility controls who can call a function.
- Always follow the Principle of Least Privilege: use the most restrictive modifier that works.
public: Callable from anywhere.external: Callable only from outside the contract. More gas-efficient thanpublicfor external calls.private: Callable only from within the defining contract.internal: Callable from within the defining contract and any child contracts (likeprotectedin Java).
Preview of the Next Lesson:
Now that we understand who can call a function, the next logical step is to define what a function is allowed to do. In the next lesson, we will learn how to control function behavior and gas costs with the state mutability keywords view and pure. These keywords tell the EVM whether a function will read from, write to, or completely ignore the contract's state, which has major implications for gas costs.