Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Апгрейдируемые контракты
upgradeable_contracts

Апгрейдируемые контракты

Proxy-паттерн, Transparent vs UUPS, столкновения слотов хранения, Initializable, OpenZeppelin Upgrades

Апгрейдируемые контракты

Неизменяемость кода — основа доверия в блокчейне. Апгрейдируемость — необходимость бизнеса. Proxy-паттерн примиряет эти противоречия

#Зачем нужна апгрейдируемость

Задеплоенный контракт нельзя изменить. Если обнаружена уязвимость, баг или нужна новая функция — без proxy придётся деплоить новый контракт и мигрировать всё состояние (данные пользователей, балансы, настройки).

Апгрейдируемость через proxy позволяет: исправлять баги без миграции данных, добавлять функции, изменять логику — при этом адрес контракта и его storage остаются неизменными.

Цена: пользователи должны доверять владельцу прокси. Реальная безопасность требует multisig (Gnosis Safe) или DAO governance для управления обновлениями.

#Как работает Proxy Pattern

Идея: разделить хранилище (proxy) и логику (implementation).

Proxy-контракт имеет простой fallback: он перенаправляет все вызовы в implementation через delegatecall. При delegatecall код implementation выполняется, но в контексте storage proxy. Таким образом:

  • Storage данные хранятся в proxy (адрес неизменен)
  • Логика в implementation (можно заменить)
User → Proxy (addr: 0xABCD, storage) → delegatecall → Implementation (logic)
                                                         (выполняется с storage 0xABCD)

Минималистичный proxy (EIP-1967):

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract SimpleProxy { // EIP-1967 стандартный слот для адреса implementation // keccak256("eip1967.proxy.implementation") - 1 bytes32 private constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; constructor(address implementation, bytes memory data) { _setImplementation(implementation); if (data.length > 0) { (bool ok,) = implementation.delegatecall(data); require(ok, "Init failed"); } } fallback() external payable { address impl = _getImplementation(); assembly { // Копируем calldata в память calldatacopy(0, 0, calldatasize()) // delegatecall: выполняем код impl в нашем storage let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0) // Копируем return data returndatacopy(0, 0, returndatasize()) // Возвращаем или revert switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } function _getImplementation() internal view returns (address impl) { assembly { impl := sload(IMPL_SLOT) } } function _setImplementation(address newImpl) internal { assembly { sstore(IMPL_SLOT, newImpl) } } }

#Проблема столкновения слотов (Storage Collision)

Это главная ловушка proxy-паттерна. Если proxy хранит address implementation в слоте 0, а implementation тоже имеет переменную в слоте 0 — они перезапишут друг друга.

Решение EIP-1967: использовать псевдослучайные слоты для метаданных proxy (admin, implementation address). Слот вычисляется как keccak256(string) - 1 — практически невозможно случайно попасть в тот же слот.

#Transparent Proxy vs UUPS

Существует две основные архитектуры апгрейдируемых контрактов:

Transparent Proxy (OpenZeppelin): администратор прокси и пользователи не могут вызывать одни и те же функции. Логика апгрейда живёт в прокси. ProxyAdmin-контракт управляет апгрейдами.

// Пользователь → Proxy → delegatecall → Implementation // Admin → Proxy → прямой вызов upgradeToAndCall

Недостаток: лишняя проверка на каждый вызов (кто обращается — admin или пользователь?), дороже на газ.

UUPS (Universal Upgradeable Proxy Standard, EIP-1822): логика апгрейда живёт в implementation. Proxy проще и дешевле. При обновлении нужно убедиться, что новая implementation тоже содержит логику апгрейда — иначе контракт станет неапгрейдируемым навсегда.

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract TokenV1 is OwnableUpgradeable, UUPSUpgradeable { uint256 public value; // Вместо constructor — initialize (вызывается только один раз при деплое proxy) function initialize(address initialOwner) public initializer { __Ownable_init(initialOwner); __UUPSUpgradeable_init(); } function setValue(uint256 _value) external onlyOwner { value = _value; } // Только owner может апгрейдить function _authorizeUpgrade(address) internal override onlyOwner {} }

#Initializable вместо Constructor

В апгрейдируемых контрактах нельзя использовать constructor — он выполняется в контексте implementation контракта, а не proxy. Вместо этого используется initialize функция с модификатором initializer (из OpenZeppelin).

initializer гарантирует, что функция вызывается ровно один раз (хранит флаг _initialized в storage). Это аналог конструктора, но выполняется через delegatecall в proxy.

Важно: при наследовании нескольких Upgradeable контрактов (OwnableUpgradeable, PausableUpgradeable) — вызывайте __Parent_init() в своём initialize, иначе родительское состояние не инициализируется.

#Деплой через OpenZeppelin Hardhat Upgrades

На практике используют плагин, который автоматически управляет proxy и проверяет совместимость storage layout:

// scripts/deploy.js const { ethers, upgrades } = require("hardhat"); async function main() { const TokenV1 = await ethers.getContractFactory("TokenV1"); // Деплоит proxy + implementation, вызывает initialize const proxy = await upgrades.deployProxy(TokenV1, [owner.address], { initializer: "initialize", kind: "uups" }); console.log("Proxy deployed to:", await proxy.getAddress()); } // scripts/upgrade.js async function upgrade() { const TokenV2 = await ethers.getContractFactory("TokenV2"); // Проверяет совместимость storage layout перед обновлением await upgrades.upgradeProxy(proxyAddress, TokenV2); }

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