Часть III · Глава 7 из 43 · Homestead

EIP-7: DELEGATECALL — занимаем код, сохраняем свой контекст

Контракты хотели переиспользовать логику общей библиотеки, не копируя её байткод в каждый из них. Но обычный CALL выполняет эту логику в собственном хранилище библиотеки — бесполезно для библиотеки, которая должна работать с вашими данными. DELEGATECALL выполняет заимствованный код в контексте вызывающего — и тем самым тихо изобрёл библиотеки, прокси и обновляемые контракты.

Обновлено 24 июн. 2026 г. · 11 мин
Предполагается
  • EVM и модель вызовов
  • хранилище и контекст

Мы входим в Часть III с самого начала: Homestead, март 2016 года, первый запланированный апгрейд Ethereum. Он выпустил маленький опкод с огромными последствиями. К этому моменту (из главы про EVM) вы знаете, что контракт — это код плюс хранилище, и что один контракт может вызвать другой. Но вызов поднимает вопрос, на который самый ранний EVM ответил плохо: если я хочу переиспользовать код другого контракта — общую библиотеку проверенной логики — чьи данные затрагивает этот код? Ответите неправильно — и общие библиотеки невозможны; ответите правильно — и откроете прокси и обновляемые контракты. EIP-7 — это правильный ответ, DELEGATECALL. Давайте выведем его.

1 шаг Переиспользование без копирования

Проблема: общая логика без копипаста

Деплой контракта стоит газ, пропорциональный размеру его кода, а одни и те же рутины — безопасная математика, переводы токенов, контроль доступа — пишутся снова и снова. Копипастить байткод библиотеки в каждый контракт расточительно и неподдерживаемо: исправьте баг — и придётся передеплоить всех. Очевидное желание — единственный, проаудированный контракт-библиотека, задеплоенный один раз, чей код могут занимать многие контракты.

все копируют один и тот же код 0x00… transfer() 0x11… transfer() 0x22… transfer() тот же bytecode — снова и снова одна реализация на всех app Aapp Bapp C Библиотека transfer() чьи данные трогает общий код?
Каждый контракт, несущий свою копию одной и той же логики перевода — это чистое дублирование: дорого деплоить и невозможно пропатчить в одном месте. Желание: задеплоить логику один раз как общую библиотеку и дать многим контрактам её выполнять. Но это поднимает настоящий вопрос — какое хранилище затрагивает общий код?

→ Шаг 2: смотрим, как обычный CALL попадает не в то хранилище.

2 шаг CALL выполняется не там

Почему обычный CALL не годится для библиотеки

Когда ваш контракт делает обычный CALL к библиотеке, EVM переключается в контекст исполнения Аккаунт, чьё хранилище, баланс и адрес читает и пишет выполняющийся код, плюс msg.sender/msg.value. Обычный CALL переключает контекст на вызываемого; код выполняется «как бы от имени» вызываемого. библиотеки. Код библиотеки выполняется, но каждый SSTORE, который он делает, пишет в хранилище библиотеки, address(this) — это библиотека, а msg.sender — это ваш контракт. Так что метод библиотеки вроде balances[msg.sender] += amount исправно обновляет отображение балансов библиотеки — не ваше. Ваше хранилище вообще не затрагивается.

CALL — выполнить библиотеку в её собственном контексте ваш контракт storage: empty ✗ никогда не записано CALL Библиотека balances[msg.sender] += x storage: WRITTEN here запись попадает в storage Library — msg.sender — ваш контракт бесполезно: library не оперирует вашими данными
Обычный CALL выполняет код библиотеки в контексте библиотеки: запись попадает в хранилище библиотеки, а для библиотеки msg.sender — это ваш контракт. Собственное хранилище вашего контракта никогда не записывается. Библиотека, изменяющая данные вызывающего, таким способом невозможна.

Код абсолютно исправен — он просто выполнился в мире не того аккаунта. На самом деле нам нужна обратная схема: код библиотеки, но наш контекст.

→ Шаг 3: DELEGATECALL.

3 шаг Занимаем код, сохраняем контекст

DELEGATECALL: код вызываемого, всё остальное — вызывающего

DELEGATECALL Вызов EVM (EIP-7), который загружает код вызываемого и выполняет его целиком в контексте вызывающего: хранилище и баланс вызывающего, адрес вызывающего как this, и — в отличие от более старого CALLCODE — исходные msg.sender и msg.value сохранены. «Выполнить этот код так, будто он мой». — это в точности та схема, которая нам была нужна. Он загружает код цели, но выполняет его в вашем контексте: чтение и запись попадают в ваше хранилище, address(this) — это вы, баланс — ваш. И он чинит недостаток CALLCODE — он сохраняет исходные msg.sender и msg.value, так что заимствованный код видит того же вызывающего и то же значение, с которыми был вызван ваш контракт. Это в точности «выполнить код этого контракта так, будто он часть моего».

DELEGATECALL — одолжить код, сохранить свой контекст ваш контракт storage: WRITTEN ✓ ваш баланс и caller runs as if it were yours заимствовать код выполняется здесь ↩ КОД библиотеки balances[msg.sender] += x только код, без storage те же опкоды, но читают/пишут ВАШИ storage, баланс и caller исправление CALLCODE: сохраняет исходные msg.sender и value
DELEGATECALL загружает код библиотеки, но выполняет его в вашем контексте: запись попадает в ВАШЕ хранилище, баланс и адрес — ваши, а исходные msg.sender/msg.value сохранены. Библиотека фактически становится дополнительными методами, прикрученными к вашему контракту.
Формула
DELEGATECALL: code = callee 1 · context = caller 2
  1. 1 выполняемые инструкции — это байткод целевой библиотеки
  2. 2 хранилище, баланс, address(this) и исходные msg.sender/value — вызывающего
Разделите то, что связывает вызов: берите код оттуда, оставляйте контекст здесь. В этом одном разделении — вся идея.

→ Шаг 4: отделяем состояние от логики — и обновляем.

4 шаг Прокси и апгрейды

Награда: библиотеки, прокси, обновляемые контракты

Как только код и контекст можно разделить, паттерны появляются сами. Вызовы library в Solidity компилируются в DELEGATECALL, так что общая логика живёт в блокчейне в одном экземпляре. Более того, можно разбить систему на тонкий прокси Минимальный контракт, который хранит всё состояние и при каждом вызове делает DELEGATECALL к отдельному контракту-реализации за логикой. Поскольку состояние живёт в прокси, замена адреса реализации обновляет поведение без перемещения данных — а адрес, который знают пользователи, никогда не меняется. , хранящий всё состояние, и отдельную реализацию, хранящую логику. Каждый вызов к прокси делегируется реализации, которая работает с хранилищем прокси. Чтобы обновиться, вы просто указываете прокси на новую реализацию — данные остаются на месте, а адрес, которым все пользуются, никогда не меняется.

состояние — в прокси, логика заменяема Proxy · 0xYou holds all state адрес никогда не меняется DELEGATECALL Реализация v1 the logic today Реализация v2 подмена логики — состояние то же логика меняется, данные на месте — так работают апгрейд-контракты та же идея, что EIP-7702 позже принёс обычным EOA
Прокси хранит всё состояние и делает DELEGATECALL своей логики к контракту-реализации. Замените реализацию с v1 на v2 — и поведение обновится, а состояние (и адрес, который знают пользователи) останутся ровно на месте. Так работают обновляемые контракты.
04 Глубже Куда двигаться дальше