Переменные состояния, видимость и константы
Переменные состояния — это данные, которые контракт хранит между транзакциями. Их видимость и неизменяемость напрямую влияют на газ.
«private» в Solidity не значит «секретно». Это лишь правило для компилятора, а данные всё равно лежат в публичном блокчейне.
Переменная, объявленная на уровне контракта (а не внутри функции), — это переменная состояния. Она живёт в storage и сохраняется навсегда. У неё есть модификатор видимости: public (доступна снаружи, компилятор создаёт геттер), internal (доступна в этом контракте и наследниках, по умолчанию), private (только в этом контракте).
Видимость — это не приватность
Важно понять: private ограничивает доступ только на уровне Solidity-кода. Само значение хранится в открытом блокчейне, и любой может прочитать слот storage напрямую. Поэтому секреты в контракте хранить нельзя ни с каким модификатором.
public -> виден всем + авто-геттер (снаружи и внутри) internal -> этот контракт + наследники (по умолчанию) private -> только этот контракт ---------------------------------------------------- !!! Все они физически читаются из storage блокчейна
constant и immutable
Два особых модификатора экономят газ. constant — значение известно на этапе компиляции и зашивается прямо в байт-код (слот storage не занимается). immutable — задаётся один раз в конструкторе и потом не меняется; тоже не занимает обычный слот storage, читается из кода. Оба дают огромную экономию по сравнению с обычной переменной.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Config {
uint256 public constant MAX_SUPPLY = 1_000_000; // в байт-коде
address public immutable deployer; // задаётся 1 раз
uint256 private counter; // обычный storage
constructor() {
deployer = msg.sender; // immutable можно присвоить тут
}
function bump() external {
counter += 1; // обычная переменная — дорогая запись в storage
}
}
Как работает под капотом (EVM/газ)
Обычное чтение переменной состояния — это опкод SLOAD (~2100 газа на холодный слот). Запись — SSTORE (до 20000). А вот constant и immutable вообще не делают SLOAD: их значения подставлены в байт-код, чтение почти бесплатно (~3 газа, как обычная константа). Поэтому всё, что не меняется после деплоя, выгодно объявлять immutable или constant.
# Та же логика на Python: стоимость доступа к разным переменным
COST = {"constant": 3, "immutable": 3, "storage_read": 2100, "storage_write": 20000}
class Config:
MAX_SUPPLY = 1_000_000 # constant — в коде
def __init__(self, deployer):
self.deployer = deployer # immutable — один раз
self.counter = 0 # обычный storage
cfg = Config("0xDEPLOYER")
gas = 0
gas += COST["constant"] # читаем MAX_SUPPLY
gas += COST["immutable"] # читаем deployer
gas += COST["storage_write"] # пишем counter
cfg.counter += 1
print("counter =", cfg.counter, "| суммарный газ:", gas)
«Та же логика на Python ▶». Видно, что обращение к constant/immutable почти бесплатно, а запись в обычный storage доминирует в счёте газа.
Частые ошибки
- Считать
private-переменную секретной — её значение читается из блокчейна напрямую. - Хранить неизменные параметры (адрес владельца, максимальный сапплай) в обычной переменной вместо
immutable/constant— переплата газа на каждом чтении. - Пытаться присвоить
immutableпосле конструктора — компилятор запретит.
Best practices
- Всё, что фиксировано на этапе компиляции — в
constant. Всё, что задаётся при деплое и не меняется — вimmutable. - Не делайте переменные
public«на всякий случай»: лишний геттер раздувает байт-код. Делайте публичными только то, что действительно читают снаружи. - Для дорогих чтений в горячих функциях кэшируйте storage в локальную переменную.
Итоги
Переменные состояния живут в storage, их видимость (public/internal/private) — правило компилятора, а не защита данных. Модификаторы constant и immutable убирают дорогие обращения к storage и серьёзно экономят газ. Дальше переходим к функциям и модификаторам.