Прокси-контракты в Solidity: delegatecall и storage
Иногда продукт хочет сохранять адрес приложения, меняя исполняемую реализацию. Для этого применяют прокси. Пользователь вызывает адрес прокси, а прокси делегирует обработку коду другого адреса. Такой механизм не возникает в базовом CourseCatalog: ранее созданный обычный экземпляр не становится прокси из-за новой настройки сайта.
Разделим три понятия: адрес интерфейса для пользователей, адрес implementation с кодом и субъект, который разрешает смену implementation. Стабильность первого не означает неизменность поведения. Наоборот, владелец пути обновления может существенно изменить будущие правила, поэтому эта власть должна быть видна пользователю.
Контекст delegatecall
При обычном вызове другого контракта его код работает с собственным состоянием. При delegatecall код implementation выполняется в контексте вызывающего прокси: address(this), storage и баланс относятся к прокси, а непосредственный msg.sender исходного вызова сохраняется в этом переходе.
R → Proxy P → delegatecall к Implementation I
код берётся из I
address(this) внутри делегированного кода = P
прикладное storage = P
msg.sender для прямого пользовательского вызова = R
Это схема модели исполнения. Она не означает, что любой вложенный будущий вызов навсегда сохранит R. Если implementation-код выполняет обычный call к target, непосредственным caller target становится P. Как в маршруте Timelock, каждый переход анализируется по фактическому виду вызова.
Место implementation
ERC-1967 описывает выделенные слоты для служебных адресов. В выбранном ERC1967Proxy адрес implementation хранится в таком слоте, чтобы не занимать обычное прикладное начало storage. Это уменьшает известный риск прямого столкновения со стандартным размещением полей приложения.
Но выделенный служебный слот не проверяет совместимость всех прикладных полей между версиями. Если V2 поменяет порядок обычных переменных, прежние байты всё ещё лежат в storage P и могут получить неправильный смысл. Прокси не переносит их по названиям переменных исходника.
Пример конструктора ниже является фрагментом будущего создания, а не выполненной транзакцией. implementation и initData должны быть заранее определены корректными адресом и ABI-байтами.
ERC1967Proxy proxy = new ERC1967Proxy(implementation, initData);
Выбранный конструктор устанавливает реализацию и при непустом initData выполняет её код через delegatecall. Это позволяет настроить storage прокси атомарно с его созданием. Инициализация, её полномочие и вызов нужны отдельно; конструктор implementation не записывает состояние всех будущих прокси.
UUPS и отдельное полномочие
В UUPS путь смены реализации находится в implementation-коде. OpenZeppelin UUPSUpgradeable требует определить _authorizeUpgrade. В уроке дальше используем толькоOwner как конкретную учебную политику. Другая система могла бы доверять Timelock, но это иной выбранный субъект, а не автоматическая характеристика UUPS.
Библиотека проверяет контекст вызова onlyProxy и предусмотренный proxiableUUID новой реализации. Такая проверка защищает определённую форму совместимости механизма обновления. Она не доказывает правильность логики новой версии, сохранение ролей, корректность математики или совместимость всех записанных структур.
Transparent proxy использует другую модель администрирования. Нельзя смешивать инструкции о ProxyAdmin с UUPS-кодом и считать, что одинаковое слово upgrade делает маршруты взаимозаменяемыми. Этот курс сохраняет одну выбранную ветвь, чтобы субъект проверки не потерялся среди похожих терминов.
Чтение через правильный адрес
Публичные getters приложения вызываются по адресу P с ABI активной реализации. Прямой getter по адресу I читает собственное storage I, которое может быть пустым или специально заблокированным. Нулевой owner у implementation не доказывает нулевого owner у прокси.
Также события, испущенные делегированным кодом, относятся к адресу P. Индексатор должен искать их там, а не только у I. Если frontend после обновления начнёт читать implementation как новый пользовательский адрес, он потеряет сохранённый прикладной контекст и может показать неверную библиотеку.
Полезно хранить в техническом отчёте оба адреса. Пользовательская постоянная ссылка ведёт к P, а provenance исполняемой версии указывает I и закреплённый исходник. История смены реализации объясняет, почему один адрес на разных блоках мог исполнять разные правила.
Immutable и конструктор
Immutable значения implementation относятся к её скомпилированному коду. Если несколько прокси используют ту же реализацию, они не получают разные immutable из своего storage только потому, что их initialize был вызван с разными параметрами. Поэтому исходный immutable publisher нельзя механически перенести как индивидуально настраиваемого владельца каждой прокси.
Обычные начальные присваивания переменным и конструктор приложения также выполняются при создании implementation, а не повторно при каждом будущем делегированном вызове. Upgradeable-модули предоставляют отдельные initializer функции. В следующем уроке это различие станет полным конкретным контрактом.
Когда обновление лишнее
Для простого реестра с небольшой областью полномочий новый обычный экземпляр и явная миграция могут оказаться понятнее. Прокси добавляет долговременный договор совместимости storage, инициализации и администратора обновлений. Его используют ради определённого требования, а не как обязательный признак современного Solidity.
Стабильный адрес удобен для интеграций, но не освобождает клиент от наблюдения изменений ABI и правил. Новая версия может добавить функцию или изменить предметную интерпретацию возвращаемых данных. Если интерфейс не готов, прежний URL продолжит существовать, но его поведение станет несовместимым.
Для самостоятельного разбора сравните прямой вызов I, вызов P с delegatecall и обычный call из P к CourseCatalog. Укажите code address, storage address и непосредственный caller каждой целевой проверки. Затем объясните, почему сохранение адреса P не означает отсутствия новой власти над его будущим поведением. Эти рассуждения остаются локальными, без развертывания и выполнения примеров.
ABI и постоянный адрес
Прокси перенаправляет байты calldata, но не объясняет их смысл. Клиент должен использовать ABI активного приложения и понимать возвращаемые данные. При добавлении новой функции старое ABI не научится ей автоматически. При изменении семантики существующей функции даже прежний selector не доказывает неизменность предметного результата.
Поэтому постоянный адрес нужно сопровождать сведениями о версии. Для собственного сайта можно сохранять историю implementation и соответствующие артефакты отдельно от публичных путей статей. Если читатель открывает урок о V1, он должен видеть, к какой модели относится листинг, даже когда действующая демонстрация уже показывает V2.
Для самостоятельного уточнения: eip-1967, introduction-to-smart-contracts.html, ERC1967Proxy.sol (5.4.0), UUPSUpgradeable.sol (5.4.0).
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.