Перейти к основному содержимому
Блокчейн06ГЛАВА 06Условия, циклы и время: как контракт принимает решенияseed · 0priv · 0.00075pub · 0.001301-я секундапервая истинная

Условия, циклы и время: как контракт принимает решения

Прочитано0%
Квизы0/5
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

До сих пор контракт умел хранить число, отдавать его и отказывать по проверке. Настоящий контракт решает: продавать ли токен сейчас, по какой цене, пускать ли этого пользователя, сколько раз повторить действие.

В эталонном контракте задания это функция 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-Й СЕКУНДЕ, А НА 301-Йseed · 0…300 сцена 0 — покупать нельзяprivate · 301…900 с0.00075 ETHpublic · с 901 с0.001 ETH300 → ещё seed900 → ещё privateветки переставлены — и через час цена 0.00075 вместо 0.001: ветка public недостижима, компилятор молчитчасы контракта — метка блока: её пишет тот, кто собирает блокдва блока подряд без паузы получили метки 1800100000 и 1800100001 — разница в секунду при нулевом реальном времени
Строгое сравнение сдвигает границу на секунду; переставленные ветки делают последнюю цену недостижимой — компилятор молчит

Три вещи, которые не увидеть глазами.

Граница смещена на секунду. Условие > 300 означает «строго больше», поэтому на 300-й секунде фаза ещё seed, а покупка ещё запрещена. Если по заданию фаза должна меняться ровно на пятой минуте, знак нужен >=. В реальной сети вызвать функцию точно в граничную секунду невозможно — ошибка живёт в коде месяцами.

Переставленные ветки убивают одну цену. Если сначала проверить > 300, то при 3600 сработает именно эта ветка, и до public дело не дойдёт никогда. Код компилируется, тесты на средних значениях проходят, а цена всегда приватная.

Часы пишет тот, кто собирает блок. Скрипт назначил блоку метку «+100 000 секунд», и контракт послушно перешёл в публичную фазу. Два блока подряд, собранные без паузы, различаются на секунду. Отсюда правило: block.timestamp годится для интервалов в минуты и часы, но не для секундной точности и тем более не как источник случайности.

Отвечено вопросов: 0 из 2
Условие фазы записано как getLifeTime() > 300. Что вернёт контракт ровно на 300-й секунде?
Ветки if переставили: мягкое условие первым. Что произойдёт?
Отвечено верно: 0 из 2

Тернарный оператор: выбор одного значения​

Когда нужно не выполнить действия, а выбрать одно из двух значений, вместо 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) по газу.
ЦИКЛ ПО РАСТУЩЕМУ СПИСКУ УБИВАЕТ ФУНКЦИЮ10 адресов73 766100 адресов300 0261000 адресов2 562 6266 754 адресаTransaction ran out of gasгаз ≈ 48 626 + 2 514 × длина очереди — прямая, а не «немного медленнее»потолок одной транзакции 16 777 216 газа кончается на 6 654 адресах: очередь никто не чистит — функция мертва навсегда
Газ растёт прямой линией: +2 514 за каждый адрес в очереди — и на 6 754 адресах функция перестаёт вызываться навсегда

Здесь важны две вещи.

Цена цикла — прямая линия. Каждый лишний адрес в очереди добавляет 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адрес первый в списке26 43624 065газаадрес последний из 10002 538 93324 077газаадреса нет вовсе2 539 08824 077газав худшем случае разница в 105 раз — а ответ обе функции дают одинаковыйу массива цена зависит от места адреса в списке, у отображения — не зависит от длины вообщеотображение — для вопроса «есть ли», массив — только чтобы показать список на экране
Один и тот же ответ: у массива цена зависит от места в списке, у отображения — не зависит ни от чего

Отображение (mapping) — это таблица «ключ → значение», из которой значение достаётся сразу, без перебора. Объявляется так:

mapping(address => bool) public inWhitelist;

// записать:
inWhitelist[msg.sender] = true;

// спросить:
if (inWhitelist[msg.sender]) { … }

Разница не в скорости на проценты, а в самой природе: у массива цена растёт вместе с длиной, у отображения — не растёт вообще. Подробно устройство отображений и массивов разберём в главе про хранение данных; здесь важен вывод для развилок и циклов.

Правило: вопрос «есть ли» задают отображению. Массив держат только для того, чтобы показать список целиком — и делают это в функции только для чтения, которая не тратит газ.

Отвечено вопросов: 0 из 2
Функция с циклом по списку заявок работала полгода, потом перестала вызываться у всех. Что случилось?
Как правильно проверять «есть ли адрес в списке»?
Отвечено верно: 0 из 2

Циклы, которые не сломают контракт​

Три приёма, которые закрывают почти все случаи.

Ограничить длину входа. Если цикл идёт по данным, которые передал вызывающий, — поставить границу:

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 с правильным знаком. Строгое или нестрогое сравнение здесь не мелочь: эксперт проверяет граничные значения по своему ключу.

Отвечено вопросов: 0 из 2
Задание требует: «позиция ликвидируется, когда LTV ≥ LLTV». Какой знак в коде?
Что писать в отчёте по ИБ про цикл по списку заявок?
Отвечено верно: 0 из 2

Что дальше​

Условия и циклы работали с готовыми данными — массивом адресов и отображением, — но брали их как данность. Следующая глава — про функции: как объявить свою, что такое параметры и возвращаемое значение, чем public отличается от private, зачем нужно view, как функция принимает деньги и как одну и ту же проверку вынести за скобки модификатором.

Отвечено вопросов: 0 из 1
Можно ли использовать block.timestamp как источник случайного числа?
Отвечено верно: 0 из 1

Углубиться​

  • block.number против block.timestamp — счёт по блокам вместо секунд; в конкурсном задании 2026 проценты начисляются именно по номерам блоков.
  • break и continue — досрочный выход и пропуск витка; экономят газ при поиске.
  • Постраничная обработка — как разбить длинную операцию на несколько транзакций и не потерять состояние между ними.
  • Правило «тяни, а не толкай» — почему рассылку денег заменяют на «каждый забирает сам»; разберём в главе про паттерны безопасности.
  • Потолок газа транзакции — отдельное правило сети, появившееся недавно; полезно знать, что старые уроки называют другое число.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоУчимся писать смарт контракты Ethereum | If else в SolidityRuslan Ravana
ВидеоSolidity и смарт-контракты Ethereum, урок #33 | Тесты, управление временем, BigNIlya Krukowski
ВидеоSolidity и смарт-контракты Ethereum, урок #23 | Timelock: ставим транзакции в очIlya Krukowski
ВидеоAre You Still Using Block.Timestamp like this? Solidity Best PracticesEatTheBlocks
ВидеоLoops in Solidity Tutorial | for vs while + Gas OptimizationConflux Network

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.