// SPDX-License-Identifier: UNLICENSED // Copyright (C) 2024 Lens Labs. All Rights Reserved. pragma solidity ^0.8.26; import {IGraphRule} from "lens-modules/contracts/core/interfaces/IGraphRule.sol"; import {IAccessControl} from "lens-modules/contracts/core/interfaces/IAccessControl.sol"; import {AccessControlLib} from "lens-modules/contracts/core/libraries/AccessControlLib.sol"; import {KeyValue, RuleChange} from "lens-modules/contracts/core/types/Types.sol"; import {Events} from "lens-modules/contracts/core/types/Events.sol"; import {IGroup} from "lens-modules/contracts/core/interfaces/IGroup.sol"; import {OwnableMetadataBasedRule} from "lens-modules/contracts/rules/base/OwnableMetadataBasedRule.sol"; import {Errors} from "lens-modules/contracts/core/types/Errors.sol"; import {Initializable} from "lens-modules/contracts/core/upgradeability/Initializable.sol"; contract GroupGatedGraphRule is OwnableMetadataBasedRule, Initializable, IGraphRule { using AccessControlLib for IAccessControl; using AccessControlLib for address; /// @custom:keccak lens.permission.SkipGate uint256 constant PID__SKIP_GATE = uint256(0xeb7f30e4c97d5211e2534aa42375c26931bd55b57a8101e5eb7918daead714eb); /// @custom:keccak lens.param.accessControl bytes32 constant PARAM__ACCESS_CONTROL = 0xcf3b0fab90208e4185bf857e0f943f6672abffb7d0898e0750beeeb991ae35fa; /// @custom:keccak lens.param.group bytes32 constant PARAM__GROUP = 0xa92ea569d1a9f915f96759ba7cea5f135d011c442b0508dbef76a309e55f4458; /// @custom:keccak lens.storage.GroupGatedGraphRule bytes32 constant STORAGE__GROUP_GATED_GRAPH_RULE = 0x0cd40a3a3781a5f1ef9fef9277b4fdc1082fe349c1d9788798b746bdd9228091; struct Configuration { address accessControl; address groupGate; } struct Storage { mapping(address graph => mapping(bytes32 configSalt => Configuration config)) configuration; } function $storage() private pure returns (Storage storage _storage) { assembly { _storage.slot := STORAGE__GROUP_GATED_GRAPH_RULE } } constructor() OwnableMetadataBasedRule(address(0), "") { _disableInitializers(); } function initialize(address owner, string memory metadataURI) external initializer { emit Events.Lens_PermissionId_Available(PID__SKIP_GATE, "lens.permission.SkipGate"); OwnableMetadataBasedRule._initialize(owner, metadataURI); } function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { Configuration memory configuration = _extractConfigurationFromParams(ruleParams); configuration.accessControl.verifyHasAccessFunction(); IGroup(configuration.groupGate).isMember(address(this)); // Aims to verify the provided address is a valid group $storage().configuration[msg.sender][configSalt] = configuration; } function processFollow( bytes32 configSalt, address, /* originalMsgSender */ address followerAccount, address accountToFollow, KeyValue[] calldata, /* primitiveParams */ KeyValue[] calldata /* ruleParams */ ) external view override { /** * Both ends of the follow connection must comply with the group-gate restriction, then the graph is purely * conformed by group members. */ _validateGroupMembership( $storage().configuration[msg.sender][configSalt].accessControl, $storage().configuration[msg.sender][configSalt].groupGate, followerAccount ); _validateGroupMembership( $storage().configuration[msg.sender][configSalt].accessControl, $storage().configuration[msg.sender][configSalt].groupGate, accountToFollow ); } function processUnfollow( bytes32, /* configSalt */ address, /* originalMsgSender */ address, /* followerAccount */ address, /* accountToUnfollow */ KeyValue[] calldata, /* primitiveParams */ KeyValue[] calldata /* ruleParams */ ) external pure override { revert Errors.NotImplemented(); } function processFollowRuleChanges( bytes32, /* configSalt */ address, /* account */ RuleChange[] calldata, /* ruleChanges */ KeyValue[] calldata /* ruleParams */ ) external pure override { revert Errors.NotImplemented(); } function _validateGroupMembership(address accessControl, address group, address account) internal view { if (!accessControl.hasAccess(account, PID__SKIP_GATE)) { require(IGroup(group).isMember(account), Errors.NotAMember()); } } function _extractConfigurationFromParams(KeyValue[] calldata params) internal pure returns (Configuration memory) { Configuration memory configuration; for (uint256 i = 0; i < params.length; i++) { if (params[i].key == PARAM__ACCESS_CONTROL) { configuration.accessControl = abi.decode(params[i].value, (address)); } else if (params[i].key == PARAM__GROUP) { configuration.groupGate = abi.decode(params[i].value, (address)); } } return configuration; } }