Storage is a permanent liability. Every public variable on-chain creates a perpetual gas cost for all future reads, a tax paid by every user and indexer like The Graph. This cost compounds with contract complexity.
The True Cost of a Public State Variable
An analysis of how the `public` visibility modifier in Solidity creates a hidden tax on deployment and execution by generating automatic getter functions and sabotaging crucial storage packing optimizations, with concrete data and mitigation strategies.
Introduction
Public state variables impose a permanent, systemic cost that most smart contract architectures ignore.
Public data is a public API. Exposing state via public getters forces a rigid, unoptimized data model. Protocols like Uniswap V3 use internal storage packing and private functions to decouple logic from interface.
The default is inefficient. The Solidity compiler's automatic getter for a public uint256 costs ~2,100 gas. A custom, optimized view function using assembly or returning a packed slot often costs under 700 gas.
Executive Summary
Public state variables are a foundational but expensive abstraction, creating systemic costs for users and protocols.
The Gas Tax on Every User
Reading a public variable from a contract is not free. Every view call from an off-chain client or another contract incurs a gas cost for the caller. For a high-frequency DApp, this creates a massive, opaque tax on user interactions.\n- Cost Multiplier: Simple queries can cost 10,000+ gas vs. a few hundred for a private variable.\n- Network Effect: This scales with user count, creating a linear cost burden on protocol adoption.
The Storage Bloat Problem
Public variables, by Solidity's design, automatically generate a getter function. This bloats the contract's bytecode and increases deployment costs and eternal storage rent. It's a permanent, upfront cost for a feature that may be rarely used.\n- Deployment Tax: Extra bytecode can increase one-time deploy costs by 5-15%.\n- State Bloat: Encourages poor data structure choices, making future upgrades more expensive and complex.
The Architectural Lock-In
Exposing state publicly creates rigid API dependencies. Changing a public variable's type or structure is a breaking change for all integrated frontends and contracts, forcing costly migrations or limiting innovation. This is the opposite of the EIP-2535 Diamonds modular approach.\n- Upgrade Fragility: A simple refactor can break countless off-chain indexers and subgraphs.\n- Innovation Tax: Teams avoid optimizing core state due to fear of breaking downstream dependencies.
The Verifier's Burden
Public state turns every node into an unpaid data server. Light clients and zk-provers must process and verify unsolicited state for any public query, increasing the verification overhead for the entire network. This is antithetical to succinct verification principles.\n- Node Load: Increases the baseline computational load for network participants.\n- ZK-Unfriendly: Public state getters are not easily batchable or SNARK-friendly, hindering zk-rollup and zk-co-processor efficiency.
Solution: Intent-Based Access
Move from public pull to private push. Use event emission and proof-carrying data (like zk proofs or signed attestations) to let users request specific state proofs on-demand. This aligns with the UniswapX and CowSwap intent-based architecture.\n- Cost Shift: Moves the gas burden from the general network to the specific requester.\n- Privacy Bonus: Reveals only the necessary data for a specific transaction, not the entire state.
Solution: Storage Proofs & ZK
Replace direct state reads with verifiable proofs. Protocols like Axiom and Herodotus enable trustless off-chain computation over historical state. This turns a costly on-chain read into a cheap, verified attestation.\n- Gas Savings: >90% reduction for complex state queries.\n- Future-Proof: Enables stateless clients and witness-based execution, the endgame for scalability.
The Core Argument: Convenience at the Expense of Permanence
Public state variables trade long-term data integrity for short-term developer convenience, creating systemic fragility.
Public state variables are a trap. They expose a contract's internal logic as a permanent API, creating an unbreakable dependency for all downstream applications. This is the technical debt of immutability.
Upgradable proxies like OpenZeppelin offer a false solution. They introduce a centralization vector and a single point of failure for the entire ecosystem built on that contract, as seen in past admin key compromises.
The core failure is architectural. Developers treat the blockchain as a traditional database, ignoring the permanent cost of a bad schema. A poorly designed struct or mapping becomes a permanent tax on all future gas usage.
Evidence: The Ethereum Name Service (ENS) migration required a new registry contract because its original design could not scale. Every dApp integrating ENS had to update, demonstrating the cascading cost of early state decisions.
The Gas Tax: Public vs. Internal/Private
A first-principles breakdown of the gas overhead for exposing state variables, comparing visibility modifiers and their impact on deployment, read, and write operations.
| Storage Operation & Cost Metric | Public State Variable | Internal State Variable | Private State Variable |
|---|---|---|---|
Deployment Bytecode Size Increase | ~200-300 bytes | 0 bytes | 0 bytes |
Automatic Getter Function Generated | |||
External Read Cost (via Getter) | ~2,300 gas (warm) | N/A | N/A |
Internal Read Cost (in-contract) | ~100 gas (sload) | ~100 gas (sload) | ~100 gas (sload) |
Write Cost (sstore) | ~20,000 gas (cold) / ~2,900 gas (warm) | ~20,000 gas (cold) / ~2,900 gas (warm) | ~20,000 gas (cold) / ~2,900 gas (warm) |
Exposed in Contract ABI | |||
Can be Read by Other Contracts | |||
Primary Use Case | External API, constants, governance parameters | Internal contract logic, inherited contracts | True encapsulation, preventing inheritance access |
Mechanics of the Waste: Getters and Storage Layout
Public state variables impose a permanent, compounding gas tax on every read operation, a cost embedded in the contract's ABI.
Public variables are not free. Declaring a variable public automatically generates a getter function in the contract's ABI. Every external call to this getter executes an SLOAD opcode, reading directly from expensive contract storage.
Storage layout dictates gas cost. The Solidity compiler packs variables into 32-byte storage slots. A poorly packed uint8 wastes 31 bytes, but the real penalty is the extra SLOAD for every subsequent variable in the unpacked slot.
The cost compounds with usage. Unlike a private variable accessed internally, a public getter's gas cost scales with the protocol's user base. For high-traffic contracts like Uniswap or Compound, this creates a permanent, avoidable gas overhead.
Evidence: An SLOAD costs 2,100 gas post-EIP-2929. A single public variable in a popular DApp like Aave, called 1 million times, wastes over 2.1 billion gas—a direct tax on users with zero functional benefit.
Real-World Impact: Lessons from Major Protocols
Public state is the default in Solidity, but its gas and security costs are often a silent tax on protocol growth and user trust.
The Uniswap V2 Router Gas Tax
The original router stored factory and WETH addresses in public state variables, costing ~5k gas per read. This added millions in cumulative fees for users and aggregators before V3's architectural shift.
- Cost: Every swap paid for an on-chain
SLOAD. - Lesson: Immutable constants or internal/private storage with getters are essential for high-frequency functions.
The MakerDAO Governance Attack Surface
Early versions exposed critical governance parameters (e.g., pause, authority) as public state. This created a massive attack vector for governance exploits, as seen in the $20M+ governance attack on the MKR contract in 2020.
- Risk: Public mutability invites flash loan governance attacks.
- Solution: Later designs use timelocks, internal functions, and decentralized multisigs to protect state.
Compound's cToken Interest Rate Model Leak
The interestRateModel address was public and upgradeable by the admin. A faulty model update in 2021 led to frozen markets and ~$100M in temporarily locked funds, highlighting the risk of exposed, mutable pointers.
- Failure: Public admin function allowed a single-point failure.
- Evolution: Modern designs like Aave use robust, time-delayed upgrade mechanisms and immutable core logic.
The Lido stETH Withdrawal Queue Bottleneck
The public mapping for the withdrawal queue state became a gas-guzzling bottleneck during the Shanghai upgrade frenzy. Each status check required an expensive storage read, slowing down protocols and increasing costs for integrators.
- Bottleneck: High-frequency reads on a complex public struct.
- Fix: Later iterations optimize for gas by using packed storage or off-chain indexing via The Graph.
OpenZeppelin's Ownable Anti-Pattern
The default Ownable contract exposes a public owner address. This has led to countless accidental renouncements and privilege escalations, locking protocols permanently. It's a canonical example of convenience creating systemic risk.
- Pitfall: Public mutability for a critical admin key.
- Best Practice: Use OpenZeppelin's AccessControl for role-based, multi-sig admin structures.
The SushiSwap MasterChef Migration Debacle
Critical reward parameters were stored in public mappings, making the $1B+ TVL protocol rigid and costly to upgrade. The contentious hard fork to MasterChef V2 was a direct result of this inflexible, gas-inefficient state design.
- Consequence: Inability to iterate forced a divisive fork.
- Architectural Debt: Technical debt in state design translates directly to community and governance risk.
FAQ: Practical Implementation
Common questions about the real-world costs and risks of exposing a public state variable in a smart contract.
The true cost is not just gas, but the permanent, irreversible exposure of contract logic and data. Every public variable creates a permanent attack surface for exploits, increases audit complexity, and can leak sensitive business logic. This design choice can lead to vulnerabilities like those exploited in the Nomad Bridge hack, where a public initialization variable was a key factor.
The Path Forward: Intent Over Convenience
The true cost of a public state variable is not gas, but the permanent, exploitable surface area it creates for your protocol.
Public state is a liability. Every variable you expose on-chain becomes a permanent part of your protocol's attack surface, inviting MEV extraction and governance attacks long after deployment.
Intent-based architectures externalize state. Protocols like UniswapX and CowSwap shift risk by letting solvers compete to fulfill user intents off-chain, minimizing the on-chain footprint attackers can target.
The cost is operational complexity. You trade a simple, expensive on-chain check for a complex off-chain system of solvers, reputation, and dispute resolution, as seen in Across and LayerZero's verification models.
Evidence: Arbitrum's 2M+ TPS capacity for fraud proofs demonstrates that the future is verifying outcomes, not executing every state transition on a public ledger.
TL;DR: Actionable Takeaways
Public state is the most expensive resource on-chain. Here's how to architect around it.
The Problem: Storage Slots Are Forever
Every public state variable consumes a storage slot, incurring a one-time write cost and a permanent rent. This is the primary driver of high gas fees for contract deployment and function calls.
- Key Cost: A single
SSTOREto a new slot costs ~20,000 gas. - Permanent Bloat: Data lives on-chain forever, burdening all future node operators.
The Solution: Pack Your Structs
EVM storage slots are 256 bits. You can pack multiple smaller variables (uint8, bool, address) into a single slot.
- Key Benefit: Reduces SSTORE and SLOAD operations by up to 10x for structs.
- Implementation: Use Solidity's
uncheckedblocks and bitwise packing libraries from OpenZeppelin.
The Problem: Public Getters Are Free Calldata
The Solidity compiler auto-generates a public getter function for every public state variable. While free for you, it forces users to pay for calldata to read on-chain state.
- Hidden Cost: Encourages off-chain indexers like The Graph but adds latency.
- Architectural Lock-in: Makes migration to zk-rollups or alternative VMs more difficult.
The Solution: Use Events & Off-Chain Indexing
Emit compact events instead of storing full state. Let indexers (The Graph, Covalent) reconstruct the dataset off-chain.
- Key Benefit: Turns state reads into log queries, moving cost from L1 to infra providers.
- Trade-off: Introduces trust assumptions in the indexer's correctness and availability.
The Problem: Transparency ≠Efficiency
Public state is often used for transparency, but it's a brute-force method. Projects like MakerDAO and Compound show that critical parameters (rates, limits) can be managed via governance events and off-chain data feeds (Chainlink).
- Real Cost: Every on-chain parameter update requires a governance vote and transaction.
- Missed Optimization: Fails to leverage Layer 2 state channels or oracle-based conditional logic.
The Solution: Ephemeral Storage & State Channels
For transient state (auction bids, game moves), use EIP-1153 transient storage or state channels (e.g., Connext, Raiden). Finalize only the net result on-chain.
- Key Benefit: Zero persistent state cost for intermediate steps.
- Architecture: Enables complex application logic impossible on vanilla L1, similar to zk-rollup validity proofs.
Get In Touch
today.
Our experts will offer a free quote and a 30min call to discuss your project.