Контракт как объект: конструктор, enum и роли
- Зачем это нужно
- Контракт — объект, который живёт по адресу
- Конструктор выполняется ровно один раз
- Два развёртывания — два независимых объекта
- enum: имена поверх чисел
- Роли: словарь пользователей и модификатор с параметром
- Один символ — зеркальная таблица доступа
- Как поймать такую ошибку
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Что дальше
- Углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
Первый блок закончился на том, что контракт — это файл с данными и функциями. Второй начинается с другого взгляда: контракт — объект. У него есть память, поведение, собственный адрес и момент рождения. Один и тот же файл, развёрнутый дважды, даёт два независимых объекта, которые ничего не знают друг о друге.
Из этого взгляда вырастает всё остальное: конструктор, который запоминает владельца; перечисление ролей; таблица прав, построенная на одном модификаторе. И ровно здесь живёт самая известная ошибка эталонного контракта задания — перевёрнутое условие, которое открывает функции всем, кроме того, кому они предназначены. В прошлый раз мы посмотрели на неё на двух адресах; теперь построим полную таблицу из четырёх участников и трёх функций.
Контракт — объект, который живёт по адресу
У объекта есть состояние — переменные в хранилище, и поведение — функции. Объект появляется в момент развёртывания и получает адрес из 20 байт, такой же по форме, как у обычного кошелька.
Ключевое: код и объект — разные вещи. Файл Counter.sol — это чертёж. Каждое развёртывание создаёт по чертежу новый объект со своей памятью.
Внутри контракта на себя ссылаются словом this: address(this) — собственный адрес, address(this).balance — сколько эфира лежит на контракте. Написать просто this.balance нельзя, компилятор подскажет правильную форму.
Конструктор выполняется ровно один раз
contract Counter {
address public owner;
uint256 public count;
constructor() {
owner = msg.sender;
}
}
Конструктора нет в списке функций развёрнутого контракта — вызвать его повторно невозможно даже теоретически. Попытка объявить его как обычную функцию (function constructor()) даёт ошибку компилятора: такое имя не делает функцию конструктором.
Два развёртывания — два независимых объекта
Первая модель разворачивает один и тот же Counter дважды, от разных людей:
── один код, два развёртывания ──
Алиса развернула: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Боб развернул: 0x8464135c8F25Da09e49BC8782676a84730C318bC
адреса совпадают? false
газ развёртывания A: 257464 B: 257464
── owner у каждого — тот, кто разворачивал ──
A.owner(): 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
B.owner(): 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
── три increment() у A, ноль у B ──
A.count(): 3
B.count(): 0
── reset(): Боб стучится в A, Алиса — в свой A ──
Боб → A.reset(): reverted with reason string 'Сбросить может только владелец'
Алиса → A.reset(): ок, A.count() = 0
── конструктор второй раз вызвать нельзя ──
функции, которые можно вызвать у развёрнутого A: count, increment, owner, reset
constructor среди них? false
── контракт создаёт контракт: new Counter() внутри фабрики ──
адрес фабрики: 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707
адрес созданного: 0x61c36a8d610163660E21a8b7359e1Cac0C9133e1
child.owner(): 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707
owner == фабрика? true
owner == Алиса? false
газ make(): 288961
Газ развёртывания совпал до единицы: байткод один и тот же. А дальше объекты живут отдельно — три вызова у первого не сдвинули счётчик второго.
Последний блок вывода — самый неожиданный. Контракт умеет создавать контракты: new Counter() внутри другого контракта разворачивает новый объект. Но msg.sender в конструкторе дочернего объекта — это адрес контракта-создателя, а не человека, который вызвал фабрику. По коду owner = msg.sender выглядит одинаково в обоих случаях; результат разный.
Если фабрика должна назначить владельцем человека, адрес передают явно: new Counter(msg.sender) с параметром в конструкторе. Иначе владельцем окажется фабрика, и все проверки прав будут отказывать настоящему пользователю.
enum: имена поверх чисел
Когда вариантов немного и они известны заранее, вместо чисел заводят перечисление:
enum Role { User, publicProvider, privateProvider, Owner }
Это собственный тип с четырьмя допустимыми значениями. Компилятор нумерует их с нуля и хранит в одном байте. Объявляется на уровне контракта, точкой с запятой не заканчивается.
Вторая модель показывает, что под именами:
── enum под капотом: имена → числа ──
Role.User = 0
Role.publicProvider = 1
Role.privateProvider = 2
Role.Owner = 3
вариантов всего: 4 uint8(type(Role).max) = 3
── число → enum ──
Role(0) = 0 (User)
Role(3) = 3 (Owner)
Role(4) → reverted with panic code 0x21
(Tried to convert a value into an enum, but the value was too big or negative)
сырые данные отката: 0x4e487b71…0021
Role(255) → reverted with panic code 0x21
── значение по умолчанию ──
current после deploy: 0 → Role.User, isOwner() = false
после setRole(Role.Owner): current = 3 isOwner() = true
── что видно снаружи: в ABI enum — это uint8 ──
setRole принимает тип: uint8
setRole(4) → Transaction reverted and Hardhat couldn't infer the reason.
(проверка диапазона при разборе аргументов откатывает БЕЗ данных)
Три следствия.
Значение по умолчанию — первый вариант. Переменная типа Role, которой ничего не присваивали, равна нулю, то есть Role.User. То же с полем структуры, лежащей в словаре: для незнакомого адреса словарь вернёт пустую запись, а в ней роль будет нулевой.
Отсюда правило проектирования: нулевой вариант должен быть самым безопасным. Гость, а не владелец. Если поставить Owner первым, любой посторонний окажется владельцем по умолчанию.
Число вне диапазона откатывает. Внутри контракта — паникой 0x21. Снаружи — ещё раньше, при разборе аргументов, и тогда отката вообще нечем объяснить: ни строки, ни кода паники.
Сравнивать enum можно только с enum. current == Role.Owner — правильно, current == 3 — ошибка компиляции. Арифметики у перечислений нет.
Роли: словарь пользователей и модификатор с параметром
Схема из эталонного контракта: структура пользователя с полем роли, словарь по адресу и один модификатор на все функции.
struct User {
string login;
address wallet;
Role role;
}
mapping(address => User) userMap;
modifier AccessControl(Role _role) {
require(userMap[msg.sender].role == _role, unicode"Недостаточно прав");
_;
}
function stopPublicPhase() public AccessControl(Role.Owner) { … }
function giveReward(address to, uint256 n) public AccessControl(Role.publicProvider) { … }
Модификатор с параметром — одна заготовка на все роли. Проверка прав сводится к одному чтению из хранилища: роль лежит в том же байте, что и остальная запись пользователя.
Конструктор заполняет первые записи — владельца и провайдеров:
constructor(address pubProv, address privProv) {
owner = msg.sender;
userMap[owner] = User("owner", owner, Role.Owner);
userMap[pubProv] = User("pubProv", pubProv, Role.publicProvider);
userMap[privProv] = User("privProv", privProv, Role.privateProvider);
}
Один символ — зеркальная таблица доступа
Третья модель разворачивает два контракта: с правильным условием == и с тем, что стоит в эталонном контракте — !=. Четыре участника, три функции:
── роли из mapping(address => User) ──
owner login="owner" role=3
pubProv login="pubProv" role=1
privProv login="privProv" role=2
stranger login="" role=0
── правильный модификатор: role == _role ──
onlyOwnerAction onlyPubProvAction onlyPrivProvAction
owner ПРОШЁЛ отказ отказ
pubProv отказ ПРОШЁЛ отказ
privProv отказ отказ ПРОШЁЛ
stranger отказ отказ отказ
── модификатор из эталона: role != _role ──
onlyOwnerAction onlyPubProvAction onlyPrivProvAction
owner отказ: _________ ПРОШЁЛ ПРОШЁЛ
pubProv ПРОШЁЛ отказ: _________ ПРОШЁЛ
privProv ПРОШЁЛ ПРОШЁЛ отказ: _________
stranger ПРОШЁЛ ПРОШЁЛ ПРОШЁЛ
── итог по перевёрнутому: кто проходит в onlyOwnerAction ──
прошли 3 из 4, и единственный отказ получил сам owner
Правильная таблица — диагональ: каждый участник проходит ровно в свою функцию, посторонний не проходит никуда. Перевёрнутая — её зеркало: посторонний с нулевой ролью проходит во все три, а владелец не может остановить публичную фазу собственного контракта.
Обратите внимание, откуда взялась строка stranger: этого адреса в словаре никогда не было. Словарь вернул пустую запись с ролью 0 — и с перевёрнутым условием пустая роль отличается от любой требуемой, значит проверка пропускает.
В эталонном контракте под этим модификатором стоят функции остановки публичной фазы, перевода токенов провайдерам и чтения данных пользователя. Это готовый пункт отчёта по логике: строка, номер, что должно быть, к чему приводит.
Как поймать такую ошибку
Перевёрнутое условие не находится ни компилятором, ни ручной проверкой из редактора: там по умолчанию вызывает тот, кто развернул контракт. Способ один — таблица:
- Взять все роли, которые есть в системе, плюс адрес, которого в системе нет.
- Взять все функции с проверкой прав.
- Вызвать каждым адресом каждую функцию и записать результат.
Правильная таблица легко узнаётся: по одному «прошёл» в строке и ни одного в строке постороннего. Любой другой рисунок — повод перечитать условие.
На чемпионате это делается скриптом за минуту и закрывает целый класс ошибок; в отчёте по логике такая таблица — лучшее обоснование пункта.
Типичные ошибки
enums вместо enum. ParserError: Expected ';' but got '{' — ровно эта ошибка стоит в эталонном контракте.
function constructor(). Такое имя не делает функцию конструктором; компилятор сообщает об этом прямо.
Сравнение enum с числом. TypeError: Built-in binary operator == cannot be applied to types enum Role and int_const 3.
Присваивание числа в переменную-перечисление. Нужно явное преобразование: Role(n).
Role(4) при четырёх вариантах. Внутри — паника 0x21; снаружи — откат без данных.
Модификатор без _;. SyntaxError: Modifier body does not contain "_".
this.balance вместо address(this).balance. Компилятор подскажет правильную форму.
Владельцем стала фабрика. new Counter() без передачи адреса: msg.sender в конструкторе — это создатель, а не человек.
Перевёрнутое условие в модификаторе. Компилируется, ручная проверка молчит, таблица доступа — зеркальная.
Слова главы
- Экземпляр контракта — объект, созданный развёртыванием; у каждого свой адрес и своя память.
constructor— блок, выполняемый один раз при развёртывании; в списке функций его нет.this— сам контракт;address(this)— его адрес,address(this).balance— баланс.new Контракт()— создание контракта из контракта;msg.senderдочернего конструктора — создатель.enum— перечисление вариантов; хранится числом, нумерация с нуля.- Значение по умолчанию — первый вариант перечисления; он же приходит из словаря для незнакомого ключа.
- Panic
0x21— попытка превратить в перечисление число вне диапазона. - Модификатор с параметром — одна заготовка проверки на все роли.
- Таблица доступа — все роли × все защищённые функции; способ поймать перевёрнутое условие.
Тренажёр: набрать вслепую
Цель: скорость ≥ 100 зн/мин, точность ≥ 88%
constructor(address pubProv, address privProv) {
На чемпионате
Модуль А, 37 баллов. Первый засчитываемый шаг демонстрации — развернуть сеть и опубликовать контракты; конструктор при этом получает адреса участников. В задании 2026 года это Lender1…Lender3 и Borrower1…Borrower3 с таблицей адресов, которую заполняет сам участник. Практический вывод: адреса передаются параметрами конструктора, а не зашиваются в код — иначе при переразвёртывании придётся править исходник.
Роли в задании тоже есть: кредитор, заёмщик, ликвидатор, владелец протокола. Каждая — вариант перечисления и строка в таблице доступа.
Модуль В, 14 баллов. Из этой главы в отчёты идут:
- Логика: перевёрнутое условие в модификаторе — с таблицей: кто проходит, кто нет, кто должен.
- Синтаксис:
enumsвместоenum. - Информационная безопасность: функции, помеченные модификатором владельца, доступны постороннему; отдельно — функции вообще без проверки прав, которые меняют состояние.
Что дальше
Контракт-объект умеет рождаться, помнить владельца и различать роли. Следующий шаг — наследование: как взять готовый контракт и дописать своё, не копируя код. Оттуда же появятся слова is, virtual и override, которые стоят в первых строках эталонного контракта задания и без которых стандартный токен не написать.
Углубиться
type(Role).max— как узнать число вариантов перечисления прямо в коде.- Перечисление снаружи — почему в описании функции оно выглядит как
uint8и что это значит для программы, которая вызывает контракт. - Полный список кодов паники —
0x01,0x11,0x12,0x21,0x31,0x32: что означает каждый. - Фабрики контрактов — когда один контракт разворачивает десятки других и как хранить их адреса.
selfdestruct— как объект удаляли раньше и почему в современных версиях это почти не работает.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Создание контрактов и конструктор — docs.soliditylang.org — что происходит при развёртывании.
- Перечисления (enum) — нумерация, преобразования, ограничения.
- Модификаторы функций — параметры и порядок выполнения.
- Коды паники — полный список, включая
0x21. - Глобальные переменные —
msg.sender,address(this),balance.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.