block-brickModule Development

This guide walks through building a module for the Algebra Integral upgradeable plugin architecture. By the end you will have a working Implementation contract and Connector: the two components that make up every module.

You can study existing official modules as reference implementations in the plugins monorepoarrow-up-right.

Part 1: Implementation Contract

The implementation contract contains all logic for your module. State is stored using ERC-7201 namespaced storagearrow-up-right: each module defines a storage library with a unique namespace slot, so module states never collide regardless of inheritance order or future upgrades.

The implementation reads and writes storage exclusively through this library, making it safe to call via delegatecall.

circle-info

The ERC-7201 namespace slot is computed offline: keccak256(abi.encode(uint256(keccak256("erc7201:algebra.storage.<yourmodule>")) - 1)) & ~bytes32(uint256(0xff)) Use a script or erc7201.xyzarrow-up-right to generate it. By convention, use erc7201:algebra.storage.<modulename> as the namespace string.

// SPDX-License-Identifier: BUSL-1.1
pragma solidity =0.8.20;

import './interfaces/IMyModulePluginImplementation.sol';

// ERC-7201 namespaced storage - defined alongside the implementation
library MyModuleStorage {
  /// @dev keccak256(abi.encode(uint256(keccak256("erc7201:algebra.storage.mymodule")) - 1)) & ~bytes32(uint256(0xff))
  bytes32 internal constant NAMESPACE = 0x/* compute offline */00;

  struct Layout {
    uint256 someValue;
    address someAddress;
    bool isActive;
  }

  function layout() internal pure returns (Layout storage l) {
    bytes32 position = NAMESPACE;
    assembly { l.slot := position }
  }
}

contract MyModulePluginImplementation is IMyModulePluginImplementation {

  /// @notice Initialize module state. Called once during plugin initialization
  function initializeMyModule(uint256 initialValue, address initialAddress) external {
    MyModuleStorage.Layout storage s = MyModuleStorage.layout();
    s.someValue = initialValue;
    s.someAddress = initialAddress;
    s.isActive = true;
  }

  /// @notice Core module logic. Called from the assembled plugin's afterSwap handler
  function processAfterSwap(bool zeroToOne) external {
    MyModuleStorage.Layout storage s = MyModuleStorage.layout();
    if (!s.isActive) return;
    // ... your logic here
    s.someValue += 1;
  }

  /// @notice Admin function. Updates configuration
  function setSomeValue(uint256 newValue) external {
    MyModuleStorage.layout().someValue = newValue;
  }
}
circle-exclamation

Conventions:

  • Function names should be clear and specific - they appear in abi.encodeCall inside the connector

  • Never use regular storage variables in the implementation - always go through the storage library

  • Keep the implementation stateless at the contract level (no immutable values that affect logic)

  • Read-only views can be implemented directly in the connector without delegatecall - they call MyModuleStorage.layout() directly, which resolves to the same slot in the proxy's context

Part 2: Connector

The connector is an abstract contract that is inherited by the default plugin. It:

  • Stores the implementation address as an immutable (set once in constructor, shared across all proxy instances)

  • Exposes the module's public interface and admin functions

  • Routes all state-changing calls to the implementation via _delegateCall()

  • Declares the module's plugin config flags

Plugin Config Flags

The MY_MODULE_PLUGIN_CONFIG constant declares which hooks the pool should call on your module. Available flags, from Plugins.sol:

Flag
Value
Hook triggered

BEFORE_SWAP_FLAG

1

beforeSwap

AFTER_SWAP_FLAG

1 << 1

afterSwap

BEFORE_POSITION_MODIFY_FLAG

1 << 2

beforeModifyPosition

AFTER_POSITION_MODIFY_FLAG

1 << 3

afterModifyPosition

BEFORE_FLASH_FLAG

1 << 4

beforeFlash

AFTER_FLASH_FLAG

1 << 5

afterFlash

AFTER_INIT_FLAG

1 << 6

afterInitialize

DYNAMIC_FEE

1 << 7

enables dynamic fee

Combine flags with |:

_authorize() and _delegateCall()

Both are inherited from BaseConnector (for _delegateCall) and resolved at the assembled plugin level (for _authorize):

  • _delegateCall(address impl, bytes memory data) - executes a delegatecall, propagates reverts with the original error. Never call an untrusted address here.

  • _authorize() - abstract in BaseConnector, implemented in UpgradeableAbstractPlugin. It checks that msg.sender has ALGEBRA_BASE_PLUGIN_MANAGER role in the factory. Call it at the top of every admin function in your connector.

Part 3: Integration into the Assembled Plugin

This part covers how to integrate your module into AlgebraUpgradeablePlugin. After completing the integration and writing tests, submit the result to the Algebra team for review.

Constructor: add the implementation address as a parameter and pass it to your connector:

defaultPluginConfig(): your module's config flags:

initialize(): call your module's initializer if needed:

Hooks: wire your module's logic into the relevant pool hook handler:

getActiveModuleNames(): add your module's name to the list:

Last updated