Skip to content

Commit 2110709

Browse files
committed
article: pendle routing example
1 parent 711c70c commit 2110709

File tree

9 files changed

+322
-0
lines changed

9 files changed

+322
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
- [Proxy pattern](./concepts/upgradeable-contracts/method-3/readme.md)
7373
- [Strategy pattern](./concepts//upgradeable-contracts/method-4/readme.md)
7474
- [Diamond pattern](./concepts/upgradeable-contracts/method-5/readme.md)
75+
- [Pendle routing way](./concepts/upgradeable-contracts/method-6/README.md)
7576
</details>
7677
</details>
7778
- <details>

concepts/upgradeable-contracts/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ _Важно!_ Контроль над обновлениями должен бы
2121
3. Использование **Proxy patterns** для делегирования вызова функций из неизменяемого прокси-контракта в изменяемый логический контракт. [Подробнее](./method-3/readme.md).
2222
4. Использование **Strategy pattern**. Создание неизменного основного контракта, который взаимодействует с гибкими вспомогательными контрактами и полагается на них для выполнения определенных функций. [Подробнее](./method-4/readme.md).
2323
5. Использование **Diamond pattern** для делегирования вызовов функций из прокси-контракта логическим контрактам. [Подробнее](./method-5/readme.md).
24+
6. Подход в Pendle маршрутизации. [Подробнее](./method-6/README.md).
2425

2526
## Плюсы и минусы обновления смарт-контрактов
2627

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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+
![](./images/pendle-routing.png)
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+
![](./images/pendle-prototype.png)
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+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.30;
3+
4+
/**
5+
* @notice Смарт-контракт, который выполняет простое действие
6+
* @dev Этот контракт используется для демонстрации вызова функции execute через делегирование вызова из смарт-контракта Router
7+
*/
8+
contract ActionSimple {
9+
event Executed(bool success);
10+
11+
/**
12+
* @notice Выполняет действие и генерирует событие Executed
13+
* @dev Эта функция вызывается через делегирование вызова из смарт-контракта Router
14+
*/
15+
function execute() external {
16+
emit Executed(true);
17+
}
18+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.30;
3+
4+
import {RouterStorage} from "./RouterStorage.sol";
5+
6+
struct SelectorsToFacet {
7+
address facet;
8+
bytes4[] selectors;
9+
}
10+
11+
interface IActionStorage {
12+
function setSelectorToFacets(SelectorsToFacet[] calldata arr) external;
13+
}
14+
15+
/**
16+
* @notice Смарт-контракт для хранения и управления селекторами функций и их соответствующими адресами фасетов
17+
* @dev Добавлять и удалять новые селекторы может только владелец контракта
18+
*/
19+
contract ActionStorage is RouterStorage {
20+
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
21+
event SelectorToFacetSet(bytes4 indexed selector, address indexed facet);
22+
23+
modifier onlyOwner() {
24+
require(msg.sender == owner(), "Ownable: caller is not the owner");
25+
_;
26+
}
27+
28+
function owner() public view returns (address) {
29+
return _getCoreStorage().owner;
30+
}
31+
32+
function setSelectorToFacets(SelectorsToFacet[] calldata arr) external onlyOwner {
33+
CoreStorage storage $ = _getCoreStorage();
34+
35+
for (uint256 i = 0; i < arr.length; i++) {
36+
SelectorsToFacet memory s = arr[i];
37+
38+
for (uint256 j = 0; j < s.selectors.length; j++) {
39+
$.selectorToFacet[s.selectors[j]] = s.facet;
40+
emit SelectorToFacetSet(s.selectors[j], s.facet);
41+
}
42+
}
43+
}
44+
45+
function selectorToFacet(bytes4 selector) external view returns (address) {
46+
CoreStorage storage $ = _getCoreStorage();
47+
return $.selectorToFacet[selector];
48+
}
49+
50+
function transferOwnership(address newOwner) external onlyOwner {
51+
CoreStorage storage $ = _getCoreStorage();
52+
53+
require(newOwner != address(0), "Ownable: zero address"); // TODO: custom error
54+
55+
$.owner = newOwner;
56+
57+
emit OwnershipTransferred($.owner, newOwner);
58+
}
59+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.30;
3+
4+
import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol";
5+
6+
import {RouterStorage} from "./RouterStorage.sol";
7+
import {IActionStorage} from "./ActionStorage.sol";
8+
9+
/**
10+
* @notice Смарт-контракт Router, который делегирует вызовы к фасетам на основе селекторов
11+
* @dev Является прокси-контрактом, который использует делегирование вызовов для выполнения функций на разные смарт-контракты (фасеты)
12+
*/
13+
contract Router is Proxy, RouterStorage {
14+
error InvalidSelector();
15+
16+
constructor(address actionStorage) {
17+
RouterStorage.CoreStorage storage $ = _getCoreStorage();
18+
$.owner = msg.sender;
19+
// Регистрируем селектор функции setSelectorToFacets в ActionStorage для последующего добавления новых селекторов функция и адресов смарт-контрактов, где эти функции реализованы
20+
$.selectorToFacet[IActionStorage.setSelectorToFacets.selector] = actionStorage;
21+
}
22+
23+
function _implementation() internal view override returns (address facet) {
24+
RouterStorage.CoreStorage storage $ = _getCoreStorage();
25+
26+
// Получаем адрес фасета для селектора функции из вызова
27+
facet = $.selectorToFacet[msg.sig];
28+
if (facet == address(0)) {
29+
revert InvalidSelector();
30+
}
31+
}
32+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.30;
3+
4+
/**
5+
* @notice Хранилище для роутера
6+
* @dev Содержит информацию о владельце и списка фасетов для селекторов функций
7+
*/
8+
abstract contract RouterStorage {
9+
struct CoreStorage {
10+
address owner;
11+
mapping(bytes4 selector => address facet) selectorToFacet;
12+
}
13+
14+
// keccak256(abi.encode("the-same-pendle-routing"))
15+
bytes32 private constant STORAGE_LOCATION = 0x25e5c12553aca6bac665d66f71e8380eae2ff9ef17f649227265212ec2f7f613;
16+
17+
function _getCoreStorage() internal pure returns (CoreStorage storage $) {
18+
assembly {
19+
$.slot := STORAGE_LOCATION
20+
}
21+
}
22+
}
125 KB
Loading
144 KB
Loading

0 commit comments

Comments
 (0)