Прокси и обновляемые контракты: delegatecall, UUPS и цена гибкости
Как устроены прокси-контракты, что делает delegatecall, чем отличаются Transparent и UUPS, зачем слоты EIP-1967 и почему Coin-Trust выбрал неизменяемый контракт.
В этой статье
Код развёрнутого контракта нельзя изменить. Это сила (правила не подменить) и слабость (найденную ошибку не исправить). Чтобы совместить стабильный адрес с возможностью обновления, придумали прокси. Но эта гибкость стоит дорого: она добавляет доверие к тому, кто может обновлять. Эта статья объясняет механику и компромисс. Основы контрактов - в статье что такое смарт-контракт.
Идея: адрес один, логика сменная
Вместо одного контракта их два:
- Прокси - тонкий контракт по постоянному адресу. Он хранит данные и ничего не умеет.
- Реализация (implementation) - контракт с кодом (логикой). Его можно заменить на новую версию.
Пользователь вызывает прокси. Тот перенаправляет вызов в текущую реализацию, но исполняет её код на своих данных. Для обновления владелец меняет адрес реализации в прокси: адрес и данные остаются прежними, а логика - новая.
delegatecall: чужой код на моих данных
Ключевая операция - delegatecall. Обычный вызов (call) исполняет код другого контракта в его контексте. А delegatecall исполняет чужой код в контексте вызывающего: тот же msg.sender, то же хранилище, тот же баланс.
// упрощённо
fallback() external payable {
address impl = _implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch ok case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) }
}
}Любой вызов, не найденный в самом прокси (fallback), пересылается в реализацию вместе с данными (формат вызова). Это работает на уровне EVM.
Где хранить адрес реализации
Адрес реализации тоже должен лежать в хранилище прокси. Если положить его в слот 0, он столкнётся с переменными реализации. Стандарт EIP-1967 определяет «случайный» слот:
slot = keccak256("eip1967.proxy.implementation") - 1
= 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbcСлот выбран через хеш, поэтому не пересекается с обычными переменными. Обозреватели блоков знают этот слот и показывают «Read as Proxy».
Виды прокси
| Схема | Где логика обновления | Особенность |
|---|---|---|
| Transparent | в прокси | админ не может вызывать логику, остальные не могут админить |
| UUPS | в реализации | дешевле в вызове, но новая версия должна содержать функцию обновления |
| Beacon | у «маяка» | один маяк обновляет сразу множество прокси |
| Minimal proxy (EIP-1167) | нет обновления | дешёвые клоны одной реализации |
Инициализаторы вместо конструктора
Конструктор исполняется при развёртывании реализации и записывает данные в её хранилище, а не в хранилище прокси. Поэтому в обновляемых контрактах вместо него используют обычную функцию initialize(), вызываемую через прокси один раз.
Совместимость раскладки хранилища
Новая реализация должна сохранять порядок и типы старых переменных. Если в версии 2 вставить новую переменную в середину, все последующие сместятся и будут читать чужие данные.
// v1
address owner; // слот 0
uint256 total; // слот 1
// v2 - ошибка: вставили поле в середину
address owner; // слот 0
uint256 fee; // слот 1 - теперь читает старое total!
uint256 total; // слот 2Правило: новые переменные - только в конец (или использовать резервные «пустые» слоты). Проверку раскладки автоматизируют инструменты.
Главный риск: доверие к тому, кто обновляет
Прокси превращают «код - закон» в «код - пока администратор не решил иначе». Тот, кто может сменить реализацию, может, в принципе, заменить логику на вредоносную и забрать средства. Поэтому доверяют не только коду, но и управлению. Меры:
- администратор - мультиподпись, а не один ключ;
- таймлок: обновление применяется с задержкой, и пользователи успевают выйти;
- публичные аудиты каждой новой версии (аудиты);
- со временем - «отказ от права обновления» (renounce) или передача управления сообществу.
Почему Coin-Trust неизменяем
Контракт эскроу развёрнут без прокси: его код нельзя изменить никем, включая разработчиков. Это сознательный выбор для сервиса, который держит чужие деньги:
- пользователям не нужно доверять будущим решениям администратора;
- исходный код верифицируется один раз и остаётся таким навсегда;
- нет класса уязвимостей с инициализаторами и раскладкой хранилища.
Цена выбора: ошибку нельзя исправить обновлением. Поэтому контракт сделан максимально простым и покрыт тестами (включая фаззинг), а у владельца есть только ограниченные административные права (права владельца). Новая версия означала бы новый контракт по новому адресу, а сделки в старом доработали бы по прежним правилам (адреса через CREATE2).
Как распознать прокси
- Обозреватель показывает вкладку «Read as Proxy» или адрес реализации.
- В коде есть
delegatecall,implementation,upgradeTo. - Читаемые функции контракта предельно малы, а основная логика - по другому адресу.
Перед использованием чужого проекта выясните, кто владеет правом обновления и есть ли таймлок.
Что почитать дальше
Узнайте, как строятся уникальные детерминированные адреса: CREATE2. Или как проверяют большие множества данных: деревья Меркла.
Читайте также
- 2 · Практика
Мультиподпись и таймлок: как управлять контрактом без единой точки отказа
Что такое мультиподпись (m из n) и таймлок, как работают кошельки вроде Safe, какие риски они закрывают, а какие нет, и как применять их для владельца и гаранта.
3 мин чтения - 3 · Под капотом
CREATE2: как один и тот же адрес контракта появляется сразу в четырёх сетях
Как работает CREATE2: формула адреса, salt и байткод, и почему контракт эскроу имеет один адрес в Ethereum, Base, Polygon и BNB Chain.
4 мин чтения
Упоминается в статьях
- Словарь терминов: блокчейн, кошельки, смарт-контракты и эскроу от A до Я
- Как читать Etherscan: проверяем контракт, транзакцию и события
- Как проверить смарт-контракт перед использованием: пошаговая инструкция
- Мультиподпись и таймлок: как управлять контрактом без единой точки отказа
- EVM изнутри: стек, память, storage, calldata и стоимость газа
- ABI: как вызов функции превращается в байты - селекторы, calldata и события
- Раскладка хранилища и упаковка переменных в Solidity
- Шаблоны контроля доступа в смарт-контрактах: owner, роли и стороны сделки
- Нестандартные токены: USDT без return, fee-on-transfer, блэклисты и decimals
- Каталог уязвимостей смарт-контрактов: 12 классических ошибок и защита от них
- Роллапы изнутри: батчи, доказательства мошенничества, ZK, блобы и вывод средств
- Аудиты и баг-баунти: как проверяют смарт-контракты и как читать отчёт
Готовы попробовать?
Создайте первую сделку за пару минут. Деньги блокируются в смарт-контракте и выплачиваются по правилам, которые видны заранее.

