|
| 1 | +# Pendle маршрутизация. Свой путь в разделение логики протокола |
| 2 | + |
| 3 | +[Pendle](https://www.pendle.finance/) - это протокол токенизации доходности c элементами трейдинга в нескольких EVM-совместимых сетях. |
| 4 | + |
| 5 | +Pendle в своих смарт-контрактах пересмотрел шаблон **Diamond** и упростил его. Кодовая база протокола делится на два репозитория, но core функционал все равно большой, поэтому существует необходимость разделять код на несколько логических групп. |
| 6 | + |
| 7 | +Протокол Pendle выстраивает взаимодействие пользователя со смарт-контрактами подобно Uniswap и другим протоколам через один смарт-контракт маршрутизации [(роутер или PendleRouterV4.sol)](https://github.com/pendle-finance/pendle-core-v2-public/blob/main/contracts/router/PendleRouterV4.sol). |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +На схеме видно, что любое действие пользователя будет делегировано к соответствующему смарт-контракту с логикой (facet). Например, логика свапа находится на смарт-контрактах [ActionSwapPTV3.sol](https://github.com/pendle-finance/pendle-core-v2-public/blob/main/contracts/router/ActionSwapPTV3.sol) и [ActionSwapYTV3.sol](https://github.com/pendle-finance/pendle-core-v2-public/blob/main/contracts/router/ActionSwapYTV3.sol). |
| 12 | + |
| 13 | +## Упрощенная реализация маршрутизации |
| 14 | + |
| 15 | +В этом разделе мы подробно построим минимально рабочий прототип системы маршрутизации вызовов, который был реализован в протоколе Pendle. |
| 16 | + |
| 17 | +### Концепт прототипа |
| 18 | + |
| 19 | +Для прототипа реализуем смарт-контракт **Router**, который будет единой точкой входа. Этот смарт-контракт будет прокси смарт-контрактом и сможет делегировать вызов к смарт-контракту `ActionSimple.sol` или другому фасету. Каждый из фасетов может реализовывать собственный функционал. Смотри схему ниже. |
| 20 | + |
| 21 | + |
| 22 | + |
| 23 | +Логично, что смарт-контракт роутера должен уметь определять список своих фасетов для делегирования вызова к нужному. Для этого реализуем отдельный смарт-контракт `ActionStorage.sol`, который будет обрабатывать список всех фасетов. |
| 24 | + |
| 25 | +### Реализация |
| 26 | + |
| 27 | +**Шаг 1** |
| 28 | + |
| 29 | +Первым делом реализуем простой смарт-контракт [ActionSimple.sol](./contracts/ActionSimple.sol), который является целевым для вызова и будет содержать всего одну функцию `execute()` и бросать событие. |
| 30 | + |
| 31 | +```solidity |
| 32 | +contract ActionSimple { |
| 33 | + event Executed(bool success); |
| 34 | +
|
| 35 | + function execute() external { |
| 36 | + emit Executed(true); |
| 37 | + } |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +Мы будем вызывать функцию `execute()` на смарт-контракте `Router.sol` и будем ожидать, что вызов будет делегирован смарт-контракту `ActionSimple.sol`. |
| 42 | + |
| 43 | +**Шаг 2** |
| 44 | + |
| 45 | +Реализуем простой смарт-контракт [Router.sol](./contracts/Router.sol), который наследуем от [Proxy.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol). Это смарт-контракт OpenZeppelin, который даст возможность делегировать вызов. |
| 46 | + |
| 47 | +Наследование от **Proxy.sol** диктует нам необходимость реализовать функцию `_implementation()` в которой мы будем определять куда делегировать вызов для исполнения. |
| 48 | + |
| 49 | +```solidity |
| 50 | +import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; |
| 51 | +
|
| 52 | +contract Router is Proxy { |
| 53 | + function _implementation() internal view override returns (address facet) {} |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +Все вызовы пришедшие на смарт-контракт `Router.sol` будут попадать в [fallback()](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol#L58) функцию, а уже оттуда вызывается функция `_implementation()`, которую мы переопределили. В следующих шагах мы напишем код этой функции. |
| 58 | + |
| 59 | +**Шаг 3** |
| 60 | + |
| 61 | +На этом шаге необходимо реализовать хранилище, которое будет содержать информацию по всем фасетам. Для этого реализуем абстрактный смарт-контракт [RouterStorage.sol](./contracts/RouterStorage.sol). |
| 62 | + |
| 63 | +```solidity |
| 64 | +abstract contract RouterStorage { |
| 65 | + // Структура для хранения списка фасетов |
| 66 | + struct CoreStorage { |
| 67 | + // Для каждого селектора функции будем хранить адрес смарт-контракта, где эта функция реализована |
| 68 | + mapping(bytes4 selector => address facet) selectorToFacet; |
| 69 | + } |
| 70 | +
|
| 71 | + // keccak256(abi.encode("the-same-pendle-routing")) |
| 72 | + bytes32 private constant STORAGE_LOCATION = 0x25e5c12553aca6bac665d66f71e8380eae2ff9ef17f649227265212ec2f7f613; |
| 73 | +
|
| 74 | + // Функция, которая будет возвращать слот с фасетами |
| 75 | + function _getCoreStorage() internal pure returns (CoreStorage storage $) { |
| 76 | + assembly { |
| 77 | + $.slot := STORAGE_LOCATION |
| 78 | + } |
| 79 | + } |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +Сделаем так, чтобы `Router.sol` наследовался от `RouterStorage.sol`. Теперь роутер может получать адрес имплементации для каждого селектора функции из вызова. |
| 84 | + |
| 85 | +```solidity |
| 86 | +import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; |
| 87 | +
|
| 88 | +import {RouterStorage} from "./RouterStorage.sol"; |
| 89 | +
|
| 90 | +contract Router is Proxy, RouterStorage { |
| 91 | + function _implementation() internal view override returns (address facet) { |
| 92 | + RouterStorage.CoreStorage storage $ = _getCoreStorage(); |
| 93 | +
|
| 94 | + // Получение адреса фасета из данных вызова |
| 95 | + facet = $.selectorToFacet[msg.sig]; |
| 96 | + if (facet == address(0)) { |
| 97 | + revert InvalidSelector(); |
| 98 | + } |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +**Шаг 4** |
| 104 | + |
| 105 | +Здесь нужно понимать, что `Router.sol` - это прокси и физически мы работаем с его **storage**. Но при этом нам необходимо записать в `mapping selectorToFacet` данные по доступным селекторам вызова и адресам исполнения. |
| 106 | + |
| 107 | +Сделаем отдельный смарт-контракт [ActionStorage.sol](./contracts/ActionStorage.sol), который будет отвечать за обновление `mapping selectorToFacet` и другие функции управления. |
| 108 | + |
| 109 | +```solidity |
| 110 | +import {RouterStorage} from "./RouterStorage.sol"; |
| 111 | +
|
| 112 | +struct SelectorsToFacet { |
| 113 | + address facet; |
| 114 | + bytes4[] selectors; |
| 115 | +} |
| 116 | +
|
| 117 | +contract ActionStorage is RouterStorage { |
| 118 | + function setSelectorToFacets(SelectorsToFacet[] calldata arr) external { |
| 119 | + CoreStorage storage $ = _getCoreStorage(); |
| 120 | +
|
| 121 | + for (uint256 i = 0; i < arr.length; i++) { |
| 122 | + SelectorsToFacet memory s = arr[i]; |
| 123 | +
|
| 124 | + for (uint256 j = 0; j < s.selectors.length; j++) { |
| 125 | + // Записываем данные по фасетам и селекторам |
| 126 | + $.selectorToFacet[s.selectors[j]] = s.facet; |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +**Шаг 5** |
| 134 | + |
| 135 | +Есть важный момент, `ActionStorage.sol` - это отдельный самостоятельный смарт-контракт, поэтому смарт-контракт `Router.sol` уже в момент деплоя должен знать об `ActionStorage.sol` и уметь делегировать вызов `setSelectorToFacets()` на него. |
| 136 | + |
| 137 | +Добавим адрес `ActionStorage.sol` в constructor роутера. |
| 138 | + |
| 139 | +```solidity |
| 140 | +import {IActionStorage} from "./ActionStorage.sol"; |
| 141 | +
|
| 142 | +contract Router is Proxy, RouterStorage { |
| 143 | + constructor(address actionStorage) { |
| 144 | + RouterStorage.CoreStorage storage $ = _getCoreStorage(); |
| 145 | + // Реализуем возможность делегировать setSelectorToFacets() вызовы на адрес смарт-контракта ActionStorage |
| 146 | + $.selectorToFacet[IActionStorage.setSelectorToFacets.selector] = actionStorage; |
| 147 | + } |
| 148 | + ... |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +Таким образом все что остается сделать, это закрыть свободный вызов функции `setSelectorToFacets()`. Для этого можно воспользоваться паттернами Ownable, AccessControl. |
| 153 | + |
| 154 | +Полная реализация смарт-контрактов: |
| 155 | +- [Router.sol](./contracts/Router.sol) |
| 156 | +- [RouterStorage.sol](./contracts/RouterStorage.sol) |
| 157 | +- [ActionStorage.sol](./contracts/ActionStorage.sol) |
| 158 | +- [ActionSimple.sol](./contracts/ActionSimple.sol) |
| 159 | + |
| 160 | +## Проверка в Remix IDE |
| 161 | + |
| 162 | +В качестве домашнего задания воспроизведите вызов функции `execute()` через смарт-контракт `Router.sol`. |
| 163 | + |
| 164 | +Для этого необходимо открыть [Remix IDE](https://remix.ethereum.org/), перенести код смарт-контрактов, задеплоить и сделать вызов. В результате вы должны получить событие `Executed()`. |
| 165 | + |
| 166 | +**Подсказка 1** |
| 167 | + |
| 168 | +Calldata для вызова функции `execute()` - `0x61461954`. Проверить можно при помощи Chisel от Foundry. Для этого вызвать команду `abi.encodeWithSignature("execute()")`. |
| 169 | + |
| 170 | +**Подсказка 2** |
| 171 | + |
| 172 | +После того, как все смарт-контракты задеплоены необходимо добавить селектор функции `execute()` и адрес смарт-контракта `ActionSimple.sol` (куда должен быть делегирован вызов) в `mapping selectorToFacet`. |
| 173 | + |
| 174 | +Для этого придется закодировать вызов функции `setSelectorToFacets()` для смарт-контракта `Router.sol`. Это можно сделать при помощи chisel, еще одного смарт-контракта или своим способом. Затем вызывать функцию `setSelectorToFacets()` на смарт-контракте `Router.sol`. |
| 175 | + |
| 176 | +## Вывод |
| 177 | + |
| 178 | +Протокол Pendle пересмотрел паттерн **Diamond** и упростил его под себя. При этом, реализованный код по-прежнему позволяет не только эффективно маршрутизировать пользовательские вызовы, но и: |
| 179 | +- Обходить проблему ограничения размера смарт-контракта. |
| 180 | +- Обновлять логику работы смарт-контрактов, добавлять новые смарт-контракты, отключать старые. |
| 181 | + |
| 182 | +Осторожно - личное мнение! Порой, лично меня останавливало внедрение паттерна **Diamond** на своем проекте. Я постоянно прикидывал насколько будет целесообразно увеличить сложность в угоду возможной масштабируемости. Поэтому использование упрощенного подхода, как в Pendle, может быть компромиссом между сложностью и масштабируемостью. |
| 183 | + |
| 184 | +## Links |
| 185 | + |
| 186 | +1. [Pendle documentation](https://docs.pendle.finance/Home) |
| 187 | +2. [Pendle github](https://github.com/pendle-finance/pendle-core-v2-public/tree/main) |
| 188 | + |
| 189 | + |
0 commit comments