Условия, циклы и время: как контракт принимает решения
- Зачем это нужно
- Условие — вопрос, на который есть только два ответа
- if и else if: первая истинная ветка побеждает
- Время контракта — это метка блока
- Фазы по времени: граничная секунда решает
- Тернарный оператор: выбор одного значения
- Цикл for: три части в скобках
- Каждый виток стоит газ, и цена растёт вместе со списком
- Отображение отвечает за одну и ту же цену при любой длине
- Циклы, которые не сломают контракт
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Что дальше
- Углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
До сих пор контракт умел хранить число, отдавать его и отказывать по проверке. Настоящий контракт решает: продавать ли токен сейчас, по какой цене, пускать ли этого пользователя, сколько раз повторить действие.
В эталонном контракте задания это функция getTokenPrice(): она выбирает одну из трёх цен по тому, сколько времени прошло с запуска. И функция подачи заявки: перед записью она пробегает весь список уже поданных заявок и сравнивает каждую с адресом отправителя.
Обе конструкции — развилка и цикл — выглядят безобидно. Обе умеют сломать решение так, что компилятор промолчит: переставленные ветки делают одну цену недостижимой, а цикл по растущему списку однажды перестаёт помещаться в транзакцию — и функция умирает навсегда. Оба случая в этой главе измерены числами.
Условие — вопрос, на который есть только два ответа
Условие — выражение, результат которого либо true, либо false. Строится из сравнений:
| Знак | Читается |
|---|---|
> < | больше, меньше |
>= <= | не меньше, не больше |
== | равно |
!= | не равно |
Два знака == — это сравнение. Один знак = — присваивание, «положить значение в ячейку», как в прошлых главах. Перепутать легко, а последствия разные: if (x = 5) в Solidity даже не скомпилируется, потому что присваивание не даёт bool.
Условия соединяются связками: && — «и» (обе части истинны), || — «или» (хотя бы одна), ! — «не» (переворачивает).
if и else if: первая истинная ветка побеждает
if (getLifeTime() > seedPhase + privPhase) {
tokenPrice = pubPrice;
} else if (getLifeTime() > seedPhase) {
tokenPrice = privPrice;
}
Контракт идёт по веткам сверху вниз и останавливается на первой истинной. Остальные даже не проверяются. Поэтому порядок веток — это часть логики, а не оформление: сначала самое строгое условие, потом более мягкое.
Время контракта — это метка блока
У контракта нет часов. Единственное время, которое он видит, — block.timestamp: число секунд с 1 января 1970 года, записанное в блок тем, кто его собрал.
function getLifeTime() public view returns (uint256) {
return block.timestamp - startTime;
}
startTime контракт запоминает в момент развёртывания, а разность даёт «сколько секунд живёт система». С этим числом и сравнивают границы фаз — те самые 10 minutes и 5 minutes, которые, как мы уже знаем, просто числа 600 и 300.
Фазы по времени: граничная секунда решает
Первая модель повторяет продажу из эталонного контракта: три фазы, три цены. Скрипт вручную задаёт метку следующего блока и печатает цену на каждом шаге:
── деплой ──
startTime из контракта: 1800000000
timestamp блока деплоя: 1800000000 (совпадают: true)
── двигаем время блока и читаем цену на каждом шаге ──
+ 0 с | getLifeTime= 0 | фаза=seed | цена=0.0 ETH
+ 60 с | getLifeTime= 60 | фаза=seed | цена=0.0 ETH
+ 299 с | getLifeTime= 299 | фаза=seed | цена=0.0 ETH
+ 300 с | getLifeTime= 300 | фаза=seed | цена=0.0 ETH
+ 301 с | getLifeTime= 301 | фаза=private | цена=0.00075 ETH
+ 899 с | getLifeTime= 899 | фаза=private | цена=0.00075 ETH
+ 900 с | getLifeTime= 900 | фаза=private | цена=0.00075 ETH
+ 901 с | getLifeTime= 901 | фаза=public | цена=0.001 ETH
+3600 с | getLifeTime=3600 | фаза=public | цена=0.001 ETH
── те же ветки, но переставленные: мягкое условие первым ──
+3600 с | правильный порядок: 0.001 ETH | ветки переставлены: 0.00075 ETH
(ждали 0.001 — ветка public недостижима)
── кто пишет часы: следующий блок с любой меткой ──
метка блока задана вручную: +100000 с → фаза=public
два блока подряд без паузы: 1800100000 → 1800100001
(разница 1 с, хотя реального времени прошло ~0)
Три вещи, которые не увидеть глазами.
Граница смещена на секунду. Условие > 300 означает «строго больше», поэтому на 300-й секунде фаза ещё seed, а покупка ещё запрещена. Если по заданию фаза должна меняться ровно на пятой минуте, знак нужен >=. В реальной сети вызвать функцию точно в граничную секунду невозможно — ошибка живёт в коде месяцами.
Переставленные ветки убивают одну цену. Если сначала проверить > 300, то при 3600 сработает именно эта ветка, и до public дело не дойдёт никогда. Код компилируется, тесты на средних значениях проходят, а цена всегда приватная.
Часы пишет тот, кто собирает блок. Скрипт назначил блоку метку «+100 000 секунд», и контракт послушно перешёл в публичную фазу. Два блока подряд, собранные без паузы, различаются на секунду. Отсюда правило: block.timestamp годится для интервалов в минуты и часы, но не для секундной точности и тем более не как источник случайности.
Тернарный оператор: выбор одного значения
Когда нужно не выполнить действия, а выбрать одно из двух значений, вместо if пишут короткую форму:
uint256 price = t > 900 ? pubPrice : privPrice;
Читается как «если t > 900, то pubPrice, иначе privPrice». Оба значения обязаны быть одного типа: смешать число и строку компилятор не даст.
Цикл for: три части в скобках
Цикл повторяет блок, пока условие истинно:
for (uint256 i = 0; i < requests.length; i++) {
require(requests[i].wallet != msg.sender, unicode"Вы уже подали заявку");
}
Есть и while (условие) { … } — цикл без счётчика, когда число повторений заранее неизвестно. В контрактах он встречается редко и по опасной причине: цикл, который может не закончиться, сжигает весь газ.
Каждый виток стоит газ, и цена растёт вместе со списком
Вторая модель повторяет функцию подачи заявки из эталонного контракта и измеряет газ при разной длине очереди:
── лимит газа блока в этой сети: 60 000 000 ──
── а сколько газа можно дать ОДНОЙ транзакции? пробуем весь блок ──
ошибка: Transaction gas limit is 60000000 and exceeds transaction gas cap of 16777216
── газ одного вызова sendRequest() при разной длине очереди ──
очередь 10 адресов → 73 766 газа
очередь 100 адресов → 300 026 газа | +2 514 газа за каждый лишний адрес
очередь 1000 адресов → 2 562 626 газа | +2 514 газа за каждый лишний адрес
прямая по точкам 10 и 1000: газ ≈ 48 626 + 2 514 × длина очереди
потолок одной транзакции 16 777 216 кончится при длине ≈ 6 654 адресов
── проверяем: доводим очередь до этой длины и зовём sendRequest() ──
в очереди 6 554 адресов → оценка газа: 16 525 382 (ещё влезает)
в очереди 6 754 адресов:
оценка газа не удалась: Transaction ran out of gas
транзакция с максимальным газом: Transaction ran out of gas
очередь никто не чистит → sendRequest() больше не вызвать никому и никогда.
Это отказ в обслуживании (DoS) по газу.
Здесь важны две вещи.
Цена цикла — прямая линия. Каждый лишний адрес в очереди добавляет 2 514 газа к каждому вызову. Это не «немного медленнее»: на тысяче адресов вызов уже в 35 раз дороже, чем на десяти.
Потолок — не лимит блока. Старые уроки говорят про 30 миллионов газа на блок. На сегодня лимит блока — 60 миллионов, но одной транзакции нельзя дать больше 16 777 216 газа: это отдельное правило сети. При росте на 2 514 газа за адрес потолок наступает примерно на 6 654 заявках — и прогон это подтверждает.
Самое неприятное — необратимость. Очередь никто не чистит, значит функция не заработает никогда, ни у кого. Это классическая уязвимость «отказ в обслуживании по газу», и в отчёте по информационной безопасности модуля В она стоит отдельным пунктом.
Проверять «есть ли адрес в списке» циклом — самая частая причина такой поломки. Список растёт от пользователей, а не от разработчика: чем успешнее проект, тем быстрее умирает функция.
Отображение отвечает за одну и ту же цену при любой длине
Третья модель задаёт один вопрос — «есть ли адрес в списке из тысячи?» — двумя способами:
── в списке 1000 адресов; газ на вопрос «есть ли адрес?» ──
первый в списке | массив: 26 436 газа (true) | mapping: 24 065 газа (true)
последний в списке | массив: 2 538 933 газа (true) | mapping: 24 077 газа (true)
отсутствует | массив: 2 539 088 газа (false)| mapping: 24 077 газа (false)
разница в худшем случае: 105 раз
массив: цена зависит от места адреса в списке; mapping: всегда одна и та же
Отображение (mapping) — это таблица «ключ → значение», из которой значение достаётся сразу, без перебора. Объявляется так:
mapping(address => bool) public inWhitelist;
// записать:
inWhitelist[msg.sender] = true;
// спросить:
if (inWhitelist[msg.sender]) { … }
Разница не в скорости на проценты, а в самой природе: у массива цена растёт вместе с длиной, у отображения — не растёт вообще. Подробно устройство отображений и массивов разберём в главе про хранение данных; здесь важен вывод для развилок и циклов.
Правило: вопрос «есть ли» задают отображению. Массив держат только для того, чтобы показать список целиком — и делают это в функции только для чтения, которая не тратит газ.
Циклы, которые не сломают контракт
Три приёма, которые закрывают почти все случаи.
Ограничить длину входа. Если цикл идёт по данным, которые передал вызывающий, — поставить границу:
require(recipients.length <= 100, unicode"За один раз не более 100 адресов");
Обрабатывать по страницам. Функция принимает «с какого по какой» и работает со срезом, а не со всем списком сразу:
function processRange(uint256 from, uint256 to) public {
require(to - from <= 100, unicode"Слишком большой диапазон");
for (uint256 i = from; i < to; i++) { … }
}
Не хранить то, что можно спросить. Существование — в отображение, список — в массив только для чтения.
Общее правило простое: цикл по данным, длину которых определяет пользователь, — источник будущей поломки. Цикл по трём фазам или по пяти ролям — нормально.
Типичные ошибки
1 minute вместо 1 minutes. ParserError: Expected ';' but got identifier — суффиксы только во множественном числе. Эта строка есть в эталонном контракте.
Один знак = в условии. if (x = 5) не даёт bool — компилятор откажет. Сравнение — всегда ==.
Опечатка .lenth вместо .length. Member "lenth" not found or not visible — тоже из эталонного контракта.
if без фигурных скобок. К условию относится только одна следующая строка; вторая выполнится всегда. Компилятор молчит.
Разные типы в ветках тернарного оператора. TypeError: True expression's type … does not match false expression's type ….
Счётчик uint8 в цикле по длинному списку. На 256-м витке переполнение и откат с паникой; счётчик всегда uint256.
Слово now. Убрано из языка в версии 0.7; пишут block.timestamp.
Цикл по массиву, который растёт от пользователей. Компилируется, работает, потом перестаёт — навсегда.
Слова главы
- Условие — выражение типа
bool; строится сравнениями==,!=,>,<,>=,<=. &&||!— «и», «или», «не».if/else if/else— ветвление; побеждает первая истинная ветка.- Блок
{ }— набор строк, выполняемых вместе. - Тернарный оператор
? :— выбор одного из двух значений. for (подготовка; условие; шаг)— цикл со счётчиком;while— без счётчика.break/continue— выйти из цикла / перейти к следующему витку.block.timestamp— метка времени блока в секундах; пишет её сборщик блока.block.number— номер блока; растёт на единицу с каждым блоком.- Отображение (mapping) — таблица «ключ → значение»; ответ за одну и ту же цену.
- Потолок газа транзакции — 16 777 216; лимит блока больше, но одной транзакции столько не дают.
- Отказ в обслуживании по газу (DoS) — функция, которая перестала помещаться в транзакцию.
Тренажёр: набрать вслепую
Цель: скорость ≥ 100 зн/мин, точность ≥ 88%
if (getLifeTime() > seedPhase + privPhase) {
На чемпионате
Модуль В, 14 баллов. Отчёты сверяются с ключом, и три пункта этой главы в ключе почти наверняка есть:
- Синтаксис:
1 minuteвместо1 minutes,requests.lenthвместо.length. - Логика: порядок и строгость условий в выборе фазы; функция «добавить минуту», доступная кому угодно без проверки прав.
- Информационная безопасность: цикл по растущему списку заявок — отказ в обслуживании по газу, с числом: при такой-то длине вызов перестанет проходить.
Формулируйте пункт отчёта как «строка N: причина → что сделать». За вредное предложение снимают, поэтому пишите то, что можете обосновать числом или цитатой из документации.
Модуль А, 37 баллов. Требования задания сплошь состоят из условий: «позиция подлежит ликвидации, когда LTV ≥ LLTV», «сумма ≥ 10 USDC», «пользователь не должен быть ликвидирован после получения займа». Каждое — это if или require с правильным знаком. Строгое или нестрогое сравнение здесь не мелочь: эксперт проверяет граничные значения по своему ключу.
Что дальше
Условия и циклы работали с готовыми данными — массивом адресов и отображением, — но брали их как данность. Следующая глава — про функции: как объявить свою, что такое параметры и возвращаемое значение, чем public отличается от private, зачем нужно view, как функция принимает деньги и как одну и ту же проверку вынести за скобки модификатором.
Углубиться
block.numberпротивblock.timestamp— счёт по блокам вместо секунд; в конкурсном задании 2026 проценты начисляются именно по номерам блоков.breakиcontinue— досрочный выход и пропуск витка; экономят газ при поиске.- Постраничная обработка — как разбить длинную операцию на несколько транзакций и не потерять состояние между ними.
- Правило «тяни, а не толкай» — почему рассылку денег заменяют на «каждый забирает сам»; разберём в главе про паттерны безопасности.
- Потолок газа транзакции — отдельное правило сети, появившееся недавно; полезно знать, что старые уроки называют другое число.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Управляющие конструкции — docs.soliditylang.org —
if, циклы, обязательные скобки. - Единицы и встроенные переменные —
block.timestamp,block.number, суффиксы времени. - Отображения — mapping — устройство таблицы «ключ → значение».
- EIP-7825: потолок газа транзакции — откуда взялось число 16 777 216.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.