Разработка Safe{Wallet} Guard

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка Safe{Wallet} Guard
Сложная
~3-5 рабочих дней
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Разработка Safe{Wallet} Guard

Safe{Wallet} — это de facto стандарт мультиподписного кошелька в Ethereum-экосистеме. Больше $100B TVL проходит через Safe контракты. Базовый функционал — M-of-N мультиподпись — закрывает большинство задач. Но корпоративные treasury, DAO-кассы и DeFi-протоколы часто нуждаются в дополнительных ограничениях: лимиты на транзакции, whitelist адресов получателей, блокировка определённых вызовов функций, временные окна для выводов. Для этого Safe предоставляет Guard-интерфейс.

Как работает Guard в архитектуре Safe

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

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

Лимиты на вывод (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 контрактов

Guard тестируем в контексте реального Safe. Используем safe-contracts пакет для деплоя Safe в Foundry-тестах:

// Foundry test
function setUp() public {
    safe = deploySafe(owners, threshold);
    guard = new SpendingLimitGuard();
    
    // Устанавливаем Guard через Safe транзакцию
    bytes memory setGuardData = abi.encodeCall(
        GuardManager.setGuard,
        address(guard)
    );
    executeSafeTx(safe, address(safe), 0, setGuardData);
}

function test_revertOnLimitExceeded() public {
    // Настраиваем лимит 1 ETH/день
    guard.setLimit(address(safe), address(0), 1 ether);
    
    // Первый перевод 0.9 ETH — проходит
    executeSafeTx(safe, recipient, 0.9 ether, "");
    
    // Второй перевод 0.2 ETH — превышает лимит
    vm.expectRevert("Daily ETH limit exceeded");
    executeSafeTx(safe, recipient, 0.2 ether, "");
}

Типичные ошибки при разработке 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 (кто может менять лимиты — только Safe или назначенный admin), нужен ли аудит-лог событий.

Разработка. BaseGuard из @safe-global/safe-contracts — базовый контракт с реализацией supportsInterface. Реализуем checkTransaction и checkAfterExecution. Тесты с реальным Safe.

Аудит. Guard с финансовыми ограничениями требует аудита — особенно логика декодирования data и граничные случаи с DelegateCall. Используем Slither и Echidna для фаззинга.

Деплой. Верификация контракта. Установка Guard через Safe UI с проверкой корректности адреса перед подписью.

Ориентиры по срокам

Guard с одним типом ограничений (например, только spending limits): 2-3 дня. Guard с комбинацией правил (whitelist + лимиты + временные окна) и тестами: 3-5 дней. Если требуется аудит — дополнительно 3-5 дней.