Group Rules

This guide will explain Group Rules and how to implement them.


Group Rules allow Group' administrators to add requirements or constraints that will be applied when someone tries to join the Group or someone tries to remove a member from it.

See the Rules concept section to learn more about them.

Existing Group Rules

SimplePaymentGroupRule

This rule can be applied to a Group to require an ERC-20 payment in order to become a member of it.

The configuration requires the ERC-20 token address, the amount of tokens required for the payment, and the recipient address where the tokens will be sent.

TokenGatedGroupRule

This rule can be applied to a Group to require an account to hold a certain balance of a token (both fungible and non-fungible are supported) in order to successfully become a member.

The configuration also requires the address of the token, the token standard used by the provided token (ERC-20, ERC-721, and ERC-1155 are supported), and the amount of tokens required to pass the gate. In case of using the ERC-1155 token standard, an additional token type ID is expected to be provided.

ApprovalGroupRule

This rule can be applied to a Group to require approval by owner or administrators of the Group in order become a member of it.

The configuration requires the address of an Access Control contract where the owner and administrators are defined with permissions to approve or reject membership requests.

Building a Group Rule

Let's illustrate the process with an example. We will build a custom Group Rule that requires accounts to have more than a certain amount of Followers in order to join the Group.

To build a custom Group Rule, you must implement the following IGroupRule interface:

interface IGroupRule {  function configure(bytes calldata data) external;
  function processJoining(    address account,    uint256 membershipId,    bytes calldata data  ) external returns (bool);
  function processRemoval(    address account,    uint256 membershipId,    bytes calldata data  ) external returns (bool);}

Each function of this interface must assume to be invoked by the Group 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.

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

It receives bytes that will be decoded into the required rule configuration parameters.

In our example, we need to decode two parameters: an address, representing the Graph where to perform the amount of followers check, and an integer, which will represent the minimum amount of Followers an account must have in order to become a member of the Group. Let's define a storage mapping to store this configuration:

contract FollowerCountGatedGroupRule is IGroupRule {    mapping(address => address) internal _graphToCheck;    mapping(address => uint256) internal _minFollowers;}

Now let's code the configure function itself, decoding the parameters and putting them into the storage:

contract FollowerCountGatedGroupRule is IGroupRule {    mapping(address => address) internal _graphToCheck;    mapping(address => uint256) internal _minFollowers;
    function configure(bytes calldata data) external {        (address graphToCheck, uint256 minFollowers) = abi.decode(data, (address, uint256));        _graphToCheck[msg.sender] = graphToCheck;        _minFollowers[msg.sender] = minFollowers;    }}

Both configuration parameters are stored in mappings that are using the Group contract address as the key, which is the msg.sender. In this way, the same rule can be reused by different Group contracts.

2

Implement the Process Joining function

Next, implement the processJoining function. This function is invoked by the Group contract every time someone tries to join the Group, so then our custom logic can be applied to shape under which conditions the operation can succeed.

The function receives the account attempting to join the Group, the membershipId (which mostly applies for Groups with tokenized memberships, so then you can join using an specific membership token), and some data in case the rule requires additional information to work.

The function must revert in case of not meeting the requirements imposed by the rule. Otherwise, it must return a boolean that indicates if the rule is applying a restriction over this operation or not. In our example, given that a restriction is indeed being applied to the joining process, we will return true.

We first get the configured Graph where to perform the check:

contract FollowerCountGatedGroupRule is IGroupRule {    mapping(address => address) internal _graphToCheck;    mapping(address => uint256) internal _minFollowers;
    // . . .
    function processJoining(        address account,        uint256 membershipId,        bytes calldata data    ) external returns (bool) {        // We get the Graph where to check the followers count        IGraph graph = IGraph(_graphToCheck[msg.sender]);    }}

Next we require that account has at least the amount of followers required by the rule configuration.

The IGraph interface contains this function in order to check the amount of followers of a given account:

function getFollowersCount(address account) external view returns (uint256);

So, let's add the requirement check:

contract FollowerCountGatedGroupRule is IGroupRule {    mapping(address => address) internal _graphToCheck;    mapping(address => uint256) internal _minFollowers;
    // . . .
    function processJoining(        address account,        uint256 membershipId,        bytes calldata data    ) external returns (bool) {        IGraph graph = IGraph(_graphToCheck[msg.sender]);        // We check if the account has the min required amount of followers        require(graph.getFollowersCount(account) >= _minFollowers[msg.sender]);    }}

Finally, we return true as the rule is being applied to the join operation:

contract FollowerCountGatedGroupRule is IGroupRule {    mapping(address => address) internal _graphToCheck;    mapping(address => uint256) internal _minFollowers;
    // . . .
    function processJoining(        address account,        uint256 membershipId,        bytes calldata data    ) external returns (bool) {        IGraph graph = IGraph(_graphToCheck[msg.sender]);        require(graph.getFollowersCount(account) >= _minFollowers[msg.sender]);        // We return true to signal the rule applies to the joining operation        return true;    }}

3

Implement the Process Removal function

Finally, implement the processRemoval function. This function is invoked by the Group contract every time a Group member is trying to be removed, so then our rule can define if this operation must succeed or not.

Note that Group members can leave a Group by themselves at any time, without restriction. This function is called when a member is trying to be removed by another account, for example, by an administrator of the Group.

The function receives the account trying to be removed of the Group, the membershipId (so maybe the decision can be based on it, e.g. first ten members cannot be removed), and some data in case the rule requires additional information to work.

The function must revert in case of not meeting the requirements imposed by the rule. Otherwise, it must return a boolean that indicates if the rule is applying a restriction over this operation or not. In our example, we do not want to apply the restriction to the removal operation, so we must implement the function to comply with the interface but just return false.

contract FollowerCountGatedGroupRule is IGroupRule {
    // . . .
    function processRemoval(        address account,        uint256 membershipId,        bytes calldata data    ) external returns (bool) {        // We return false to signal the rule does not apply to the removal operation        return false;    }}

Now the FollowerCountGatedGroupRule is ready to be applied into any Group. See the full code below:

contract FollowerCountGatedGroupRule is IGroupRule {    mapping(address => address) internal _graphToCheck;    mapping(address => uint256) internal _minFollowers;
    function configure(bytes calldata data) external {        (address graphToCheck, uint256 minFollowers) = abi.decode(data, (address, uint256));        _graphToCheck[msg.sender] = graphToCheck;        _minFollowers[msg.sender] = minFollowers;    }
    function processJoining(        address account,        uint256 membershipId,        bytes calldata data    ) external returns (bool) {        // We get the Graph where to check the followers count        IGraph graph = IGraph(_graphToCheck[msg.sender]);        // We check if the account has the min required amount of followers        require(graph.getFollowersCount(account) >= _minFollowers[msg.sender]);        // We return true to signal the rule applies to the joining operation        return true;    }
    function processRemoval(        address account,        uint256 membershipId,        bytes calldata data    ) external returns (bool) {        // We return false to signal the rule does not apply to the removal operation        return false;    }}

Stay tuned for API integration of rules and more guides!