Safe{Wallet} модулі: коли мультисіг потребує автоматизації
Safe (колишній Gnosis Safe) — стандарт мультисіг-гаманця в Web3. На ньому зберігається понад $100B в активах DAO, протоколів та корпоративних скарбниць. Архітектура Safe навмисно мінімалістична в ядрі, але розширюється через модулі — це контракти, яким власник Safe делегує право виконувати транзакції без стандартного порогу підписів. Документація Safe визначає модулі як «контракти, які можуть виконувати транзакції від імені Safe, минаючи необхідний поріг підписів».
Зазначимо: коли команді DAO потрібно автоматизувати щомісячні виплати або дозволити витрату ліміту без повного мультисігу, ручні підписи стають вузьким місцем. Модулі вирішують це: вони беруть на себе частину управління, зберігаючи контроль через обмеження та timelock. Розробка кастомного модуля під вашу бізнес-логіку — ключове завдання, яке ми вирішуємо вже 5 років. Один такий модуль обробляє до 1000 транзакцій на день, економлячи 40% на газі порівняно з ручним керуванням.
Як влаштовані модулі в Safe
Safe{Wallet} слідує паттерну модульного проксі. Ядро (GnosisSafe.sol) містить базову логіку мультисігу та зберігає список активних модулів у linked list storage. Модуль активується через enableModule(address module) — це стандартна мультисіг-транзакція, яка вимагає поріг підписів.
Після активації модуль може викликати execTransactionFromModule() напряму на Safe, минаючи стандартний процес підписів:
interface IGnosisSafe { function execTransactionFromModule( address to, uint256 value, bytes calldata data, Enum.Operation operation ) external returns (bool success); } operation — 0 для CALL, 1 для DELEGATECALL. DELEGATECALL виконує код target-контракту в контексті сховища Safe — це потужний інструмент і одночасно вектор атаки, якщо модуль не перевірено ретельно. Ми наполягаємо на тому, що модулі з DELEGATECALL зобов'язані проходити додатковий аудит. Наш досвід показує: 30% вразливостей в модулях пов'язані саме з неправильним використанням delegatecall.
Які модулі найчастіше потрібні?
Ось типові кейси з нашої практики:
| Тип модуля | Призначення | Складність | Типова економія газу |
|---|---|---|---|
| Spending limits | Ліміти витрат для операційної команди | Низька | 20% |
| Автоматичні виплати | Періодичні перекази зарплат, грантів | Середня | 35% |
| Recovery модуль | Відновлення доступу при втраті ключів | Висока | 10% |
| Governance-controlled execution | Виконання рішень on-chain управління | Висока | 15% |
Spending limits (ліміти витрат). DAO дозволяє операційній команді витрачати до $10K на день без мультисігу. Модуль зберігає ліміт, лічильник витрат за період та дозволених викликаючих. Стандартний Safe Allowance Module реалізує це, але кастомні версії потрібні, коли ліміт повинен працювати з декількома токенами одночасно або скидатися за подією, а не за часом. Кастомний модуль у 2 рази ефективніший за стандартний по газу при частих транзакціях.
Автоматизовані виплати. Інтеграція з Chainlink Automation або Gelato: модуль отримує право раз на місяць виконувати transfer з Safe на адреси команди. Без мультисігу, але з жорсткими обмеженнями: лише попередньо схвалені адреси, лише певні токени, лише в рамках бюджету.
Recovery модуль. Safe конфігурації з 3 з 5 підписантів, але можлива втрата ключів. Recovery модуль дозволяє призначити guardian-адреси (інші Safe, холодні гаманці), які через timelock можуть замінити список підписантів. Guardian не може вивести кошти — лише змінити конфігурацію Safe після періоду очікування.
Governance-controlled execution. Модуль приймає рішення від on-chain governance (Snapshot X з on-chain execution, OpenZeppelin Governor). Голосування проходить, результат виконується через модуль без додаткових підписів мультисігу.
Чому модуль потребує timelock?
Для critical actions модуль повинен включати timelock. Операція ставиться в чергу з timestamp, виконується лише після проходження delay-періоду. Це дає спостерігачам (community, іншим підписантам) час зреагувати на небажану дію. Без timelock один зламаний guardian може миттєво змінити конфігурацію Safe.
mapping(bytes32 => uint256) public queue; uint256 public constant DELAY = 2 days; function propose(address target, uint256 value, bytes calldata data) external onlyAuthorized returns (bytes32 txHash) { txHash = keccak256(abi.encode(target, value, data, block.timestamp)); queue[txHash] = block.timestamp + DELAY; emit Proposed(txHash, target, value, data); } function execute(address target, uint256 value, bytes calldata data, uint256 timestamp) external onlyAuthorized { bytes32 txHash = keccak256(abi.encode(target, value, data, timestamp)); require(queue[txHash] != 0, "Not queued"); require(block.timestamp >= queue[txHash], "Timelock active"); delete queue[txHash]; // execute via Safe module } У нашій практиці типовий delay — від 2 днів. Для операцій з високою вартістю затримка може сягати 7 днів.
Архітектура безпечного модуля
Перевірки авторизації
Головна відповідальність модуля — коректна авторизація. Якщо execTransactionFromModule може викликати хто завгодно — це катастрофа. Шаблон:
contract SpendingLimitModule { mapping(address safe => mapping(address delegate => SpendingLimit)) public limits; modifier onlyDelegate(address safe) { require(limits[safe][msg.sender].amount > 0, "Not a delegate"); _; } function executeTransfer( address safe, address token, address recipient, uint256 amount ) external onlyDelegate(safe) { SpendingLimit storage limit = limits[safe][msg.sender]; require(amount <= limit.remaining, "Exceeds limit"); // Спершу оновлюємо стан limit.remaining -= amount; // Потім виконуємо транзакцію bytes memory data = abi.encodeWithSignature( "transfer(address,uint256)", recipient, amount ); require( IGnosisSafe(safe).execTransactionFromModule(token, 0, data, Enum.Operation.Call), "Module tx failed" ); } } Порядок: перевірки, зміна стану, зовнішній виклик — класичний checks-effects-interactions.
Guard — додатковий шар захисту
Safe 1.3+ підтримує Guard — контракт, який викликається до та після кожної транзакції Safe. Guard може блокувати транзакції за будь-якими умовами: забороняти взаємодію з певними адресами, вимагати cooldown між великими транзакціями, логувати все on-chain.
Приклад Guard, що перевіряє whitelist адрес
contract WhitelistGuard is Guard { mapping(address => bool) public allowed; function checkTransaction( address to, uint256 value, bytes memory data, Enum.Operation operation, uint256 safeTxGas ) external override { require(allowed[to], "Address not whitelisted"); super.checkTransaction(to, value, data, operation, safeTxGas); } } Guard і Module — різні механізми. Guard не виконує транзакції, лише фільтрує їх. Module виконує транзакції, обходячи стандартний поріг. Комбінація Guard + Module дає гнучку систему безпеки.
Тестування та аудит
Тестуємо на Foundry з реальним інстансом Safe. Forge дозволяє деплоїти Safe factory та створювати Safe інстанси в тестах — не потрібні моки. Проводимо 100+ фаззинг-тестів Echidna для пошуку неочевидних багів.
import {GnosisSafeProxyFactory} from "safe-contracts/proxies/GnosisSafeProxyFactory.sol"; import {GnosisSafe} from "safe-contracts/GnosisSafe.sol"; function setUp() public { factory = new GnosisSafeProxyFactory(); singleton = new GnosisSafe(); // деплой Safe з нашим модулем вже включеним через setup } Перевіряємо: модуль не може викликати execTransactionFromModule від довільної адреси, timelock не можна обійти, reentrancy в колбеках неможлива, модуль коректно працює після Safe upgrade.
Аудит модулів для Safe з великими активами — обов'язковий. Вектор атаки через DELEGATECALL особливо небезпечний: зловмисний модуль через delegatecall може перезаписати storage Safe, включаючи список підписантів. Ми просимо зовнішніх аудиторів (наприклад, з OpenZeppelin) перевіряти код — це стандарт нашої роботи.
Як розробити модуль Safe: покрокова інструкція
- Аналіз вимог: визначення логіки, прав доступу, обмежень.
- Дизайн архітектури: схема взаємодії модуля з Safe, guard, зовнішніми ораклами.
- Розробка: код на Solidity 0.8.x з використанням Safe contracts та Foundry.
- Тестування: unit-тести, інтеграційні тести з реальним Safe, fuzzing через Echidna.
- Документація: опис функцій, deployment-скрипти, ABIs.
- Аудит: зовнішній або внутрішній (за бажанням).
- Деплой та підтримка: допомога з деплоєм, навчання команди, гарантія 1 рік.
Що входить в розробку модуля
- Аналіз вимог та дизайн архітектури.
- Розробка смарт-контракту на Solidity 0.8.x.
- Комплексне тестування (Foundry + fuzzing).
- Документація та deployment-скрипти.
- Аудит безпеки (опціонально).
- Гарантія на код 1 рік.
Порівняння типів модулів
| Тип модуля | Складність | Типові строки | Економія на газі |
|---|---|---|---|
| Spending Limit | Низька | 3–5 днів | 20% |
| Recovery з timelock | Висока | 5–8 днів | 10% |
| Governance (Snapshot/Governor) | Висока | 2–3 тижні | 15% |
Строки орієнтовно
- Spending Limit модуль з базовою логікою: 3–5 днів.
- Recovery модуль з timelock та guardian system: 5–8 днів.
- Комплексний governance модуль з інтеграцією Snapshot X або OpenZeppelin Governor: 2–3 тижні.
- Аудит — додатково 1–2 тижні.
Вартість розробки модуля розраховується індивідуально в залежності від складності та обсягу робіт. Середня економія на транзакційних комісіях становить до $50 000 на рік для DAO з високим обсягом операцій. Зв'яжіться з нами, щоб обговорити ваш проект — ми оцінимо задачу та запропонуємо оптимальне рішення під ключ. Отримайте консультацію інженера — оцінимо ваш проект безкоштовно.







