feat: LensModule interface with ERC165 standard and metadata [P-21] T-16815

Co-authored-by: Alan <donosonaumczuk@gmail.com>
This commit is contained in:
vicnaum
2023-10-18 19:42:05 +02:00
parent 48ec9ab4bc
commit e8eb87ff97
15 changed files with 194 additions and 18 deletions

View File

@@ -4,6 +4,11 @@ pragma solidity ^0.8.15;
import {IModuleRegistry} from 'contracts/interfaces/IModuleRegistry.sol';
import {IERC20Metadata} from '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol';
import {ILensModule} from 'contracts/modules/interfaces/ILensModule.sol';
import {IPublicationActionModule} from 'contracts/interfaces/IPublicationActionModule.sol';
import {IFollowModule} from 'contracts/interfaces/IFollowModule.sol';
import {IReferenceModule} from 'contracts/interfaces/IReferenceModule.sol';
/**
* @title ModuleRegistry
@@ -12,7 +17,14 @@ import {IERC20Metadata} from '@openzeppelin/contracts/token/ERC20/extensions/IER
* @custom:upgradeable Transparent upgradeable proxy without initializer.
*/
contract ModuleRegistry is IModuleRegistry {
event ModuleRegistered(address indexed moduleAddress, uint256 indexed moduleType, uint256 timestamp);
bytes4 private constant LENS_MODULE_INTERFACE_ID = bytes4(keccak256(abi.encodePacked('LENS_MODULE')));
event ModuleRegistered(
address indexed moduleAddress,
uint256 indexed moduleType,
string metadata,
uint256 timestamp
);
event erc20CurrencyRegistered(
address indexed erc20CurrencyAddress,
@@ -22,6 +34,9 @@ contract ModuleRegistry is IModuleRegistry {
uint256 timestamp
);
error NotLensModule();
error ModuleDoesNotSupportType(uint256 moduleType);
mapping(address moduleAddress => uint256 moduleTypesBitmap) internal registeredModules;
mapping(address erc20CurrencyAddress => bool) internal registeredErc20Currencies;
@@ -44,12 +59,36 @@ contract ModuleRegistry is IModuleRegistry {
if (isAlreadyRegisteredAsThatType) {
return false;
} else {
emit ModuleRegistered(moduleAddress, moduleType, block.timestamp);
if (!ILensModule(moduleAddress).supportsInterface(LENS_MODULE_INTERFACE_ID)) {
revert NotLensModule();
}
validateModuleSupportsType(moduleAddress, moduleType);
string memory metadata = ILensModule(moduleAddress).getModuleMetadataURI();
emit ModuleRegistered(moduleAddress, moduleType, metadata, block.timestamp);
registeredModules[moduleAddress] |= (1 << moduleType);
return true;
}
}
function validateModuleSupportsType(address moduleAddress, uint256 moduleType) internal view {
bool supportsInterface;
if (moduleType == uint256(IModuleRegistry.ModuleType.PUBLICATION_ACTION_MODULE)) {
supportsInterface = ILensModule(moduleAddress).supportsInterface(
type(IPublicationActionModule).interfaceId
);
} else if (moduleType == uint256(IModuleRegistry.ModuleType.FOLLOW_MODULE)) {
supportsInterface = ILensModule(moduleAddress).supportsInterface(type(IFollowModule).interfaceId);
} else if (moduleType == uint256(IModuleRegistry.ModuleType.REFERENCE_MODULE)) {
supportsInterface = ILensModule(moduleAddress).supportsInterface(type(IReferenceModule).interfaceId);
}
if (!supportsInterface) {
revert ModuleDoesNotSupportType(moduleType);
}
}
function getModuleTypes(address moduleAddress) public view returns (uint256) {
return registeredModules[moduleAddress];
}

View File

@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {ILensModule} from 'contracts/modules/interfaces/ILensModule.sol';
abstract contract LensModule is ILensModule {
/// @inheritdoc ILensModule
function supportsInterface(bytes4 interfaceID) public pure virtual override returns (bool) {
return interfaceID == bytes4(keccak256(abi.encodePacked('LENS_MODULE')));
}
}

View File

@@ -10,7 +10,19 @@ import {Clones} from '@openzeppelin/contracts/proxy/Clones.sol';
import {Errors} from 'contracts/modules/constants/Errors.sol';
import {HubRestricted} from 'contracts/base/HubRestricted.sol';
contract CollectPublicationAction is HubRestricted, IPublicationActionModule {
import {LensModule} from 'contracts/modules/LensModule.sol';
/**
* @title CollectPublicationAction
* @author LensProtocol
* @notice An Publication Action module that allows users to collect publications.
* @custom:upgradeable Transparent upgradeable proxy without initializer.
*/
contract CollectPublicationAction is LensModule, HubRestricted, IPublicationActionModule {
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IPublicationActionModule).interfaceId || super.supportsInterface(interfaceID);
}
struct CollectData {
address collectModule;
address collectNFT;
@@ -204,4 +216,8 @@ contract CollectPublicationAction is HubRestricted, IPublicationActionModule {
function isCollectModuleRegistered(address collectModule) external view returns (bool) {
return _collectModuleRegistered[collectModule];
}
function getModuleMetadataURI() external pure returns (string memory) {
return 'https://docs.lens.xyz/';
}
}

View File

@@ -17,7 +17,13 @@ import {PublicDrop} from '@seadrop/lib/SeaDropStructs.sol';
import {LensSeaDropCollection} from 'contracts/modules/act/seadrop/LensSeaDropCollection.sol';
import {ILensHub} from 'contracts/interfaces/ILensHub.sol';
contract SeaDropMintPublicationAction is HubRestricted, IPublicationActionModule {
import {LensModule} from 'contracts/modules/LensModule.sol';
contract SeaDropMintPublicationAction is LensModule, HubRestricted, IPublicationActionModule {
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IPublicationActionModule).interfaceId || super.supportsInterface(interfaceID);
}
uint256 constant MAX_BPS = 10_000;
ISeaDrop public immutable SEADROP;
@@ -274,4 +280,8 @@ contract SeaDropMintPublicationAction is HubRestricted, IPublicationActionModule
_rescaleFees(profileId, pubId, lensTreasuryFeeBps, publicDrop);
}
}
function getModuleMetadataURI() external pure returns (string memory) {
return 'https://docs.lens.xyz/';
}
}

View File

@@ -9,6 +9,8 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {HubRestricted} from 'contracts/base/HubRestricted.sol';
import {LensModule} from 'contracts/modules/LensModule.sol';
/**
* @notice A struct containing the necessary data to execute follow actions on a given profile.
*
@@ -28,7 +30,11 @@ struct FeeConfig {
*
* @notice This follow module charges a fee for every follow.
*/
contract FeeFollowModule is FeeModuleBase, HubRestricted, IFollowModule {
contract FeeFollowModule is LensModule, FeeModuleBase, HubRestricted, IFollowModule {
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IFollowModule).interfaceId || super.supportsInterface(interfaceID);
}
using SafeERC20 for IERC20;
mapping(uint256 profileId => FeeConfig config) internal _feeConfig;
@@ -116,4 +122,8 @@ contract FeeFollowModule is FeeModuleBase, HubRestricted, IFollowModule {
function getFeeConfig(uint256 profileId) external view returns (FeeConfig memory) {
return _feeConfig[profileId];
}
function getModuleMetadataURI() external pure returns (string memory) {
return 'https://docs.lens.xyz/';
}
}

View File

@@ -5,17 +5,23 @@ pragma solidity ^0.8.10;
import {Errors} from 'contracts/modules/constants/Errors.sol';
import {IFollowModule} from 'contracts/interfaces/IFollowModule.sol';
import {LensModule} from 'contracts/modules/LensModule.sol';
/**
* @title RevertFollowModule
* @author Lens Protocol
*
* @notice This follow module rejects all follow attempts.
*/
contract RevertFollowModule is IFollowModule {
contract RevertFollowModule is LensModule, IFollowModule {
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IFollowModule).interfaceId || super.supportsInterface(interfaceID);
}
/// @inheritdoc IFollowModule
function initializeFollowModule(
uint256, /* profileId */
address, /* transactionExecutor */
uint256 /* profileId */,
address /* transactionExecutor */,
bytes calldata /* data */
) external pure override returns (bytes memory) {
return '';
@@ -26,12 +32,16 @@ contract RevertFollowModule is IFollowModule {
* @notice Processes a follow by rejecting it, reverting the transaction. Parameters are ignored.
*/
function processFollow(
uint256, /* followerProfileId */
uint256, /* followTokenId */
address, /* transactionExecutor */
uint256, /* profileId */
uint256 /* followerProfileId */,
uint256 /* followTokenId */,
address /* transactionExecutor */,
uint256 /* profileId */,
bytes calldata /* data */
) external pure override returns (bytes memory) {
revert Errors.FollowInvalid();
}
function getModuleMetadataURI() external pure returns (string memory) {
return 'https://docs.lens.xyz/';
}
}

View File

@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0;
import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol';
interface ILensModule is IERC165 {
/// @dev for now we check for keccak('LENS_MODULE');
/// Override this and add the type(IModuleInterface).interfaceId for corresponding module type
function supportsInterface(bytes4 interfaceID) external view returns (bool);
/// @notice Human-readable description of the module
// Can be JSON
// Can be contract source code
// Can be github link
// Can be ipfs with documentation
// etc
function getModuleMetadataURI() external view returns (string memory);
}

View File

@@ -10,6 +10,8 @@ import {IReferenceModule} from 'contracts/interfaces/IReferenceModule.sol';
import {HubRestricted} from 'contracts/base/HubRestricted.sol';
import {FollowValidationLib} from 'contracts/modules/libraries/FollowValidationLib.sol';
import {LensModule} from 'contracts/modules/LensModule.sol';
/**
* @notice Struct representing the module configuration for certain publication.
*
@@ -40,7 +42,11 @@ struct ModuleConfig {
* only to profiles that are at most at `n` degrees of separation from the source profile, which is expected to be set
* as the author of the root publication.
*/
contract DegreesOfSeparationReferenceModule is HubRestricted, IReferenceModule {
contract DegreesOfSeparationReferenceModule is LensModule, HubRestricted, IReferenceModule {
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IReferenceModule).interfaceId || super.supportsInterface(interfaceID);
}
error InvalidDegreesOfSeparation();
error OperationDisabled();
error ProfilePathExceedsDegreesOfSeparation();
@@ -301,4 +307,8 @@ contract DegreesOfSeparationReferenceModule is HubRestricted, IReferenceModule {
revert NotInheritingPointedPubConfig();
}
}
function getModuleMetadataURI() external pure returns (string memory) {
return 'https://docs.lens.xyz/';
}
}

View File

@@ -7,6 +7,8 @@ import {HubRestricted} from 'contracts/base/HubRestricted.sol';
import {Types} from 'contracts/libraries/constants/Types.sol';
import {FollowValidationLib} from 'contracts/modules/libraries/FollowValidationLib.sol';
import {LensModule} from 'contracts/modules/LensModule.sol';
/**
* @title FollowerOnlyReferenceModule
* @author Lens Protocol
@@ -14,7 +16,11 @@ import {FollowValidationLib} from 'contracts/modules/libraries/FollowValidationL
* @notice A simple reference module that validates that comments, quotes or mirrors originate from a profile that
* follows the profile of the original publication.
*/
contract FollowerOnlyReferenceModule is HubRestricted, IReferenceModule {
contract FollowerOnlyReferenceModule is LensModule, HubRestricted, IReferenceModule {
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IReferenceModule).interfaceId || super.supportsInterface(interfaceID);
}
constructor(address hub) HubRestricted(hub) {}
/**
@@ -85,4 +91,8 @@ contract FollowerOnlyReferenceModule is HubRestricted, IReferenceModule {
}
return '';
}
function getModuleMetadataURI() external pure returns (string memory) {
return 'https://docs.lens.xyz/';
}
}

View File

@@ -8,6 +8,8 @@ import {Errors} from 'contracts/modules/constants/Errors.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
import {Types} from 'contracts/libraries/constants/Types.sol';
import {LensModule} from 'contracts/modules/LensModule.sol';
interface IToken {
/**
* @dev Returns the amount of ERC20/ERC721 tokens owned by `account`.
@@ -32,7 +34,11 @@ struct GateParams {
*
* @notice A reference module that validates that the user who tries to reference has a required minimum balance of ERC20/ERC721 token.
*/
contract TokenGatedReferenceModule is HubRestricted, IReferenceModule {
contract TokenGatedReferenceModule is LensModule, HubRestricted, IReferenceModule {
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IReferenceModule).interfaceId || super.supportsInterface(interfaceID);
}
uint256 internal constant UINT256_BYTES = 32;
event TokenGatedReferencePublicationCreated(
@@ -154,4 +160,8 @@ contract TokenGatedReferenceModule is HubRestricted, IReferenceModule {
}
return balance;
}
function getModuleMetadataURI() external pure returns (string memory) {
return 'https://docs.lens.xyz/';
}
}

View File

@@ -10,6 +10,10 @@ import {MockModule} from 'test/mocks/MockModule.sol';
* @dev This is a simple mock Action module to be used for testing revert cases on processAction.
*/
contract MockActionModule is MockModule, IPublicationActionModule {
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IPublicationActionModule).interfaceId || super.supportsInterface(interfaceID);
}
error MockActionModuleReverted();
function testMockActionModule() public {

View File

@@ -3,11 +3,16 @@
pragma solidity ^0.8.15;
import {IFollowModule} from 'contracts/interfaces/IFollowModule.sol';
import {LensModule} from 'contracts/modules/LensModule.sol';
/**
* @dev This is a simple mock follow module to be used for testing.
*/
contract MockFollowModule is IFollowModule {
contract MockFollowModule is LensModule, IFollowModule {
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IFollowModule).interfaceId || super.supportsInterface(interfaceID);
}
function testMockFollowModule() public {
// Prevents being counted in Foundry Coverage
}
@@ -29,4 +34,8 @@ contract MockFollowModule is IFollowModule {
uint256 profileId,
bytes calldata data
) external pure override returns (bytes memory) {}
function getModuleMetadataURI() external pure override returns (string memory) {
return 'https://docs.lens.xyz/';
}
}

View File

@@ -3,11 +3,16 @@
pragma solidity ^0.8.15;
import {IFollowModule} from 'contracts/interfaces/IFollowModule.sol';
import {LensModule} from 'contracts/modules/LensModule.sol';
/**
* @dev This is a simple mock follow module to be used for testing revert cases on processFollow.
*/
contract MockFollowModuleWithRevertFlag is IFollowModule {
contract MockFollowModuleWithRevertFlag is LensModule, IFollowModule {
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IFollowModule).interfaceId || super.supportsInterface(interfaceID);
}
error MockFollowModuleReverted();
function testMockFollowModuleWithRevertFlag() public {
@@ -34,4 +39,8 @@ contract MockFollowModuleWithRevertFlag is IFollowModule {
}
return '';
}
function getModuleMetadataURI() external pure override returns (string memory) {
return 'https://docs.lens.xyz/';
}
}

View File

@@ -2,7 +2,9 @@
pragma solidity ^0.8.15;
abstract contract MockModule {
import {LensModule} from 'contracts/modules/LensModule.sol';
abstract contract MockModule is LensModule {
error MockModuleReverted();
function testMockModule() public {
@@ -17,4 +19,8 @@ abstract contract MockModule {
}
return data;
}
function getModuleMetadataURI() external pure override returns (string memory) {
return 'https://docs.lens.xyz/';
}
}

View File

@@ -10,6 +10,10 @@ import {MockModule} from 'test/mocks/MockModule.sol';
* @dev This is a simple mock follow module to be used for testing.
*/
contract MockReferenceModule is MockModule, IReferenceModule {
function supportsInterface(bytes4 interfaceID) public pure override returns (bool) {
return interfaceID == type(IReferenceModule).interfaceId || super.supportsInterface(interfaceID);
}
function testMockReferenceModule() public {
// Prevents being counted in Foundry Coverage
}