Функции: видимость и мутабельность

У каждой функции в Solidity два независимых ярлыка: кто её может вызвать и что она делает с состоянием.
Забыли пометить функцию как view — переплатили газ. Сделали внутреннюю функцию public — открыли дыру. Сигнатура функции в Solidity — это уже половина безопасности.

Объявление функции несёт несколько модификаторов. Первая ось — видимость: public (вызывается отовсюду), external (только снаружи, дешевле для больших аргументов), internal (этот контракт и наследники), private (только этот контракт). Вторая ось — мутабельность: view (только читает состояние), pure (не трогает состояние вообще), payable (может принимать ETH), либо без пометки (меняет состояние).

   ВИДИМОСТЬ            МУТАБЕЛЬНОСТЬ
   =========           ============
   external -> извне    pure    -> не читает/не пишет состояние
   public   -> везде    view    -> читает, но не пишет
   internal -> +наслед. payable -> принимает ETH
   private  -> только    (без)  -> пишет состояние (нужна транзакция)
              здесь
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Wallet {
    uint256 public total;

    // меняет состояние — нужна транзакция и газ
    function deposit() external payable {
        total += msg.value; // msg.value — сколько ETH прислали
    }

    // только читает
    function getTotal() external view returns (uint256) {
        return total;
    }

    // чистая функция — ничего из состояния не трогает
    function double(uint256 x) external pure returns (uint256) {
        return x * 2;
    }
}

Как работает под капотом (EVM/газ)

Когда транзакция приходит в контракт, EVM по первым 4 байтам calldata (селектору функции) выбирает нужную функцию. external-функции читают аргументы прямо из calldata, не копируя их в память — это дешевле для больших массивов. view и pure при внешнем вызове исполняются через eth_call без газа, но если их вызвать из меняющей функции — газ за чтение/вычисление всё равно платится в рамках транзакции. payable разрешает прикреплять ETH; без неё транзакция с ненулевым value откатится.

# Та же логика на Python: «движок» вызова по селектору
class Wallet:
    def __init__(self):
        self.total = 0
    def deposit(self, value):       # payable
        self.total += value
    def get_total(self):            # view
        return self.total
    @staticmethod
    def double(x):                  # pure
        return x * 2

# имитация диспетчеризации по имени функции (селектору)
w = Wallet()
calls = [("deposit", 5), ("deposit", 3), ("get_total", None)]
for name, arg in calls:
    fn = getattr(w, name)
    print(name, "->", fn(arg) if arg is not None else fn())
print("double(21) =", Wallet.double(21))

«Та же логика на Python ▶». EVM так же выбирает функцию по идентификатору и исполняет её; deposit меняет состояние, get_total и double — нет.

Частые ошибки

  • Сделать вспомогательную функцию public, хотя её должны вызывать только внутри — это лишняя точка входа и риск.
  • Забыть payable на функции, которая должна принимать ETH — пользователи не смогут отправить эфир.
  • Не пометить читающую функцию как view — теряется и оптимизация, и читаемость.

Best practices

  • Давайте минимально необходимую видимость: по умолчанию private/internal, повышайте только осознанно.
  • Для функций, вызываемых только снаружи и с большими аргументами, предпочитайте external вместо public.
  • Помечайте view/pure везде, где можно: компилятор проверит, что вы случайно не пишете состояние.

Конструктор, receive и fallback

Помимо обычных функций есть три особые. Конструктор (constructor) исполняется ровно один раз — в момент деплоя — и задаёт начальное состояние (например, владельца). После деплоя его вызвать нельзя. Две специальные функции отвечают за приём «голого» ETH без данных: receive() external payable срабатывает, когда контракту присылают эфир пустой транзакцией, а fallback() — когда вызвали несуществующую функцию или прислали данные, под которые нет совпадения. Если ни одной из них нет и контракт не payable, попытка отправить ему ETH откатится. Понимание этих функций важно для безопасности: именно receive/fallback атакующего срабатывают во время reentrancy, когда жертва переводит ему эфир, — об этом мы подробно поговорим в разделе безопасности.

Итоги

Функция описывается двумя осями: видимость (кто вызывает) и мутабельность (что делает с состоянием). Правильные ярлыки — это и газ, и безопасность, и читаемость. Дальше — модификаторы доступа и require.

Проверьте себя
1. Какая мутабельность нужна функции, которая должна принимать ETH вместе с вызовом?
Aview
Bpure
Cpayable
Dinternal
2. Чем external-функция выгоднее public при больших аргументах?
AОна быстрее компилируется
BОна читает аргументы прямо из calldata без копирования в память
CОна не требует газа никогда
DОна автоматически становится view