EIP-7: DELEGATECALL — занимаем код, сохраняем свой контекст
Контракты хотели переиспользовать логику общей библиотеки, не копируя её байткод в каждый из них. Но обычный CALL выполняет эту логику в собственном хранилище библиотеки — бесполезно для библиотеки, которая должна работать с вашими данными. DELEGATECALL выполняет заимствованный код в контексте вызывающего — и тем самым тихо изобрёл библиотеки, прокси и обновляемые контракты.
- EVM и модель вызовов
- хранилище и контекст
Мы входим в Часть III с самого начала: Homestead, март 2016 года, первый запланированный апгрейд Ethereum. Он выпустил маленький опкод с огромными последствиями. К этому моменту (из главы про EVM) вы знаете, что контракт — это код плюс хранилище, и что один контракт может вызвать другой. Но вызов поднимает вопрос, на который самый ранний EVM ответил плохо: если я хочу переиспользовать код другого контракта — общую библиотеку проверенной логики — чьи данные затрагивает этот код? Ответите неправильно — и общие библиотеки невозможны; ответите правильно — и откроете прокси и обновляемые контракты. EIP-7 — это правильный ответ, DELEGATECALL. Давайте выведем его.
Проблема: общая логика без копипаста
Деплой контракта стоит газ, пропорциональный размеру его кода, а одни и те же рутины — безопасная математика, переводы токенов, контроль доступа — пишутся снова и снова. Копипастить байткод библиотеки в каждый контракт расточительно и неподдерживаемо: исправьте баг — и придётся передеплоить всех. Очевидное желание — единственный, проаудированный контракт-библиотека, задеплоенный один раз, чей код могут занимать многие контракты.
→ Шаг 2: смотрим, как обычный CALL попадает не в то хранилище.
Почему обычный CALL не годится для библиотеки
Когда ваш контракт делает обычный CALL к библиотеке, EVM переключается в контекст исполнения Аккаунт, чьё хранилище, баланс и адрес читает и пишет выполняющийся код, плюс msg.sender/msg.value. Обычный CALL переключает контекст на вызываемого; код выполняется «как бы от имени» вызываемого. библиотеки. Код библиотеки выполняется, но каждый SSTORE, который он делает, пишет в хранилище библиотеки, address(this) — это библиотека, а msg.sender — это ваш контракт. Так что метод библиотеки вроде balances[msg.sender] += amount исправно обновляет отображение балансов библиотеки — не ваше. Ваше хранилище вообще не затрагивается.
Код абсолютно исправен — он просто выполнился в мире не того аккаунта. На самом деле нам нужна обратная схема: код библиотеки, но наш контекст.
→ Шаг 3: DELEGATECALL.
DELEGATECALL: код вызываемого, всё остальное — вызывающего
DELEGATECALL Вызов EVM (EIP-7), который загружает код вызываемого и выполняет его целиком в контексте вызывающего: хранилище и баланс вызывающего, адрес вызывающего как this, и — в отличие от более старого CALLCODE — исходные msg.sender и msg.value сохранены. «Выполнить этот код так, будто он мой». — это в точности та схема, которая нам была нужна. Он загружает код цели, но выполняет его в вашем контексте: чтение и запись попадают в ваше хранилище, address(this) — это вы, баланс — ваш. И он чинит недостаток CALLCODE — он сохраняет исходные msg.sender и msg.value, так что заимствованный код видит того же вызывающего и то же значение, с которыми был вызван ваш контракт. Это в точности «выполнить код этого контракта так, будто он часть моего».
- 1 выполняемые инструкции — это байткод целевой библиотеки
- 2 хранилище, баланс, address(this) и исходные msg.sender/value — вызывающего
→ Шаг 4: отделяем состояние от логики — и обновляем.
Награда: библиотеки, прокси, обновляемые контракты
Как только код и контекст можно разделить, паттерны появляются сами. Вызовы library в Solidity компилируются в DELEGATECALL, так что общая логика живёт в блокчейне в одном экземпляре. Более того, можно разбить систему на тонкий прокси Минимальный контракт, который хранит всё состояние и при каждом вызове делает DELEGATECALL к отдельному контракту-реализации за логикой. Поскольку состояние живёт в прокси, замена адреса реализации обновляет поведение без перемещения данных — а адрес, который знают пользователи, никогда не меняется. , хранящий всё состояние, и отдельную реализацию, хранящую логику. Каждый вызов к прокси делегируется реализации, которая работает с хранилищем прокси. Чтобы обновиться, вы просто указываете прокси на новую реализацию — данные остаются на месте, а адрес, которым все пользуются, никогда не меняется.