Перейти к основному содержимому
Блокчейн09ГЛАВА 09Контракт как объект: конструктор, enum и роли.sol0x5FbD…count 30x8464…count 0объект, не файл

Контракт как объект: конструктор, enum и роли

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

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

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

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

Контракт — объект, который живёт по адресу​

У объекта есть состояние — переменные в хранилище, и поведение — функции. Объект появляется в момент развёртывания и получает адрес из 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
ОДИН КОД, ДВА РАЗВЁРТЫВАНИЯ — ДВА РАЗНЫХ ОБЪЕКТАCounter.solразвернула Алиса0x5FbD…0aa3owner = Алисаcount = 3развернул Боб0x8464…18BCowner = Бобcount = 0газ развёртывания одинаков — 257 464: байткод один. Различаются адрес и памятьпри new Counter() из другого контракта owner дочернего — адрес контракта-создателя, а не человека
Байткод один — газ развёртывания одинаков; различаются адрес, память и владелец

Газ развёртывания совпал до единицы: байткод один и тот же. А дальше объекты живут отдельно — три вызова у первого не сдвинули счётчик второго.

Последний блок вывода — самый неожиданный. Контракт умеет создавать контракты: new Counter() внутри другого контракта разворачивает новый объект. Но msg.sender в конструкторе дочернего объекта — это адрес контракта-создателя, а не человека, который вызвал фабрику. По коду owner = msg.sender выглядит одинаково в обоих случаях; результат разный.

Важно

Если фабрика должна назначить владельцем человека, адрес передают явно: new Counter(msg.sender) с параметром в конструкторе. Иначе владельцем окажется фабрика, и все проверки прав будут отказывать настоящему пользователю.

Отвечено вопросов: 0 из 2
Один и тот же код развернули дважды. Что общего у двух объектов?
Фабрика делает new Counter(), внутри конструктора owner = msg.sender. Кто станет владельцем?
Отвечено верно: 0 из 2

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.
(проверка диапазона при разборе аргументов откатывает БЕЗ данных)
ENUM — ЭТО ИМЕНА ПОВЕРХ ЧИСЕЛ 0…3User= 0publicProvider= 1privateProvider= 2Owner= 3значение по умолчаниюRole(4) внутри контрактаpanic code 0x21 — значение вне диапазонаsetRole(4) снаружиоткат вообще без данных: ни строки, ни паникиснаружи параметр-enum виден как uint8: проверка диапазона срабатывает при разборе аргументовотсюда правило: нулевой вариант должен быть самым безопасным — гость, а не владелец
Значение по умолчанию — первый вариант; число вне диапазона даёт панику внутри и откат без объяснений снаружи

Три следствия.

Значение по умолчанию — первый вариант. Переменная типа 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
ОДИН СИМВОЛ В МОДИФИКАТОРЕ — ЗЕРКАЛЬНАЯ ТАБЛИЦА ДОСТУПАrole == _rolerole != _role (эталон КЗ)ownerpubProvprivProvstrangerдиагональ: каждый — в свою функциюинверсия: прошли 3 из 4в перевёрнутом варианте единственный отказ получает сам владелец, а посторонний проходит везде
Правильное условие даёт диагональ — каждый в свою функцию; перевёрнутое даёт инверсию, и посторонний проходит везде

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

Обратите внимание, откуда взялась строка stranger: этого адреса в словаре никогда не было. Словарь вернул пустую запись с ролью 0 — и с перевёрнутым условием пустая роль отличается от любой требуемой, значит проверка пропускает.

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

Отвечено вопросов: 0 из 2
Адреса stranger никогда не было в mapping. Какая у него роль?
Модификатор написан как require(role != _role). Кто пройдёт в функцию для владельца?
Отвечено верно: 0 из 2

Как поймать такую ошибку​

Перевёрнутое условие не находится ни компилятором, ни ручной проверкой из редактора: там по умолчанию вызывает тот, кто развернул контракт. Способ один — таблица:

  1. Взять все роли, которые есть в системе, плюс адрес, которого в системе нет.
  2. Взять все функции с проверкой прав.
  3. Вызвать каждым адресом каждую функцию и записать результат.

Правильная таблица легко узнаётся: по одному «прошёл» в строке и ни одного в строке постороннего. Любой другой рисунок — повод перечитать условие.

На чемпионате это делается скриптом за минуту и закрывает целый класс ошибок; в отчёте по логике такая таблица — лучшее обоснование пункта.

Типичные ошибки​

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.
  • Информационная безопасность: функции, помеченные модификатором владельца, доступны постороннему; отдельно — функции вообще без проверки прав, которые меняют состояние.
Отвечено вопросов: 0 из 2
Почему адреса участников передают в конструктор параметрами, а не пишут в коде?
Как обосновать пункт отчёта про перевёрнутое условие?
Отвечено верно: 0 из 2

Что дальше​

Контракт-объект умеет рождаться, помнить владельца и различать роли. Следующий шаг — наследование: как взять готовый контракт и дописать своё, не копируя код. Оттуда же появятся слова is, virtual и override, которые стоят в первых строках эталонного контракта задания и без которых стандартный токен не написать.

Отвечено вопросов: 0 из 1
Сколько раз можно вызвать конструктор?
Отвечено верно: 0 из 1

Углубиться​

  • type(Role).max — как узнать число вариантов перечисления прямо в коде.
  • Перечисление снаружи — почему в описании функции оно выглядит как uint8 и что это значит для программы, которая вызывает контракт.
  • Полный список кодов паники — 0x01, 0x11, 0x12, 0x21, 0x31, 0x32: что означает каждый.
  • Фабрики контрактов — когда один контракт разворачивает десятки других и как хранить их адреса.
  • selfdestruct — как объект удаляли раньше и почему в современных версиях это почти не работает.

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

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

Видео по теме

ВидеоSolidity и смарт-контракты Ethereum, урок #4 | Типы данных array, bytes, enum, sIlya Krukowski
ВидеоSolidity и смарт-контракты Ethereum, урок #11 | Наследование, конструкторы, supeIlya Krukowski
ВидеоSolidity и смарт-контракты Ethereum, урок #36 | Ролевая система (access control)Ilya Krukowski
ВидеоConstructor | Solidity 0.8Smart Contract Programmer
ВидеоEnum | Solidity 0.8Smart Contract Programmer

Комментарии

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