Оголошуємо SignPass: дозволене Спонсорство газу за допомогою Мета-транзакції і проксі
У співпраці з ARCx ми випустили 10 скінів EthSign Sign Pass NFT, які доповнюють їх DeFi Passport раніше на цьому тижні. В якості бонусу ми плануємо спонсорувати всі збори за газ, що стягуються власниками SignPass NFT при використанні EthSign. Технічно це включає в себе використання контролю дозволів та мета-транзакцій для спонсорування gas лише в тому випадку, якщо абонент є власником нашого NFT. Ці теми тривіальні для реалізації самі по собі, але все може трохи ускладнитися, коли ми спробуємо реалізувати їх разом. Крім того, ми хочемо, щоб все, що ми в кінцевому підсумку створюємо, було модульним, універсальним і, найголовніше, підключеним і відтворюваним. Це виявилося досить забавним випробуванням, так що давайте відразу перейдемо до технічних деталей!
Код і анотації: https://github.com/boyuanx/Solidity-Calls
Проблема
Найпростіше, що можна зробити, - це вбудувати відповідність стандарту ERC2771 прямо в смарт-контракт EthSign 3.0. Хоча він був розгорнутий кілька місяців тому, його можна оновити, і тому у нас є можливість змінити його реалізацію. Однак ми хотіли б уникнути цього з кількох причин:
- Якщо код працює добре, то постарайтеся не змінювати його.
- Це забезпечує щільну зв'язок між компонентами.
- Ми хочемо, щоб рішення було універсальним і модульним.
План
"Всі проблеми в інформатиці можуть бути вирішені за допомогою іншого рівня непрямості". - Девід Уілер
Єдиний спосіб зробити наше рішення дійсно підключеним і працюючим, не зачіпаючи існуючий контракт, - це використовувати проксі-сервер. Проксі-сервери не є чимось новим-це базовий механізм, який дозволяє оновлювати смарт-контракти. По суті, проксі-сервер відстежує адресу базового контракту на реалізацію і делегує виконання реалізації, самостійно керуючи сховищем. Використання delegatecall дозволяє оновлювати логіку контракту, не зачіпаючи сховище.
У нашому випадку використання ми хотіли б створити проксі-сервер, який діє тільки як дозволений шлюз ERC2771, який перевіряє відповідність вимогам користувача і, якщо відповідає вимогам, перенаправляє виклик в контракт на впровадження, не стягуючи з користувача ніяких зборів за газ. Оскільки ми все ще хочемо використовувати базове сховище контракту на реалізацію, замість delegatecall буде використовуватися call. У нас буде спеціальна стаття, в якій детально пояснюється різниця між викликом, кодом виклику і делегованим викликом.
Сам проксі-сервер надзвичайно простий і зрозумілий. Ми будемо використовувати резервну функцію і деякі хитрощі ABI, щоб змусити її працювати з будь-яким смарт-контрактом. Резервна функція-це спеціальна функція, яка викликається, коли запитана сигнатура функції не може бути знайдена. Зазвичай це призводить до повернення або зупинки коду операції, але ми можемо перепрофілювати його, щоб перенаправити вхідний дзвінок на наш контракт на реалізацію.
Страта
На даний момент нам дійсно потрібно виконати лише 4 речі:
- Напишіть проксі-контракт за допомогою виклику
- Включити сумісність з ERC2771
- Перевірка дозволів користувача
- Удоскональте ABI в JS
Написання дозволеного проксі-сервера
Ця частина дуже проста, оскільки ми можемо використовувати бібліотеку контрактів OpenZeppelin. Вони надають попередньо встановлений проксі-сервер, який ми можемо використовувати відразу ж:
import “@openzeppelin/contracts/proxy/Proxy.sol”;abstract contract AbstractProxy is Proxy { ... }
По-перше, нам потрібно було б реалізувати дві віртуальні функції:
function _implementation() internal view virtual returns (address);function _beforeFallback() internal virtual {}
_implementation () відносно проста, оскільки вона просто повертає адресу нашого контракту на реалізацію, в той час як _beforeFallback() більш цікавий. Як ви можете зробити висновок з назви, _beforeFallback() викликається до виконання фактичної переадресації, і, таким чином, ми можемо використовувати його, щоб визначити, чи дійсно виклик функції повинен бути переадресований чи ні. Ми не будемо описувати, що писати в цій функції, оскільки вона повністю відрізняється від випадку до випадку, але ось пара ідей:
- Перевірте, чи містить відправник певний NFT
- Перевірте, чи зареєстрований відправник у зіставленні
- Потім нам потрібно перевизначити і змінити наступну функцію:
function _delegate(address implementation) internal virtual { ... }
_delegate () - це функція, яка обробляє фактичну пересилку з використанням Solidity assembly. Ви можете переглянути той самий код з детальними коментарями в сховищі GitHub, але заради форматування я видалив їх з наведених нижче фрагментів.
За замовчуванням він використовує delegatecall:
assembly { calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) }}
Як ми вже згадували раніше, замість цього ми хочемо використовувати call. Для цього потрібно тільки зміна в один рядок:
let result := call(gas(), implementation, callvalue(), 0, calldatasize(), 0, 0)
Зверніть увагу, що тепер у нас є додатковий callvalue() в переданих аргументах.
Одне важливе застереження, яке необхідно мати на увазі: msg.sender більше не буде вказувати на передбачуваного абонента. Замість цього він завжди вказує на проксі-контракт. Ми детально розглянемо цю динаміку в окремій статті. Існують різні способи вирішення цієї проблеми, одним з яких є використання ecrecover(), але було б непогано побачити додавання нового типу виклику, який перенаправляє msg.sender, оскільки виклик ecrecover () також споживає деяку кількість газу.
Хоча рішення, врешті-решт, не є повністю підключеним і відтворюваним, перевага полягає в тому, що немає необхідності змінювати будь-які існуючі зовнішні інтерфейси. Замість цього ми можемо додати ті, які призначені для виклику "оператором" від імені відправника, подібно до того, як це робиться в різних контрактах з токенами, таких як permit() in DAI.
Забезпечення відповідності стандарту ERC2771
Інтеграція ERC2771 дуже проста з OpenZeppelin. Нам просто потрібно імпортувати, успадковувати від відповідного модуля і викликати батьківський конструктор:
import “@openzeppelin/contracts/metatx/ERC2771Context.sol”;abstract contract AbstractProxy is Proxy, ERC2771Context { constructor(address trustedForwarder) ERC2771Context(trustedForwarder) { ... }}
Обман Абі
У той час як наш проксі-контракт тепер готовий до роботи, в зовнішньому інтерфейсі виникла проблема. Коли ми створюємо екземпляр контракту в web3.js або ethers.JS, фреймворк зчитує дані з ABI, щоб визначити список функцій, які нам дозволено викликати. Якщо ми використовуємо проксі-сервер ABI, нам буде дозволено викликати тільки функції, зазначені в проксі-контракті. Але ми хочемо викликати функції в контракті на реалізацію, і тому ми повинні прикріпити ABI реалізації до екземпляра проксі-сервера. Це абсолютно нормально, тому що ми впровадили резервну функцію, і вона правильно перенаправляє виклик в контракт на реалізацію, де ABI буде відповідати.
Вуаля!
Тепер ми створили дозволений проксі-контракт, який дозволяє нам оплачувати плату за газ для обраних користувачів. Комбінація ERC2771 і дозволеного доступу, безумовно, відкриває безліч можливостей в якості базової архітектури SignPass.
наступні кроки
Дуже скоро ми випустимо велику партію SignPass на BSC і Polygon, слідкуйте за оновленнями! Перші користувачі, партнери по протоколу і інвестори EthSign отримають SignPass після цього випуску, ми дякуємо Вам за вашу підтримку на цьому шляху!