Safe{Wallet} Plugin Development
Safe (formerly Gnosis Safe) is the de-facto standard for multisig custody in Web3. $100B+ TVL, thousands of DAOs and protocols. But Safe's basic functionality is just multisig. Plugins (formerly Modules) extend it into a fully programmable treasury: automatic payments, role-based access, DeFi integrations, spending limits without separate multisig voting.
Safe Extensions Architecture in Safe v1.4+
Safe{Core} Protocol is a modular system on top of Safe Account. Since version 1.4, Safe moved to a new architecture with three types of extensions:
| Type | What it does |
|---|---|
| Plugin | Executes transactions on behalf of Safe without multisig |
| Hook | Validates transactions before/after execution |
| Function Handler | Handles calls to Safe through fallback |
Plugin is the most powerful type. It can call execTransactionFromModule() on the Safe contract, bypassing the signature threshold. This is why plugin installation requires full multisig approval.
interface ISafeProtocolPlugin {
function name() external view returns (string memory);
function version() external view returns (string memory);
function metadataHash() external view returns (bytes32);
}
A plugin is registered in SafeProtocolRegistry — a whitelist of approved extensions. A custom plugin for mainnet Safe either needs to pass an audit for inclusion in the official registry, or be used through a custom Manager.
Example: Spending Limit Plugin
The most common use case is restricted access for operational expenses. Instead of voting 3/5 on every payment, the team installs a plugin that allows a specific address to spend up to N USDC per day without multisig.
contract SpendingLimitPlugin is ISafeProtocolPlugin {
struct AllowanceConfig {
uint128 dailyLimit;
uint128 spent;
uint64 resetTimestamp;
}
// safe => token => delegate => config
mapping(address => mapping(address => mapping(address => AllowanceConfig))) public allowances;
function executeSpend(
ISafeProtocolManager manager,
ISafe safe,
address token,
address to,
uint128 amount
) external {
AllowanceConfig storage config = allowances[address(safe)][token][msg.sender];
// reset daily limit
if (block.timestamp >= config.resetTimestamp + 1 days) {
config.spent = 0;
config.resetTimestamp = uint64(block.timestamp);
}
require(config.spent + amount <= config.dailyLimit, "Daily limit exceeded");
config.spent += amount;
// Transfer through Safe without multisig
bytes memory data = abi.encodeCall(IERC20.transfer, (to, amount));
SafeTransaction memory tx = SafeTransaction({
to: token,
value: 0,
data: data,
operation: Enum.Operation.Call
});
(, bytes memory returnData) = manager.executeTransaction(safe, tx);
}
}
The official allowances module from Safe already implements similar logic — for standard spending limits, it's better to use it. A custom plugin is needed when non-standard logic is required: multi-token limits, role-based models, DeFi integration.
Hook: Transaction Validation Before Execution
A Hook allows you to add additional checks to any Safe transaction:
contract TransactionGuardHook is ISafeProtocolHook {
mapping(address => bool) public blockedAddresses;
function preCheck(
ISafe safe,
SafeTransaction calldata tx,
uint8 executionType,
bytes calldata executionMeta
) external view returns (bytes memory preCheckData) {
// Prevent transactions to blacklist addresses
require(!blockedAddresses[tx.to], "Blocked address");
// Prevent calls to dangerous functions
if (tx.data.length >= 4) {
bytes4 selector = bytes4(tx.data[:4]);
require(!blockedSelectors[selector], "Blocked function");
}
return abi.encode(block.timestamp);
}
function postCheck(ISafe safe, bool success, bytes calldata preCheckData) external {
// post-execution logic
}
}
Typical Hook use cases: compliance (blocking transactions to sanctioned addresses via Chainalysis oracle), budget control (preventing budget overruns), whitelist (only pre-approved recipient addresses).
Safe{Core} Protocol Manager
In the new architecture, a Plugin doesn't call Safe directly — only through SafeProtocolManager. The Manager is a mediator that verifies the plugin is enabled for the specific Safe and that the registry approves it.
// Enable plugin through multisig Safe transaction
function enablePlugin(address plugin, uint8 permissions) external authorized {
ISafeProtocolManager(MANAGER).enablePlugin(plugin, permissions);
}
permissions is a bitmask: EXECUTE_DELEGATECALL (0x01), EXECUTE_CALL (0x02). DelegateCall should be used carefully — a plugin with delegatecall rights executes in Safe's context and can modify its storage.
Testing Safe Plugins
Tests through Foundry with mainnet fork and real Safe:
contract SpendingLimitTest is Test {
ISafe safe;
SpendingLimitPlugin plugin;
function setUp() public {
// Fork mainnet
vm.createFork(MAINNET_RPC);
// Deploy Safe through SafeProxyFactory
safe = ISafe(safeFactory.createProxyWithNonce(
SAFE_SINGLETON,
initData,
block.timestamp
));
// Deploy and enable plugin
plugin = new SpendingLimitPlugin();
vm.prank(address(safe));
manager.enablePlugin(address(plugin), 2);
}
function test_SpendWithinLimit() public {
vm.prank(delegate);
plugin.executeSpend(manager, safe, USDC, recipient, 100e6);
assertEq(IERC20(USDC).balanceOf(recipient), 100e6);
}
function testFail_ExceedDailyLimit() public {
vm.prank(delegate);
plugin.executeSpend(manager, safe, USDC, recipient, 10000e6); // > daily limit
}
}
Development Process
Analytics (0.5-1 day). Determine extension type (Plugin/Hook/FunctionHandler), required permissions, integrations (DeFi protocols, oracles, other modules).
Development (2-3 days). Plugin contract, Foundry tests with mainnet fork. Safe registry integration. If custom Manager is needed — additional day.
Audit and review (0.5-1 day). Special attention to reentrancy through execTransactionFromModule, correctness of permission checks, impossibility of bypassing multisig through plugin under undesirable conditions.
Frontend integration (1 day). Safe Apps SDK for displaying plugin interface directly in Safe{Wallet} UI.
Total: 3-5 days for a typical plugin. Complex multi-module systems with DeFi integrations — up to 2 weeks.







