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:
- Safe owners enable module see
- module prepares target call
- module calls
Safe.execTransactionFromModule(...) - Safe verifies module is enabled
- 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:
- propose the same transfer again
- collect signatures again
- repeat forever
That is operationally expensive and error-prone.
With a module, you write the policy once, then trigger execution when due.
- Full source in this git StipendModule.sol
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);
}
What it stores:
- safeWallet
- token
- recipient
- amount
- lastExecuted
- interval = 4 weeks
What stipend() does:
- verifies interval has passed
- ABI-encodes transfer(recipient, amount)
- updates lastExecuted
- requests Safe execution via
execTransactionFromModule
Execution path:
any caller
-> StipendModule.stipend()
-> Safe.execTransactionFromModule(token, transferData, Call)
-> ERC20.transfer(recipient, amount)
Why this is useful:
- deterministic recurring transfer
- no monthly signer fatigue
- signer control still exists because owners can disable the module
Runbook
- Deploy module with Safe, token, recipient, amount
- Enable module in Safe with
enableModule(moduleAddress) - Wait for interval
- Call
stipend() - Repeat calls every interval (manual or automated)
- Disable module when policy changes
Security Notes Before Production
This sample is intentionally simple. Harden before mainnet:
Access policy: stipend() is permissionless by design in this sample
Observability: emit events to improve traceability
Emergency controls : consider pause or kill-switch patterns aligned with governance model
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
- Gnosis Safe Modules docs: https://docs.safe.global/advanced/smart-account-modules
- How to add a module: https://help.safe.global/en/articles/40826-add-a-module
- Safe modules examples: https://github.com/safe-fndn/safe-modules
- Safe smart account contracts: https://github.com/safe-fndn/safe-smart-account
Top comments (0)