Create Module Contract

Prerequisite: Read “Create Core Contract” before creating a Module.

Installation

  • Install Forge from Foundry

    For assistance, refer to the Foundry installation guide.

    forge init
    forge install thirdweb-dev/modular-contracts --no-commit

    Add the Thirdweb modular contracts to foundry.toml under remappings:

    remappings = ['@thirdweb-dev=lib/modular-contracts/']
  • Create Module

  • Create a New Module File

    Create a new file called CounterModule.sol in the src folder and start with the following code:

    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.20;
    import {Module} from "@thirdweb-dev/src/Module.sol";
    contract CounterModule is Module {
    function getModuleConfig()
    public
    pure
    override
    returns (ModuleConfig memory config)
    {}
    }

    Note
    The Module contract is the base contract that needs to be inherited for this contract to be recognized as a Module Contract, and we need to implement the getModuleConfig function to prevent the contract to be marked as abstract.

  • Create a Storage Library

    Create a library called CounterStorage responsible for holding the state of the Module Contract:

    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.20;
    import {Module} from "@thirdweb-dev/src/Module.sol";
    contract CounterModule is Module {
    function getModuleConfig()
    public
    pure
    override
    returns (ModuleConfig memory config)
    {}
    }
    // 👇👇👇👇👇👇👇👇👇
    library CounterStorage {
    /// @custom:storage-location erc7201:token.minting.counter
    bytes32 public constant COUNTER_STORAGE_POSITION =
    keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
    ~bytes32(uint256(0xff));
    struct Data {
    uint256 step;
    }
    function data() internal pure returns (Data storage data_) {
    bytes32 position = COUNTER_STORAGE_POSITION;
    assembly {
    data_.slot := position
    }
    }
    }

    Note
    The library CounterStorage uses the ERC-7201: Namespace storage layout to store the data. Learn more about ERC-7201.

  • Set Up Storage Access Function

    Set up the function _counterStorage to access the storage from the CounterStorage library:

    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.20;
    import {Module} from "@thirdweb-dev/src/Module.sol";
    contract CounterModule is Module {
    function getModuleConfig()
    public
    pure
    override
    returns (ModuleConfig memory config)
    {}
    // 👇👇👇👇👇👇👇👇👇
    function _counterStorage()
    internal
    pure
    returns (CounterStorage.Data storage)
    {
    return CounterStorage.data();
    }
    }
    library CounterStorage {
    /// @custom:storage-location erc7201:token.minting.counter
    bytes32 public constant COUNTER_STORAGE_POSITION =
    keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
    ~bytes32(uint256(0xff));
    struct Data {
    uint256 step;
    }
    fun2ction data() internal pure returns (Data storage data_) {
    bytes32 position = COUNTER_STORAGE_POSITION;
    assembly {
    data_.slot := position
    }
    }
    }
  • Set Up Fallback Functions

    Set up fallback functions that act as the setters and getters for step:

    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.20;
    import {Module} from "@thirdweb-dev/src/Module.sol";
    contract CounterModule is Module {
    function getModuleConfig()
    public
    pure
    override
    returns (ModuleConfig memory config)
    {}
    function _counterStorage()
    internal
    pure
    returns (CounterStorage.Data storage)
    {
    return CounterStorage.data();
    }
    // 👇👇👇👇👇👇👇👇👇
    // Fallback Functions
    function getStep() external view returns (uint256) {
    return _counterStorage().step;
    }
    function setStep(uint256 _step) external {
    _counterStorage().step = _step;
    }
    }
    library CounterStorage {
    /// @custom:storage-location erc7201:token.minting.counter
    bytes32 public constant COUNTER_STORAGE_POSITION =
    keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
    ~bytes32(uint256(0xff));
    struct Data {
    uint256 step;
    }
    function data() internal pure returns (Data storage data_) {
    bytes32 position = COUNTER_STORAGE_POSITION;
    assembly {
    data_.slot := position
    }
    }
    }

    Note
    Fallback functions are extra functionalities that a core contract can use via the Solidity fallback function.

  • Set Up Callback Function

    Set up a callback function beforeIncrement that increases the given count by step:

    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.20;
    import {Module} from "@thirdweb-dev/src/Module.sol";
    contract CounterModule is Module {
    function getModuleConfig()
    public
    pure
    override
    returns (ModuleConfig memory config)
    {}
    function _counterStorage()
    internal
    pure
    returns (CounterStorage.Data storage)
    {
    return CounterStorage.data();
    }
    // Fallback Functions
    function getStep() external view returns (uint256) {
    return _counterStorage().step;
    }
    function setStep(uint256 _step) external {
    _counterStorage().step = _step;
    }
    // 👇👇👇👇👇👇👇👇👇
    function beforeIncrement(uint256 count) external view returns (uint256) {
    return count + _counterStorage().step;
    }
    }
    library CounterStorage {
    /// @custom:storage-location erc7201:token.minting.counter
    bytes32 public constant COUNTER_STORAGE_POSITION =
    keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
    ~bytes32(uint256(0xff));
    struct Data {
    uint256 step;
    }
    function data() internal pure returns (Data storage data_) {
    bytes32 position = COUNTER_STORAGE_POSITION;
    assembly {
    data_.slot := position
    }
    }
    }

    Note
    Callback functions are hook-like functionalities that can be used before or after the main functionality of a core contract. In this snippet, the beforeIncrement callback is used before the main increment functionality.

  • Set Up Module Config

    Lastly, set up the getModuleConfig functionallity as this is the one which is responsible for communicating to the core contract:

    //SPDX-License-Identifier: MIT
    pragma solidity ^0.8.20;
    import {Module} from "@thirdweb-dev/src/Module.sol";
    // 👇👇👇👇👇👇👇👇👇
    import {Role} from "@thirdweb-dev/src/Role.sol";
    contract CounterModule is Module {
    // 👇👇👇👇👇👇👇👇👇
    function getModuleConfig()
    public
    pure
    override
    returns (ModuleConfig memory config)
    {
    // Callback Function array of one element
    config.callbackFunctions = new CallbackFunction[](1);
    // Fallback Function array of two elements
    config.fallbackFunctions = new FallbackFunction[](2);
    //adding the functions to the arrays
    config.callbackFunctions[0] = CallbackFunction(
    this.beforeIncrement.selector
    );
    config.fallbackFunctions[0] = FallbackFunction({
    selector: this.getStep.selector,
    permissionBits: 0
    });
    config.fallbackFunctions[1] = FallbackFunction({
    selector: this.setStep.selector,
    permissionBits: Role._MANAGER_ROLE
    });
    // Required interfaces for the Module
    config.requiredInterfaces = new bytes4[](1);
    config.requiredInterfaces[0] = 0x00000001;
    // register the intallation callback
    config.registerInstallationCallback = true;
    }
    function _counterStorage()
    internal
    pure
    returns (CounterStorage.Data storage)
    {
    return CounterStorage.data();
    }
    // Fallback Functions
    function getStep() external view returns (uint256) {
    return _counterStorage().step;
    }
    function setStep(uint256 _step) external {
    _counterStorage().step = _step;
    }
    function beforeIncrement(uint256 count) external view returns (uint256) {
    return count + _counterStorage().step;
    }
    }
    library CounterStorage {
    /// @custom:storage-location erc7201:token.minting.counter
    bytes32 public constant COUNTER_STORAGE_POSITION =
    keccak256(abi.encode(uint256(keccak256("counter")) - 1)) &
    ~bytes32(uint256(0xff));
    struct Data {
    uint256 step;
    }
    function data() internal pure returns (Data storage data_) {
    bytes32 position = COUNTER_STORAGE_POSITION;
    assembly {
    data_.slot := position
    }
    }
    }

In the next tutorial, learn how to deploy this modular contract and attach it to the Core contract.