Help & Support

Post Rules

This guide will explain how to use Post Rules and how to implement custom ones.

Using Post Rules

When creating an new Post or a Quote, you can specify Post Rules that control how other Accounts can interact with it. These restrictions carry over to any Comments, or Comments on Comments, that are made on the post.

For Custom Feeds, these rules work in combination with any Feed Rules set on the given feed.

This section presumes you are familiar with the process of creating a Post on Lens.

Followers Only Post Rule

Lens provides a built-in FollowerOnlyPostRule, which restricts interactions with a post to only those Accounts that follow the author. You can specify if the restriction applies to the Global Lens Graph or a Custom Graph.

Additionally, you can specify whether followers are allowed to comment, quote, and/or repost.

Post with Followers Only Rule
import { evmAddress, uri } from "@lens-protocol/client";
const result = await post(sessionClient, {  contentUri: uri("lens://4f91ca…"),  rules: {    required: [      {        followersOnlyRule: {          graph: evmAddress("0x1234…"),          quotesRestricted: true,          repliesRestricted: true,          repostRestricted: true,        },      },    ],  },});

Custom Post Rules

You can also configure custom Post Rules as follows:

Post with Custom Rules
import {  blockchainData,  evmAddress,  PostRuleExecuteOn,  uri,} from "@lens-protocol/client";
const result = await post(sessionClient, {  contentUri: uri("lens://4f91ca…"),  rules: {    required: [      {        unknownRule: {          address: evmAddress("0x1234…"),          executeOn: [PostRuleExecuteOn.CreatingPost],          params: [            {              raw: {                // 32 bytes key (e.g., keccak(name))                key: blockchainData("0xac5f04…"),                // an ABI encoded value                value: blockchainData("0x00"),              },            },          ],        },      },    ],  },});

Where raw[n].key and raw[n].value are encoded hex strings specific to the desired rule.

Combining Rules

Additionally, multiple rules can be combined:

Post with Multiple Rules
import { evmAddress, uri, PostRuleExecuteOn } from "@lens-protocol/client";
const result = await post(sessionClient, {  contentUri: uri("lens://4f91ca…"),  rules: {    required: [      {        followersOnlyRule: {          // …        },      },    ],    anyOf: [      {        unknownRule: {          address: evmAddress("0x1234…"),          // …        },      },      {        unknownRule: {          address: evmAddress("0x5678…"),          // …        },      },    ],  },});

Building a Post Rule

Let's illustrate the process with an example. We will build a custom Post Rule that only allows a limited amount of replies to a Post.

To build a custom Post Rule, you must implement the following IPostRule interface:

IPostRule.sol
interface IPostRule {    function configure(bytes32 configSalt, uint256 postId, KeyValue[] calldata ruleParams) external;
    function processCreatePost(        bytes32 configSalt,        uint256 rootPostId,        uint256 postId,        CreatePostParams calldata postParams,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external;
    function processEditPost(        bytes32 configSalt,        uint256 rootPostId,        uint256 postId,        EditPostParams calldata postParams,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external;}

Each function of this interface must assume to be invoked by the Feed contract where the rule is applied. In other words, assume the msg.sender will be the Feed contract.

A Lens dependency package with all relevant interfaces will be available soon.

1

Implement the Configure Function

First, implement the configure function. This function initializes any required state for the rule to work correctly for a specific post configuration.

It receives three parameters:

  • configSalt: A 32-byte value allowing the same rule contract to be used multiple times with different configurations for the same post. The triple (Feed Address, Post ID, Configuration Salt) identifies a unique rule configuration instance.

  • postId: The ID of the post for which this rule is configured for.

  • ruleParams: An array of key-value pairs that can be used to pass any custom configuration parameters to the rule. Each key is bytes32, we put the hash of the parameter name there, and each value is bytes, we set the ABI-encoded parameter value there. Being an array allows the rule to define which parameters are optional and which are required, acting accordingly when any of them are not present.

The configure function can be called multiple times by the same Feed, passing the same postId and configSalt, to update an existing rule configuration (i.e., reconfigure it).

Reply posts and reposts cannot have their own rules configured directly on them. Instead, they inherit the rules enforced by their root post. Only root posts (original posts and quotes) can have rules configured directly on them.

In our example, we need to configure the maximum number of replies allowed for the given postId. We'll decode an integer parameter from ruleParams. Let's define storage mappings for the replies limit and for the counter:

contract LimitedRepliesPostRule is IPostRule {    mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 limit))) internal _repliesLimit;    mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 counter))) internal _repliesCounter;}

The configuration parameters are stored in the mappings using the Namespace contract address (msg.sender) and the configuration salt as keys. With this setup, the same rule can be used by different Namespaces, as well as be used by the same Namespace many times.

Now let's code the configure function itself:

contract LimitedRepliesPostRule is IPostRule {    mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 limit))) internal _repliesLimit;    mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 counter))) internal _repliesCounter;
    function configure(bytes32 configSalt, uint256 postId, KeyValue[] calldata ruleParams) external override {        uint256 limit = 0;        for (uint256 i = 0; i < ruleParams.length; i++) {            if (ruleParams[i].key == keccak256("lens.param.repliesLimit")) {                limit = abi.decode(ruleParams[i].value, (uint256));                break;            }        }
        // Require a limit to be explicitly set during configuration        require(limit > 0);                // We store the limit...        _repliesLimit[msg.sender][postId][configSalt] = limit;        // ...but we do not do anything with the counter here, allowing reconfiguration of the limit        // without losing the current reply count.    }}

As you can see, we treat the repliesLimit parameter as required. And the rule treats the counter state carefully, by not overriding its value, knowing that the rule can be reconfigured at any time.

2

Implement the Process Create Post Function

Next, implement the processCreatePost function. This function is invoked by the Feed contract every time a Post is being created in relation to the Post that has this rule configured (which ID is rootPostId), to decide to allow or reject its creation.

It receives:

  • configSalt: Identifies the specific rule configuration instance, in combination with the feed address (msg.sender) and the rootPostId.

  • rootPostId: The ID of the post where the rule was configured.

  • postId: The ID of the new post trying to be created.

  • postParams: The parameters of the new post being created (includes author, contentURI, repliedPostId, quotedPostId, repostedPostId, etc).

  • primitiveParams: Additional parameters that were passed to the Feed's createPost function call.

  • ruleParams: Additional parameters specific to this rule execution.

The function must revert if the rule's requirements are not met.

For our example, we only care about applying the restriction when the post being created is a reply. We can identify a reply by checking if postParams.repliedPostId is non-zero.

  1. Check if postParams.repliedPostId != 0.

  2. If it is a reply, increment the reply counter for the rootPostId and configSalt.

  3. Retrieve the repliesLimit for the rootPostId and configSalt.

  4. Require that the incremented counter does not exceed the limit.

import {CreatePostParams} from "contracts/core/interfaces/IFeed.sol"; // Assumed importimport {Errors} from "contracts/core/types/Errors.sol"; // Assumed import
contract LimitedRepliesPostRule is IPostRule {    mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 limit))) internal _repliesLimit;    mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 counter))) internal _repliesCounter;

    // ...
    function processCreatePost(        bytes32 configSalt,        uint256 rootPostId,        uint256 postId,        CreatePostParams calldata postParams,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        // Only apply the restriction if the post being created is a reply        if (postParams.repliedPostId != 0) {            // Increment the reply counter            uint256 replyCounter = ++_repliesCounter[msg.sender][rootPostId][configSalt];
            // Get the reply limit             uint256 replyLimit = _repliesLimit[msg.sender][rootPostId][configSalt];
            // Require the counter to not exceed the limit            require(replyCounter <= replyLimit);        }    }}

3

Implement the Process Edit Post Function

Next, implement the processEditPost function. This function is invoked by the Feed contract every time a Post related to the Post that has this rule configured (which ID is rootPostId) is being edited, then the rule can decide to approve or reject the editing of it.

It receives:

  • configSalt: Identifies the specific rule configuration instance, in combination with the feed address (msg.sender) and the rootPostId.

  • rootPostId: The ID of the post where the rule was configured.

  • postId: The ID of the new post trying to be edited.

  • postParams: The parameters of the post to be edited.

  • primitiveParams: Additional parameters that were passed to the Feed's editPost function call.

  • ruleParams: Additional parameters specific to this rule execution.

The function must revert if the rule's requirements are not met.

In our example, we only restrict the amount of replies at creation time, not the editing of existing posts (i.e. existing replies are allowed to be edited). Therefore, this function does not need to impose any restrictions.

As good practice, we revert with NotImplemented error in case the rule is accidentally enabled for this selector.

contract LimitedRepliesPostRule is IPostRule {
    // ...
    function processEditPost(        bytes32 configSalt,        uint256 rootPostId,        uint256 postId,        EditPostParams calldata postParams,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external pure override {        revert Errors.NotImplemented();    }}

Now the LimitedRepliesPostRule is ready to be applied to any root Post under any Feed. See the full code below:

contract LimitedRepliesPostRule is IPostRule {    mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 limit))) internal _repliesLimit;    mapping(address feed => mapping(uint256 postId => mapping(bytes32 configSalt => uint256 counter))) internal _repliesCounter;
    function configure(bytes32 configSalt, uint256 postId, KeyValue[] calldata ruleParams) external override {        uint256 limit = 0;        for (uint256 i = 0; i < ruleParams.length; i++) {            if (ruleParams[i].key == keccak256("lens.param.repliesLimit")) {                limit = abi.decode(ruleParams[i].value, (uint256));                break;            }        }
        // Require a limit to be explicitly set during configuration        require(limit > 0);
        // We store the limit...        _repliesLimit[msg.sender][postId][configSalt] = limit;        // ...but we do not do anything with the counter here, allowing reconfiguration of the limit        // without losing the current reply count.    }
    function processCreatePost(        bytes32 configSalt,        uint256 rootPostId,        uint256 postId,        CreatePostParams calldata postParams,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        // Only apply the restriction if the post being created is a reply        if (postParams.repliedPostId != 0) {            // Increment the reply counter            uint256 replyCounter = ++_repliesCounter[msg.sender][rootPostId][configSalt];
            // Get the reply limit             uint256 replyLimit = _repliesLimit[msg.sender][rootPostId][configSalt];
            // Require the counter to not exceed the limit            require(replyCounter <= replyLimit);        }    }
    function processEditPost(        bytes32 configSalt,        uint256 rootPostId,        uint256 postId,        EditPostParams calldata postParams,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external pure override {        revert Errors.NotImplemented();    }}

Stay tuned for API integration of rules and more guides!