Functions, Modifiers & Visibility
Learn how to write functions, use modifiers for access control, and understand visibility specifiers.
Declaring and Calling Functions
Functions are the primary way users and other contracts interact with your smart contract. A Solidity function declaration includes the function keyword, a name, parameters, visibility specifier, optional state mutability modifier, and return types. For example: "function getBalance(address user) public view returns (uint256)".
Functions can return multiple values, a powerful feature not available in many traditional languages. You declare multiple return types with "returns (uint256, bool, address)" and return them as a tuple: "return (balance, isActive, owner)". Callers can destructure these returns: "(uint256 bal, bool active, ) = contract.getBalance(user)" — note the empty slot to skip values you do not need.
Solidity distinguishes between internal and external function calls. Internal calls use a simple jump instruction within the contract's bytecode and are cheaper. External calls create a new message call context, which involves encoding parameters, creating a new call frame, and decoding the return data. When a contract calls one of its own public functions using "this.myFunction()", it performs an external call, which costs more gas than calling the same function internally.
The "constructor" is a special function that executes exactly once during contract deployment. It is used to initialize state variables and set the contract owner. Constructors cannot be called after deployment and their code is not included in the deployed contract's bytecode — they exist only in the creation bytecode. The "receive" function handles plain ETH transfers with no calldata, while the "fallback" function is invoked when a call matches no function signature or when calldata is sent with an ETH transfer.
Visibility Specifiers
Every function and state variable in Solidity must have a visibility specifier that controls who can access it. There are four levels: public, external, internal, and private. Choosing the correct visibility is essential for both security and gas efficiency.
Public functions and variables can be called from anywhere — externally by users and other contracts, and internally within the contract and its derived contracts. When you declare a state variable as public, the compiler automatically generates a getter function. For example, "uint256 public totalSupply" creates a function "totalSupply()" that returns the value. Public functions have slightly higher gas costs than external functions when called externally because they must copy calldata arguments to memory.
External functions can only be called from outside the contract — by users or other contracts. They cannot be called internally unless you use "this.functionName()". The advantage of external functions is that they can read arguments directly from calldata without copying them to memory, which saves gas when dealing with large arrays or bytes parameters. Use external for functions that are only meant to be called from outside.
Internal functions can only be called within the contract itself or by contracts that inherit from it. They are similar to protected methods in object-oriented languages. Internal is the default visibility for state variables. Private functions are the most restrictive — they can only be called within the contract that defines them, not even by derived contracts. However, it is important to understand that "private" does not mean the data is hidden from the world. All data on the blockchain is publicly readable; private merely restricts access at the smart contract level. Anyone can read private variables by directly inspecting the contract's storage slots.
Function Modifiers and State Mutability
Modifiers are reusable pieces of code that wrap function logic, most commonly used for access control and input validation. You define a modifier with the "modifier" keyword and use the underscore placeholder "_" to indicate where the original function's code should execute. For example: "modifier onlyOwner() { require(msg.sender == owner, 'Not the owner'); _; }". You then apply it to functions: "function withdraw() public onlyOwner { ... }".
Modifiers execute their code before or after the function body depending on where the underscore is placed. Placing "_" at the end means the modifier's checks run first (preconditions). Placing it at the beginning means the function runs first and the modifier code runs after (postconditions). You can even place code both before and after the underscore for combined pre and post checks.
Multiple modifiers can be chained on a single function, and they execute in the order listed. For instance, "function sensitiveAction() public onlyOwner whenNotPaused" first checks ownership, then checks the paused state. This composability lets you build complex access control from simple, reusable building blocks.
State mutability keywords tell the compiler and users what a function does with contract state. "view" functions read state but do not modify it — they cost no gas when called externally (not within a transaction). "pure" functions neither read nor modify state and are used for utility calculations. Functions without either keyword can read and modify state. The "payable" modifier allows a function to receive ETH — without it, any transaction sending ETH to the function will revert. The compiler enforces these declarations, catching accidental state modifications in view functions at compile time.