Help & Support

Feed Rules

This guide explains how to use Feed Rules and how to implement custom ones.

Feed Rules allow administrators to add requirements or constraints when creating content on a Feed.

Using Feed Rules

Lens provides four built-in Feed rules:

  • SimplePaymentFeedRule - Requires an ERC-20 payment to post on the Feed.

  • TokenGatedFeedRule - Requires an account to hold a certain token to post on the Feed.

  • GroupGatedFeedRule - Requires an account to be a member of a certain Group to post on the Feed.

For the SimplePaymentFeedRule, a 1.5% Lens treasury fee is deducted from the payment before the remaining amount is transferred to the designated recipient.

It is also possible to use custom Feed Rules to extend the functionality of your Feed.

Create a Feed with Rules

As part of creating Custom Feeds, you can pass a rules object that defines the required rules and/or an anyOf set, where satisfying any one rule allows posting on the Feed. These rules can be built-in or custom.

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

import { bigDecimal, evmAddress, uri } from "@lens-protocol/client";import { createFeed } from "@lens-protocol/client/action";
const result = await createFeed(sessionClient, {  metadataUri: uri("lens://4f91ca…"),  rules: {    required: [      {        simplePaymentRule: {          cost: {            currency: evmAddress("0x5678…"),            value: bigDecimal("10.42"),          },          recipient: evmAddress("0x9012…"),        },      },    ],  },});

Update a Feed Rules

More details on this coming soon.

Building a Feed Rule

Let's illustrate the process with an example. We will build the GroupGatedFeedRule described above, a rule that requires accounts to be a member of a certain Group in order to Create a Post on the Feed.

To build a custom Feed Rule, you must implement the following IFeedRule interface:

interface IFeedRule {    function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external;
    function processCreatePost(        bytes32 configSalt,        uint256 postId,        CreatePostParams calldata postParams,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external;
    function processEditPost(        bytes32 configSalt,        uint256 postId,        EditPostParams calldata postParams,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external;
    function processDeletePost(        bytes32 configSalt,        uint256 postId,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external;
    function processPostRuleChanges(        bytes32 configSalt,        uint256 postId,        RuleChange[] calldata ruleChanges,        KeyValue[] calldata ruleParams    ) external;}

Each function of this interface must assume to be invoked by the Feed contract. 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 has the purpose of initializing any required state for the rule to work properly.

It receives two parameters, a 32-byte configuration salt (configSalt), and an array of custom parameters as key-value pairs (ruleParams).

The configSalt is there to allow the same rule contract to be used many times, with different configurations, for the same Feed. So, for a given Feed Rule implementation, the pair (Feed Address, Configuration Salt) should identify a rule configuration.

For example, we could want to achieve the restriction "To post on this Feed, you must be a member of Group A or Group B". In that case, instead of writing a whole new contract that receives two groups instead of one, we would just configure the GroupGatedFeedRule rule twice in the same Feed, once for Group A with some configuration salt, and once for Group B with another configuration salt.

The configure function can be called multiple times by the same Feed passing the same configuration salt in order to update that rule configuration (i.e. reconfigure it).

The ruleParams is 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. Given that ruleParams is an array, this allows the rule to define which parameters are optional and which are required, acting accordingly when any of them are not present.

In our example, we need to decode an address parameter, which will represent the Group contract where the account trying to post must belong to. Let's define a storage mapping to store this configuration:

contract GroupGatedFeedRule is IFeedRule {    mapping(address feed => mapping(bytes32 configSalt => address group)) internal _groupGate;}

The configuration is stored in the mapping using the Feed contract address and the configuration salt as keys. With this setup, the same rule can be used by different Feeds, as well as be used by the same Feed many times.

Now let's code the configure function itself, decoding the required address parameter and storing it in the mapping:

contract GroupGatedFeedRule is IFeedRule {    mapping(address feed => mapping(bytes32 configSalt => address group)) internal _groupGate;
    function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override {        address group = address(0);        for (uint256 i = 0; i < ruleParams.length; i++) {            if (ruleParams[i].key == keccak256("lens.param.group")) {                group = abi.decode(ruleParams[i].value, (address));                break;            }        }        // Aims to check if the passed group contract is valid        IGroup(group).isMember(address(this));        _groupGate[msg.sender][configSalt] = group;    }}

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 trying to be created, so then our custom logic can be applied to shape under which conditions this operation can succeed.

The function receives the configuration salt (configSalt), so we know which configuration to use, the ID the Feed assigned to the Post (postId), the parameters of the Post (postParams, including things like author and contentURI), an array of key-value pairs with the custom parameters passed to the Feed (primitiveParams), and an array of key-value pairs in case the rule requires additional parameters to work (ruleParams).

The function must revert in case of not meeting the requirements imposed by the rule.

Groups follow the IGroup interface, which has the following function to query if an account is a member of the group or not:

function isMember(address account) external view returns (bool);

Now let's code the processCreatePost function taking all the described above into account:

contract GroupGatedFeedRule is IFeedRule {    mapping(address feed => mapping(bytes32 configSalt => address group)) internal _groupGate;
    // . . .
    function processCreatePost(        bytes32 configSalt,        uint256 postId,        CreatePostParams calldata postParams,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        require(IGroup(_groupGate[msg.sender][configSalt]).isMember(postParams.author));    }

3

Implement the Process Edit Post function

Next, implement the processEditPost function. This function is invoked by the Feed contract every time a Post is trying to be edited, so then our custom logic can be applied to shape under which conditions this operation can succeed.

The function receives the configuration salt (configSalt), so we know which configuration to use, the ID the Feed assigned to the Post (postId), the parameters of the Post to edit (postParams), an array of key-value pairs with the custom parameters passed to the Feed (primitiveParams), and an array of key-value pairs in case the rule requires additional parameters to work (ruleParams).

The function must revert in case of not meeting the requirements imposed by the rule. In this case, given that to reach the point of editing a post, the post must have been created before, we can assume that the author of the post met the conditions to create it, and do not impose any restriction on the edit operation. As a good practice, we revert for unimplemented rule functions, as it is safer in case the rule becomes accidentally applied.

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

4

Implement the Process Delete Post function

Next, implement the processDeletePost function. This function is invoked by the Feed contract every time a Post is trying to be deleted, so then our custom logic can be applied to shape under which conditions this operation can succeed.

The function receives the configuration salt (configSalt), so we know which configuration to use, the ID the Feed assigned to the Post (postId), an array of key-value pairs with the custom parameters passed to the Feed (primitiveParams), and an array of key-value pairs in case the rule requires additional parameters to work (ruleParams).

The function must revert in case of not meeting the requirements imposed by the rule. In this case, given that to reach the point of deleting a post, the post must have been created before, we can assume that the author of the post met the conditions to create it, and do not impose any restriction on the delete operation. As a good practice, we revert for unimplemented rule functions, as it is safer in case the rule becomes accidentally applied.

contract GroupGatedFeedRule is IFeedRule {
    // . . .
    function processDeletePost(        bytes32 configSalt,        uint256 postId,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        revert Errors.NotImplemented();    }

5

Implement the Process Post Rule Changes function

Finally, implement the processPostRuleChanges function. This function is invoked by the Feed contract every time an account makes a change on the Post Rules of a Post it authors, so then our Feed rule can define if this change must be accepted or not.

The function receives the configuration salt (configSalt), so we know which configuration to use, the ID the Feed assigned to the Post which rules are being changed (postId), the array of rules changes (ruleChanges), and an array of key-value pairs in case the rule requires additional parameters to work (ruleParams).

The function must revert in case of not meeting the requirements imposed by the rule. In this case, the rule is not focused on controlling Post Rules changes, so we revert as a good practice to avoid unintended effects on if the rule gets applied accidentally.

contract GroupGatedFeedRule is IFeedRule {
    // . . .
    function processPostRuleChanges(        bytes32 configSalt,        uint256 postId,        RuleChange[] calldata ruleChanges,        KeyValue[] calldata ruleParams    ) external override {        revert Errors.NotImplemented();    }}

Now the GroupGatedFeedRule is ready to be applied into any Feed. See the full code below:

contract GroupGatedFeedRule is IFeedRule {    mapping(address feed => mapping(bytes32 configSalt => address group)) internal _groupGate;
    function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override {        address group = address(0);        for (uint256 i = 0; i < ruleParams.length; i++) {            if (ruleParams[i].key == keccak256("lens.param.group")) {                group = abi.decode(ruleParams[i].value, (address));                break;            }        }        // Aims to check if the passed group contract is valid        IGroup(group).isMember(address(this));        _groupGate[msg.sender][configSalt] = group;    }
    function processCreatePost(        bytes32 configSalt,        uint256 postId,        CreatePostParams calldata postParams,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        require(IGroup(_groupGate[msg.sender][configSalt]).isMember(postParams.author));    }
    function processEditPost(        bytes32 configSalt,        uint256 postId,        EditPostParams calldata postParams,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        revert Errors.NotImplemented();    }
    function processDeletePost(        bytes32 configSalt,        uint256 postId,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        revert Errors.NotImplemented();    }
    function processPostRuleChanges(        bytes32 configSalt,        uint256 postId,        RuleChange[] calldata ruleChanges,        KeyValue[] calldata ruleParams    ) external override {        revert Errors.NotImplemented();    }}

Stay tuned for API integration of rules and more guides!