Every smart contract is a collection of functions. Functions define what your contract can do: accept payments, transfer tokens, update balances, enforce rules. Choosing the wrong visibility on a single function can expose your contract to unauthorized withdrawals. Choosing the wrong mutability label can cost your users gas they never needed to spend.
This is one of those details that separates a contract that works in testing from one that holds up on mainnet.
What are functions, in plain English
Think of a smart contract as a vending machine bolted to the sidewalk. The buttons on the front are your public functions: anyone walking by can press them. The internal wiring that routes coins and dispenses items, that is your internal logic. And the maintenance panel on the back, locked with a key, those are your private functions that only the machine’s owner can access.
Each button does something specific. One accepts a coin. Another checks the inventory. A third dispenses a drink.
In Solidity, each of those actions is a function with a name, a set of inputs (parameters), and a set of rules about who can call it and what it is allowed to do.
The critical difference from a real vending machine: once you deploy a smart contract, you cannot swap out the buttons or rewire the internals. The functions you define at deployment are the functions your contract will have forever (unless you build in an upgrade pattern from the start). Getting them right the first time matters.
Why this matters for your project
Visibility is not just a technical detail. It is your contract’s access control policy. A function marked public is callable by every wallet and every contract on the network. If that function moves funds or changes ownership, you have just given the entire world a button that can drain your treasury.
The Parity wallet hack, which froze over $150 million in ETH, was rooted in a library contract whose initialization function was left publicly callable. Someone called it, took ownership, and then self-destructed the library.
State mutability affects your users’ wallets directly. A function that reads data but is not marked view will cost gas every time it is called externally. Mark it view, and that same call is free. Multiply that across thousands of users and you are either saving them real money or burning it for no reason.
Getting visibility and mutability right also makes your code easier to audit. When a security reviewer sees a function marked private, they know it cannot be called from outside. When they see pure, they know it touches no state. These labels are promises the compiler enforces, and they shrink the surface area an auditor has to worry about.
How it works (the 30-second version)
Every Solidity function has two key properties: visibility (who can call it) and state mutability (what it is allowed to do with blockchain state).
Visibility
| Visibility | Who can call it | When to use |
|---|---|---|
public | Anyone, including other contracts | Default for user-facing actions |
external | Only from outside the contract | Gas-optimized for external-only calls |
internal | This contract + inherited contracts | Helper functions shared across inheritance |
private | Only this exact contract | Internal helpers that should never be overridden |
One important note: private and internal only prevent other contracts from calling the function. The code and data are still visible to anyone reading the blockchain. Do not use visibility as a way to hide secrets.
State mutability
| Keyword | Can read state? | Can modify state? | Costs gas? |
|---|---|---|---|
| (default) | Yes | Yes | Yes |
view | Yes | No | Free when called externally |
pure | No | No | Free when called externally |
payable | Yes | Yes | Yes, and can receive ETH |
A view function can read storage, check balances, and return values, but it cannot write anything. A pure function is even more restricted: it can only operate on its input parameters and local variables, with no access to storage or blockchain data at all. Both are free to call from outside the contract because they require no transaction and no mining.
The payable keyword is unique. Without it, any function that receives ETH will automatically revert. If your function needs to accept payments, payable is not optional.
Here is what this looks like in practice:
contract Vault {
mapping(address => uint256) public balances;
// payable: accepts ETH, modifies state
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// view: reads state, free to call externally
function getBalance(address user) public view returns (uint256) {
return balances[user];
}
// pure: no state access at all, free to call externally
function calculateFee(uint256 amount) public pure returns (uint256) {
return amount * 3 / 100;
}
// internal: only this contract and children can call it
function _validateWithdrawal(uint256 amount) internal view returns (bool) {
return balances[msg.sender] >= amount;
}
} Four functions, four different combinations of visibility and mutability. deposit() is public and payable because anyone should be able to send ETH. getBalance() is public and view because it only reads data.
calculateFee() is pure because it just does math. _validateWithdrawal() is internal because it is a helper that external users should never call directly.
How Doodledapp makes this visual
In Doodledapp, you do not write these keywords. You configure them through nodes and toggles on a visual canvas.
The Function node is the starting point for every function flow. You give it a name, toggle its visibility, and define its parameters. When you connect other nodes to its execution output, you are building the function body visually, left to right.
The Constructor node works the same way but represents the special function that runs exactly once when the contract is deployed. It has a parameter list for any values you want to set at deployment time (like an initial supply or an owner address).
The Return node terminates a function flow and sends data back to the caller. You connect a value to its input handle, and that becomes the return value. A function with no Return node returns nothing.
The Msg.Value node outputs the amount of ETH sent with the current transaction. This is how you access the payment in a payable function, equivalent to reading msg.value in Solidity.
| Solidity concept | Doodledapp equivalent |
|---|---|
function transfer() public | Function node with name “transfer” and Public toggle on |
function _helper() internal | Function node with Public toggle off |
constructor(uint256 supply) | Constructor node with parameter list |
return amount; | Return node with value connected |
msg.value | Msg.Value node (outputs ETH sent) |
The visual layout gives you something Solidity code does not: a spatial sense of complexity. A function with a clean, linear flow from entry to return is easy to understand and audit. A function that branches into multiple paths with nested conditions is visually dense, and that density is a signal to simplify. You can see the shape of your logic before you compile it.
Common mistakes to avoid
Leaving functions public by default. In Solidity, if you forget to specify visibility, the compiler defaults to no visibility (which causes an error in modern versions). But many developers still default to public out of habit.
Every function should have the most restrictive visibility that still works. If nothing outside the contract calls it, make it internal or private.
Forgetting to mark read-only functions as view. If your function only reads from storage but you do not add the view keyword, external callers will pay gas for what should be a free operation. The Solidity compiler will warn you, but only if you have warnings enabled. In Doodledapp, the build output will surface these for you.
Making a function payable when it should not be. If a function does not need to receive ETH, do not mark it payable. The non-payable check (which reverts if ETH is sent) costs a trivial amount of gas but protects users from accidentally sending ETH to a function that has no logic to handle it. That ETH would be locked in the contract forever.
Assuming private means hidden. All blockchain data is public. Marking a function or variable private prevents other contracts from calling or reading it, but anyone can still inspect the contract’s bytecode and storage slots directly. Never store passwords, secret keys, or sensitive data in a smart contract, regardless of visibility.
The bottom line
Functions are where your contract meets the outside world. Visibility controls who gets access. Mutability controls what they can do.
Getting both right is not about following conventions: it is about preventing exploits and saving your users money. Whether you are writing Solidity by hand or building visually in Doodledapp, these decisions deserve deliberate attention on every function you create.
- Solidity Documentation: Contracts (Functions, Visibility, State Mutability)
- Base Documentation: Function Visibility and State Mutability
- Ethereum.org: Introduction to Smart Contracts
- ImmuneBytes: Solidity Security Vulnerability, Function Default Visibilities
- 0xMacro: Solidity Gas Optimizations Cheat Sheet