Разработка модулей для Safe{Wallet}
Safe (бывший Gnosis Safe) — стандарт мультисиг-кошелька в Web3. На нём хранится больше $100B в активах DAO, протоколов и корпоративных казначейств. Архитектура Safe намеренно минималистична в ядре, но расширяема через модули — это контракты, которым владелец Safe делегирует право выполнять транзакции без стандартного порога подписей.
Как устроены модули в 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 — это мощный инструмент и одновременно вектор атаки если модуль не проверен тщательно.
Типичные кейсы для кастомных модулей
Spending limits (лимиты трат). DAO разрешает операционной команде тратить до $10K в день без мультисига. Модуль хранит лимит, счётчик трат за период и разрешённых вызывающих. Стандартный Safe Allowance Module реализует это, но кастомные версии нужны когда лимит должен работать с несколькими токенами одновременно или сбрасываться по событию, а не по времени.
Автоматизированные выплаты. Интеграция с Chainlink Automation или Gelato: модуль получает право раз в месяц выполнять transfer из Safe на адреса команды. Без мультисига, но с жёсткими ограничениями: только предодобренные адреса, только определённые токены, только в рамках бюджета.
Recovery модуль. Safe конфигурации с 3/5 подписантов, но что если 3 ключа потеряны? Recovery модуль позволяет назначить guardian-адреса (другие Safe, холодные кошельки), которые через timelock могут заменить список подписантов. Guardian не может вывести средства — только изменить конфигурацию Safe после периода ожидания.
Governance-controlled execution. Модуль принимает решения от on-chain governance (Snapshot X с on-chain execution, OpenZeppelin Governor). Голосование проходит, результат исполняется через модуль без дополнительных подписей мультисига.
Архитектура безопасного модуля
Проверки авторизации
Главная ответственность модуля — корректная авторизация. Если 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 и Module — разные механизмы. Guard не выполняет транзакции, только фильтрует их. Module выполняет транзакции, обходя стандартный порог.
Timelock в модуле
Для critical actions модуль должен включать timelock. Операция ставится в очередь с timestamp, выполняется только после прохождения delay-периода. Это даёт наблюдателям (community, другим подписантам) время среагировать на нежелательное действие.
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
}
Тестирование и аудит
Тестируем на Foundry с реальным инстансом Safe. Forge позволяет деплоить Safe factory и создавать Safe инстансы в тестах — не нужны моки.
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, включая список подписантов.
Сроки
Spending Limit модуль с базовой логикой: 3-5 дней. Recovery модуль с timelock и guardian system: 5-8 дней. Комплексный governance модуль с интеграцией Snapshot X или OpenZeppelin Governor: 2-3 недели. Аудит — дополнительно 1-2 недели.
Стоимость рассчитывается после уточнения требований к управлению и количеству интегрируемых систем.







