Module 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 monorepo.
Part 1: Implementation Contract
The implementation contract contains all logic for your module. State is stored using ERC-7201 namespaced storage: 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.
// 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;
}
}The implementation contract must not have a constructor that sets storage - it is called via delegatecall, so any constructor state goes into the implementation's own storage, not the proxy's. Only the functions called via delegatecall write to the proxy's namespaced storage.
Conventions:
Function names should be clear and specific - they appear in
abi.encodeCallinside the connectorNever use regular
storagevariables in the implementation - always go through the storage libraryKeep the implementation stateless at the contract level (no
immutablevalues that affect logic)Read-only views can be implemented directly in the connector without
delegatecall- they callMyModuleStorage.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:
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 adelegatecall, propagates reverts with the original error. Never call an untrusted address here._authorize()- abstract inBaseConnector, implemented inUpgradeableAbstractPlugin. It checks thatmsg.senderhasALGEBRA_BASE_PLUGIN_MANAGERrole 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