Паттерн Diamond Proxy
Есть еще один вид прокси контрактов под названием Diamond. В этом уроке мы его не рассматриваем, но упомянуть стоит.
Diamond используется в крупных проектах, где существует большое количество смарт-контрактов, которые нужно иногда обновлять.
В этом случае исполняемый контракт разбивается не несколько более мелких контрактов, которые и управляются через прокси.
#proxy #diamond
Есть еще один вид прокси контрактов под названием Diamond. В этом уроке мы его не рассматриваем, но упомянуть стоит.
Diamond используется в крупных проектах, где существует большое количество смарт-контрактов, которые нужно иногда обновлять.
В этом случае исполняемый контракт разбивается не несколько более мелких контрактов, которые и управляются через прокси.
#proxy #diamond
Простая идея реализации прокси контракта
В начале урока лектор приводит пример простой реализации прокси контракта. Так вот, его использовать не надо! Он показал его просто для того, чтобы вы поняли основную суть данного паттерна.
Я просто повторю основные моменты тут.
Есть контракт Proxy, в котором всего три функции: setImplementation(), _delegate() и fallback().
В первой устанавливается контракт, куда мы будем передавать все действия.
В _delegate() как раз прописывается логика отправки запроса и получения ответа в исполняемый контракт при помощи delegatecall.
Тут интересно то, что написана она с помощью assembly. Мы принимает селектор функции из исполняемого контракта (берем его из памяти), затем передаем через delegatecall в другой контракт и принимаем оттуда ответ. Если приходит "0", то показываем ошибку, так как данных нет, а значит в другом контракте что-то пошло не так. Если данные получены, то показываем их.
Также интерес представляет функция fallback(). Как мы помним, она вызывается в том случае, когда в контракте нет функции с таким именем, которую пытаются тут вызвать. Следовательно, в этом случае вызывается fallback, которая в свою очередь вызывает _delegate.
И вот по этой цепочке и происходит работа прокси. При этом все данные остаются тут, а не в исполняемом контракте.
Это самый простой способ работы с прокси, поэтому еще раз повторяю, что его не следует использовать в реальных проектах!
#proxy #upgradeable #transparent #uups
В начале урока лектор приводит пример простой реализации прокси контракта. Так вот, его использовать не надо! Он показал его просто для того, чтобы вы поняли основную суть данного паттерна.
Я просто повторю основные моменты тут.
Есть контракт Proxy, в котором всего три функции: setImplementation(), _delegate() и fallback().
В первой устанавливается контракт, куда мы будем передавать все действия.
В _delegate() как раз прописывается логика отправки запроса и получения ответа в исполняемый контракт при помощи delegatecall.
Тут интересно то, что написана она с помощью assembly. Мы принимает селектор функции из исполняемого контракта (берем его из памяти), затем передаем через delegatecall в другой контракт и принимаем оттуда ответ. Если приходит "0", то показываем ошибку, так как данных нет, а значит в другом контракте что-то пошло не так. Если данные получены, то показываем их.
Также интерес представляет функция fallback(). Как мы помним, она вызывается в том случае, когда в контракте нет функции с таким именем, которую пытаются тут вызвать. Следовательно, в этом случае вызывается fallback, которая в свою очередь вызывает _delegate.
И вот по этой цепочке и происходит работа прокси. При этом все данные остаются тут, а не в исполняемом контракте.
Это самый простой способ работы с прокси, поэтому еще раз повторяю, что его не следует использовать в реальных проектах!
#proxy #upgradeable #transparent #uups
👍1
Наследование из openzeppelin
Прежде чем использовать контракты из openzeppelin, нужно сначала установить их в свой проект.
Сделать это можно двумя способами. И так как тема у нас про обновляемые контракты, то сразу покажу установку для них.
1. Вам необходимо прописать следующие строки в консоли:
npm install @openzeppelin/contracts
npm install --save-dev @openzeppelin/hardhat-upgrades
npm install --save-dev @nomiclabs/hardhat-ethers ethers # peer dependencies
На случай, если возникнут проблемы с версиями пакетов, вы всегда можете посмотреть актуальные команды по ссылкам тут и тут.
2. Можно также зайти в файл package.json в папке своего проекта, и в блоке dependencies прописать:
"@openzeppelin/contracts": "^4.7.2",
"@openzeppelin/contracts-upgradeable": "^4.7.2"
Затем зайти в консоль и выполнить команду:
npm install
После чего у вас установятся все необходимые пакеты.
При этом после добавления openzeppelin/contracts-upgradeable вам следует открыть файл hardhat.config.json в своем проекте и добавить строки в начале:
import "@openzeppelin/hardhat-upgrades";
(если вы используете typescript)
или
require('@openzeppelin/hardhat-upgrades');
(если используете javascript)
После этого уже в файле своего контракта вы сможете импортировать контракты openzeppelin и наследовать от них. Для этого нужно будет добавлять следующие строки (как в примере урока):
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
и т.д.
#proxy #upgradeable #npm #openzeppelin
Прежде чем использовать контракты из openzeppelin, нужно сначала установить их в свой проект.
Сделать это можно двумя способами. И так как тема у нас про обновляемые контракты, то сразу покажу установку для них.
1. Вам необходимо прописать следующие строки в консоли:
npm install @openzeppelin/contracts
npm install --save-dev @openzeppelin/hardhat-upgrades
npm install --save-dev @nomiclabs/hardhat-ethers ethers # peer dependencies
На случай, если возникнут проблемы с версиями пакетов, вы всегда можете посмотреть актуальные команды по ссылкам тут и тут.
2. Можно также зайти в файл package.json в папке своего проекта, и в блоке dependencies прописать:
"@openzeppelin/contracts": "^4.7.2",
"@openzeppelin/contracts-upgradeable": "^4.7.2"
Затем зайти в консоль и выполнить команду:
npm install
После чего у вас установятся все необходимые пакеты.
При этом после добавления openzeppelin/contracts-upgradeable вам следует открыть файл hardhat.config.json в своем проекте и добавить строки в начале:
import "@openzeppelin/hardhat-upgrades";
(если вы используете typescript)
или
require('@openzeppelin/hardhat-upgrades');
(если используете javascript)
После этого уже в файле своего контракта вы сможете импортировать контракты openzeppelin и наследовать от них. Для этого нужно будет добавлять следующие строки (как в примере урока):
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
и т.д.
#proxy #upgradeable #npm #openzeppelin
OpenZeppelin Docs
Contracts
The official documentation for OpenZeppelin Libraries and Tools
Upgradeable ERC721. Часть 1
Отдельный урок про ERC721 у нас будет на следующей неделе, так как потребуется пара дней, чтобы научиться работать с ним. А сейчас мы, на его примере, рассмотрим, как работают обновляемые контракты.
Для начала нам нужно импортировать все необходимые обновляемые контракты с openzeppelin, а также прокси контракт Initializable.
Коммит по уроку можно посмотреть тут.
Так как конструкторы, как я понял из урока, в обновляемых контракта могут работать некорректно, было принято решение заменить его новой функцией initialize() с модификатором initializer. По правилам она вызывается только один раз при разворачивании контракта, поэтому ее можно как бы назвать неким конструктором обновляемых контрактов.
И уже в этой функции мы можем другие функции, с помощью которых можно установить владельца контракта или передать данные о токене, например, в ERC721.
Также необходимо функции, в которых используются обновляемые контракты, также обновить.
#proxy #upgradeable #transparent
Отдельный урок про ERC721 у нас будет на следующей неделе, так как потребуется пара дней, чтобы научиться работать с ним. А сейчас мы, на его примере, рассмотрим, как работают обновляемые контракты.
Для начала нам нужно импортировать все необходимые обновляемые контракты с openzeppelin, а также прокси контракт Initializable.
Коммит по уроку можно посмотреть тут.
Так как конструкторы, как я понял из урока, в обновляемых контракта могут работать некорректно, было принято решение заменить его новой функцией initialize() с модификатором initializer. По правилам она вызывается только один раз при разворачивании контракта, поэтому ее можно как бы назвать неким конструктором обновляемых контрактов.
И уже в этой функции мы можем другие функции, с помощью которых можно установить владельца контракта или передать данные о токене, например, в ERC721.
Также необходимо функции, в которых используются обновляемые контракты, также обновить.
#proxy #upgradeable #transparent
👍1
Upgradeable ERC721. Часть 2
По умолчанию все обновляемые контракты используют transparent proxy. Для того, чтобы переключиться на UUPS proxy нам нужно сделать следующие действия.
1. Добавить в импортируемые файлы с openzeppelin строку
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
2. Добавить наследование от UUPS в наш контракт;
3. В функции initialize() добавить функцию __UUPSUpgradeable_init(), чтобы при разворачивании система поняла, что мы используем именно UUPS;
4. Также вспоминаем, что в случае UUPS proxy все права администратора записываются в обновляемом контракте, т.е. том, который пишем мы. Поэтому нужно добавить функцию администрирования.
function _authorizeUpgrade(address newImplementation) internal onlyOwner override {}
Этих действий будет достаточно, чтобы развернуть UUPS прокси к нашему контракту.
#proxy #upgradeable #uups
По умолчанию все обновляемые контракты используют transparent proxy. Для того, чтобы переключиться на UUPS proxy нам нужно сделать следующие действия.
1. Добавить в импортируемые файлы с openzeppelin строку
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
2. Добавить наследование от UUPS в наш контракт;
3. В функции initialize() добавить функцию __UUPSUpgradeable_init(), чтобы при разворачивании система поняла, что мы используем именно UUPS;
4. Также вспоминаем, что в случае UUPS proxy все права администратора записываются в обновляемом контракте, т.е. том, который пишем мы. Поэтому нужно добавить функцию администрирования.
function _authorizeUpgrade(address newImplementation) internal onlyOwner override {}
Этих действий будет достаточно, чтобы развернуть UUPS прокси к нашему контракту.
#proxy #upgradeable #uups
Деплой Upgradeable ERC721. Часть 3
Отдельно деплой контрактов мы будем разбирать на следующей неделе, а пока пройдемся по коду из урока.
В начале файла лектор подключает три импорта:
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers, upgrades } from "hardhat";
С "chai" и "ethers" мы уже знакомы, поэтому остановлюсь на двух других.
"upgrades" - это плагин, который стал доступен, когда мы установили пакет hardhat-upgrades. Он помогает разворачивать и тестировать прокси контракты.
"loadFixture" - это также новый плагин, который был добавлен в toolbox. Лектор, говорит, что это новая фича в hardhat, но на момент, когда мы учились устанавливать среду разработки, он уже был доступен и я рассказывал, как его установить.
В данном тесте у нас нет обычной функции beforeEach(), которая вызывалась перед каждым тестом. В этом случае мы пишем отдельную функцию dep() (или деплой).
Также используем ethers.getSigners(), чтобы получить адрес пользователя, который разворачивает контракт, а также getContractFactory(), чтобы указать, с каким контрактом мы работаем.
И вот дальше мы используем плагин "upgrades", для деплоя прокси контракта, где в аргументах передаем разворачиваемый контракт, аргументы для функции initialize() в нашем контракте, если необходимо, и набор опций для прокси:
const token = await upgrades.deployProxy(NFTFactory, [], {
initializer: 'initialize',
kind: 'uups',
});
Если мы работаем с UUPS контрактами, то добавляем сюда kind: "uups". Если же с transparent proxy, то оставляем только initializer.
Из этой функции деплоя нам нужно вернуть контракт и адрес деплоера.
Далее в тестах, с помощью плагина loadFixture(), мы обращаемся к функции нашего деплоя, откуда получаем token (наш прокси) и deployer.
const { token, deployer } = await loadFixture(dep);
После этого можно писать тесты, как мы уже делали раньше.
Далее посмотрим, как работать с обновленным контрактом в тестах.
Для начала его также нужно получить через getContractFactory(), и уже потом использовать плагин upgrades, чтобы обновить прокси, передав в него адрес прокси и новый контракт.
const token2 = await upgrades.upgradeProxy(token.address, NFTFactoryv2);
После этого мы можем писать тесты уже для обновленного контакта.
P.S. Для меня сейчас деплой контрактов также немного сложная тема. Потребуется некоторое время и практика, чтобы понять суть и свободно ориентироваться в коде.
#proxy #upgradeable #transparent #uups #deploy
Отдельно деплой контрактов мы будем разбирать на следующей неделе, а пока пройдемся по коду из урока.
В начале файла лектор подключает три импорта:
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers, upgrades } from "hardhat";
С "chai" и "ethers" мы уже знакомы, поэтому остановлюсь на двух других.
"upgrades" - это плагин, который стал доступен, когда мы установили пакет hardhat-upgrades. Он помогает разворачивать и тестировать прокси контракты.
"loadFixture" - это также новый плагин, который был добавлен в toolbox. Лектор, говорит, что это новая фича в hardhat, но на момент, когда мы учились устанавливать среду разработки, он уже был доступен и я рассказывал, как его установить.
В данном тесте у нас нет обычной функции beforeEach(), которая вызывалась перед каждым тестом. В этом случае мы пишем отдельную функцию dep() (или деплой).
Также используем ethers.getSigners(), чтобы получить адрес пользователя, который разворачивает контракт, а также getContractFactory(), чтобы указать, с каким контрактом мы работаем.
И вот дальше мы используем плагин "upgrades", для деплоя прокси контракта, где в аргументах передаем разворачиваемый контракт, аргументы для функции initialize() в нашем контракте, если необходимо, и набор опций для прокси:
const token = await upgrades.deployProxy(NFTFactory, [], {
initializer: 'initialize',
kind: 'uups',
});
Если мы работаем с UUPS контрактами, то добавляем сюда kind: "uups". Если же с transparent proxy, то оставляем только initializer.
Из этой функции деплоя нам нужно вернуть контракт и адрес деплоера.
Далее в тестах, с помощью плагина loadFixture(), мы обращаемся к функции нашего деплоя, откуда получаем token (наш прокси) и deployer.
const { token, deployer } = await loadFixture(dep);
После этого можно писать тесты, как мы уже делали раньше.
Далее посмотрим, как работать с обновленным контрактом в тестах.
Для начала его также нужно получить через getContractFactory(), и уже потом использовать плагин upgrades, чтобы обновить прокси, передав в него адрес прокси и новый контракт.
const token2 = await upgrades.upgradeProxy(token.address, NFTFactoryv2);
После этого мы можем писать тесты уже для обновленного контакта.
P.S. Для меня сейчас деплой контрактов также немного сложная тема. Потребуется некоторое время и практика, чтобы понять суть и свободно ориентироваться в коде.
#proxy #upgradeable #transparent #uups #deploy
Уязвимости с прокси контрактами
Мы уже встречали несколько задач, где нужно было знать логическую работу прокси контрактов (и transparent, и uups). А сейчас я, на основе этой статьи, перевел список самых популярных уязвимостей.
1. Не инициализированный контракт Логики. Из предыдущих уроков мы знаем, что конструктор может быть только в контрактах прокси, иначе происходят внутренние ошибки и работа контрактов может нарушиться. Поэтому в контракте Логики вместо конструктора используется функция initialize, которая действует по тому же принципу.
Контракт должен быть обязательно инициализирован, иначе это может привести к смене владельца и краже всех средств. Если вы используете библиотеку Initializable.sol от openzeppelin, то проверьте булеву переменную _initialized. Если она вернет вам 0 / false, то контракт еще не инициализирован.
2. Ошибки с переменными в хранилище. На канале уже было несколько постов на эту тему. Основным моментом тут является то, то переменные в обоих контрактах должны быть с одинаковыми названиями и в строгом порядке.
3. Function Clashing Vulnerability. Сложно перевести дословно тут, но суть заключается в том, что селекторы в функциях (те, которые 4 первых байта) могут быть одинаковыми в контрактах прокси и логики. Эту часть уязвимости можно проверить специальными инструментами, типа Slither.
4. Metamorphic Contract Rug Vulnerability. Еще одна уязвимость, которую даже не знаю как перевести правильно. Суть ее заключается в процессе создания адреса контракта с create2, который был представлен в хардфорке EIP-1014.
Create2 позволяет деплоить контракт с адресом, который может быть просчитан заранее. К слову, простой create так делать не может. Таким образом можно задеплоить контракт с функцией selfdestruct, а затем уничтожить его и задеплоить на тот же адрес новый контракт с новым функционалом.
Вообще, если встретите контракты в сети, которые были созданы с create2, то проверьте их код на наличие selfdestruct() или delegatecall. В случае наличия таковых, контракт может быть создан мошенниками.
5. Delegatecall with Selfdestruct Vulnerability. Тут и так все понятно: допустим, если контракт А имеет функцию delegatecall, а контракт В - selfdestruct(), то есть вероятность уничтожить контракт А и заблокировать всю логику.
6. Delegatecall to Arbitrary Address. Встречается, когда delegatecall передает исполнения из прокси контракта и использует его переменные состояния или другой контекст. Обращайте на это внимание.
7. Проверка на наличие контракта. Когда используется delegatecall, то в нем не существует проверки на наличие контракта, в смысле существует ли он вообще. При этом даже если его нет, вернется true значение! Для этого желательно делать проверку на существование внешнего контракта.
Как мы видим из списка, в работе с прокси контрактами первым делом стоит обращать внимание на три вещи: initialize, delegatecall и слоты памяти. Это основные моменты, где кроются уязвимости.
#proxy #security
Мы уже встречали несколько задач, где нужно было знать логическую работу прокси контрактов (и transparent, и uups). А сейчас я, на основе этой статьи, перевел список самых популярных уязвимостей.
1. Не инициализированный контракт Логики. Из предыдущих уроков мы знаем, что конструктор может быть только в контрактах прокси, иначе происходят внутренние ошибки и работа контрактов может нарушиться. Поэтому в контракте Логики вместо конструктора используется функция initialize, которая действует по тому же принципу.
Контракт должен быть обязательно инициализирован, иначе это может привести к смене владельца и краже всех средств. Если вы используете библиотеку Initializable.sol от openzeppelin, то проверьте булеву переменную _initialized. Если она вернет вам 0 / false, то контракт еще не инициализирован.
2. Ошибки с переменными в хранилище. На канале уже было несколько постов на эту тему. Основным моментом тут является то, то переменные в обоих контрактах должны быть с одинаковыми названиями и в строгом порядке.
3. Function Clashing Vulnerability. Сложно перевести дословно тут, но суть заключается в том, что селекторы в функциях (те, которые 4 первых байта) могут быть одинаковыми в контрактах прокси и логики. Эту часть уязвимости можно проверить специальными инструментами, типа Slither.
4. Metamorphic Contract Rug Vulnerability. Еще одна уязвимость, которую даже не знаю как перевести правильно. Суть ее заключается в процессе создания адреса контракта с create2, который был представлен в хардфорке EIP-1014.
Create2 позволяет деплоить контракт с адресом, который может быть просчитан заранее. К слову, простой create так делать не может. Таким образом можно задеплоить контракт с функцией selfdestruct, а затем уничтожить его и задеплоить на тот же адрес новый контракт с новым функционалом.
Вообще, если встретите контракты в сети, которые были созданы с create2, то проверьте их код на наличие selfdestruct() или delegatecall. В случае наличия таковых, контракт может быть создан мошенниками.
5. Delegatecall with Selfdestruct Vulnerability. Тут и так все понятно: допустим, если контракт А имеет функцию delegatecall, а контракт В - selfdestruct(), то есть вероятность уничтожить контракт А и заблокировать всю логику.
6. Delegatecall to Arbitrary Address. Встречается, когда delegatecall передает исполнения из прокси контракта и использует его переменные состояния или другой контекст. Обращайте на это внимание.
7. Проверка на наличие контракта. Когда используется delegatecall, то в нем не существует проверки на наличие контракта, в смысле существует ли он вообще. При этом даже если его нет, вернется true значение! Для этого желательно делать проверку на существование внешнего контракта.
Как мы видим из списка, в работе с прокси контрактами первым делом стоит обращать внимание на три вещи: initialize, delegatecall и слоты памяти. Это основные моменты, где кроются уязвимости.
#proxy #security
👍3
Кратко о Beacon Proxy
Пару раз в контрактах на аудит встречал интеграции openzeppelin с Beacon Proxy. Я знал и работал с transparent и uups, но про beacon ничего не знал. Давайте поговорим о нем немного.
Beacon Proxy - это такой прокси паттерн,в котором несколько прокси контрактов ссылаются на один контракт Исполнения. Например, у вас есть несколько прокси контрактов, и все они работают с одним контрактом, в котором выполняются все действия. Если бы мы использовали uups или transparent, но нам вручную бы пришлось обновлять контракт Исполнения в каждом прокси, что не очень удобно и может занять некоторое время.
С Beacon Proxy мы можем обновить ссылку на адрес контракта Исполнения только в нем, и все остальные прокси контракты "подцепят" это.
Другими словами Beacon Proxy это некая прослойка между прокси контрактами и контрактом Исполнения.
Информации про работу с beacon не так много, все в основном ссылаются на обычные прокси, но есть неплохой пример его использования в этой статье. Для новичком может быть слегка сложновато, особенно с js файлом. Поэтому стоит все повторить в своем редакторе, прописав строчку за строчкой.
#proxy #beacon
Пару раз в контрактах на аудит встречал интеграции openzeppelin с Beacon Proxy. Я знал и работал с transparent и uups, но про beacon ничего не знал. Давайте поговорим о нем немного.
Beacon Proxy - это такой прокси паттерн,в котором несколько прокси контрактов ссылаются на один контракт Исполнения. Например, у вас есть несколько прокси контрактов, и все они работают с одним контрактом, в котором выполняются все действия. Если бы мы использовали uups или transparent, но нам вручную бы пришлось обновлять контракт Исполнения в каждом прокси, что не очень удобно и может занять некоторое время.
С Beacon Proxy мы можем обновить ссылку на адрес контракта Исполнения только в нем, и все остальные прокси контракты "подцепят" это.
Другими словами Beacon Proxy это некая прослойка между прокси контрактами и контрактом Исполнения.
Информации про работу с beacon не так много, все в основном ссылаются на обычные прокси, но есть неплохой пример его использования в этой статье. Для новичком может быть слегка сложновато, особенно с js файлом. Поэтому стоит все повторить в своем редакторе, прописав строчку за строчкой.
#proxy #beacon
🔥3
Тонкости с прокси переменными
Зацените еще одну классную задачу от Immunefi.
P.S. Эти ребята проводят свои bug bounty программы и аудиты контрактов, и умеют подмечать некоторые нюансы и уязвимости, которые сложно придумать просто так для задач или ctf. Обожаю их!
С первого взгляда все ок. Импортированные контракты от надежной компании, мало кода, и все выглядит хорошо. Опять же, если не знать "внутреннюю кухню".
Вы можете сами остановиться на этом моменте и взглянуть на скрин внимательно.
В общем, ошибка тут кроется в конфликте переменных в памяти. Да, ранее в постах я уже писал об этом баге, но в такой реализации встречаю впервые.
Какая же тут ошибка? Тут только одна переменная _IMPLEMENTATION_SLOT, в чем же тут конфликт?
А проблема в том, что контракт Implementation наследует от Ownable, а уже там, в первом слоте памяти, хранится другая переменная _owner.
Таким образом переменная в прокси будет перезаписываться при инициализации контракта Implementation.
Но тут есть еще один момент. Если мы обозначим _IMPLEMENTATION_SLOT как constant или immutable, то конфликта в памяти не возникнет.
Все потому, что constant и immutable не занимают слоты в памяти контракта. Если я правильно понял, то они зашиваются прямо в байткод контракта (constant - сразу, immutable - в момент работы конструктора).
Теперь мы начнем обращать внимание и на такие детали в контрактах. А казалось, про переменные я знаю все.
#proxy #security
Зацените еще одну классную задачу от Immunefi.
P.S. Эти ребята проводят свои bug bounty программы и аудиты контрактов, и умеют подмечать некоторые нюансы и уязвимости, которые сложно придумать просто так для задач или ctf. Обожаю их!
С первого взгляда все ок. Импортированные контракты от надежной компании, мало кода, и все выглядит хорошо. Опять же, если не знать "внутреннюю кухню".
Вы можете сами остановиться на этом моменте и взглянуть на скрин внимательно.
В общем, ошибка тут кроется в конфликте переменных в памяти. Да, ранее в постах я уже писал об этом баге, но в такой реализации встречаю впервые.
Какая же тут ошибка? Тут только одна переменная _IMPLEMENTATION_SLOT, в чем же тут конфликт?
А проблема в том, что контракт Implementation наследует от Ownable, а уже там, в первом слоте памяти, хранится другая переменная _owner.
Таким образом переменная в прокси будет перезаписываться при инициализации контракта Implementation.
Но тут есть еще один момент. Если мы обозначим _IMPLEMENTATION_SLOT как constant или immutable, то конфликта в памяти не возникнет.
Все потому, что constant и immutable не занимают слоты в памяти контракта. Если я правильно понял, то они зашиваются прямо в байткод контракта (constant - сразу, immutable - в момент работы конструктора).
Теперь мы начнем обращать внимание и на такие детали в контрактах. А казалось, про переменные я знаю все.
#proxy #security
ReentrancyGuard в прокси?
Необычный способ реализации ReentrancyGuard предложила одна из команд на мероприятии ETHDenver 2023 Hackathon.
Основной смысл заключается в том, чтобы вместо добавления модификатора ReentrancyGuard в каждую функцию в контракте Логики, создать защиту только в Прокси контракте.
Не смотря на то, что это поможет избежать популярных багов и недочетов со стороны команды разработчиков при последующих обновлениях, система не до конца проверена и безопасна.
Более подробно можно прочитать в этой статье.
Возможно, мы скоро увидим этот концепт в конкурсных аудитах.
#reentrancy #security #proxy
Необычный способ реализации ReentrancyGuard предложила одна из команд на мероприятии ETHDenver 2023 Hackathon.
Основной смысл заключается в том, чтобы вместо добавления модификатора ReentrancyGuard в каждую функцию в контракте Логики, создать защиту только в Прокси контракте.
Не смотря на то, что это поможет избежать популярных багов и недочетов со стороны команды разработчиков при последующих обновлениях, система не до конца проверена и безопасна.
Более подробно можно прочитать в этой статье.
Возможно, мы скоро увидим этот концепт в конкурсных аудитах.
#reentrancy #security #proxy
Организация storage в прокси контракте
В процессе аудита протокола Arcade встретился лицом к лицу с не совсем обычной организацией storage в контракте. Не совсем обычной для меня. Возможно, более опытные разработчики уже сталкивались с подобной реализацией.
Я могу ошибиться в некоторых моментах описания и прошу поправить меня, если вдруг, что не так.
P.S. В посте будут приведены ссылки на контракты из открытого конкурсного репозитория, актуального на время написания поста.
Итак, у нас есть прокси контакт - NFTBoostVault, который наследует функции от двух библиотек, которые и позволяют работать с памятью контракта так, чтобы при обновлениях не было коллизии в данных - Storage и NFTBoostVaultStorage.
Примечательно здесь то, что базовые типы данных, например address и uint, которые не поддерживают определенные места хранения в storage (как mapping), хранятся в структуре с одноименным названием, например:
struct Address {
address data;
}
Так в конструкторе или функции мы можем установить / обновить для них значения с помощью:
Storage.set(Storage.uint256Ptr("locked"), 1);
где Storage.uint256Ptr("locked") - место в памяти, а 1 - значения для установки.
Или также для типов address:
Storage.set(Storage.addressPtr("manager"), manager);
где manager - тип address из аргументов функции.
Доставать значения из такого слота памяти можно при помощи других функций из библиотеки:
function getIsLocked() public view override returns (uint256) {
return Storage.uint256Ptr("locked").data;
}
т.е. мы запрашиваем в storage слот с хешем uint256Ptr("locked") и достаем оттуда необходимые данные.
Но еще интереснее дела обстоят с работой mapping. Посмотрите на следующий код:
NFTBoostVaultStorage.Registration storage registration = _getRegistrations()[msg.sender];
Здесь мы достаем структурные данные из памяти, которые хранятся в виде mapping! Вот его другие функции:
function _getRegistrations() internal pure returns (mapping(address => NFTBoostVaultStorage.Registration) storage) {
return NFTBoostVaultStorage.mappingAddressToRegistrationPtr("registrations");
}
function mappingAddressToRegistrationPtr(
string memory name
) internal pure returns (mapping(address => Registration) storage data) {
bytes32 offset = keccak256(abi.encodePacked(REGISTRATION_TYPEHASH, name));
assembly {
data.slot := offset
}
}
Я и сейчас на 100% не уверен как работает "под капотом" преобразование данные в _getRegistrations()[msg.sender], так, чтобы получился mapping. Но выглядит интересно.
Solidity не перестает меня удивлять!
В общем, если вас это заинтересовало, то советую самим посмотреть контракты и попробовать чуть лучше разобраться. Буду также рад, если дадите свои комментарии по этому вопросу.
P.S. Отдельное спасибо @elawbek, что помог мне чуть лучше понять, как это все работает.
#storage #mapping #proxy
В процессе аудита протокола Arcade встретился лицом к лицу с не совсем обычной организацией storage в контракте. Не совсем обычной для меня. Возможно, более опытные разработчики уже сталкивались с подобной реализацией.
Я могу ошибиться в некоторых моментах описания и прошу поправить меня, если вдруг, что не так.
P.S. В посте будут приведены ссылки на контракты из открытого конкурсного репозитория, актуального на время написания поста.
Итак, у нас есть прокси контакт - NFTBoostVault, который наследует функции от двух библиотек, которые и позволяют работать с памятью контракта так, чтобы при обновлениях не было коллизии в данных - Storage и NFTBoostVaultStorage.
Примечательно здесь то, что базовые типы данных, например address и uint, которые не поддерживают определенные места хранения в storage (как mapping), хранятся в структуре с одноименным названием, например:
struct Address {
address data;
}
Так в конструкторе или функции мы можем установить / обновить для них значения с помощью:
Storage.set(Storage.uint256Ptr("locked"), 1);
где Storage.uint256Ptr("locked") - место в памяти, а 1 - значения для установки.
Или также для типов address:
Storage.set(Storage.addressPtr("manager"), manager);
где manager - тип address из аргументов функции.
Доставать значения из такого слота памяти можно при помощи других функций из библиотеки:
function getIsLocked() public view override returns (uint256) {
return Storage.uint256Ptr("locked").data;
}
т.е. мы запрашиваем в storage слот с хешем uint256Ptr("locked") и достаем оттуда необходимые данные.
Но еще интереснее дела обстоят с работой mapping. Посмотрите на следующий код:
NFTBoostVaultStorage.Registration storage registration = _getRegistrations()[msg.sender];
Здесь мы достаем структурные данные из памяти, которые хранятся в виде mapping! Вот его другие функции:
function _getRegistrations() internal pure returns (mapping(address => NFTBoostVaultStorage.Registration) storage) {
return NFTBoostVaultStorage.mappingAddressToRegistrationPtr("registrations");
}
function mappingAddressToRegistrationPtr(
string memory name
) internal pure returns (mapping(address => Registration) storage data) {
bytes32 offset = keccak256(abi.encodePacked(REGISTRATION_TYPEHASH, name));
assembly {
data.slot := offset
}
}
Я и сейчас на 100% не уверен как работает "под капотом" преобразование данные в _getRegistrations()[msg.sender], так, чтобы получился mapping. Но выглядит интересно.
Solidity не перестает меня удивлять!
В общем, если вас это заинтересовало, то советую самим посмотреть контракты и попробовать чуть лучше разобраться. Буду также рад, если дадите свои комментарии по этому вопросу.
P.S. Отдельное спасибо @elawbek, что помог мне чуть лучше понять, как это все работает.
#storage #mapping #proxy
🔥6👍4
Diamond Proxy
Делал тут небольшую презентацию про прокси контракты и вспомнил, что на канале просто упоминал про Diamond паттерн. Пора сделать чуть более подробное описание этого необычного прокси.
Итак, этот EIP был впервые представлен в феврале 2020 года и получил свой финальный вид в феврале 2020 года. Он определяет стандарт, так называемых, модульных контрактов и методов хранения информации в нем.
Кратко описывая этот паттерн, можно сказать, что у нас есть главный Diamond контракт, который хранит всю пользовательскую информацию, как и обычный Proxy (transparent, uups), а также данные об external функциях вспомогательных контрактов.
Вспомогательные контракты называются Facets.
Когда в Diamond контракт приходит вызов какой-либо функции, то он попадает в fallback, в котором идет запрос к хранилищу Diamond и ищется селектор нужной функции. По селектору определяется Facet и вызов через delegatecall перенаправляется туда.
Также существует, скажем так, главный Facet, который содержит функцию diamondCut. С помощью нее мы можем добавлять новые селекторы функций и другие адреса Facets в наш Diamond контракт.
Вообще, с помощью этого паттерна нам не нужно заботится о размере контрактов, так как с помощью такой модульной системы мы можем строить реально большие протоколы! Это бывает полезно, когда мы не знаем, насколько большим будет наш Dapp, и сколько функций потребуется в итоге.
Очень интересно организована система storage в Diamond прокси. Но об этом более подробно можно прочитать по этой прекрасной ссылке!
#diamond #proxy
Делал тут небольшую презентацию про прокси контракты и вспомнил, что на канале просто упоминал про Diamond паттерн. Пора сделать чуть более подробное описание этого необычного прокси.
Итак, этот EIP был впервые представлен в феврале 2020 года и получил свой финальный вид в феврале 2020 года. Он определяет стандарт, так называемых, модульных контрактов и методов хранения информации в нем.
Кратко описывая этот паттерн, можно сказать, что у нас есть главный Diamond контракт, который хранит всю пользовательскую информацию, как и обычный Proxy (transparent, uups), а также данные об external функциях вспомогательных контрактов.
Вспомогательные контракты называются Facets.
Когда в Diamond контракт приходит вызов какой-либо функции, то он попадает в fallback, в котором идет запрос к хранилищу Diamond и ищется селектор нужной функции. По селектору определяется Facet и вызов через delegatecall перенаправляется туда.
Также существует, скажем так, главный Facet, который содержит функцию diamondCut. С помощью нее мы можем добавлять новые селекторы функций и другие адреса Facets в наш Diamond контракт.
Вообще, с помощью этого паттерна нам не нужно заботится о размере контрактов, так как с помощью такой модульной системы мы можем строить реально большие протоколы! Это бывает полезно, когда мы не знаем, насколько большим будет наш Dapp, и сколько функций потребуется в итоге.
Очень интересно организована система storage в Diamond прокси. Но об этом более подробно можно прочитать по этой прекрасной ссылке!
#diamond #proxy
👍8🔥2❤1
Необычное использование прокси
В еще одном конкурсном аудите обнаружил для себя необычный подход к использованию прокси контрактов, которое немного перевернуло мое представление об этом паттерне.
Смотрите, в обычной реализации мы делаем как? Мы пишем прокси контракт, а затем добавляем к нему контракт Логики. Все данные хранятся в прокси, а Логику мы можем менять хоть каждый день.
В проекте Sparkn по-другому.
P.S. Репо, возможно, будет открыто только на время конкурса.
Здесь три контракта: proxy, distributor и proxyFactory.
Proxy - самая простая реализация прокси паттерна с одной лишь fallback функцией.
Distributor - контракт Логики для прокси с разными функциями, основная из которых это перечисление средств пользователям.
ProxyFactory - основной контракт для взаимодействия.
Смотрите, что получается. Контракты proxyFactory и distributor постоянные. Т.е. distributor хоть и является контрактом Логики для прокси, но в данном случае он не обновляемый, так как функционал для этого не заложен.
В proxyFactor есть функция для деплоя прокси контракта и определения его адреса наперед, основываясь на определенных параметрах, в том числе на адресе пользователя.
Итак, суть в том, что пользователь может заранее вычислить свой адрес прокси и отправить туда некоторую сумму для распределения пользователям. Затем он вызывает функцию деплоя proxy, сразу после чего идет туда вызов и с помощью delegatecall вызывается функция из distributor, которая делает рассылку активов для установленных пользователей.
Другими словами, каждый взятый пользователь может сделать деплой прокси, на котором будут лежать токены, и уже с него через фиксированный контракт distributor сделать рассылку.
Два контракта постоянных, и неограниченное количество индивидуальных контрактов для пользователей.
На мой взгляд сделано очень круто!
#proxy
В еще одном конкурсном аудите обнаружил для себя необычный подход к использованию прокси контрактов, которое немного перевернуло мое представление об этом паттерне.
Смотрите, в обычной реализации мы делаем как? Мы пишем прокси контракт, а затем добавляем к нему контракт Логики. Все данные хранятся в прокси, а Логику мы можем менять хоть каждый день.
В проекте Sparkn по-другому.
P.S. Репо, возможно, будет открыто только на время конкурса.
Здесь три контракта: proxy, distributor и proxyFactory.
Proxy - самая простая реализация прокси паттерна с одной лишь fallback функцией.
Distributor - контракт Логики для прокси с разными функциями, основная из которых это перечисление средств пользователям.
ProxyFactory - основной контракт для взаимодействия.
Смотрите, что получается. Контракты proxyFactory и distributor постоянные. Т.е. distributor хоть и является контрактом Логики для прокси, но в данном случае он не обновляемый, так как функционал для этого не заложен.
В proxyFactor есть функция для деплоя прокси контракта и определения его адреса наперед, основываясь на определенных параметрах, в том числе на адресе пользователя.
Итак, суть в том, что пользователь может заранее вычислить свой адрес прокси и отправить туда некоторую сумму для распределения пользователям. Затем он вызывает функцию деплоя proxy, сразу после чего идет туда вызов и с помощью delegatecall вызывается функция из distributor, которая делает рассылку активов для установленных пользователей.
Другими словами, каждый взятый пользователь может сделать деплой прокси, на котором будут лежать токены, и уже с него через фиксированный контракт distributor сделать рассылку.
Два контракта постоянных, и неограниченное количество индивидуальных контрактов для пользователей.
На мой взгляд сделано очень круто!
#proxy
👍5
Письменное задание в Spearbit
Для тех, кто не в курсе, Spearbit крутая зарубежная компания, которая занимается безопасностью смарт контрактов. Пару раз в год они набирают к себе аудиторов. Для этого требуется пройти тест из 4 вариантов ответа на время и после технического интервью еще выполнить письменное задание.
Предлагаю вашему вниманию одно из таких заданий от февраля 2022 года. Прекрасная практика для аудиторов и тех, кто любит решать задачи.
Есть два контракта: прокси и Логики. Деплой логики делается только однажды для всех пользователей. Прокси - для каждого свой.
Пользователи держат все свои активы на прокси контракте и, в случае необходимости, посылают вызовы на контракт Логики.
Тут есть критическая уязвимость. Задание: найти ее и дать свои рекомендации по ее устранению.
Вы получите дополнительный бонус, если в рекомендациях расскажите о нишевом решении для предотвращения уязвимости. Еще один бонус получите, если в рекомендациях расскажите, как можно убрать уязвимость, убрав два слова в контракте, и изменив всего одно.
Вот ссылка на репо задания: https://github.com/spearbit-audits/writing-exercise
Удачи в поисках!
#proxy #bug #spearbit
Для тех, кто не в курсе, Spearbit крутая зарубежная компания, которая занимается безопасностью смарт контрактов. Пару раз в год они набирают к себе аудиторов. Для этого требуется пройти тест из 4 вариантов ответа на время и после технического интервью еще выполнить письменное задание.
Предлагаю вашему вниманию одно из таких заданий от февраля 2022 года. Прекрасная практика для аудиторов и тех, кто любит решать задачи.
Есть два контракта: прокси и Логики. Деплой логики делается только однажды для всех пользователей. Прокси - для каждого свой.
Пользователи держат все свои активы на прокси контракте и, в случае необходимости, посылают вызовы на контракт Логики.
Тут есть критическая уязвимость. Задание: найти ее и дать свои рекомендации по ее устранению.
Вы получите дополнительный бонус, если в рекомендациях расскажите о нишевом решении для предотвращения уязвимости. Еще один бонус получите, если в рекомендациях расскажите, как можно убрать уязвимость, убрав два слова в контракте, и изменив всего одно.
Вот ссылка на репо задания: https://github.com/spearbit-audits/writing-exercise
Удачи в поисках!
#proxy #bug #spearbit
👍11❤1🔥1
Баг в протоколе Lybra
Интересная находка была в конкурсном аудите протокола Lybra, в наборе которого были прокси контракты.
При всей очевидности проблемы, найти ее смог только один аудитор. Почему очевидной? Просто потому, что этому обучают всех с момента знакомства с прокси контрактами.
Для протокола был создан отдельный контракт конфигуратор, который так и называется LybraConfigurator. По своей сути, это был Логический контракт для прокси контракта.
Проблема заключалась в том, что тут был конструктор, с помощью которого устанавливались переменные в память, что и было ошибкой.
Как мы знаем, конструктор исполняется только один раз, во время деплоя контракта, и позволяет сделать изначальную установку переменных в контракт. Но запись идет в память данного контракта, а не в память прокси, как это положено в данном случае. Поэтому на момент? когда были бы развернуты контракты в сети, то две переменные в прокси остались бы с нулевым значением.
Вот такой очевидный и не очень баг.
Более детально о нем можно прочитать тут, в отчете code4rena.
Будьте внимательны при работе с прокси!
#proxy #constructor
Интересная находка была в конкурсном аудите протокола Lybra, в наборе которого были прокси контракты.
При всей очевидности проблемы, найти ее смог только один аудитор. Почему очевидной? Просто потому, что этому обучают всех с момента знакомства с прокси контрактами.
Для протокола был создан отдельный контракт конфигуратор, который так и называется LybraConfigurator. По своей сути, это был Логический контракт для прокси контракта.
Проблема заключалась в том, что тут был конструктор, с помощью которого устанавливались переменные в память, что и было ошибкой.
Как мы знаем, конструктор исполняется только один раз, во время деплоя контракта, и позволяет сделать изначальную установку переменных в контракт. Но запись идет в память данного контракта, а не в память прокси, как это положено в данном случае. Поэтому на момент? когда были бы развернуты контракты в сети, то две переменные в прокси остались бы с нулевым значением.
Вот такой очевидный и не очень баг.
Более детально о нем можно прочитать тут, в отчете code4rena.
Будьте внимательны при работе с прокси!
#proxy #constructor
👍4
Обновляемые контракты (Transparent proxy). Часть 1
Смотрел я тут несколько аудитов и отчетов, где присутствовал прокси паттерн и подумал, что вполне можно на канале повторить некоторый материал. Новички смогут что-то новое для себя найти и более продвинутые участники повторить материал.
Transparent Upgradeable Proxy (далее TUP) - это паттерн для обновления прокси контрактов, исключающий возможность столкновения селекторов функций.
Хороший прокси контракт должен обладать как минимум двумя следующими характеристиками:
- слотом для хранения адреса контракта Логики;
- механизмом, позволяющим администратору изменять адрес Логики;
Стандарт ERC1967 устанавливает, где должен находиться слот памяти, содержащий адрес контракта Логики, так, чтобы вероятность коллизии при хранении была минимальной. Однако он не определяет, как обновить сам контракт.
Столкновение селекторов функций (Function Selector Clashing)
Объявление публичных функций внутри прокси для обновления адреса Логики влечет за собой возможность столкновения селекторов функций.
Вот простой пример:
Помните, что fallback всегда проверяется в последнюю очередь?
Перед вызовом fallback контракт прокси проверит, совпадает ли 4-байтовый селектор функции с changeImplementation (или любой другой публичной функцией в прокси).
И если в прокси объявлена публичная функция, могут возникнуть два вида столкновений селекторов функций:
1. Если контракт Логики реализует функцию с той же подписью, что и функция в прокси, то эта функция не будет вызвана, так как вызов пойдет в сам прокси.
2. Если в контракте Логики есть функция с тем же селектором функций, что и публичная функция в прокси, она также будет невызываемой по той же причине. Этот сценарий представляет собой столкновение селекторов функций. Вероятность того, что у двух разных функций будет одинаковый селектор, равна 1 к 4,29 миллиарда; селектор функции состоит из 4 байт, поэтому существует 4,29 миллиарда возможностей. Это небольшая вероятность, но не нулевая. Например, у clash550254402() тот же селектор функции, что и у proxyAdmin().
А далее мы поговорим, как TUP помогает решить эту проблему.
#proxy #transparent
Смотрел я тут несколько аудитов и отчетов, где присутствовал прокси паттерн и подумал, что вполне можно на канале повторить некоторый материал. Новички смогут что-то новое для себя найти и более продвинутые участники повторить материал.
Transparent Upgradeable Proxy (далее TUP) - это паттерн для обновления прокси контрактов, исключающий возможность столкновения селекторов функций.
Хороший прокси контракт должен обладать как минимум двумя следующими характеристиками:
- слотом для хранения адреса контракта Логики;
- механизмом, позволяющим администратору изменять адрес Логики;
Стандарт ERC1967 устанавливает, где должен находиться слот памяти, содержащий адрес контракта Логики, так, чтобы вероятность коллизии при хранении была минимальной. Однако он не определяет, как обновить сам контракт.
Столкновение селекторов функций (Function Selector Clashing)
Объявление публичных функций внутри прокси для обновления адреса Логики влечет за собой возможность столкновения селекторов функций.
Вот простой пример:
contract ProxyUnsafe {
function changeImplementation(
address newImplementation
) public {
// some code...
}
fallback(bytes calldata data) external payable (bytes memory) {
(bool ok, bytes memory data) = getImplementation().delegatecall(data);
require(ok, "delegatecall failed");
return data;
}
}
contract Implementation {
// an identical function is declared here -- they will clash
function changeImplementation(
address newImplementation
) public {
}
//...
}Помните, что fallback всегда проверяется в последнюю очередь?
Перед вызовом fallback контракт прокси проверит, совпадает ли 4-байтовый селектор функции с changeImplementation (или любой другой публичной функцией в прокси).
И если в прокси объявлена публичная функция, могут возникнуть два вида столкновений селекторов функций:
1. Если контракт Логики реализует функцию с той же подписью, что и функция в прокси, то эта функция не будет вызвана, так как вызов пойдет в сам прокси.
2. Если в контракте Логики есть функция с тем же селектором функций, что и публичная функция в прокси, она также будет невызываемой по той же причине. Этот сценарий представляет собой столкновение селекторов функций. Вероятность того, что у двух разных функций будет одинаковый селектор, равна 1 к 4,29 миллиарда; селектор функции состоит из 4 байт, поэтому существует 4,29 миллиарда возможностей. Это небольшая вероятность, но не нулевая. Например, у clash550254402() тот же селектор функции, что и у proxyAdmin().
А далее мы поговорим, как TUP помогает решить эту проблему.
#proxy #transparent
1👍5🔥3👏1
Обновляемые контракты (Transparent proxy). Часть 2
Transparent Upgradeable Proxy - это паттерн, полностью исключающий возможность столкновения селекторов функций.
В частности, TUP предписывает, что в прокси контракте не должно быть никаких публичных функций, кроме fallback.
Но если есть только функция fallback, как нам вызвать функцию для обновления прокси?
Ответ заключается в том, чтобы определить, является ли отправитель msg.sender администратором.
Из этого следует, что администратор не может напрямую использовать прокси, поскольку его вызовы всегда направляются в сторону if. Однако, используя немного другой механизм, который мы обсудим позже, администратор все же может делать вызовы прокси, а тот делегировать вызов в Логику.
Изменение неизменяемого администратора
В приведенном выше фрагменте кода администратор является неизменяемым. Это означает, что контракт технически не соответствует стандарту ERC-1967, который гласит, что администратор должен храниться в слоте хранения 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 или bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1).
Для того, чтобы стать совместимым со стандартом, TUP должен хранить адрес администратора в этом слоте памяти, но не использовать его для других целей.
Наличие адреса в этом слоте будет сигнализировать другим программам, что контракт является прокси (одна из целей ERC-1967). Однако чтение из хранилища при каждом обращении к прокси добавляет к вызову еще 2100 газа. Поэтому желательно использовать неизменяемую переменную.
«Смена» администратора
Однако все же желательно иметь возможность обновлять адрес администратора - но изначально это кажется невозможным.
TUP позволяет изменять админский контракт прокси двумя способами. Во-первых, он назначает другой контракт, известный как ProxyAdmin, администратором прокси.
Во-вторых, владелец ProxyAdmin является «истинным» администратором. ProxyAdmin просто направляет вызовы от владельца к прокси. «Истинный» администратор вызывает ProxyAdmin, а ProxyAdmin вызывает Transparent Proxy. Изменив владельца ProxyAdmin, мы можем изменить, кто имеет возможность обновлять Transparent Proxy.
Далее поговорим об этом интересном контракте.
#proxy #transparent
Transparent Upgradeable Proxy - это паттерн, полностью исключающий возможность столкновения селекторов функций.
В частности, TUP предписывает, что в прокси контракте не должно быть никаких публичных функций, кроме fallback.
Но если есть только функция fallback, как нам вызвать функцию для обновления прокси?
Ответ заключается в том, чтобы определить, является ли отправитель msg.sender администратором.
contract Proxy is ERC1967 {
address immutable admin;
constructor(address admin_) {
admin = admin_
}
fallback() external payable {
if (msg.sender == admin) {
// upgrade logic
} else {
// delegatecall to implementation
}
}
}
Из этого следует, что администратор не может напрямую использовать прокси, поскольку его вызовы всегда направляются в сторону if. Однако, используя немного другой механизм, который мы обсудим позже, администратор все же может делать вызовы прокси, а тот делегировать вызов в Логику.
Изменение неизменяемого администратора
В приведенном выше фрагменте кода администратор является неизменяемым. Это означает, что контракт технически не соответствует стандарту ERC-1967, который гласит, что администратор должен храниться в слоте хранения 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 или bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1).
Для того, чтобы стать совместимым со стандартом, TUP должен хранить адрес администратора в этом слоте памяти, но не использовать его для других целей.
Наличие адреса в этом слоте будет сигнализировать другим программам, что контракт является прокси (одна из целей ERC-1967). Однако чтение из хранилища при каждом обращении к прокси добавляет к вызову еще 2100 газа. Поэтому желательно использовать неизменяемую переменную.
«Смена» администратора
Однако все же желательно иметь возможность обновлять адрес администратора - но изначально это кажется невозможным.
TUP позволяет изменять админский контракт прокси двумя способами. Во-первых, он назначает другой контракт, известный как ProxyAdmin, администратором прокси.
Во-вторых, владелец ProxyAdmin является «истинным» администратором. ProxyAdmin просто направляет вызовы от владельца к прокси. «Истинный» администратор вызывает ProxyAdmin, а ProxyAdmin вызывает Transparent Proxy. Изменив владельца ProxyAdmin, мы можем изменить, кто имеет возможность обновлять Transparent Proxy.
Далее поговорим об этом интересном контракте.
#proxy #transparent
1👍4
Обновляемые контракты (Transparent proxy). Часть 3
AdminProxy
Ниже приведен код из OpenZeppelin AdminProxy. Обратите внимание, что есть только одна функция upgradeAndCall(), которая может вызывать upgradeToAndCall() только на прокси.
Существует распространенное заблуждение, что администратор Transparent proxy не может использовать контракт, потому что его вызовы переадресуются на upgrade. Однако владелец AdminProxy может использовать Proxy без проблем, как показано на диаграмме на скрине.
На самом деле, существует механизм, позволяющий администратору ProxyAdmin сделать произвольный вызов прокси, как следует из названия функции upgradeToAndCall().
Сделать прокси non-upgradeable?
Если владелец будет изменен на нулевой адрес или другой смарт-контракт, который не сможет правильно использовать функцию upgradeAndCall() (или изменить владельца), то Transparent proxy больше не будет обновляемым. Это может произойти, например, если владельцем AdminProxy будет установлен другой контракт AdminProxy.
TUP от OpenZeppelin реализует стандарт с помощью трех контрактов:
1. Proxy.sol
2. ERC1967Proxy.sol (наследует Proxy.sol)
3. TransparentUgradeableProxy.sol (наследует ERC1967Proxy.sol)
Базовым контрактом является Proxy.sol. Получив адрес реализации, он отправляет delegate call в Логику. Функция _implementation() не реализована в Proxy - она переопределена и реализована его дочерним ERC1967Proxy, что позволяет ему вернуть соответствующий слот хранения.
ERC1967Proxy.sol наследует от Proxy.sol. Он добавляет (и переопределяет) внутреннюю функцию _implementation(), которая возвращает адрес реализации, хранящийся в слоте, указанном ERC-1967. Однако Transparent proxy не будет использовать эту функцию - вместо этого он использует свою собственную неизменяемую переменную.
А о самом TransparentUpgradeableProxy.sol мы поговорим в следующий раз.
#proxy #transparent
AdminProxy
Ниже приведен код из OpenZeppelin AdminProxy. Обратите внимание, что есть только одна функция upgradeAndCall(), которая может вызывать upgradeToAndCall() только на прокси.
pragma solidity ^0.8.20;
import {ITransparentUpgradeableProxy} from "./TransparentUpgradeableProxy.sol";
import {Ownable} from "../../access/Ownable.sol";
contract ProxyAdmin is Ownable {
string public constant UPGRADE_INTERFACE_VERSION = "5.0.0";
constructor(address initialOwner) Ownable(initialOwner) {}
function upgradeAndCall(
ITransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}
}
Существует распространенное заблуждение, что администратор Transparent proxy не может использовать контракт, потому что его вызовы переадресуются на upgrade. Однако владелец AdminProxy может использовать Proxy без проблем, как показано на диаграмме на скрине.
На самом деле, существует механизм, позволяющий администратору ProxyAdmin сделать произвольный вызов прокси, как следует из названия функции upgradeToAndCall().
Сделать прокси non-upgradeable?
Если владелец будет изменен на нулевой адрес или другой смарт-контракт, который не сможет правильно использовать функцию upgradeAndCall() (или изменить владельца), то Transparent proxy больше не будет обновляемым. Это может произойти, например, если владельцем AdminProxy будет установлен другой контракт AdminProxy.
TUP от OpenZeppelin реализует стандарт с помощью трех контрактов:
1. Proxy.sol
2. ERC1967Proxy.sol (наследует Proxy.sol)
3. TransparentUgradeableProxy.sol (наследует ERC1967Proxy.sol)
Базовым контрактом является Proxy.sol. Получив адрес реализации, он отправляет delegate call в Логику. Функция _implementation() не реализована в Proxy - она переопределена и реализована его дочерним ERC1967Proxy, что позволяет ему вернуть соответствующий слот хранения.
ERC1967Proxy.sol наследует от Proxy.sol. Он добавляет (и переопределяет) внутреннюю функцию _implementation(), которая возвращает адрес реализации, хранящийся в слоте, указанном ERC-1967. Однако Transparent proxy не будет использовать эту функцию - вместо этого он использует свою собственную неизменяемую переменную.
А о самом TransparentUpgradeableProxy.sol мы поговорим в следующий раз.
#proxy #transparent
1👍5
Обновляемые контракты (Transparent proxy). Часть 4
TransparentUpgradeableProxy.sol наследует от ERC1967Proxy.sol. В конструкторе этого контракта разворачивается ProxyAdmin и устанавливается как адрес неизменяемого admin (первая переменная в контракте).
Рассмотрим случай, когда отправителем msg.sender является _proxyAdmin. В этом случае вызов направляется в _dispatchUpgradeToAndCall(), но _fallback() сначала проверяет, что предоставленный селектор функций является селектором функций для upgradeToAndCall.
«Селектор» здесь не является "настоящим" селектором, поскольку Transparent Upgradeable Proxy не имеет публичных функций. Однако, чтобы позволить ProxyAdmin сделать вызов интерфейса (вызов высокого уровня), он должен принять от ProxyAdmin кодированные ABI calldata для upgradeToAndCall().
Напомн, что ProxyAdmin делает интерфейсный вызов upgradeToAndCall в прокси, хотя у прокси нет публичных функций, кроме fallback (код ProxyAdmin показан далее):
Выше представлено видео, демонстрирующее все три блока кода рядом друг с другом и то, как различные контракты в цепочке наследования (Proxy, ERC1967Proxy и TransparentUpgradeableProxy) взаимодействуют друг с другом.
В следующем посте поговорим о том, почему функция называется upgradeToAndCall(), а не просто upgradeTo().
#proxy #transparent
TransparentUpgradeableProxy.sol наследует от ERC1967Proxy.sol. В конструкторе этого контракта разворачивается ProxyAdmin и устанавливается как адрес неизменяемого admin (первая переменная в контракте).
contract TransparentUpgradeableProxy is ERC1967Proxy {
address private immutable _admin;
error ProxyDeniedAdminAccess();
constructor(address _logic, address initialOwner, bytes memory _data) payable ERC1967Proxy(_logic, _data) {
_admin = address(new ProxyAdmin(initialOwner));
// Set the storage value and emit an event for ERC-1967 compatibility
ERC1967Utils.changeAdmin(_proxyAdmin());
}
function _proxyAdmin() internal view virtual returns (address) {
return _admin;
}
function _fallback() internal virtual override {
if (msg.sender == _proxyAdmin()) {
if (msg.sig != ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
revert ProxyDeniedAdminAccess();
} else {
_dispatchUpgradeToAndCall();
}
} else {
super._fallback();
}
}
function _dispatchUpgradeToAndCall() private {
(address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes));
ERC1967Utils.upgradeToAndCall(newImplementation, data);
}
}
Рассмотрим случай, когда отправителем msg.sender является _proxyAdmin. В этом случае вызов направляется в _dispatchUpgradeToAndCall(), но _fallback() сначала проверяет, что предоставленный селектор функций является селектором функций для upgradeToAndCall.
«Селектор» здесь не является "настоящим" селектором, поскольку Transparent Upgradeable Proxy не имеет публичных функций. Однако, чтобы позволить ProxyAdmin сделать вызов интерфейса (вызов высокого уровня), он должен принять от ProxyAdmin кодированные ABI calldata для upgradeToAndCall().
Напомн, что ProxyAdmin делает интерфейсный вызов upgradeToAndCall в прокси, хотя у прокси нет публичных функций, кроме fallback (код ProxyAdmin показан далее):
contract ProxyAdmin is Ownable {
string public constant UPGRADE_INTERFACE_VERSION = "5.0.0";
constructor(address initialOwner) Ownable(initialOwner) {}
function upgradeAndCall(
ITransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
@> proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}
}Выше представлено видео, демонстрирующее все три блока кода рядом друг с другом и то, как различные контракты в цепочке наследования (Proxy, ERC1967Proxy и TransparentUpgradeableProxy) взаимодействуют друг с другом.
В следующем посте поговорим о том, почему функция называется upgradeToAndCall(), а не просто upgradeTo().
#proxy #transparent
👍6
Обновляемые контракты (Transparent proxy). Часть 5
Почему функция называется upgradeToAndCall(), а не просто upgradeTo()?
При обновлении контракта Логики можно сделать вызов, как если бы ProxyAdmin был msg.sender и транзакция делегировала вызов в Логику, как если бы это было обычное взаимодействие с прокси. Конечно, внутри fallback() этого не произойдет, потому что вызовы ProxyAdmin направляются в логику обновления.
Приведенный ниже код взят из ERC1967Utils.sol, с которым TransparentUpgradeableProxy компонуется для обеспечения возможности обновления слота Логики. Библиотека предоставляет внутреннюю вспомогательную функцию для обновления слота, содержащего адрес Логики.
Он будет выполнять delegatecall в контракт Логики только в том случае, если data.length > 0.
upgradeToAndCall() также выполняет delegatecall от прокси к Логике в той же транзакции, что и обновление. Это то же самое, как если бы ProxyAdmin вызвал прокси, используя любые calldata, указанные в data, а затем прокси сделал бы delegatecall в самой Логике.
Таким образом, ProxyAdmin может делать произвольные вызовы прокси.
Обратите внимание, что upgradeToAndCall не требует, чтобы обновленный контракт был другой реализацией - можно «обновиться» до той же самой Логики.
Из этого следует, что контракт ProxyAdmin может делать произвольные delegatecall в контракт Логики через прокси - но отправителем msg.sender с точки зрения Transparent Proxy является ProxyAdmin.
Единственное ограничение, которое накладывает ProxyAdmin на обновление, заключается в том, что он не может обновить пустой контракт (адрес без байткода). Функция _setImplementation проверяет, что длина кода новой реализации больше нуля.
Подведем итоги:
1. Transparent Upgradeable Proxy - это шаблон проектирования для предотвращения столкновения селекторов функций между прокси и Логикой;
2. Функция fallback является единственной публичной функцией в Transparent Upgradeable Proxy;
3. Функциональность обновления может быть вызвана только администратором через функцию fallback. Все вызовы с неадминистративных адресов превращаются в delegatecalls в прокси;
4. Transparent Upgradeable Proxy использует неизменяемую переменную для хранения адреса администратора, чтобы сэкономить газ. Для того, чтобы соответствовать ERC-1967, он хранит адрес администратора в слоте admin, указанном в ERC-1967, даже если он никогда не читает из этого слота;
5. Поскольку администратор не может быть изменен, он устанавливается в смарт-контракт под названием AdminProxy. AdminProxy раскрывает единственную функцию upgradeAndCall(), которая может быть вызвана только владельцем AdminProxy. Владелец AdminProxy может быть изменен.
#proxy #transparent
Почему функция называется upgradeToAndCall(), а не просто upgradeTo()?
При обновлении контракта Логики можно сделать вызов, как если бы ProxyAdmin был msg.sender и транзакция делегировала вызов в Логику, как если бы это было обычное взаимодействие с прокси. Конечно, внутри fallback() этого не произойдет, потому что вызовы ProxyAdmin направляются в логику обновления.
Приведенный ниже код взят из ERC1967Utils.sol, с которым TransparentUpgradeableProxy компонуется для обеспечения возможности обновления слота Логики. Библиотека предоставляет внутреннюю вспомогательную функцию для обновления слота, содержащего адрес Логики.
/**
* @dev Performs implementation upgrade with additional setup call if data is nonempty.
* This function is payable only if the setup call is performed, otherwise `msg.value` is rejected
* to avoid stuck value in the contract.
*
* Emits an {IERC1967-Upgraded} event.
*/
function upgradeToAndCall(address newImplementation, bytes memory data) internal {
_setImplementation(newImplementation);
emit IERC1967.Upgraded(newImplementation);
if (data.length > 0) {
Address.functionDelegateCall(newImplementation, data);
} else {
_checkNonPayable();
}
}
Он будет выполнять delegatecall в контракт Логики только в том случае, если data.length > 0.
upgradeToAndCall() также выполняет delegatecall от прокси к Логике в той же транзакции, что и обновление. Это то же самое, как если бы ProxyAdmin вызвал прокси, используя любые calldata, указанные в data, а затем прокси сделал бы delegatecall в самой Логике.
Таким образом, ProxyAdmin может делать произвольные вызовы прокси.
Обратите внимание, что upgradeToAndCall не требует, чтобы обновленный контракт был другой реализацией - можно «обновиться» до той же самой Логики.
Из этого следует, что контракт ProxyAdmin может делать произвольные delegatecall в контракт Логики через прокси - но отправителем msg.sender с точки зрения Transparent Proxy является ProxyAdmin.
Единственное ограничение, которое накладывает ProxyAdmin на обновление, заключается в том, что он не может обновить пустой контракт (адрес без байткода). Функция _setImplementation проверяет, что длина кода новой реализации больше нуля.
/**
* @dev Stores a new address in the ERC-1967 implementation slot.
*/
function _setImplementation(address newImplementation) private {
if (newImplementation.code.length == 0) {
revert ERC1967InvalidImplementation(newImplementation);
}
StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation;
}
Подведем итоги:
1. Transparent Upgradeable Proxy - это шаблон проектирования для предотвращения столкновения селекторов функций между прокси и Логикой;
2. Функция fallback является единственной публичной функцией в Transparent Upgradeable Proxy;
3. Функциональность обновления может быть вызвана только администратором через функцию fallback. Все вызовы с неадминистративных адресов превращаются в delegatecalls в прокси;
4. Transparent Upgradeable Proxy использует неизменяемую переменную для хранения адреса администратора, чтобы сэкономить газ. Для того, чтобы соответствовать ERC-1967, он хранит адрес администратора в слоте admin, указанном в ERC-1967, даже если он никогда не читает из этого слота;
5. Поскольку администратор не может быть изменен, он устанавливается в смарт-контракт под названием AdminProxy. AdminProxy раскрывает единственную функцию upgradeAndCall(), которая может быть вызвана только владельцем AdminProxy. Владелец AdminProxy может быть изменен.
#proxy #transparent
👍5