mirror of
https://github.com/lens-protocol/core.git
synced 2026-01-09 14:18:04 -05:00
feat: ProtocolSharedRevenueMinFeeMintModule
This commit is contained in:
@@ -34,7 +34,7 @@ abstract contract FeeModuleBase {
|
||||
return HUB.getTreasuryData();
|
||||
}
|
||||
|
||||
function _validateDataIsExpected(bytes calldata data, address currency, uint256 amount) internal pure {
|
||||
function _validateDataIsExpected(bytes calldata data, address currency, uint256 amount) internal pure virtual {
|
||||
(address decodedCurrency, uint256 decodedAmount) = abi.decode(data, (address, uint256));
|
||||
if (decodedAmount != amount || decodedCurrency != currency) {
|
||||
revert Errors.ModuleDataMismatch();
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.10;
|
||||
|
||||
import {BaseFeeCollectModule} from 'contracts/modules/act/collect/base/BaseFeeCollectModule.sol';
|
||||
import {BaseFeeCollectModuleInitData, BaseProfilePublicationData} from 'contracts/modules/interfaces/IBaseFeeCollectModule.sol';
|
||||
import {ICollectModule} from 'contracts/modules/interfaces/ICollectModule.sol';
|
||||
import {LensModuleMetadata} from 'contracts/modules/LensModuleMetadata.sol';
|
||||
import {LensModule} from 'contracts/modules/LensModule.sol';
|
||||
import {ImmutableOwnable} from 'contracts/misc/ImmutableOwnable.sol';
|
||||
import {ModuleTypes} from 'contracts/modules/libraries/constants/ModuleTypes.sol';
|
||||
import {ILensHub} from 'contracts/interfaces/ILensHub.sol';
|
||||
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
import {Errors} from 'contracts/modules/constants/Errors.sol';
|
||||
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
|
||||
|
||||
struct ProtocolSharedRevenueMinFeeMintModuleInitData {
|
||||
uint160 amount;
|
||||
uint96 collectLimit;
|
||||
address currency;
|
||||
uint96 currentCollects;
|
||||
address recipient;
|
||||
uint16 referralFee;
|
||||
bool followerOnly;
|
||||
uint72 endTimestamp;
|
||||
address creatorFrontend;
|
||||
}
|
||||
|
||||
struct ProtocolSharedRevenueMinFeeMintModulePublicationData {
|
||||
uint160 amount;
|
||||
uint96 collectLimit;
|
||||
address currency;
|
||||
uint96 currentCollects;
|
||||
address recipient;
|
||||
uint16 referralFee;
|
||||
bool followerOnly;
|
||||
uint72 endTimestamp;
|
||||
address creatorFrontend;
|
||||
}
|
||||
|
||||
// Splits (in BPS)
|
||||
struct ProtocolSharedRevenueDistribution {
|
||||
uint16 creatorSplit;
|
||||
uint16 protocolSplit;
|
||||
uint16 creatorFrontendSplit;
|
||||
uint16 executorFrontendSplit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @title ProtocolSharedRevenueMinFeeMint
|
||||
* @author Lens Protocol
|
||||
*
|
||||
* @notice This is a simple Lens CollectModule implementation, allowing customization of time to collect,
|
||||
* number of collects and whether only followers can collect.
|
||||
*
|
||||
* You can build your own collect modules by inheriting from BaseFeeCollectModule and adding your
|
||||
* functionality along with getPublicationData function.
|
||||
*/
|
||||
contract ProtocolSharedRevenueMinFeeMintModule is BaseFeeCollectModule, LensModuleMetadata {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
address mintFeeToken;
|
||||
uint256 mintFeeAmount;
|
||||
ProtocolSharedRevenueDistribution protocolSharedRevenueDistribution;
|
||||
|
||||
mapping(uint256 profileId => mapping(uint256 pubId => address creatorFrontend))
|
||||
internal _creatorFrontendByPublicationByProfile;
|
||||
|
||||
constructor(
|
||||
address hub,
|
||||
address actionModule,
|
||||
address moduleRegistry,
|
||||
address moduleOwner
|
||||
) BaseFeeCollectModule(hub, actionModule, moduleRegistry) LensModuleMetadata(moduleOwner) {}
|
||||
|
||||
/**
|
||||
* @inheritdoc ICollectModule
|
||||
* @notice This collect module levies a fee on collects and supports referrals. Thus, we need to decode data.
|
||||
* @param data The arbitrary data parameter, decoded into BaseFeeCollectModuleInitData struct:
|
||||
* amount: The collecting cost associated with this publication. 0 for free collect.
|
||||
* collectLimit: The maximum number of collects for this publication. 0 for no limit.
|
||||
* currency: The currency associated with this publication.
|
||||
* referralFee: The referral fee associated with this publication.
|
||||
* followerOnly: True if only followers of publisher may collect the post.
|
||||
* endTimestamp: The end timestamp after which collecting is impossible. 0 for no expiry.
|
||||
* recipient: Recipient of collect fees.
|
||||
*
|
||||
* @return An abi encoded bytes parameter, which is the same as the passed data parameter.
|
||||
*/
|
||||
function initializePublicationCollectModule(
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
address /* transactionExecutor */,
|
||||
bytes calldata data
|
||||
) external override onlyActionModule returns (bytes memory) {
|
||||
ProtocolSharedRevenueMinFeeMintModuleInitData memory initData = abi.decode(
|
||||
data,
|
||||
(ProtocolSharedRevenueMinFeeMintModuleInitData)
|
||||
);
|
||||
|
||||
BaseFeeCollectModuleInitData memory baseInitData = BaseFeeCollectModuleInitData({
|
||||
amount: initData.amount,
|
||||
collectLimit: initData.collectLimit,
|
||||
currency: initData.currency,
|
||||
referralFee: initData.referralFee,
|
||||
followerOnly: initData.followerOnly,
|
||||
endTimestamp: initData.endTimestamp,
|
||||
recipient: initData.recipient
|
||||
});
|
||||
|
||||
if (initData.creatorFrontend != address(0)) {
|
||||
_creatorFrontendByPublicationByProfile[profileId][pubId] = initData.creatorFrontend;
|
||||
}
|
||||
|
||||
_validateBaseInitData(baseInitData);
|
||||
_storeBasePublicationCollectParameters(profileId, pubId, baseInitData);
|
||||
return '';
|
||||
}
|
||||
|
||||
function processCollect(
|
||||
ModuleTypes.ProcessCollectParams calldata processCollectParams
|
||||
) external override onlyActionModule returns (bytes memory) {
|
||||
if (
|
||||
_dataByPublicationByProfile[processCollectParams.publicationCollectedProfileId][
|
||||
processCollectParams.publicationCollectedId
|
||||
].amount == 0
|
||||
) {
|
||||
_handleMintFee(processCollectParams);
|
||||
}
|
||||
|
||||
// Regular processCollect:
|
||||
|
||||
_validateAndStoreCollect(processCollectParams);
|
||||
|
||||
if (processCollectParams.referrerProfileIds.length == 0) {
|
||||
_processCollect(processCollectParams);
|
||||
} else {
|
||||
_processCollectWithReferral(processCollectParams);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Internal functions
|
||||
|
||||
function _handleMintFee(ModuleTypes.ProcessCollectParams calldata processCollectParams) internal {
|
||||
if (mintFeeAmount == 0) {
|
||||
return;
|
||||
}
|
||||
address creator = ILensHub(HUB).ownerOf(processCollectParams.publicationCollectedProfileId);
|
||||
uint256 creatorAmount = (mintFeeAmount * protocolSharedRevenueDistribution.creatorSplit) / 10000;
|
||||
|
||||
address protocol = ILensHub(HUB).getTreasury();
|
||||
uint256 protocolAmount = (mintFeeAmount * protocolSharedRevenueDistribution.protocolSplit) / 10000;
|
||||
|
||||
address creatorFrontend = _creatorFrontendByPublicationByProfile[
|
||||
processCollectParams.publicationCollectedProfileId
|
||||
][processCollectParams.publicationCollectedId];
|
||||
uint256 creatorFrontendAmount = (mintFeeAmount * protocolSharedRevenueDistribution.creatorFrontendSplit) /
|
||||
10000;
|
||||
|
||||
if (creatorFrontend != address(0)) {
|
||||
IERC20(mintFeeToken).safeTransferFrom(
|
||||
processCollectParams.transactionExecutor,
|
||||
creatorFrontend,
|
||||
creatorFrontendAmount
|
||||
);
|
||||
} else {
|
||||
// If there's no creatorFrontend specified - we give that amount to the publication creator
|
||||
creatorAmount += creatorFrontendAmount;
|
||||
}
|
||||
|
||||
(, , address executorFrontend) = abi.decode(processCollectParams.data, (address, uint256, address));
|
||||
uint256 executorFrontendAmount = (mintFeeAmount * protocolSharedRevenueDistribution.executorFrontendSplit) /
|
||||
10000;
|
||||
|
||||
if (executorFrontend != address(0)) {
|
||||
IERC20(mintFeeToken).safeTransferFrom(
|
||||
processCollectParams.transactionExecutor,
|
||||
executorFrontend,
|
||||
executorFrontendAmount
|
||||
);
|
||||
} else {
|
||||
// If there's no creatorFrontend specified - we give that amount to the publication creator
|
||||
creatorAmount += executorFrontendAmount;
|
||||
}
|
||||
|
||||
IERC20(mintFeeToken).safeTransferFrom(processCollectParams.transactionExecutor, creator, creatorAmount);
|
||||
IERC20(mintFeeToken).safeTransferFrom(processCollectParams.transactionExecutor, protocol, protocolAmount);
|
||||
}
|
||||
|
||||
function _validateDataIsExpected(bytes calldata data, address currency, uint256 amount) internal pure override {
|
||||
(address decodedCurrency, uint256 decodedAmount, ) = abi.decode(data, (address, uint256, address));
|
||||
if (decodedAmount != amount || decodedCurrency != currency) {
|
||||
revert Errors.ModuleDataMismatch();
|
||||
}
|
||||
}
|
||||
|
||||
// OnlyOwner functions
|
||||
|
||||
function setMintFeeParams(address token, uint256 amount) external onlyOwner {
|
||||
if (amount > 0 && token == address(0)) {
|
||||
revert Errors.InvalidParams();
|
||||
}
|
||||
mintFeeToken = token;
|
||||
mintFeeAmount = amount;
|
||||
}
|
||||
|
||||
function setProtocolSharedRevenueDistribution(
|
||||
ProtocolSharedRevenueDistribution memory distribution
|
||||
) external onlyOwner {
|
||||
if (
|
||||
distribution.creatorSplit +
|
||||
distribution.protocolSplit +
|
||||
distribution.creatorFrontendSplit +
|
||||
distribution.executorFrontendSplit !=
|
||||
BPS_MAX
|
||||
) {
|
||||
revert Errors.InvalidParams();
|
||||
}
|
||||
protocolSharedRevenueDistribution = distribution;
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
function getMintFeeParams() external view returns (address, uint256) {
|
||||
return (mintFeeToken, mintFeeAmount);
|
||||
}
|
||||
|
||||
function getProtocolSharedRevenueDistribution() external view returns (ProtocolSharedRevenueDistribution memory) {
|
||||
return protocolSharedRevenueDistribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the publication data for a given publication, or an empty struct if that publication was not
|
||||
* initialized with this module.
|
||||
*
|
||||
* @param profileId The token ID of the profile mapped to the publication to query.
|
||||
* @param pubId The publication ID of the publication to query.
|
||||
*
|
||||
* @return The BaseProfilePublicationData struct mapped to that publication.
|
||||
*/
|
||||
function getPublicationData(
|
||||
uint256 profileId,
|
||||
uint256 pubId
|
||||
) external view returns (ProtocolSharedRevenueMinFeeMintModulePublicationData memory) {
|
||||
BaseProfilePublicationData memory baseData = getBasePublicationData(profileId, pubId);
|
||||
address creatorFrontend = _creatorFrontendByPublicationByProfile[profileId][pubId];
|
||||
return
|
||||
ProtocolSharedRevenueMinFeeMintModulePublicationData({
|
||||
amount: baseData.amount,
|
||||
collectLimit: baseData.collectLimit,
|
||||
currency: baseData.currency,
|
||||
currentCollects: baseData.currentCollects,
|
||||
recipient: baseData.recipient,
|
||||
referralFee: baseData.referralFee,
|
||||
followerOnly: baseData.followerOnly,
|
||||
endTimestamp: baseData.endTimestamp,
|
||||
creatorFrontend: creatorFrontend
|
||||
});
|
||||
}
|
||||
|
||||
function supportsInterface(
|
||||
bytes4 interfaceID
|
||||
) public pure override(BaseFeeCollectModule, LensModule) returns (bool) {
|
||||
return BaseFeeCollectModule.supportsInterface(interfaceID) || LensModule.supportsInterface(interfaceID);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user