Group Rules
This guide explains how to use Group Rules and how to implement custom ones.
Group Rules allow administrators to add requirements or constraints for joining or leaving a Group.
Lens provides four built-in Group rules:
SimplePaymentGroupRule - Requires an ERC-20 payment to join the Group.
TokenGatedGroupRule - Requires an account to hold a certain token to join the Group.
MembershipApprovalGroupRule - Requires approval by owner or administrators of the Group to join.
BanMemberGroupRule - Prevents an account from joining the Group if banned.
AdditionRemovalPidGroupRule - Requires an account to have special permissions (typically equivalent to be Owner or Admin) in order to add or remove members from the Group.
For the SimplePaymentGroupRule, 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 Group Rules to extend the functionality of your Group.
Using Group Rules
This section presumes you are familiar with the process of creating a Group on Lens.
Simple Payment Group Rule
This rule requires an ERC-20 payment to join a Group. Configuration includes the ERC-20 token address, the payment amount, and the recipient address.
- TypeScript
- GraphQL
- React
Simple Payment Group Rule
import { bigDecimal, evmAddress, uri } from "@lens-protocol/client";import { createGroup } from "@lens-protocol/client/actions";
const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { simplePaymentRule: { cost: { currency: evmAddress("0x5678…"), value: bigDecimal("10.42"), }, recipient: evmAddress("0x9012…"), }, }, ], },});
Token Gated Group Rule
This rule requires holding a certain balance of a token (fungible or non-fungible) to join a Group.
Configuration includes the token address, the token standard (ERC-20, ERC-721, or ERC-1155), and the required token amount. For ERC-1155 tokens, an additional token type ID is required.
- TypeScript
- GraphQL
- React
Token Gated Group Rule
import { bigDecimal, evmAddress, uri, TokenStandard,} from "@lens-protocol/client";import { createGroup } from "@lens-protocol/client/actions";
const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { tokenGatedRule: { token: { currency: evmAddress("0x1234…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, }, ], },});
Membership Approval Group Rule
This rule requires approval by the owner or administrators to join a Group.
- TypeScript
- GraphQL
- React
Membership Approval Group Rule
import { bigDecimal, evmAddress, uri, TokenStandard,} from "@lens-protocol/client";import { createGroup } from "@lens-protocol/client/actions";
const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [{ membershipApprovalRule: { enable: true } }], },});
See the Membership Approvals guide for more information on how to handle membership approval.
Ban Member Group Rule
This rule prevents an account from joining a Group and is automatically enabled when a Group is created.
See the Banned Accounts guide for more information on how to handle banned accounts.
Custom Group Rules
You can also use custom rules by specifying the rule contract address, when it applies, and the configuration parameters as key-value pairs.
- TypeScript
- GraphQL
- React
Custom Group Rule
import { blockchainData, evmAddress, GroupRuleExecuteOn, uri,} from "@lens-protocol/client";import { createGroup } from "@lens-protocol/client/actions";
const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { unknownRule: { address: evmAddress("0x1234…"), executeOn: [GroupRuleExecuteOn.Joining], params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded value value: blockchainData("0x00"), }, }, ], }, }, ], },});
Combining Rules
Additionally, multiple rules can be combined:
- TypeScript
- GraphQL
- React
Combining Rules
import { bigDecimal, evmAddress, TokenStandard, uri,} from "@lens-protocol/client";import { createGroup } from "@lens-protocol/client/actions";
const result = await createGroup(sessionClient, { metadataUri: uri("lens://4f91c…"), rules: { required: [ { simplePaymentRule: { cost: { currency: evmAddress("0x5678…"), value: bigDecimal("10.42"), }, recipient: evmAddress("0x9012…"), }, }, ], anyOf: [ { tokenGatedRule: { token: { currency: evmAddress("0x1234…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, }, { tokenGatedRule: { token: { currency: evmAddress("0x3456…"), standard: TokenStandard.Erc721, value: bigDecimal("1"), }, }, }, ], },});
Update a Group Rules
To update a Group rules configuration, follow these steps.
You MUST be authenticated as a Builder, Account Manager, or Account Owner and be either the owner or an admin of the Group to update its rules.
First, inspect the:
group.banningEnabled flag to know if the Group has the BanMemberGroupRule enabled.
group.membershipApprovalEnabled flag to know if the Group has the MembershipApprovalGroupRule enabled.
And inspect the group.rules field to know details on all rules configuration of the Group.
- TypeScript
- GraphQL
The configuration for the built-in rules with one or more parameters is as follows.
- SimplePaymentGroupRule
- TokenGatedGroupRule
Key | Type | Description | |
---|---|---|---|
assetContract | EvmAddress | Address of the ERC-20 token contract. | |
assetName | String | Name of the ERC-20 token. | |
assetSymbol | String | Symbol of the ERC-20 token. | |
amount | BigDecimal | Minimum amount of the ERC-20 token required to join. |
Keep note of the Rule IDs you might want to remove.
Next, update the rules configuration of the Group as follows.
- TypeScript
- GraphQL
- React
Use the updateGroupRules action to update the rules configuration of a given group.
- TypeScript
- GraphQL
- React
Then, handle the result using the adapter for the library of your choice:
See the Transaction Lifecycle guide for more information on how to determine the status of the transaction.
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(bytes32 configSalt, KeyValue[] calldata ruleParams) external;
function processAddition( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external;
function processRemoval( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external;
function processJoining( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external;
function processLeaving( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external;}
Each function of this interface must assume to be invoked by the Group contract. In other words, assume the msg.sender will be the Group contract.
A Lens dependency package with all relevant interfaces will be available soon.
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 Group. So, for a given Group Rule implementation, the pair (Group Address, Configuration Salt) should identify a rule configuration.
For example, let's think about the TokenGatedGroupRule, we could want to achieve the restriction "To join this Group, you must hold 10 WETH and 1 WBTC". However, the TokenGatedGroupRule only allows to configure a single token as a gate. So, instead of writing a whole new contract that receives two tokens instead of one, we would just configure the TokenGatedGroupRule rule twice in the same Group, once for WETH with some configuration salt, and once for WBTC with another configuration salt.
The configure function can be called multiple times by the same Group 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 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 group => mapping(bytes32 configSalt => address graph)) internal _graphToCheck; mapping(address group => mapping(bytes32 configSalt => uint256 minFollowers)) internal _minFollowers;}
The configuration is stored in the mapping using the Group contract address and the configuration salt as keys. With this setup, the same rule can be used by different Groups, as well as be used by the same Group many times.
Now let's code the configure function itself, decoding the parameters and putting them into the storage:
contract FollowerCountGatedGroupRule is IGroupRule { mapping(address group => mapping(bytes32 configSalt => address graph)) internal _graphToCheck; mapping(address group => mapping(bytes32 configSalt => uint256 minFollowers)) internal _minFollowers;
function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { address graphToCheck; uint256 minFollowers = 100; for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param,graph")) { graphToCheck = abi.decode(ruleParams[i].value, (address)); } else if (ruleParams[i].key == keccak256("lens.param,minFollowers")) { minFollowers = abi.decode(ruleParams[i].value, (uint256)); } } // Aims to check if the graph is a valid Graph contract IGraph(graph).getFollowersCount(address(this)); _graphToCheck[msg.sender][configSalt] = graphToCheck; _minFollowers[msg.sender][configSalt] = minFollowers; }}
As you can see, we treated the minimum followers parameter as optional, setting a default value of 100 followers for it in case that the parameter was not found in the ruleParams. On the other hand, the graph parameter is treated as required, without default value, and needing to be found as a valid graph address in order for the getFollowersCount call to not revert.
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 configuration salt (configSalt), so we know which configuration to use, the account attempting to join the Group (account), an array of key-value pairs with the custom parameters passed to the Group (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 first get the configured Graph where to perform the check:
contract FollowerCountGatedGroupRule is IGroupRule { mapping(address group => mapping(bytes32 configSalt => address graph)) internal _graphToCheck; mapping(address group => mapping(bytes32 configSalt => uint256 minFollowers)) internal _minFollowers;
// . . .
function processJoining( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { // We get the Graph where to check the followers count IGraph graph = IGraph(_graphToCheck[msg.sender][configSalt]); }}
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 and we are done with this function:
contract FollowerCountGatedGroupRule is IGroupRule { mapping(address group => mapping(bytes32 configSalt => address graph)) internal _graphToCheck; mapping(address group => mapping(bytes32 configSalt => uint256 minFollowers)) internal _minFollowers;
// . . .
function processJoining( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { IGraph graph = IGraph(_graphToCheck[msg.sender][configSalt]); // We check if the account has the min required amount of followers require(graph.getFollowersCount(account) >= _minFollowers[msg.sender][configSalt]); }}
Next, implement the processLeaving function. This function is invoked by the Group contract every time someone tries to leave the Group, so then our custom logic can be applied to shape under which conditions the operation can succeed.
The parameters are the same as in the processJoining function except that the account is trying to leave the Group instead of joining it.
Given that we do not want to apply any restriction to the leaving operation, we can just revert the function with a NotImplemented error, which is the safest approach in case someone accidentally applies this rule:
contract FollowerCountGatedGroupRule is IGroupRule {
// . . .
function processLeaving( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); }}
Next, 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.
The function receives the configuration salt (configSalt), so we know which configuration to use, the address that is trying to remove the member (originalMsgSender), the account trying to be removed of the Group (account), an array of key-value pairs with the custom parameters passed to the Group (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 removal operation. As a good practice, we revert for unimplemented rule functions, as it is safer in case the rule becomes accidentally applied.
contract FollowerCountGatedGroupRule is IGroupRule {
// . . .
function processRemoval( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); }}
Finally, implement the processAddition function. This function is invoked by the Group contract every time a Group member is trying to be added, so then our rule can define if this operation must succeed or not.
The parameters are the same as in the processRemoval function except that the account is trying to be added to the Group instead of removed from it.
Same as in the processRemoval, here in the processAddition we do not want to impose any restriction, so we revert the function with a NotImplemented error:
contract FollowerCountGatedGroupRule is IGroupRule {
// . . .
function processAddition( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); }}
Now the FollowerCountGatedGroupRule is ready to be applied into any Group. See the full code below:
contract FollowerCountGatedGroupRule is IGroupRule { mapping(address group => mapping(bytes32 configSalt => address graph)) internal _graphToCheck; mapping(address group => mapping(bytes32 configSalt => uint256 minFollowers)) internal _minFollowers;
function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { address graphToCheck; uint256 minFollowers = 100; for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param,graph")) { graphToCheck = abi.decode(ruleParams[i].value, (address)); } else if (ruleParams[i].key == keccak256("lens.param,minFollowers")) { minFollowers = abi.decode(ruleParams[i].value, (uint256)); } } // Aims to check if the graph is a valid Graph contract IGraph(graph).getFollowersCount(address(this)); _graphToCheck[msg.sender][configSalt] = graphToCheck; _minFollowers[msg.sender][configSalt] = minFollowers; }
function processAddition( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); }
function processRemoval( bytes32 configSalt, address originalMsgSender, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); }
function processJoining( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { // We get the Graph where to check the followers count IGraph graph = IGraph(_graphToCheck[msg.sender][configSalt]); // We check if the account has the min required amount of followers require(graph.getFollowersCount(account) >= _minFollowers[msg.sender][configSalt]); }
function processLeaving( bytes32 configSalt, address account, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external override { revert Errors.NotImplemented(); }}
Stay tuned for API integration of rules and more guides!