DEV Community

doncesarts
doncesarts

Posted on

How to Build a Gnosis Safe Module: Minimal Example in Solidity

If you have read Safe docs and still felt stuck building your first module it is not a surprise, the official documentation is good at definitions and use cases, but the examples are just too complex for a practical grasp of the mechanics.

The Core Safe Module Mental Model

A Safe module is a contract that the Safe owners explicitly authorize.

After it is enabled, that module can request Safe to execute transactions through the module execution path.

The important part: The transaction executes from the Safe context.

That means the call spends Safe funds and uses Safe permissions, not module funds.

Modules are extensions with unlimited access to a Safe that can be added to a Safe by its owners.
⚠️ WARNING: Modules are a security risk since they can execute arbitrary transactions, so only trusted and audited modules should be added to a Safe. A malicious module can completely takeover a Safe.

Minimal flow:

  1. Safe owners enable module see
  2. module prepares target call
  3. module calls Safe.execTransactionFromModule(...)
  4. Safe verifies module is enabled
  5. Safe executes call as Safe account

That is why modules can execute on behalf of a Gnosis Safe.

The Minimal Example: StipendModule

A gnosis safe wallet multisig needs to send a given amount of tokens to a recipient every X weeks.

Without a module, every period usually means:

  1. propose the same transfer again
  2. collect signatures again
  3. repeat forever

That is operationally expensive and error-prone.

With a module, you write the policy once, then trigger execution when due.

Module Snipped

contract StipendModule {
    address public immutable safeWallet;
    address public immutable token;
    address public immutable recipient;
    uint256 public lastExecuted;
    uint256 public immutable amount;
    uint256 public constant interval = 4 weeks;

    error MultiSendNotConfigured();
    error ZeroAddress();
    error ZeroAmount();
    error PrematureExecution(uint256 currentTime, uint256 nextAllowedTime);

    constructor(address _safeWallet, address _token, address _recipient, uint256 _amount) {
        if (_safeWallet == address(0)) revert ZeroAddress();
        if (_token == address(0)) revert ZeroAddress();
        if (_recipient == address(0)) revert ZeroAddress();
        if (_amount == 0) revert ZeroAmount();
        safeWallet = _safeWallet;
        token = _token;
        recipient = _recipient;
        amount = _amount;
        lastExecuted = block.timestamp;
    }

    /**
     * @notice  Execute a call via the Safe contract
     * @param to    Destination address to call
     * @param data  Data payload of the transaction
     * @return success bool for success
     */
    function _execCallFromModule(address to, bytes memory data)
        internal
        virtual
        returns (bool success)
    {
        IModuleManager safe = IModuleManager(safeWallet);

        success = safe.execTransactionFromModule({
            to: to, value: 0, data: data, operation: Enum.Operation.Call
        });
        require(success, "!success");
        return success;
    }

    function stipend() external {
        if (block.timestamp < lastExecuted + interval) {
            revert PrematureExecution(block.timestamp, lastExecuted + interval);
        }
        bytes memory data = abi.encodeWithSignature("transfer(address,uint256)", recipient, amount);
        lastExecuted = block.timestamp;
        _execCallFromModule(token, data);
    }
Enter fullscreen mode Exit fullscreen mode

What it stores:

  1. safeWallet
  2. token
  3. recipient
  4. amount
  5. lastExecuted
  6. interval = 4 weeks

What stipend() does:

  1. verifies interval has passed
  2. ABI-encodes transfer(recipient, amount)
  3. updates lastExecuted
  4. requests Safe execution via execTransactionFromModule

Execution path:

any caller
  -> StipendModule.stipend()
      -> Safe.execTransactionFromModule(token, transferData, Call)
          -> ERC20.transfer(recipient, amount)
Enter fullscreen mode Exit fullscreen mode

Why this is useful:

  • deterministic recurring transfer
  • no monthly signer fatigue
  • signer control still exists because owners can disable the module

Runbook

  1. Deploy module with Safe, token, recipient, amount
  2. Enable module in Safe with enableModule(moduleAddress)
  3. Wait for interval
  4. Call stipend()
  5. Repeat calls every interval (manual or automated)
  6. Disable module when policy changes

Security Notes Before Production

This sample is intentionally simple. Harden before mainnet:

  1. Access policy: stipend() is permissionless by design in this sample

  2. Observability: emit events to improve traceability

  3. Emergency controls : consider pause or kill-switch patterns aligned with governance model

  4. Token assumptions: test exact token behavior (non-standard ERC20 behavior exists)

Where to Go Next

If you understand this stipend module pattern, you already understand the core mechanism behind most Safe module and how it can lead to streamline operations of DAOs.

This is a very simple example of how to build a module, explore Zodiac or Gnosis Safe Module repositries for more:

References

Top comments (0)