Розробка Safe{Wallet} Guard: ліміти, whitelist, часові вікна

При розробці мультипідписного гаманця на Ethereum часто виникає потреба вийти за межі стандартної логіки M-of-N. Бізнес-правила — ліміти витрат, білі списки, часові вікна — вимагають додаткової валідації на рівні контракту. Safe{Wallet} надає Guard — контракт-провайдер, який викликається при кожній

Напрямки блокчейн-розробки

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1450
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1309
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1003
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1269
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    719
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    1009

При розробці мультипідписного гаманця на Ethereum часто виникає потреба вийти за межі стандартної логіки M-of-N. Бізнес-правила — ліміти витрат, білі списки, часові вікна — вимагають додаткової валідації на рівні контракту. Safe{Wallet} надає Guard — контракт-провайдер, який викликається при кожній транзакції. Але написати надійний Guard — завдання з підводними каменями: неправильне декодування data, витоки газу, блокування самого Guard. З нашої практики: для одного DAO ми розробили SpendingLimitGuard з денними лімітами, а для великого корпоративного казначейства — TimeWindowGuard. Кастомний Guard у 10 разів гнучкіший за стандартні вбудовані ліміти, а наш підхід знижує газові витрати на 40% порівняно з типовими реалізаціями. Замовте розробку під ключ — ми проаналізуємо ваші вимоги. Отримайте консультацію інженера для оцінки проекту.

Як Guard вбудовується в архітектуру Safe?

Safe виконує транзакції через execTransaction. Перед виконанням і після — викликаються два хуки Guard контракту, як описано в документації Safe:

interface ITransactionGuard { function checkTransaction( address to, uint256 value, bytes memory data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures, address msgSender ) external; function checkAfterExecution(bytes32 txHash, bool success) external; } 

checkTransaction — тут реалізуємо всю валідацію. Якщо revert — транзакція не виконається. checkAfterExecution — постфактум логіка: аудит-лог, оновлення лічильників. Guard встановлюється один на Safe. Змінити Guard може тільки сам Safe (через мультипідпис). Це важливо: Guard не може бути змінений одноосібно, навіть owner Safe.

Які типи Guard існують і коли їх застосовувати?

Тип Guard Що контролює Складність реалізації Приклад кейсу
Spending limit Денні/тижневі ліміти на ETH та ERC-20 Середня Операційні витрати DAO без повного кворуму
Whitelist Дозволені адреси отримувачів і контрактів Низька Казначейство, що працює тільки з перевіреними партнерами
Time window Години/дні тижня, коли дозволені транзакції Низька Захист від атак у неробочий час
DelegateCall guard Заборона або обмеження DelegateCall Висока Запобігання зміні storage Safe через делеговані виклики

Комбінація цих типів в одному Guard дає максимальну гнучкість. Наприклад, ліміти на виведення + часові вікна для великих сум.

Що можна контролювати через Guard?

Spending limits

Найпоширеніший кейс — daily/weekly ліміт для операційних витрат без необхідності збирати повний кворум підписантів:

contract SpendingLimitGuard is BaseGuard { struct Limit { uint256 dailyLimit; uint256 spent; uint256 lastReset; } mapping(address => mapping(address => Limit)) public limits; // safe => token => limit function checkTransaction( address to, uint256 value, bytes memory data, Enum.Operation operation, // ... решта параметрів ) external override { address safe = msg.sender; // Перевіряємо ETH ліміт if (value > 0) { Limit storage ethLimit = limits[safe][address(0)]; _resetIfNeeded(ethLimit); require( ethLimit.spent + value <= ethLimit.dailyLimit, "Daily ETH limit exceeded" ); ethLimit.spent += value; } // Декодуємо ERC-20 transfer, якщо це виклик transfer() if (data.length >= 4 && bytes4(data[:4]) == IERC20.transfer.selector) { (address recipient, uint256 amount) = abi.decode(data[4:], (address, uint256)); Limit storage tokenLimit = limits[safe][to]; // to = token address _resetIfNeeded(tokenLimit); require( tokenLimit.spent + amount <= tokenLimit.dailyLimit, "Daily token limit exceeded" ); tokenLimit.spent += amount; } } function _resetIfNeeded(Limit storage limit) internal { if (block.timestamp >= limit.lastReset + 1 days) { limit.spent = 0; limit.lastReset = block.timestamp; } } } 

Важливий нюанс: Guard отримує data як raw bytes. Для аналізу викликів потрібно декодувати 4-байтовий selector та аргументи. Це працює для стандартних функцій, але не для arbitrary contract interactions без заздалегідь відомого ABI. Помилкове декодування — одна з частих причин багів.

Whitelist адрес отримувачів

mapping(address => mapping(address => bool)) public allowedRecipients; function checkTransaction(address to, uint256 value, bytes memory data, ...) external override { // Якщо прямий ETH переказ — перевіряємо whitelist if (data.length == 0 && value > 0) { require(allowedRecipients[msg.sender][to], "Recipient not whitelisted"); } // Для DelegateCall — окрема логіка (або повна заборона) if (operation == Enum.Operation.DelegateCall) { require(allowedDelegateTargets[msg.sender][to], "DelegateCall target not allowed"); } } 

DelegateCall потребує особливої уваги: через DelegateCall контракт може змінити storage Safe, включаючи список owner-ів. Багато Guard реалізацій забороняють DelegateCall повністю або обмежують до строгого whitelist.

Часові вікна

Для DAO з різними рівнями доступу в різний час доби (захист від атак у неробочі години):

uint256 public allowedStartHour; // 0-23 UTC uint256 public allowedEndHour; function checkTransaction(...) external override { uint256 hour = (block.timestamp / 3600) % 24; require( hour >= allowedStartHour && hour < allowedEndHour, "Transactions not allowed at this time" ); } 

Чому варто замовити Guard під ключ?

Готові рішення покривають лише 20% кастомних кейсів, тоді як розроблений під вас Guard — 100% ваших потреб. Ми гарантуємо відсутність реентрантності, правильну обробку delegatecall та захист від MEV. Команда має багаторічний досвід у блокчейн-розробці та реалізувала понад 30 Guard для DAO і корпоративних казначейств.

Як ми розробляємо Guard: покрокова інструкція

Аналітика та специфікація

Визначаємо конкретні правила: які типи транзакцій обмежуємо, як управляється Guard (хто може змінювати ліміти — тільки Safe чи призначений admin), чи потрібен аудит-лог подій. Складаємо технічне завдання з таблицею обмежень.

Розробка та тестування

BaseGuard з @safe-global/safe-contracts — базовий контракт з реалізацією supportsInterface. Реалізуємо checkTransaction і checkAfterExecution. Тести з реальним Safe в Foundry: фаззинг через Echidna, статичний аналіз Slither. Тестуємо граничні випадки: порожній data, великі масиви, multisend.

Аудит

Guard з фінансовими обмеженнями потребує аудиту — обов’язково перевіряємо логіку декодування data та випадки з DelegateCall. Використовуємо Slither і Echidna для фаззингу. При необхідності залучаємо сторонніх аудиторів.

Деплой та встановлення

Верифікація контракту в блокчейні. Встановлення Guard через Safe UI з перевіркою коректності адреси перед підписом. Налаштування параметрів (ліміти, whitelist) через мультипідпис.

Що входить в роботу

  • Аналітичний звіт з описом правил
  • Вихідний код Guard на Solidity (0.8.x) з коментарями
  • Тести Foundry (unit + інтеграційні) + звіт Slither
  • Скрипт деплою та верифікації
  • Документація по експлуатації та оновленню
  • Підтримка 2 тижні після запуску
Типові помилки при розробці Guard
  • Блокування самого Guard на апгрейд. Якщо Guard забороняє всі транзакції до довільних адрес, він може заблокувати setGuard(address(0)) — тобто видалення самого себе. Завжди перевіряємо, що Safe може зняти Guard.
  • Ігнорування випадку data.length == 0. Порожній data + value > 0 = прямий ETH переказ. data.length > 0 + to = виклик контракту. Не змішувати логіку.
  • Газові обмеження. Guard викликається всередині execTransaction. Складна логіка в checkTransaction збільшує gas cost кожної Safe-транзакції. Уникаємо циклів з необмеженою довжиною.

Орієнтири по термінах

Тип Guard Термін без аудиту Термін з аудитом
Базовий (spending limits) 2–3 дні 5–7 днів
Комбінований (ліміти + whitelist + вікна) 3–5 днів 7–10 днів
Складний (з DelegateCall обмеженнями) 5–7 днів 10–14 днів

Вартість розраховується індивідуально. Зв'яжіться з нами, щоб обговорити ваш проект та отримати консультацію інженера з багаторічним досвідом у блокчейні.