Перейти к основному содержимому
Блокчейн11ГЛАВА 11Интерфейсы: договор между контрактамиVaultTokenIapprove → transferFromдоговор без тела

Интерфейсы: договор между контрактами

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

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

В задании 2026 года хранилище принимает депозиты в USDC. USDC — отдельный контракт, развёрнутый кем-то другим по своему адресу. Хранилище знает про него ровно одно: двадцать байт адреса.

А у адреса нет функций. Строка token.balanceOf(address(this)) при объявлении address token; не компилируется вовсе — компилятор не знает, что по этому адресу что-то умеет считать балансы.

Нужен договор: список функций, которые контракт по тому адресу обязуется предоставлять. Это и есть интерфейс. Из него же вырастает двухшаговый протокол «разрешить — списать», без которого не работает ни один перевод токенов в системе задания.

Интерфейс — список подписей без тел​

interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
}
Нажми на часть выражения

Внутри интерфейса разрешено немногое: подписи функций, события, свои ошибки, структуры и перечисления. Запрещено — тела функций, переменные состояния, конструктор, модификаторы и любая видимость, кроме external.

ЧТО МОЖНО И ЧЕГО НЕЛЬЗЯ В ИНТЕРФЕЙСЕможно· подписи функций без тел· только external· события и свои ошибки· структуры и перечисления· наследовать другой интерфейснельзя· тело функции· переменные состояния· конструктор· модификаторы· public или internalcontract X is IVault обязывает реализовать все функции договора — иначе «should be marked as abstract»приведение IERC20(addr) ничего не проверяет в сети: это обещание компилятору, а не сети
Интерфейс описывает только форму вызова; всё, что связано с состоянием и поведением, в нём запрещено

Приведение адреса к договору​

address tokenAddress = 0x5FbD…;
IERC20 token = IERC20(tokenAddress); // приведение типа
uint256 mine = token.balanceOf(address(this));

Запись IERC20(tokenAddress) не делает ничего в сети. Это приведение типа — такое же, как uint8(x) из главы про типы, только для адреса. Байты остаются те же двадцать; меняется лишь то, что компилятор теперь разрешает писать вызовы.

Важно

Из этого следует главное: приведение к интерфейсу — обещание компилятору, а не сети. Никакой проверки «а точно ли по этому адресу такой контракт» не происходит. Если адрес неверный, узнаете об этом только при вызове — и в самой неудобной форме.

Двухшаговый протокол: approve, потом transferFrom​

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

Разрешения хранятся в токене вложенным словарём: «владелец → кому → сколько». Отсюда два шага:

  1. Пользователь вызывает у токена: approve(vault, 1000) — «хранилищу можно списать с меня до 1000».
  2. Пользователь вызывает у хранилища: deposit(600) — а хранилище внутри вызывает у токена transferFrom(пользователь, себя, 600).

Первая модель проходит оба шага и показывает, что происходит:

── шаг 1: deposit(600) без approve ──
allowance(lender → vault): 0
откат: reverted with reason string 'MiniToken: не хватает разрешения (allowance)'
первые 4 байта данных отката: 0x08c379a0 — это код Error(string)
баланс lender после отката: 1000
deposits[lender] после отката: 0

── шаг 2: approve(vault, 1000), потом deposit(600) ──
allowance после approve: 1000
lender: 1000 → 400
vault: 0 → 600
deposits[lender]: 600
allowance после deposit: 400 (1000 − 600: разрешение расходуется)
газ deposit(600): 82171

── шаг 3: transfer(vault, 100) напрямую, мимо deposit ──
токенов на контракте (totalHeld): 700
deposits[lender]: 600 — хранилище о 100 не знает
allowance не тронут: 400
КОНТРАКТ НЕ БЕРЁТ ТОКЕНЫ САМ — ЕМУ ИХ РАЗРЕШАЮТпользовательтокенхранилище1. approve(vault, 1000)2. deposit(600)3. transferFromlender: 1000 → 400 · vault: 0 → 600allowance: 1000 → 400 — разрешение расходуетсягаз вызова с внешним переходом: 82 171без шага 1 депозит откатывается'MiniToken: не хватает разрешения'текст написал токен, получил его вызвавшийперевод напрямую на адрес хранилища деньги доставит, но учёт о них не узнает: 700 на балансе, 600 в записи
Разрешение выдают токену, депозит вызывают у хранилища; перевод напрямую доставит деньги, но мимо учёта

Три наблюдения.

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

Разрешение расходуется. После списания 600 из разрешённой тысячи осталось 400. Разрешение — это лимит, а не разовый пропуск.

Перевод напрямую ломает учёт. Если пользователь переведёт токены прямо на адрес хранилища, деньги придут, но deposit никто не вызывал — и запись не появится. На балансе 700, в учёте 600. Отсюда правило: хранилище считает свои записи, а не свой баланс.

Отвечено вопросов: 0 из 2
Пользователь вызвал deposit(600), не сделав approve. Что произойдёт?
Пользователь перевёл токены прямо на адрес хранилища, минуя deposit. Что в учёте?
Отвечено верно: 0 из 2

Возвращаемый признак надо проверять​

По стандарту переводы возвращают true или false, и стандарт прямо требует обрабатывать false. Компилятор к этому не принуждает:

token.transferFrom(msg.sender, address(this), amount); // так писать нельзя
require(token.transferFrom(msg.sender, address(this), amount), "…"); // а так правильно

Первая строка компилируется без единого предупреждения. Токен, который в случае неудачи возвращает false вместо отката, тихо ничего не переведёт — а хранилище запишет депозит.

Три способа ошибиться адресом — три разных отката​

Вторая модель подсовывает хранилищу неверные адреса и вызывает через интерфейс:

── 1. адрес обычного аккаунта (EOA), кода нет ──
код по адресу, байт: 0
deposit(): Transaction reverted: function returned an unexpected amount of data
данные отката: 0x
pokeToken(): Transaction reverted: function call to a non-contract account

── 2. контракт есть, а функции transferFrom нет ──
код по адресу, байт: 63
deposit(): Transaction reverted: function selector was not recognized
and there's no fallback function

── 3. transferFrom есть, но не возвращает bool ──
код по адресу, байт: 862
deposit(): Transaction reverted: function returned an unexpected amount of data
баланс lender в NoBoolToken после всего: 1000 —
списание внутри откатилось вместе со всем вызовом
ОДИН ИНТЕРФЕЙС, ТРИ НЕВЕРНЫХ АДРЕСА — ТРИ РАЗНЫХ ОТКАТАобычный аккаунт, кода 0 байтfunction returned an unexpected amount of dataконтракт есть, нужной функции нетfunction selector was not recognized and there's no fallback functionфункция есть, но не возвращает boolfunction returned an unexpected amount of dataни у одного отката нет причины: данные пустые, текста нет — интерфейс ничего не проверяет заранеетретий случай — настоящий USDT: селектор совпал, перевод внутри прошёл, но ответ не той длины откатил всё
У всех трёх откатов пустые данные: причины нет, потому что интерфейс ничего не проверяет заранее

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

Третий случай самый коварный, потому что это не выдумка: так устроен реальный крупный токен. Функция transferFrom у него есть и работает, но ничего не возвращает. Селектор совпал, перевод внутри прошёл — а вызывающий ждал 32 байта ответа, получил ноль и откатил всю транзакцию вместе с переводом.

Отсюда практический вывод для площадки: свои токены пишите строго по стандарту, с returns (bool). Если приходится работать с чужим — используют обёртку, которая умеет обрабатывать оба поведения.

Интерфейс, абстрактный контракт и наследование​

Три похожие вещи, которые легко перепутать:

Есть тела функцийПеременные состоянияМожно развернуть
interfaceнетнетнет
abstract contractчастичноданет
contractдадада

Контракт может объявить, что выполняет договор: contract MiniToken is IERC20. Тогда компилятор проверит, что все функции интерфейса реализованы. Пропустили одну:

TypeError: Contract "SimpleVault" should be marked as abstract.
Note: Missing implementation:
function depositOf(address account) external view returns (uint256);

Компилятор прямо показывает, чего не хватает.

Третья модель прогоняет через компилятор семь неправильных интерфейсов:

── 1. функция с телом в интерфейсе ──
TypeError: Functions in interfaces cannot have an implementation.

── 2. переменная состояния в интерфейсе ──
TypeError: Variables cannot be declared in interfaces.

── 3. public вместо external ──
TypeError: Functions in interfaces must be declared external.

── 4. конструктор в интерфейсе ──
TypeError: Constructor cannot be defined in interfaces.

── 5. контракт не реализовал функцию интерфейса ──
TypeError: Contract "SimpleVault" should be marked as abstract.
Note: Missing implementation: …

Один договор — все токены системы​

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

contract Market {
IERC20 public loanToken;
IERC20 public collateralToken;

constructor(address _loan, address _collateral) {
loanToken = IERC20(_loan);
collateralToken = IERC20(_collateral);
}
}

Контракт хранит адрес, а не тип токена. Поменялся токен — поменялся адрес при развёртывании, код остался прежним. Так же маркет разговаривает с хранилищем, а дашборд — со всеми контрактами сразу: через договоры.

Отвечено вопросов: 0 из 2
Что делает запись IERC20(tokenAddress)?
Почему у хранилища поле объявлено как IERC20, а не как конкретный контракт?
Отвечено верно: 0 из 2

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

Тело функции в интерфейсе. Functions in interfaces cannot have an implementation.

Переменная состояния в интерфейсе. Variables cannot be declared in interfaces.

public вместо external. Functions in interfaces must be declared external.

Конструктор в интерфейсе. Constructor cannot be defined in interfaces.

Контракт не реализовал функцию договора. Contract "X" should be marked as abstract плюс подсказка, какой именно функции не хватает.

Вызов функции у переменной типа address. Компилятор не знает о такой функции — нужно приведение к интерфейсу.

Результат перевода не проверен. Компилируется молча; токен вернул false, перевода не было, запись появилась.

Депозит без предварительного разрешения. Откат с текстом токена — и это правильное поведение, а не ошибка контракта.

Учёт по балансу вместо записей. Перевод напрямую на адрес контракта ломает такой учёт мгновенно.

Слова главы​

  • interface — договор: подписи функций без тел; развернуть нельзя.
  • Подпись функции (сигнатура) — имя и типы параметров; из неё считается селектор.
  • IERC20(addr) — приведение адреса к договору; в сети не проверяется ничего.
  • Внешний вызов — переход исполнения в другой контракт; там msg.sender — адрес вызывающего контракта.
  • allowance — разрешение «владелец → кому → сколько»; расходуется при списании.
  • approve / transferFrom — два шага протокола: разрешить, затем списать.
  • is IERC20 — контракт обязуется реализовать все функции договора.
  • Абстрактный контракт — между интерфейсом и контрактом: есть состояние и часть тел.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 100 зн/мин, точность ≥ 88%

interface IERC20 {

На чемпионате​

Модуль А, 37 баллов. Демонстрация идёт по шагам вида «зайти под Lender1 и внести депозит 1000 USDC в Vault1, показать изменения всех затронутых балансов». За этим шагом стоит вся эта глава: разрешение, вызов депозита, внешний вызов токена, изменение трёх чисел — баланс пользователя, баланс хранилища, запись учёта.

Практические выводы:

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

Модуль В, 14 баллов. Отсюда идут пункты про непроверенный результат перевода (информационная безопасность), про учёт по балансу вместо записей (логика) и про жёстко зашитые адреса (оптимизация и сопровождение).

Отвечено вопросов: 0 из 2
Эксперт выполняет шаг «внести депозит 1000 USDC» и видит откат. Что забыли?
Почему нельзя считать депозиты по балансу контракта?
Отвечено верно: 0 из 2

Что дальше​

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

Отвечено вопросов: 0 из 1
Что общего у трёх откатов при неверном адресе токена?
Отвечено верно: 0 из 1

Углубиться​

  • Проверка существования контракта — почему у функций без возвращаемого значения текст отката другой и что изменилось в версии 0.8.10.
  • Обёртки для нестандартных токенов — как библиотеки обрабатывают токены без returns (bool).
  • try/catch при внешнем вызове — как не упасть вместе с чужим контрактом.
  • Селектор из подписи — почему transferFrom(address,address,uint256) даёт именно эти четыре байта.
  • Обнуление разрешения — известная тонкость: некоторые токены требуют сначала approve(0), и только потом новое значение.

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

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

Видео по теме

ВидеоSolidity и смарт-контракты Ethereum, урок #12 | Интерфейсы, библиотеки, тестыIlya Krukowski
ВидеоИнтерфейсы в Solidity: Пишем работающий обменник токенов с нуляGuide DAO
ВидеоSolidity | Урок 9. interfacesАртем Кукуруза
Видео4007 Язык Solidity Часть 5 Вызов функций из других смарт контрактовProgramming Innopolis STEM
ВидеоSolidity Interface Tutorial | How To Connect The Lego Bricks Of DeFiJames Bachini

Комментарии

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