Guides

Creating a Follow Module

Build your first module!

πŸ“˜

Getting Set Up

This tutorial assumes that:

a) you are working with the Lens Protocol repository and have your environment set up correctly. See the "Walkthrough" section for more, and...
b) You are at least vaguely familiar with solidity smart contract development or programming in general.

Creating the Contract

So here's the plan: We're going to create a follow module that only allows users to follow if they include a special code. Of course, this is just for fun, and in practice, this makes no sense as the code would inherently be public on the blockchain. But, humor me for a bit!

Let's start off by creating a file called SecretCodeFollowModule.sol in the contracts/modules/follow/ directory. We're working with solidity 0.8.19, so we'll use that as our pragma.

Since we're building a follow module, let's import the interface (which is basically a "blueprint" detailing every function we should include), and the basic LensModule abstract base contract. We're also importing another contract that implements one of the interface's functions for us, and one that exposes the hub contract as an immutable with a modifier.

pragma solidity 0.8.19;

import {IFollowModule} from 'contracts/interfaces/IFollowModule.sol';

import {LensModule} from 'contracts/modules/LensModule.sol';

import {HubRestricted} from 'contracts/base/HubRestricted.sol';

Next up, let's define our contract. We'll inherit from the imported interface:

...
contract SecretCodeFollowModule is LensModuleMetadata, HubRestricted, IFollowModule {
    
}

At this point, your linter or compiler is probably pretty upset, and with good reason! We're inheriting from an interface, but we aren't implementing any of the functions. The interface is like an outline, we've got to fill in the blanks now, and implement our functions!

That's right, it's time to actually build the contract. :sunglasses:

πŸ“˜

Privacy On-Chain

This is just an example, keep in mind that nothing published on-chain is ever private, including our passcodes here. Even before something is pushed on-chain, it's visible unless you use a special privacy-preserving provider that obscures transaction pool transactions, but that's beyond the scope of this guide!

Implementing IERC165 interface support and Metadata

As this is the follow module, we need to override and implement corresponding IERC165 interfaceId support:

 function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
     return interfaceID == type(IFollowModule).interfaceId || super.supportsInterface(interfaceID);
 }

And let's also hardcode our ModuleMetadataURI with the getter (see Module Metadata Standard):

// In this case: import and inherit 'contracts/modules/LensModule.sol' instead of 'LensModuleMetadata'

function getModuleMetadataURI() external view returns (string memory) {
    return 'yourModuleMetadataUriHere';
}

Or if you want to set your ModuleMetadataURI dynamically, you can use the LensModuleMetadata.sol setter/getter template (but be aware that API doesn't yet support changing the metadata, and the one that was set before the Registration will be used).

For the reason why we need to do that see Registering a Module section.

Implementing Follow Module Functions

So, if we take a quick look at the IFollowModule interface (or in the specification section on the left), we can see the different functions we've got to implement to have our follow module ready. These are:

  1. initializeFollowModule() which is called when a profile sets this module as its follow module.
  2. processFollow() which is called when a user attempts to follow a given profile with this module set as its follow module.

Before we copy over the functions, let's go ahead and include a constructor. All we've got to do is construct the HubRestricted contract which allows us to make sure that module functions can only be called from the LensHub:

...
    constructor(address hub) HubRestricted(hub) {}
...

Great! Now we can access the immutable hub address via an address variable called HUB! We've also got access to a modifier onlyHub(), which we'll use for our one state-changing function.

Before we get ahead of ourselves, let's appease our angry linter and finally copy over the interface functions in our contract, adding empty brackets (which is equivalent to implementing no logic) and the onlyHub modifier to our initializeFollowModule implementation so only the hub can call it:

...
    function initializeFollowModule(
				uint256 profileId,
        address transactionExecutor,
        bytes calldata data
    )
				external
        override
        onlyHub
        returns (bytes memory)
    {}

    function processFollow(
				uint256 followerProfileId,
        uint256 followerTokenId
        address transactionExecutor,
        uint256 targetProfileId,
        bytes calldata data
    ) external override {}
...

Sweet! At this point, we've appeased our compiler overlord, and it's time to start implementing our logic!

Implementing Custom Logic

Alright, so here's how this module is going to work:

  1. Allow profile owners to set a secret number as a passcode on follow module initialization
  2. Only allow users to follow if they pass the correct passcode

We're going to need some additional features to satisfy the criteria outlined above. First, somewhere to store the passcodes; second, a way for profile owners to set them on initialization; and third, a way to validate that users attempting to follow pass the correct passcode.

Let's go back above our constructor and create a new mapping called _passcodeByProfile and a new error (which we'll throw when users pass the wrong passcode) called PasscodeInvalid():

...
contract SecretCodeFollowModule is HubRestricted, IFollowModule {
    error PasscodeInvalid();

    mapping(uint256 => uint256) internal _passcodeByProfile;

    constructor...

This mapping we just created will use profile IDs as keys and their respective passcodes as values. Simple enough! Now it's time to build our initialization mechanism, for which we'll use the initializeFollowModule() function:

...
    function initializeFollowModule(
				uint256 profileId,
        address transactionExecutor,
        bytes calldata data
    )
        external
        override
        onlyHub
        returns (bytes memory)
    {
        uint256 passcode = abi.decode(data, (uint256));
        _passcodeByProfile[profileId] = passcode;
        return data;
    }
...

As a quick explanation, first we decode the passcode from the arbitrary data (passed by the profile owner), then we set it as the profile's passcode.

At this point, you might be wondering about why this function returns a bytes memory parameter. This is basically any state-altering data that should be emitted by an event. In our case, we'll just pass the original data as that includes the passcode which we're using to alter state.

We're almost there! The last step is to validate that users pass the correct passcode when attempting to follow. Since this function does not modify state, but reads from it, we can restrict its visibility to view, too:

...
    function processFollow(
				uint256 followerProfileId,
        uint256 followerTokenId
        address transactionExecutor,
        uint256 targetProfileId,
        bytes calldata data
    ) external view override {
        uint256 passcode = abi.decode(data, (uint256));
        if (passcode != _passcodeByProfile[profileId]) {
          revert PasscodeInvalid();
        }
    }
...

To go over what we just built, the first line decodes the passcode from the arbitrary data (passed by the user attempting to follow) and the second line reverts the execution if it's not the right passcode.

We don't need to make this function onlyHub because it doesn't change any state.

πŸ“˜

Solidity Tip

As good practice, it's always a good idea to restrict function scope as much as possible within reason. Solidity functions that don't modify state but read from it should be marked view, and functions that neither read nor modify state should be marked pure. In this case, you might have noticed that the followModuleTransferHook() can be marked pure, too, although this serves no purpose as the function is empty.

Recap

And that's it! You've successfully created your own follow module. Let's take a look at our full SecretCodeFollowModule.sol file:

pragma solidity 0.8.19;

import {IFollowModule} from 'contracts/interfaces/IFollowModule.sol';
import {LensModule} from 'contracts/modules/LensModule.sol';
import {HubRestricted} from 'contracts/base/HubRestricted.sol';

contract SecretCodeFollowModule is LensModule, HubRestricted, IFollowModule {
    error PasscodeInvalid();

    mapping(uint256 => uint256) internal _passcodeByProfile;

    constructor(address hub) HubRestricted(hub) {}

  	function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
    	return interfaceID == type(IFollowModule).interfaceId || super.supportsInterface(interfaceID);
 		}
  
    function initializeFollowModule(
				uint256 profileId,
        address transactionExecutor,
        bytes calldata data
    )
        external
        override
        onlyHub
        returns (bytes memory)
    {
        uint256 passcode = abi.decode(data, (uint256));
        _passcodeByProfile[profileId] = passcode;
        return data;
    }

    function processFollow(
				uint256 followerProfileId,
        uint256 followerTokenId
        address transactionExecutor,
        uint256 targetProfileId,
        bytes calldata data
    ) external view override {
        uint256 passcode = abi.decode(data, (uint256));
        if (passcode != _passcodeByProfile[profileId]) {
          revert PasscodeInvalid();
        }
    }
  
    function getModuleMetadataURI() external view returns (string memory) {
        return 'yourModuleMetadataUriHere';
    }
}

What’s Next

Let's learn how to register our newly created module to let others know that it exists: