Build a custom module
Zodiac is an open standard. Anyone can write a module that controls an avatar through the
IAvatar interface. This page shows the base contracts, a minimal module, and the
deployment pattern.
Install the contracts package:
npm install @gnosis-guild/zodiac-coreThe contracts moved. @gnosis-guild/zodiac-core holds the base contracts. The
older packages @gnosis.pm/zodiac and @gnosis-guild/zodiac (v5) do not — v5
of @gnosis-guild/zodiac is the address registry and tooling.
The base contracts
| Contract | Use it for |
|---|---|
Module.sol | A contract that makes the avatar execute transactions. |
GuardableModule.sol | The same, plus a guard hook that checks each transaction before and after execution. |
Modifier.sol | A contract between modules and the avatar. It keeps its own list of enabled modules and exposes the IAvatar interface to them. |
GuardableModifier.sol | The same, plus a guard hook. |
Every base gives you: avatar and target addresses with owner-gated setters, an
owner, and the internal functions exec(to, value, data, operation) and
execAndReturnData(...) that forward to IAvatar(target).execTransactionFromModule….
A minimal module
This module lets its owner make the Safe execute arbitrary calls. It is the smallest useful skeleton:
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.24;
import { Module } from "@gnosis-guild/zodiac-core/contracts/core/Module.sol";
import { Operation } from "@gnosis-guild/zodiac-core/contracts/core/Operation.sol";
contract MyModule is Module {
constructor(address _owner, address _avatar, address _target) {
bytes memory initParams = abi.encode(_owner, _avatar, _target);
setUp(initParams);
}
/// The factory calls this function when it deploys a proxy.
function setUp(bytes memory initParams) public override initializer {
(address _owner, address _avatar, address _target) = abi.decode(
initParams,
(address, address, address)
);
_transferOwnership(_owner);
avatar = _avatar;
target = _target;
emit AvatarSet(address(0), _avatar);
emit TargetSet(address(0), _target);
}
function executeCall(
address to,
uint256 value,
bytes calldata data
) external onlyOwner returns (bool success) {
success = exec(to, value, data, Operation.Call);
}
}The rules that make a module factory-compatible:
- Write all initialization in
setUp(bytes memory initParams)with theinitializermodifier. Decode your parameters frominitParams. - Call
setUpfrom the constructor too. This initializes the mastercopy itself, so nobody can claim it. - Send transactions through
exec/execAndReturnData. Never call the Safe directly. - Keep
avatarandtargetseparate. Your module then works behind a Delay or Roles Modifier without changes: the deployer pointstargetat the modifier.
A runnable project with tests is in the zodiac-mod-starter-kit .
Write a modifier
Inherit Modifier instead of Module. Your contract must override
execTransactionFromModule and execTransactionFromModuleReturnData, and use the
moduleOnly modifier on both. Callers first need enableModule on your modifier. Call
setupModules() in your setUp. The Delay Modifier
(source ) is a compact real
example.
Deploy through the factory
All Zodiac instances are EIP-1167 minimal proxies of a mastercopy, deployed by the Module
Proxy Factory (0x000000000000aDdB49795b0f9bA5BC298cDda236, all chains):
function deployModule(address masterCopy, bytes memory initializer, uint256 saltNonce)
public returns (address proxy)initializer is the encoded setUp call. The factory deploys with CREATE2, so the proxy
address is a function of (mastercopy, initializer, saltNonce). The TypeScript helpers in
@gnosis-guild/zodiac-core wrap this:
import {
encodeDeployProxy,
predictProxyAddress,
} from '@gnosis-guild/zodiac-core'
import {
deployMastercopy,
deployProxy,
verifyMastercopy,
} from '@gnosis-guild/zodiac-core/tooling'encodeDeployProxy/predictProxyAddress— build the deployment payload and predict the address, for use inside apps and batched Safe transactions.deployMastercopy— deploy your own mastercopy through the ERC-2470 singleton factory, for one canonical address on every chain.deployProxy— deploy an instance in scripts and tests.verifyMastercopy— verify the source on the block explorer.
Checklist before you ship
- Test the module behind a Delay Modifier and behind a Roles Modifier, not only directly against a Safe.
- Make sure
setUpcannot run twice, on the proxy and on the mastercopy. - Get an audit. A module is a direct path into user Safes.
- Publish the mastercopy address, the source verification, and the audit report.