Help & Support

Graph Rules

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

Group Rules allow administrators to add requirements or constraints for when accounts follow other accounts on a given Graph.

Using Graph Rules

Lens provides three built-in Group rules:

  • TokenGatedGraphRule - Requires an account to hold a certain token to follow another account.

  • GroupGatedGraphRule - Requires an account to be a member of a certain Group to follow another account.

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

Create a Graph with Rules

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

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

import {  bigDecimal,  evmAddress,  uri,  TokenStandard,} from "@lens-protocol/client";import { createGraph } from "@lens-protocol/client/actions";
const result = await createGraph(sessionClient, {  metadataUri: uri("lens://4f91c…"),  rules: {    required: [      {        tokenGatedRule: {          token: {            currency: evmAddress("0x1234…"),            standard: TokenStandard.Erc721,            value: bigDecimal("1"),          },        },      },    ],  },});

Update a Graph Rules

More details on this coming soon.

Building a Graph Rule

Let's illustrate the process with an example. We will build a custom Graph Rule that will require accounts to wait a certain amount of time after Following someone in order to be allowed to Follow again.

To build a custom Graph Rule, you must implement the following IGraphRule interface:

interface IGraphRule {    function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external;
    function processFollow(        bytes32 configSalt,        address originalMsgSender,        address followerAccount,        address accountToFollow,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external;
    function processUnfollow(        bytes32 configSalt,        address originalMsgSender,        address followerAccount,        address accountToUnfollow,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external;
    function processFollowRuleChanges(        bytes32 configSalt,        address account,        RuleChange[] calldata ruleChanges,        KeyValue[] calldata ruleParams    ) external;}

Each function of this interface must assume to be invoked by the Graph contract. In other words, assume the msg.sender will be the Graph 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 Graph. So, for a given Graph Rule implementation, the pair (Graph Address, Configuration Salt) should identify a rule configuration.

For example, let's think about the GroupGatedGraphRule, we could want to achieve the restriction "To follow on this Graph, you must be a member of Group A or Group B". However, the GroupGatedGraphRule only allows to configure a single Group as a gate. So, instead of writing a whole new contract that receives two groups instead of one, we would just configure the GroupGatedGraphRule rule twice in the same Graph, 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 Graph 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 integer parameter, which will represent the amount of time in seconds required to elapse until allowing an account to Follow again. Let's define a storage mapping to store this configuration:

contract FollowCooldownGraphRule is IGraphRule {
    mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown;}

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

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

contract FollowCooldownGraphRule is IGraphRule {
    mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown;
    function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override {        uint256 cooldown = 1 days;        for (uint256 i = 0; i < ruleParams.length; i++) {            if (ruleParams[i].key == keccak256("lens.param.followCooldown")) {                cooldown = abi.decode(ruleParams[i].value, (uint256));                break;            }        }        _followCooldown[msg.sender][configSalt] = cooldown;    }
}

As you can see, we treated the cooldown parameter as optional setting a default cooldown time value of 1 day in case of the parameter was not found in the ruleParams. However, if we want the parameter to be required, we could have initialized it with a value of 0 and then revert if it is still set to 0 after the search for-loop.

2

Implement the Process Follow function

Next, implement the processFollow function. This function is invoked by the Graph contract every time a Follow is executed, 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 address that initiated the follow transaction in the Graph (originalMsgSender), the account acting as the follower (followerAccount), the account being followed (accountToFollow), an array of key-value pairs with the custom parameters passed to the Graph (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.

We need to introduce another mapping, which will store the timestamp of the last Follow performed by a given Follower account in a given Graph:

contract FollowCooldownGraphRule is IGraphRule {    mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown;
    mapping(address graph => mapping(bytes32 configSalt => (mapping(address account => uint256 timestamp)))) internal _lastFollowTimestamp;}

Now we can get the time elapsed since the last Follow executed by the Follower account in this Graph:

contract FollowCooldownGraphRule is IGraphRule {    mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown;
    mapping(address graph => mapping(bytes32 configSalt => (mapping(address account => uint256 timestamp)))) internal _lastFollowTimestamp;
    // . . .
    function processFollow(        bytes32 configSalt,        address originalMsgSender,        address followerAccount,        address accountToFollow,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        // We get the time elapsed since the last Follow for the given configuration        uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][configSalt][followerAccount];    }}

Next we require the elapsed time to be greater than the configured Follow cooldown period:

contract FollowCooldownGraphRule is IGraphRule {    mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown;
    mapping(address graph => mapping(bytes32 configSalt => (mapping(address account => uint256 timestamp)))) internal _lastFollowTimestamp;
    // . . .
    function processFollow(        bytes32 configSalt,        address originalMsgSender,        address followerAccount,        address accountToFollow,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][configSalt][followerAccount];        // We require the elapsed time to be greater than the configured Follow cooldown period        require(timeElapsedSinceLastFollow > _followCooldown[msg.sender][configSalt]);    }}

And to finish the implementation of this function, we update the last Follow timestamp for this Follower account:

contract FollowCooldownGraphRule is IGraphRule {    mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown;
    mapping(address graph => mapping(bytes32 configSalt => (mapping(address account => uint256 timestamp)))) internal _lastFollowTimestamp;
    // . . .
    function processFollow(        bytes32 configSalt,        address originalMsgSender,        address followerAccount,        address accountToFollow,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][configSalt][followerAccount];        require(timeElapsedSinceLastFollow > _followCooldown[msg.sender][configSalt]);        // We update the last Follow timestamp for this Follower account        _lastFollowTimestamp[followerAccount][configSalt][accountToFollow] = block.timestamp;    }}

3

Implement the Process Unfollow function

Next, implement the processUnfollow function. This function is invoked by the Graph contract every time an Unfollow is executed, 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 address that initiated the unfollow transaction in the Graph (originalMsgSender), the account unfollowing (followerAccount), the account about to be unfollowed (accountToUnfollow), an array of key-value pairs with the custom parameters passed to the Graph (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, we do not want to impose any restriction on the unfollow operation. As a good practice, we revert for unimplemented rule functions, as it is safer in case the rule becomes accidentally applied.

contract FollowCooldownGraphRule is IGraphRule {
    // . . .
    function processUnfollow(        bytes32 configSalt,        address originalMsgSender,        address followerAccount,        address accountToUnfollow,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        revert Errors.NotImplemented();    }

4

Implement the Process Follow Rule Changes function

Finally, implement the processFollowRuleChanges function. This function is invoked by the Graph contract every time an account makes a change on its Follow Rules, so then our 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 account changing its Follow Rules, 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 Follow Rules changes, so we revert as a good practice to avoid unintended effects on if the rule gets applied accidentally.

contract FollowCooldownGraphRule is IGraphRule {
    // . . .
    function processFollowRuleChanges(        bytes32 configSalt,        address account,        RuleChange[] calldata ruleChanges,        KeyValue[] calldata ruleParams    ) external override {        revert Errors.NotImplemented();    }}

Now the FollowCooldownGraphRule is ready to be applied into any Graph. See the full code below:

contract FollowCooldownGraphRule is IGraphRule {    mapping(address graph => mapping(bytes32 configSalt => uint256 cooldown)) internal _followCooldown;
    mapping(address graph => mapping(bytes32 configSalt => (mapping(address account => uint256 timestamp)))) internal _lastFollowTimestamp;
    function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override {        uint256 cooldown = 1 days;        for (uint256 i = 0; i < ruleParams.length; i++) {            if (ruleParams[i].key == keccak256("lens.param.followCooldown")) {                cooldown = abi.decode(ruleParams[i].value, (uint256));                break;            }        }        _followCooldown[msg.sender][configSalt] = cooldown;    }
    function processFollow(        bytes32 configSalt,        address originalMsgSender,        address followerAccount,        address accountToFollow,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        // We get the time elapsed since the last Follow for the given configuration        uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][configSalt][followerAccount];        // We require the elapsed time to be greater than the configured Follow cooldown period        require(timeElapsedSinceLastFollow > _followCooldown[msg.sender][configSalt]);        // We update the last Follow timestamp for this Follower account        _lastFollowTimestamp[followerAccount][configSalt][accountToFollow] = block.timestamp;    }
    function processUnfollow(        bytes32 configSalt,        address originalMsgSender,        address followerAccount,        address accountToUnfollow,        KeyValue[] calldata primitiveParams,        KeyValue[] calldata ruleParams    ) external override {        revert Errors.NotImplemented();    }
    function processFollowRuleChanges(        bytes32 configSalt,        address account,        RuleChange[] calldata ruleChanges,        KeyValue[] calldata ruleParams    ) external override {        revert Errors.NotImplemented();    }}

Stay tuned for API integration of rules and more guides!