$ sudo teach IT
Модуль 3 из 7

Функции, конструктор и модификаторы

Переменные описывают, что контракт знает. Функции - что он умеет. Научим его запускаться в конструкторе и вежливо, но твёрдо отказывать неправильным вызовам.

Вы узнаете
  • видимость функций и состояния: public, external, internal, private, view, pure, payable;
  • конструктор - как задать создателя, цель и дедлайн при развёртывании;
  • require, revert и собственные ошибки; модификаторы для повторяющихся проверок.

3.1Видимость: кто может вызвать функцию

У каждой функции обязательно указывается видимость. Она отвечает на вопрос «откуда функцию можно вызвать»:

Видимость Кто может вызвать
external Только извне (другой аккаунт или контракт). Изнутри - нельзя напрямую. Для входных точек контракта это самый дешёвый вариант.
public И извне, и изнутри. У переменных состояния public ещё и создаёт функцию-геттер.
internal Только изнутри контракта и его наследников. Наружу не видна. Для вспомогательной логики.
private Только внутри самого контракта, даже наследникам недоступна.
«невидимая» не значит «недоступная»

internal и private ограничивают доступ на уровне кода. Снаружи такую функцию напрямую не вызвать: у неё нет точки входа в ABI. Но до неё можно добраться через любую public- или external-функцию, которая её вызывает, а значения private-переменных и вовсе читаются прямо из хранилища блокчейна. Безопасность обеспечивают проверки внутри функций, а не слово private.

3.2Изменчивость состояния: view, pure, payable

Второй набор пометок говорит, как функция обращается с состоянием и деньгами:

Пометка Что обещает
(нет пометки) Функция может менять состояние. Её вызов - транзакция, стоит газа.
view Только читает состояние, не меняет. Вызов «снаружи» бесплатен.
pure Не читает и не меняет состояние - чистое вычисление над аргументами.
payable Функция умеет принимать эфир. Без этой пометки присланный в неё эфир приведёт к откату.
solidity
// только читает - view
function isActive() public view returns (bool) {
    return block.timestamp < deadline;
}

// чистое вычисление - pure
function double(uint256 x) public pure returns (uint256) {
    return x * 2;
}

Если из этого модуля запомнить одно слово, пусть это будет payable. Функция приёма вкладов обязана быть payable, иначе контракт откажется от каждого присланного вей. Краудфандинг, который не принимает деньги, выглядит гордо, но бесполезен.

3.3Конструктор: запускаем кампанию

Конструктор - особая функция, которая выполняется один раз, в момент развёртывания контракта, и больше никогда. В нём задают стартовые значения. Именно здесь мы заполним immutable-поля из модуля 2.

solidity
contract Crowdfunding {
    address public immutable owner;
    uint256 public immutable goal;
    uint256 public immutable deadline;

    constructor(uint256 _goal, uint256 _durationDays) {
        owner    = msg.sender;                        // тот, кто развернул контракт
        goal     = _goal;
        deadline = block.timestamp + _durationDays * 1 days;
    }
}

Здесь впервые встречается msg.sender - адрес того, кто вызвал функцию (в конструкторе - того, кто разворачивает контракт). Это одна из важнейших глобальных переменных; подробно разберём её в модуле 4. А 1 days - удобный литерал времени, равный 86 400 секундам.

3.4require и revert: отклоняем неверные вызовы

Контракт обязан уметь говорить «нет»: вклад после дедлайна, вывод денег чужаком, слишком маленькая сумма. Для этого есть механизм отката.

Инструмент Когда применять
require(условие, "текст") Проверка входных данных и прав. Не выполнилось - откат с сообщением. Основной рабочий инструмент.
revert("текст") / revert MyError() Безусловный откат - обычно в развилке if, где условие сложнее одной строки.
assert(условие) Проверка того, что «никогда не должно нарушаться». Срабатывание - признак бага в коде, а не плохого ввода.

При откате отменяется всё, что функция успела натворить, а остаток газа возвращается. Функция либо отрабатывает целиком, либо как будто её не вызывали вовсе. Промежуточных состояний не бывает - никаких «деньги списали, а запись не сделали».

- А если откат случился на последней строке длинной функции? Всё, что было выше, тоже отменится?

- Всё, до последнего байта. Сохранить половину транзакции блокчейн не умеет. Поэтому проверки обычно ставят в самом начале функции: незачем жечь газ на работу, которую всё равно выбросят.

solidity
function contribute() external payable {
    require(block.timestamp < deadline, "campaign ended");
    require(msg.value >= MIN_CONTRIBUTION, "contribution too small");
    // ... учёт вклада добавим в модуле 4
}
Собственные ошибки - дешевле и понятнее

С версии 0.8.4 вместо текстовых строк можно объявлять custom errors. Они тратят меньше газа и могут нести данные:

solidity
error CampaignEnded();
error BelowMinimum(uint256 sent, uint256 required);

function contribute() external payable {
    if (block.timestamp >= deadline) revert CampaignEnded();
    if (msg.value < MIN_CONTRIBUTION) revert BelowMinimum(msg.value, MIN_CONTRIBUTION);
}

3.5Модификаторы: проверка, вынесенная за скобки

Одна и та же проверка часто нужна многим функциям: «только владелец», «только пока кампания идёт». Копировать require в каждую - верный способ однажды забыть его в самой важной. Поэтому проверку выносят в модификатор - обёртку, которую навешивают на функцию.

solidity
modifier onlyOwner() {
    require(msg.sender == owner, "not the owner");
    _;   // сюда подставится тело самой функции
}

modifier whileActive() {
    require(block.timestamp < deadline, "campaign ended");
    _;
}

// применяем: функция выполнится, только если прошли обе проверки
function cancel() external onlyOwner {
    // ... доступно только создателю
}

Строка _; - место, куда вставится тело функции. Код до неё выполнится как проверка перед вызовом, код после - уже после. Модификаторы делают намерение явным: прямо в сигнатуре видно, что cancel - только для владельца.

Не прячьте важное в модификаторах

Модификатор удобен для коротких проверок. Не стоит прятать в нём сложную логику или переводы денег: читатель кода ожидает увидеть там require, а не изменение состояния. Паттерн доступа «только владелец» настолько частый, что в продвинутом курсе мы заменим наш onlyOwner на проверенную реализацию из библиотеки OpenZeppelin.

Вывод модуля

Видимость задаёт, откуда функцию можно вызвать; пометки view / pure / payable - как она обращается с состоянием и деньгами. Конструктор выполняется один раз и задаёт создателя, цель и дедлайн. Неверные вызовы отбивают require и revert - при откате отменяется всё.

Повторяющиеся проверки выносят в модификаторы. Каркас краудфандинга уже умеет запускаться и отклонять некорректные вклады - осталось научить его вести учёт.

Дальше · модуль 4

Кампания запускается, но контракт не помнит, кто сколько внёс. В следующем модуле появится учёт вкладчиков - и проверка через tx.origin, на которой попадаются почти все новички.