Graph Rules
This guide will explain Graph Rules and how to implement them.
Graph Rules allow Graphs' administrators to add requirements or constraints that will be applied when any Account performs a Follow or a change in its Follow Rules.
See the Rules concept section to learn more about them.
Existing Graph Rules
TokenGatedGraphRule
This rule can be applied to a Graph to require both accounts, the follower account and the one being followed, to hold a certain balance of a token (both fungible and non-fungible are supported) in order to successfully execute the follow operation under the context of that Graph. Applying this rule allows to have a Graph purely made of connections between accounts that hold a certain token.
The configuration requires to input 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.
RestrictedSignersGraphRule
This rule can be applied to make a Custom Graph to have restricted access. This means the rule applies a restriction that makes the Graph to only accept Follows and Follow Rules changes that are signed by an address that belongs to a set of trusted signers.
The rule configuration requires to input an array of addresses and boolean values indicating for each address if it should be added or removed from the list of trusted signers. Rules can be reconfigured, meaning signers can be dynamically added or removed over time.
In addition, an optional string can be provided for each address in order to label each of the signers.
The rule, when processing each operation, will require an EIP-712 signature that matches the details (function selector and parameters) of the operation being executed, signed by one of the configured trusted signers.
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(bytes calldata data) external;
function processFollow( address followerAccount, address accountToFollow, uint256 followId, bytes calldata data ) external returns (bool);
function processFollowRulesChange( address account, RuleConfiguration[] calldata followRules, bytes calldata data ) external returns (bool);}
Each function of this interface must assume to be invoked by the Graph 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.
The configure function can be called multiple times by the same Graph 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 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 => uint256) internal _followCooldown;}
Now let's code the configure function itself, decoding the integer parameter and storing it in the mapping:
contract FollowCooldownGraphRule is IGraphRule {
mapping(address => uint256) internal _followCooldown;
function configure(bytes calldata data) external override { _followCooldown[msg.sender] = abi.decode(data, (uint256)); }}
The configuration is stored in the mapping using the Graph contract address as the key, which is the msg.sender. So the same rule can be reused by different Graphs.
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 account executing the follow (followerAccount), the account being followed (accountToFollow), the followId (only applies if Follows are tokenized, so you can use some specific Follow Token to Follow), 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 Follow operation, we will return true.
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 => uint256) internal _followCooldown;
mapping(address => mapping(address => uint256)) 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 => uint256) internal _followCooldown;
mapping(address => mapping(address => uint256)) internal _lastFollowTimestamp;
// . . .
function processFollow( address followerAccount, address accountToFollow, uint256 followId, bytes calldata data ) external override returns (bool) { // We get the time elapsed since the last Follow uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][followerAccount]; }}
Next we require the elapsed time to be greater than the configured Follow cooldown period:
contract FollowCooldownGraphRule is IGraphRule { mapping(address => uint256) internal _followCooldown;
mapping(address => mapping(address => uint256)) internal _lastFollowTimestamp;
// . . .
function processFollow( address followerAccount, address accountToFollow, uint256 followId, bytes calldata data ) external override returns (bool) { uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][followerAccount]; // We require the elapsed time to be greater than the configured Follow cooldown period require(timeElapsedSinceLastFollow > _followCooldown[msg.sender]); }}
And to finish the implementation of this function, we update the last Follow timestamp for this Follower account, and we return true as the rule is being applied to the Follow operation:
contract FollowCooldownGraphRule is IGraphRule { mapping(address => uint256) internal _followCooldown;
mapping(address => mapping(address => uint256)) internal _lastFollowTimestamp;
// . . .
function processFollow( address followerAccount, address accountToFollow, uint256 followId, bytes calldata data ) external override returns (bool) { uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][followerAccount]; require(timeElapsedSinceLastFollow > _followCooldown[msg.sender]); // We update the last Follow timestamp for this Follower account _lastFollowTimestamp[followerAccount][accountToFollow] = block.timestamp; // We return true to signal that the rule is being applied when executing a Follow return true; }}
Finally, implement the processFollowRulesChange 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 account changing its Follow Rules, the configuration of the rules being changed (followRules), 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 Follow Rules Changes operation, so we must implement the function to comply with the interface but just return false.
contract FollowCooldownGraphRule is IGraphRule {
// . . .
function processFollowRulesChange( address account, RuleConfiguration[] calldata followRules, bytes calldata data ) external override returns (bool) { // We return false to signal that we do not apply the rule when changing Follow Rules return false; }}
Now the FollowCooldownGraphRule is ready to be applied into any Graph. See the full code below:
contract FollowCooldownGraphRule is IGraphRule { mapping(address => uint256) internal _followCooldown;
mapping(address => mapping(address => uint256)) internal _lastFollowTimestamp;
function configure(bytes calldata data) external override { _followCooldown[msg.sender] = abi.decode(data, (uint256)); }
function processFollow( address followerAccount, address accountToFollow, uint256 followId, bytes calldata data ) external override returns (bool) { // We get the time elapsed since the last Follow uint256 timeElapsedSinceLastFollow = block.timestamp - _lastFollowTimestamp[msg.sender][followerAccount]; // We require the elapsed time to be greater than the configured Follow cooldown period require(timeElapsedSinceLastFollow > _followCooldown[msg.sender]); // We update the last Follow timestamp for this Follower account _lastFollowTimestamp[followerAccount][accountToFollow] = block.timestamp; // We return true to signal that the rule is being applied when executing a Follow return true; }
function processFollowRulesChange( address account, RuleConfiguration[] calldata followRules, bytes calldata data ) external override returns (bool) { // We return false to signal that we do not apply the rule when changing Follow Rules return false; }}
Stay tuned for API integration of rules and more guides!