feat: Permissionless modules - Impl in progress

Co-authored-by: Victor Naumik <vicnaum@gmail.com>
This commit is contained in:
donosonaumczuk
2023-09-08 18:59:00 +01:00
parent 605ea54dd0
commit 03e5d9c31a
15 changed files with 146 additions and 282 deletions

View File

@@ -536,8 +536,9 @@ contract LensHub is
function getPublication(
uint256 profileId,
uint256 pubId
) external view override returns (Types.Publication memory) {
return _publications[profileId][pubId];
) external view override returns (Types.PublicationMemory memory) {
// TODO: Maybe we need to add some assembly here if this doesn't work
return Types.PublicationMemory(_publications[profileId][pubId]);
}
/// @inheritdoc ILensProtocol
@@ -547,8 +548,4 @@ contract LensHub is
) external view override returns (Types.PublicationType) {
return PublicationLib.getPublicationType(profileId, pubId);
}
function getActionModuleById(uint256 id) external view override returns (address) {
return _actionModules[id];
}
}

View File

@@ -55,11 +55,7 @@ abstract contract LensHubStorage {
mapping(uint256 blockerProfileId => mapping(uint256 blockedProfileId => bool isBlocked)) internal _blockedStatus; // Slot 27
mapping(uint256 id => address actionModule) internal _actionModules; // Slot 28
uint256 internal _profileRoyaltiesBps; // Slot 28
uint256 internal _maxActionModuleIdUsed; // Slot 29
uint256 internal _profileRoyaltiesBps; // Slot 30
mapping(address migrationAdmin => bool allowed) internal _migrationAdminWhitelisted; // Slot 31
mapping(address migrationAdmin => bool allowed) internal _migrationAdminWhitelisted; // Slot 29
}

View File

@@ -405,15 +405,6 @@ interface ILensProtocol {
*/
function isBlocked(uint256 profileId, uint256 byProfileId) external view returns (bool);
/**
* @notice Returns the address of the action module associated with the given whitelist ID, address(0) if none.
*
* @param id The ID of the module whose address wants to be queried.
*
* @return address The address of the action module associated with the given ID.
*/
function getActionModuleById(uint256 id) external view returns (address);
/**
* @notice Returns the URI associated with a given publication.
* This is used to store the publication's metadata, e.g.: content, images, etc.
@@ -442,7 +433,7 @@ interface ILensProtocol {
*
* @return Publication The publication struct associated with the queried publication.
*/
function getPublication(uint256 profileId, uint256 pubId) external view returns (Types.Publication memory);
function getPublication(uint256 profileId, uint256 pubId) external view returns (Types.PublicationMemory memory);
/**
* @notice Returns the type of a given publication.

View File

@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
interface IModuleRegistry {
enum ModuleType {
__, // Just to avoid 0 as valid ModuleType
PUBLICATION_ACTION_MODULE,
REFERENCE_MODULE,
FOLLOW_MODULE
}
// Modules functions
function registerModule(address moduleAddress, uint256 moduleType) external returns (bool);
function getModuleTypes(address moduleAddress) external view returns (uint256);
function isModuleRegistered(address moduleAddress) external view returns (bool);
function isModuleRegisteredAs(address moduleAddress, uint256 moduleType) external view returns (bool);
// Currencies functions
function registerErc20Currency(address currencyAddress) external returns (bool);
function isErc20CurrencyRegistered(address currencyAddress) external view returns (bool);
}

View File

@@ -25,10 +25,7 @@ library ActionLib {
publicationActionParams.publicationActedId
);
address actionModuleAddress = publicationActionParams.actionModuleAddress;
uint256 actionModuleId = StorageLib.actionModuleRegisterData()[actionModuleAddress].id;
if (!_isActionEnabled(_actedOnPublication, actionModuleId)) {
if (!_isActionEnabled(_actedOnPublication, publicationActionParams.actionModuleAddress)) {
// This will also revert for:
// - Non-existent action modules
// - Non-existent publications
@@ -44,19 +41,20 @@ library ActionLib {
publicationActionParams.publicationActedId
);
bytes memory actionModuleReturnData = IPublicationActionModule(actionModuleAddress).processPublicationAction(
Types.ProcessActionParams({
publicationActedProfileId: publicationActionParams.publicationActedProfileId,
publicationActedId: publicationActionParams.publicationActedId,
actorProfileId: publicationActionParams.actorProfileId,
actorProfileOwner: actorProfileOwner,
transactionExecutor: transactionExecutor,
referrerProfileIds: publicationActionParams.referrerProfileIds,
referrerPubIds: publicationActionParams.referrerPubIds,
referrerPubTypes: referrerPubTypes,
actionModuleData: publicationActionParams.actionModuleData
})
);
bytes memory actionModuleReturnData = IPublicationActionModule(publicationActionParams.actionModuleAddress)
.processPublicationAction(
Types.ProcessActionParams({
publicationActedProfileId: publicationActionParams.publicationActedProfileId,
publicationActedId: publicationActionParams.publicationActedId,
actorProfileId: publicationActionParams.actorProfileId,
actorProfileOwner: actorProfileOwner,
transactionExecutor: transactionExecutor,
referrerProfileIds: publicationActionParams.referrerProfileIds,
referrerPubIds: publicationActionParams.referrerPubIds,
referrerPubTypes: referrerPubTypes,
actionModuleData: publicationActionParams.actionModuleData
})
);
emit Events.Acted(publicationActionParams, actionModuleReturnData, transactionExecutor, block.timestamp);
return actionModuleReturnData;
@@ -64,12 +62,8 @@ library ActionLib {
function _isActionEnabled(
Types.Publication storage _publication,
uint256 actionModuleId
address actionModuleAddress
) private view returns (bool) {
if (actionModuleId == 0) {
return false;
}
uint256 actionModuleIdBitmapMask = 1 << (actionModuleId - 1);
return actionModuleIdBitmapMask & _publication.enabledActionModulesBitmap != 0;
return _publication.actionModuleEnabled[actionModuleAddress];
}
}

View File

@@ -9,6 +9,7 @@ import {Events} from 'contracts/libraries/constants/Events.sol';
import {StorageLib} from 'contracts/libraries/StorageLib.sol';
import {IFollowModule} from 'contracts/interfaces/IFollowModule.sol';
import {IFollowNFT} from 'contracts/interfaces/IFollowNFT.sol';
import {IModuleRegistry} from 'contracts/interfaces/IModuleRegistry.sol';
library ProfileLib {
function ownerOf(uint256 profileId) internal view returns (address) {
@@ -75,7 +76,7 @@ library ProfileLib {
address followModule,
bytes memory followModuleInitData
) private returns (bytes memory) {
ValidationLib.validateFollowModuleRegistered(followModule);
IModuleRegistry(StorageLib.getModuleRegistry()).registerModule(followModule, IModuleRegistry.ModuleType.Follow);
return IFollowModule(followModule).initializeFollowModule(profileId, transactionExecutor, followModuleInitData);
}

View File

@@ -10,8 +10,11 @@ import {IReferenceModule} from 'contracts/interfaces/IReferenceModule.sol';
import {ILegacyReferenceModule} from 'contracts/interfaces/ILegacyReferenceModule.sol';
import {StorageLib} from 'contracts/libraries/StorageLib.sol';
import {IPublicationActionModule} from 'contracts/interfaces/IPublicationActionModule.sol';
import {IModuleRegistry} from 'contracts/interfaces/IModuleRegistry.sol';
library PublicationLib {
address constant MODULE_REGISTRY = address(0xC0FFEE); // TODO: Pass constant or make libs contracts and manually DELEGATECALL to them
/**
* @notice Publishes a post to a given profile.
*
@@ -498,25 +501,13 @@ library PublicationLib {
}
bytes[] memory actionModuleInitResults = new bytes[](params.actionModules.length);
uint256 enabledActionModulesBitmap;
uint256 i;
while (i < params.actionModules.length) {
Types.ActionModuleWhitelistData memory actionModuleWhitelistData = StorageLib.actionModuleRegisterData()[
params.actionModules[i]
];
if (!actionModuleRegisterData.isRegistered) {
revert Errors.NotRegistered();
}
uint256 actionModuleIdBitmapMask = 1 << (actionModuleRegisterData.id - 1);
if (enabledActionModulesBitmap & actionModuleIdBitmapMask != 0) {
revert Errors.AlreadyEnabled();
}
enabledActionModulesBitmap |= actionModuleIdBitmapMask;
IModuleRegistry(MODULE_REGISTRY).registerModule(
params.actionModules[i],
uint256(IModuleRegistry.ModuleType.PUBLICATION_ACTION_MODULE)
);
actionModuleInitResults[i] = IPublicationActionModule(params.actionModules[i]).initializePublicationAction(
params.profileId,
@@ -530,10 +521,6 @@ library PublicationLib {
}
}
StorageLib
.getPublication(params.profileId, params.pubId)
.enabledActionModulesBitmap = enabledActionModulesBitmap;
return actionModuleInitResults;
}

View File

@@ -40,12 +40,8 @@ library StorageLib {
//////////////////////////////////
uint256 constant DELEGATED_EXECUTOR_CONFIG_MAPPING_SLOT = 26;
uint256 constant BLOCKED_STATUS_MAPPING_SLOT = 27;
uint256 constant ACTION_MODULES_SLOT = 28;
uint256 constant MAX_ACTION_MODULE_ID_USED_SLOT = 29;
uint256 constant PROFILE_ROYALTIES_BPS_SLOT = 30;
uint256 constant MIGRATION_ADMINS_WHITELISTED_MAPPING_SLOT = 31;
uint256 constant MAX_ACTION_MODULE_ID_SUPPORTED = 255;
uint256 constant PROFILE_ROYALTIES_BPS_SLOT = 28;
uint256 constant MIGRATION_ADMINS_WHITELISTED_MAPPING_SLOT = 29;
function getPublication(
uint256 profileId,
@@ -142,24 +138,6 @@ library StorageLib {
}
}
function actionModuleById() internal pure returns (mapping(uint256 => address) storage _actionModules) {
assembly {
_actionModules.slot := ACTION_MODULES_SLOT
}
}
function incrementMaxActionModuleIdUsed() internal returns (uint256) {
uint256 incrementedId;
assembly {
incrementedId := add(sload(MAX_ACTION_MODULE_ID_USED_SLOT), 1)
sstore(MAX_ACTION_MODULE_ID_USED_SLOT, incrementedId)
}
if (incrementedId > MAX_ACTION_MODULE_ID_SUPPORTED) {
revert Errors.MaxActionModuleIdReached();
}
return incrementedId;
}
function getGovernance() internal view returns (address _governance) {
assembly {
_governance := sload(GOVERNANCE_SLOT)

View File

@@ -48,18 +48,6 @@ library ValidationLib {
}
}
function validateReferenceModuleRegistered(address referenceModule) internal view {
if (!StorageLib.referenceModuleRegistered()[referenceModule]) {
revert Errors.NotRegistered();
}
}
function validateFollowModuleRegistered(address followModule) internal view {
if (!StorageLib.followModuleRegistered()[followModule]) {
revert Errors.NotRegistered();
}
}
function validateProfileCreatorWhitelisted(address profileCreator) internal view {
if (!StorageLib.profileCreatorWhitelisted()[profileCreator]) {
revert Errors.NotWhitelisted();

View File

@@ -32,9 +32,6 @@ library Errors {
error NonERC721ReceiverImplementer();
error AlreadyEnabled();
// Internal Errors
error MaxActionModuleIdReached(); // This means we need an upgrade
// Module Errors
error InitParamsInvalid();
error ActionNotAllowed();

View File

@@ -130,11 +130,7 @@ library Types {
* Posts, V1 publications and publications rooted in V1 publications don't have it set.
* @param rootPubId The publication ID of the root post (to determine if comments/quotes and mirrors come from it).
* Posts, V1 publications and publications rooted in V1 publications don't have it set.
* @param enabledActionModulesBitmap The action modules enabled in a given publication as a bitmap.
* The bitmap is a uint256 where each bit represents an action module: 1 if the publication uses it, 0 if not.
* You can use getActionModuleById() to get the address of the action module associated with a given bit.
* In the future this can be replaced with a getter that allows to query the bitmap by index, if there are more
* than 256 action modules.
* @param actionModuleEnabled The action modules enabled in a given publication.
*/
struct Publication {
uint256 pointedProfileId;
@@ -147,7 +143,21 @@ library Types {
PublicationType pubType;
uint256 rootProfileId;
uint256 rootPubId;
uint256 enabledActionModulesBitmap; // In future this can be (uint256 => uint256) mapping if we need >256 modules
mapping(address => bool) actionModuleEnabled;
}
struct PublicationMemory {
uint256 pointedProfileId;
uint256 pointedPubId;
string contentURI;
address referenceModule;
address __DEPRECATED__collectModule; // Deprecated in V2
address __DEPRECATED__collectNFT; // Deprecated in V2
// Added in Lens V2, so these will be zero for old publications:
PublicationType pubType;
uint256 rootProfileId;
uint256 rootPubId;
// bytes32 __ACTION_MODULE_ENABLED_MAPPING; // Mappings are not supported in memory.
}
/**

View File

@@ -2,67 +2,71 @@
pragma solidity ^0.8.15;
contract ModuleRegistry {
event ModuleRegistered(
address indexed moduleAddress,
ModuleType indexed moduleType,
address registrar,
import {IModuleRegistry} from 'contracts/interfaces/IModuleRegistry.sol';
import {IERC20Metadata} from '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol';
contract ModuleRegistry is IModuleRegistry {
event ModuleRegistered(address indexed moduleAddress, uint256 indexed moduleType, uint256 timestamp);
event erc20CurrencyRegistered(
address indexed erc20CurrencyAddress,
string name,
string symbol,
uint8 decimals,
uint256 timestamp
);
struct Module {
address registrar;
bool isPublicationActionModule;
bool isReferenceModule;
bool isFollowModule;
}
mapping(address moduleAddress => uint256 moduleTypesBitmap) internal registeredModules;
enum ModuleType {
NOT_REGISTERED,
PUBLICATION_ACTION_MODULE,
REFERENCE_MODULE,
FOLLOW_MODULE
}
mapping(address erc20CurrencyAddress => bool) internal registeredErc20Currencies;
mapping(address => Module) public modules;
// Modules
/// @dev This is frontrunnable, so...
function register(address moduleAddress, ModuleType moduleType) public {
if (moduleType == ModuleType.NOT_REGISTERED) {
revert('Module type cannot be NOT_REGISTERED');
function registerModule(address moduleAddress, uint256 moduleType) public returns (bool registrationWasPerformed) {
// This will fail if moduleType is out of range for `IModuleRegistry.ModuleType`
require(moduleType > 0 && moduleType < uint256(type(IModuleRegistry.ModuleType).max));
bool isAlreadyRegisteredAsThatType = registeredModules[moduleAddress] & (1 << moduleType) != 0;
if (isAlreadyRegisteredAsThatType) {
return false;
} else {
emit ModuleRegistered(moduleAddress, moduleType, block.timestamp);
registeredModules[moduleAddress] |= (1 << moduleType);
return true;
}
if (modules[moduleAddress].moduleType != ModuleType.NOT_REGISTERED) {
revert('Module already registered');
}
function getModuleTypes(address moduleAddress) public view returns (uint256) {
return registeredModules[moduleAddress];
}
function isModuleRegistered(address moduleAddress) external view returns (bool) {
return registeredModules[moduleAddress] != 0;
}
function isModuleRegisteredAs(address moduleAddress, uint256 moduleType) public view returns (bool) {
require(moduleType <= type(uint8).max);
return registeredModules[moduleAddress] & (1 << moduleType) != 0;
}
// Currencies
function registerErc20Currency(address currencyAddress) public returns (bool registrationWasPerformed) {
bool isAlreadyRegistered = registeredErc20Currencies[currencyAddress];
if (isAlreadyRegistered) {
return false;
} else {
uint8 decimals = IERC20Metadata(currencyAddress).decimals();
string memory name = IERC20Metadata(currencyAddress).name();
string memory symbol = IERC20Metadata(currencyAddress).symbol();
emit erc20CurrencyRegistered(currencyAddress, name, symbol, decimals, block.timestamp);
registeredErc20Currencies[currencyAddress] = true;
return true;
}
if (moduleAddress.code.length == 0) {
revert('Module address is not a contract');
}
modules[moduleAddress] = Module(msg.sender, moduleType);
emit ModuleRegistered(moduleAddress, moduleType, msg.sender, block.timestamp);
}
function getModuleType(address moduleAddress) public view returns (ModuleType) {
return modules[moduleAddress].moduleType;
}
function getModuleRegistrar(address moduleAddress) public view returns (address) {
return modules[moduleAddress].registrar;
}
function isRegistered(address moduleAddress) public view returns (bool) {
return modules[moduleAddress].moduleType != ModuleType.NOT_REGISTERED;
}
function areRegistered(address[] calldata moduleAddresses) public view returns (bool) {
uint256 i;
while (i < moduleAddresses.length) {
if (modules[moduleAddresses[i]].moduleType == ModuleType.NOT_REGISTERED) {
return false;
}
unchecked {
++i;
}
}
return true;
function isErc20CurrencyRegistered(address currencyAddress) external view returns (bool) {
return registeredErc20Currencies[currencyAddress];
}
}

View File

@@ -73,9 +73,14 @@ contract CollectPublicationAction is HubRestricted, IPublicationActionModule {
MODULE_GLOBALS = moduleGlobals;
}
function registerCollectModule(address collectModule) external {
_collectModuleRegistered[collectModule] = true;
function registerCollectModule(address collectModule) public returns (bool) {
bool isAlreadyRegistered = _collectModuleRegistered[collectModule];
if (isAlreadyRegistered) {
return false;
}
emit CollectModuleRegistered(collectModule, block.timestamp);
_collectModuleRegistered[collectModule] = true;
return true;
}
function initializePublicationAction(
@@ -85,9 +90,11 @@ contract CollectPublicationAction is HubRestricted, IPublicationActionModule {
bytes calldata data
) external override onlyHub returns (bytes memory) {
(address collectModule, bytes memory collectModuleInitData) = abi.decode(data, (address, bytes));
if (!_collectModuleRegistered[collectModule]) {
revert Errors.NotRegistered();
}
registerCollectModule(collectModule);
// TODO
// if (_collectDataByPub[profileId][pubId].collectModule != address(0)) {
// revert Errors.AlreadyInitialized();
// }
_collectDataByPub[profileId][pubId].collectModule = collectModule;
ICollectModule(collectModule).initializePublicationCollectModule(
profileId,

View File

@@ -148,81 +148,6 @@ contract ActTest is ReferralSystemTest {
testAct();
}
function testGetActionModuleById(address secondActionModule) public {
address firstActionModule = makeAddr('FIRST_ACTION_MODULE');
vm.assume(firstActionModule != secondActionModule);
Types.ActionModuleRegisterData memory whitelistData = hub.getActionModuleRegisterData(secondActionModule);
vm.assume(whitelistData.id == 0);
vm.assume(whitelistData.isRegistered == false);
hub.registerActionModule(firstActionModule);
whitelistData = hub.getActionModuleRegisterData(firstActionModule);
uint256 firstActionModuleId = whitelistData.id;
assertTrue(whitelistData.isRegistered);
hub.registerActionModule(secondActionModule);
whitelistData = hub.getActionModuleRegisterData(secondActionModule);
uint256 secondActionModuleId = whitelistData.id;
assertTrue(whitelistData.isRegistered);
assertEq(hub.getActionModuleById(firstActionModuleId), firstActionModule);
assertEq(hub.getActionModuleById(secondActionModuleId), secondActionModule);
}
// Will not work on fork with more complicated action modules because we don't know how to initialize them
function testGetEnabledActionModulesBitmap(uint8 enabledActionModulesBitmap) public {
vm.assume(enabledActionModulesBitmap != 0);
address[] memory actionModules = new address[](9);
for (uint256 i = 1; i < actionModules.length; i++) {
if (hub.getActionModuleById(i) == address(0)) {
actionModules[i] = makeAddr(string.concat('ACTION_MODULE_', vm.toString(i)));
vm.etch(actionModules[i], address(mockActionModule).code);
hub.registerActionModule(actionModules[i]);
} else {
actionModules[i] = hub.getActionModuleById(i);
}
}
// Count enabledActionModules from a bitmap
uint8 enabledActionModulesCount = 0;
for (uint256 i = 0; i < 8; i++) {
if (enabledActionModulesBitmap & (1 << i) != 0) {
enabledActionModulesCount++;
}
}
// Pick enabledActionModulesCount action modules
address[] memory enabledActionModules = new address[](enabledActionModulesCount);
uint256 enabledActionModulesIndex = 0;
for (uint256 i = 0; i < 8; i++) {
if (enabledActionModulesBitmap & (1 << i) != 0) {
enabledActionModules[enabledActionModulesIndex] = actionModules[i + 1];
enabledActionModulesIndex++;
}
}
bytes[] memory enabledActionModulesInitDatas = new bytes[](enabledActionModulesCount);
for (uint256 i = 0; i < enabledActionModulesCount; i++) {
enabledActionModulesInitDatas[i] = abi.encode(true);
}
Types.PostParams memory postParams = _getDefaultPostParams();
postParams.actionModules = enabledActionModules;
postParams.actionModulesInitDatas = enabledActionModulesInitDatas;
vm.prank(defaultAccount.owner);
uint256 pubId = hub.post(postParams);
assertEq(
hub.getPublication(defaultAccount.profileId, pubId).enabledActionModulesBitmap,
enabledActionModulesBitmap
);
}
}
contract ActMetaTxTest is ActTest, MetaTxNegatives {

View File

@@ -148,43 +148,4 @@ contract GovernanceFunctionsTest is BaseTest {
assertEq(hub.isReferenceModuleRegistered(referenceModule), true);
}
function testWhitelistActionModule_initially(address actionModule) public {
Types.ActionModuleRegisterData memory whitelistData = hub.getActionModuleRegisterData(actionModule);
vm.assume(whitelistData.id == 0);
vm.assume(whitelistData.isRegistered == false);
hub.registerActionModule(actionModule);
whitelistData = hub.getActionModuleRegisterData(actionModule);
assertTrue(whitelistData.isRegistered);
}
function testGetActionModuleRegisterData(address secondActionModule) public {
address firstActionModule = makeAddr('FIRST_ACTION_MODULE');
vm.assume(firstActionModule != secondActionModule);
Types.ActionModuleRegisterData memory registerData = hub.getActionModuleRegisterData(secondActionModule);
vm.assume(registerData.id == 0);
vm.assume(registerData.isRegistered == false);
registerData = hub.getActionModuleRegisterData(firstActionModule);
assertEq(registerData.id, 0);
assertFalse(registerData.isRegistered);
hub.registerActionModule(firstActionModule);
registerData = hub.getActionModuleRegisterData(firstActionModule);
uint256 firstActionModuleId = registerData.id;
assertTrue(registerData.isRegistered);
hub.registerActionModule(secondActionModule);
registerData = hub.getActionModuleRegisterData(secondActionModule);
uint256 secondActionModuleId = registerData.id;
assertTrue(registerData.isRegistered);
assertEq(secondActionModuleId, firstActionModuleId + 1);
}
}