Skip to Content
DevelopersBuildBuild a module

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-core

The 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

ContractUse it for
Module.solA contract that makes the avatar execute transactions.
GuardableModule.solThe same, plus a guard hook that checks each transaction before and after execution.
Modifier.solA contract between modules and the avatar. It keeps its own list of enabled modules and exposes the IAvatar interface to them.
GuardableModifier.solThe 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:

  1. Write all initialization in setUp(bytes memory initParams) with the initializer modifier. Decode your parameters from initParams.
  2. Call setUp from the constructor too. This initializes the mastercopy itself, so nobody can claim it.
  3. Send transactions through exec / execAndReturnData. Never call the Safe directly.
  4. Keep avatar and target separate. Your module then works behind a Delay or Roles Modifier without changes: the deployer points target at 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

  1. Test the module behind a Delay Modifier and behind a Roles Modifier, not only directly against a Safe.
  2. Make sure setUp cannot run twice, on the proxy and on the mastercopy.
  3. Get an audit. A module is a direct path into user Safes.
  4. Publish the mastercopy address, the source verification, and the audit report.
Last updated on