При розробці мультипідписного гаманця на 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 днів |
Вартість розраховується індивідуально. Зв'яжіться з нами, щоб обговорити ваш проект та отримати консультацію інженера з багаторічним досвідом у блокчейні.







