diff --git a/contracts/modules/FeeModuleBase.sol b/contracts/modules/FeeModuleBase.sol index 0c71250..ad17e43 100644 --- a/contracts/modules/FeeModuleBase.sol +++ b/contracts/modules/FeeModuleBase.sol @@ -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(); diff --git a/contracts/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.sol b/contracts/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.sol new file mode 100644 index 0000000..dac6a14 --- /dev/null +++ b/contracts/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.sol @@ -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); + } +} diff --git a/test/modules/act/collect/BaseFeeCollectModule.t.sol b/test/modules/act/collect/BaseFeeCollectModule.t.sol index 9f639a2..8e1a783 100644 --- a/test/modules/act/collect/BaseFeeCollectModule.t.sol +++ b/test/modules/act/collect/BaseFeeCollectModule.t.sol @@ -245,7 +245,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase { referrerProfileIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(), referrerPubTypes: _emptyPubTypesArray(), - data: abi.encode(currency, passedAmount) + data: _getCollectParamsData(address(currency), passedAmount) }) ); } @@ -291,7 +291,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase { referrerProfileIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(), referrerPubTypes: _emptyPubTypesArray(), - data: abi.encode(passedCurrency, amount) + data: _getCollectParamsData(passedCurrency, exampleInitData.amount) }) ); } @@ -334,7 +334,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase { referrerProfileIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(), referrerPubTypes: _emptyPubTypesArray(), - data: abi.encode(currency, exampleInitData.amount) + data: _getCollectParamsData(address(currency), exampleInitData.amount) }) ); } @@ -378,7 +378,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase { referrerProfileIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(), referrerPubTypes: _emptyPubTypesArray(), - data: abi.encode(currency, exampleInitData.amount) + data: _getCollectParamsData(address(currency), exampleInitData.amount) }) ); } @@ -422,7 +422,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase { referrerProfileIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(), referrerPubTypes: _emptyPubTypesArray(), - data: abi.encode(currency, exampleInitData.amount) + data: _getCollectParamsData(address(currency), exampleInitData.amount) }) ); } @@ -439,7 +439,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase { referrerProfileIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(), referrerPubTypes: _emptyPubTypesArray(), - data: abi.encode(currency, exampleInitData.amount) + data: _getCollectParamsData(address(currency), exampleInitData.amount) }) ); } @@ -517,7 +517,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase { referrerProfileIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(), referrerPubTypes: _emptyPubTypesArray(), - data: abi.encode(currency, amount) + data: _getCollectParamsData(address(currency), amount) }) ); } @@ -563,7 +563,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase { referrerProfileIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(), referrerPubTypes: _emptyPubTypesArray(), - data: abi.encode(currency, exampleInitData.amount) + data: _getCollectParamsData(address(currency), exampleInitData.amount) }) ); @@ -571,6 +571,10 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase { assertEq(fetchedData.currentCollects, collects); } } + + function _getCollectParamsData(address currency, uint160 amount) internal virtual returns (bytes memory) { + return abi.encode(currency, amount); + } } contract BaseFeeCollectModule_FeeDistribution is BaseFeeCollectModuleBase { diff --git a/test/modules/act/collect/MultirecipientCollectModule.t.sol b/test/modules/act/collect/MultirecipientCollectModule.t.sol index 6259852..fc9c9f6 100644 --- a/test/modules/act/collect/MultirecipientCollectModule.t.sol +++ b/test/modules/act/collect/MultirecipientCollectModule.t.sol @@ -29,6 +29,7 @@ contract MultirecipientCollectModule_Initialization is return MultirecipientCollectModuleBase.getEncodedInitData(); } + // TODO: WTF? function testCannotInitializeWithNonWhitelistedCurrency( uint256 profileId, uint256 pubId, diff --git a/test/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.base.t.sol b/test/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.base.t.sol new file mode 100644 index 0000000..12afa47 --- /dev/null +++ b/test/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.base.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.10; + +import 'forge-std/Test.sol'; +import {ProtocolSharedRevenueDistribution, ProtocolSharedRevenueMinFeeMintModule, ProtocolSharedRevenueMinFeeMintModuleInitData} from 'contracts/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.sol'; +import {BaseFeeCollectModuleBase} from 'test/modules/act/collect/BaseFeeCollectModule.base.t.sol'; +import {MockCurrency} from 'test/mocks/MockCurrency.sol'; + +contract ProtocolSharedRevenueMinFeeMintModuleBase is BaseFeeCollectModuleBase { + function testProtocolSharedRevenueMinFeeMintModuleBase() public { + // Prevents being counted in Foundry Coverage + } + + using stdJson for string; + + uint16 constant BPS_MAX = 10000; + + address creatorFrontendAddress = makeAddr('CREATOR_FRONTEND'); + address executorFrontendAddress = makeAddr('EXECUTOR_FRONTEND'); + + MockCurrency bonsai; + uint256 mintFee = 10 ether; + ProtocolSharedRevenueMinFeeMintModule mintFeeModule; + ProtocolSharedRevenueMinFeeMintModuleInitData mintFeeModuleExampleInitData; + + function setUp() public virtual override { + super.setUp(); + + // Deploy & Whitelist ProtocolSharedRevenueMinFeeMintModule + if (fork && keyExists(json, string(abi.encodePacked('.', forkEnv, '.ProtocolSharedRevenueMinFeeMintModule')))) { + mintFeeModule = ProtocolSharedRevenueMinFeeMintModule( + json.readAddress(string(abi.encodePacked('.', forkEnv, '.ProtocolSharedRevenueMinFeeMintModule'))) + ); + console.log('Testing against already deployed module at:', address(mintFeeModule)); + } else { + vm.prank(deployer); + mintFeeModule = new ProtocolSharedRevenueMinFeeMintModule( + address(hub), + collectPublicationAction, + address(moduleRegistry), + address(this) + ); + } + + baseFeeCollectModule = address(mintFeeModule); + if (address(currency) == address(0)) { + currency = new MockCurrency(); + } + + bonsai = new MockCurrency(); + + vm.startPrank(mintFeeModule.owner()); + mintFeeModule.setMintFeeParams(address(bonsai), mintFee); + mintFeeModule.setProtocolSharedRevenueDistribution( + ProtocolSharedRevenueDistribution({ + creatorSplit: 5000, + protocolSplit: 2000, + creatorFrontendSplit: 1500, + executorFrontendSplit: 1500 + }) + ); + vm.stopPrank(); + } + + function getEncodedInitData() internal virtual override returns (bytes memory) { + mintFeeModuleExampleInitData.amount = exampleInitData.amount; + mintFeeModuleExampleInitData.collectLimit = exampleInitData.collectLimit; + mintFeeModuleExampleInitData.currency = exampleInitData.currency; + mintFeeModuleExampleInitData.referralFee = exampleInitData.referralFee; + mintFeeModuleExampleInitData.followerOnly = exampleInitData.followerOnly; + mintFeeModuleExampleInitData.endTimestamp = exampleInitData.endTimestamp; + mintFeeModuleExampleInitData.recipient = exampleInitData.recipient; + mintFeeModuleExampleInitData.creatorFrontend = creatorFrontendAddress; + + return abi.encode(mintFeeModuleExampleInitData); + } +} diff --git a/test/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.t.sol b/test/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.t.sol new file mode 100644 index 0000000..2578d9e --- /dev/null +++ b/test/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.t.sol @@ -0,0 +1,612 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.10; + +import 'forge-std/Test.sol'; +import {ProtocolSharedRevenueMinFeeMintModuleBase} from 'test/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.base.t.sol'; +import {IBaseFeeCollectModule} from 'contracts/modules/interfaces/IBaseFeeCollectModule.sol'; +import {BaseFeeCollectModule_Initialization, BaseFeeCollectModule_ProcessCollect, BaseFeeCollectModule_FeeDistribution} from 'test/modules/act/collect/BaseFeeCollectModule.t.sol'; +import {BaseFeeCollectModuleBase} from 'test/modules/act/collect/BaseFeeCollectModule.base.t.sol'; +import {ProtocolSharedRevenueDistribution, ProtocolSharedRevenueMinFeeMintModule, ProtocolSharedRevenueMinFeeMintModulePublicationData} from 'contracts/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.sol'; +import {Errors as ModuleErrors} from 'contracts/modules/constants/Errors.sol'; +import {MockCurrency} from 'test/mocks/MockCurrency.sol'; +import {ModuleTypes} from 'contracts/modules/libraries/constants/ModuleTypes.sol'; + +///////// +// Publication Creation with ProtocolSharedRevenueMinFeeMintModule +// +contract ProtocolSharedRevenueMinFeeMintModule_Initialization is + ProtocolSharedRevenueMinFeeMintModuleBase, + BaseFeeCollectModule_Initialization +{ + function setUp() public override(ProtocolSharedRevenueMinFeeMintModuleBase, BaseFeeCollectModuleBase) { + ProtocolSharedRevenueMinFeeMintModuleBase.setUp(); + } + + function getEncodedInitData() + internal + override(ProtocolSharedRevenueMinFeeMintModuleBase, BaseFeeCollectModuleBase) + returns (bytes memory) + { + return ProtocolSharedRevenueMinFeeMintModuleBase.getEncodedInitData(); + } + + // Negatives + + // TODO: WTF? + function testCannotInitializeWithNonWhitelistedCurrency( + uint256 profileId, + uint256 pubId, + address transactionExecutor + ) public { + vm.assume(profileId != 0); + vm.assume(pubId != 0); + vm.assume(transactionExecutor != address(0)); + + exampleInitData.amount = 0; + + vm.expectRevert(ModuleErrors.InitParamsInvalid.selector); + vm.prank(collectPublicationAction); + IBaseFeeCollectModule(baseFeeCollectModule).initializePublicationCollectModule( + profileId, + pubId, + transactionExecutor, + getEncodedInitData() + ); + } + + // Scenarios + + function testInitializeWithCorrectInitData( + uint256 profileId, + uint256 pubId, + address transactionExecutor, + uint160 amount, + uint96 collectLimit, + address whitelistedCurrency, + uint16 referralFee, + bool followerOnly, + uint72 currentTimestamp, + uint72 endTimestamp, + address recipient + ) public override {} + + function testInitializeWithCorrectInitData( + uint256 profileId, + uint256 pubId, + address transactionExecutor, + uint160 amount, + uint96 collectLimit, + address whitelistedCurrency, + uint16 referralFee, + bool followerOnly, + uint72 currentTimestamp, + uint72 endTimestamp, + address recipient, + address creatorFrontend + ) public { + vm.assume(profileId != 0); + vm.assume(pubId != 0); + vm.assume(amount != 0); + vm.assume(transactionExecutor != address(0)); + vm.assume(whitelistedCurrency != address(0)); + + if (endTimestamp > 0) { + currentTimestamp = uint72(bound(uint256(currentTimestamp), 0, uint256(endTimestamp) - 1)); + } + vm.warp(currentTimestamp); + + mintFeeModuleExampleInitData.amount = amount; + mintFeeModuleExampleInitData.collectLimit = collectLimit; + mintFeeModuleExampleInitData.currency = whitelistedCurrency; + mintFeeModuleExampleInitData.referralFee = uint16(bound(uint256(referralFee), 0, BPS_MAX)); + mintFeeModuleExampleInitData.followerOnly = followerOnly; + mintFeeModuleExampleInitData.endTimestamp = endTimestamp; + mintFeeModuleExampleInitData.recipient = recipient; + mintFeeModuleExampleInitData.creatorFrontend = creatorFrontend; + + vm.prank(collectPublicationAction); + IBaseFeeCollectModule(baseFeeCollectModule).initializePublicationCollectModule( + profileId, + pubId, + transactionExecutor, + getEncodedInitData() + ); + + ProtocolSharedRevenueMinFeeMintModulePublicationData memory fetchedData = ProtocolSharedRevenueMinFeeMintModule( + baseFeeCollectModule + ).getPublicationData(profileId, pubId); + assertEq(fetchedData.currency, mintFeeModuleExampleInitData.currency, 'MockCurrency initialization mismatch'); + assertEq(fetchedData.amount, mintFeeModuleExampleInitData.amount, 'Amount initialization mismatch'); + assertEq( + fetchedData.referralFee, + mintFeeModuleExampleInitData.referralFee, + 'Referral fee initialization mismatch' + ); + assertEq( + fetchedData.followerOnly, + mintFeeModuleExampleInitData.followerOnly, + 'Follower only initialization mismatch' + ); + assertEq( + fetchedData.endTimestamp, + mintFeeModuleExampleInitData.endTimestamp, + 'End timestamp initialization mismatch' + ); + assertEq( + fetchedData.collectLimit, + mintFeeModuleExampleInitData.collectLimit, + 'Collect limit initialization mismatch' + ); + assertEq( + fetchedData.creatorFrontend, + mintFeeModuleExampleInitData.creatorFrontend, + 'CreatorFrontend initialization mismatch' + ); + } +} + +////////////// +// Collect with ProtocolSharedRevenueMinFeeMintModule +// +contract ProtocolSharedRevenueMinFeeMintModule_ProcessCollect is + ProtocolSharedRevenueMinFeeMintModuleBase, + BaseFeeCollectModule_ProcessCollect +{ + function testProtocolSharedRevenueMinFeeMintModule_Collect() public { + // Prevents being counted in Foundry Coverage + } + + address exampleExecutorFrontend = executorFrontendAddress; + + function setUp() public override(ProtocolSharedRevenueMinFeeMintModuleBase, BaseFeeCollectModuleBase) { + ProtocolSharedRevenueMinFeeMintModuleBase.setUp(); + } + + function getEncodedInitData() + internal + override(ProtocolSharedRevenueMinFeeMintModuleBase, BaseFeeCollectModuleBase) + returns (bytes memory) + { + return ProtocolSharedRevenueMinFeeMintModuleBase.getEncodedInitData(); + } + + function _getCollectParamsData(address currency, uint160 amount) internal override returns (bytes memory) { + return abi.encode(currency, amount, exampleExecutorFrontend); + } + + // Scenarios + + function testCanCollectIfAllConditionsAreMet( + uint256 pubId, + address transactionExecutor, + uint160 amount, + uint96 collectLimit, + uint16 referralFee, + bool followerOnly, + uint72 currentTimestamp, + uint72 endTimestamp, + address recipient, + address collectorProfileOwner + ) public override {} + + function testCanCollectIfAllConditionsAreMet( + uint256 pubId, + address transactionExecutor, + uint160 amount, + uint96 collectLimit, + uint16 referralFee, + bool followerOnly, + uint72 currentTimestamp, + uint72 endTimestamp, + address recipient + ) public { + address collectorProfileOwner = makeAddr('COLLECTOR_PROFILE_OWNER'); + address executorFrontend = makeAddr('EXECUTOR_FRONTEND'); + exampleExecutorFrontend = executorFrontend; + + bonsai.mint(collectorProfileOwner, 10 ether); + + vm.prank(collectorProfileOwner); + bonsai.approve(baseFeeCollectModule, 10 ether); + + super.testCanCollectIfAllConditionsAreMet( + pubId, + transactionExecutor, + amount, + collectLimit, + referralFee, + followerOnly, + currentTimestamp, + endTimestamp, + recipient, + collectorProfileOwner + ); + } + + struct Balances { + uint256 creator; + uint256 protocol; + uint256 creatorFrontend; + uint256 executorFrontend; + uint256 collector; + } + + Balances balancesBefore; + Balances balancesAfter; + Balances balancesChange; + + function testMintFeeDistribution_FreePost( + uint256 pubId, + address transactionExecutor, + uint96 collectLimit, + uint16 referralFee, + bool followerOnly, + uint72 currentTimestamp, + uint72 endTimestamp, + address recipient + ) public { + address collectorProfileOwner = makeAddr('COLLECTOR_PROFILE_OWNER'); + address executorFrontend = makeAddr('EXECUTOR_FRONTEND'); + + bonsai.mint(collectorProfileOwner, 10 ether); + + vm.prank(collectorProfileOwner); + bonsai.approve(baseFeeCollectModule, 10 ether); + + balancesBefore = Balances({ + creator: bonsai.balanceOf(defaultAccount.owner), + protocol: bonsai.balanceOf(hub.getTreasury()), + creatorFrontend: bonsai.balanceOf(creatorFrontendAddress), + executorFrontend: bonsai.balanceOf(executorFrontend), + collector: bonsai.balanceOf(collectorProfileOwner) + }); + + exampleExecutorFrontend = executorFrontend; + super.testCanCollectIfAllConditionsAreMet( + pubId, + transactionExecutor, + 0, + collectLimit, + referralFee, + followerOnly, + currentTimestamp, + endTimestamp, + recipient, + collectorProfileOwner + ); + + balancesAfter = Balances({ + creator: bonsai.balanceOf(defaultAccount.owner), + protocol: bonsai.balanceOf(hub.getTreasury()), + creatorFrontend: bonsai.balanceOf(creatorFrontendAddress), + executorFrontend: bonsai.balanceOf(executorFrontend), + collector: bonsai.balanceOf(collectorProfileOwner) + }); + + balancesChange = Balances({ + creator: balancesAfter.creator - balancesBefore.creator, + protocol: balancesAfter.protocol - balancesBefore.protocol, + creatorFrontend: balancesAfter.creatorFrontend - balancesBefore.creatorFrontend, + executorFrontend: balancesAfter.executorFrontend - balancesBefore.executorFrontend, + collector: balancesBefore.collector - balancesAfter.collector + }); + + uint256 expectedCreatorFee = (mintFee * mintFeeModule.getProtocolSharedRevenueDistribution().creatorSplit) / + BPS_MAX; + uint256 expectedProtocolFee = (mintFee * mintFeeModule.getProtocolSharedRevenueDistribution().protocolSplit) / + BPS_MAX; + uint256 expectedCreatorFrontendFee = (mintFee * + mintFeeModule.getProtocolSharedRevenueDistribution().creatorFrontendSplit) / BPS_MAX; + uint256 expectedExecutorFrontendFee = (mintFee * + mintFeeModule.getProtocolSharedRevenueDistribution().executorFrontendSplit) / BPS_MAX; + + assertEq(balancesChange.creator, expectedCreatorFee, 'Creator balance change wrong'); + assertEq(balancesChange.protocol, expectedProtocolFee, 'Protocol balance change wrong'); + assertEq(balancesChange.creatorFrontend, expectedCreatorFrontendFee, 'CreatorFrontend balance change wrong'); + assertEq(balancesChange.executorFrontend, expectedExecutorFrontendFee, 'ExecutorFrontend balance change wrong'); + assertEq(balancesChange.collector, mintFee, 'Collector balance change wrong'); + } + + function testMintFeeDistribution_FreePost_WithoutFrontends( + uint256 pubId, + address transactionExecutor, + uint96 collectLimit, + uint16 referralFee, + bool followerOnly, + uint72 currentTimestamp, + uint72 endTimestamp, + address recipient + ) public { + address collectorProfileOwner = makeAddr('COLLECTOR_PROFILE_OWNER'); + + address executorFrontend = address(0); + creatorFrontendAddress = address(0); + + bonsai.mint(collectorProfileOwner, 10 ether); + + vm.prank(collectorProfileOwner); + bonsai.approve(baseFeeCollectModule, 10 ether); + + balancesBefore = Balances({ + creator: bonsai.balanceOf(defaultAccount.owner), + protocol: bonsai.balanceOf(hub.getTreasury()), + creatorFrontend: bonsai.balanceOf(creatorFrontendAddress), + executorFrontend: bonsai.balanceOf(executorFrontend), + collector: bonsai.balanceOf(collectorProfileOwner) + }); + + console.log('creatorFrontend balance before: %s', balancesBefore.creatorFrontend); + + exampleExecutorFrontend = executorFrontend; + super.testCanCollectIfAllConditionsAreMet( + pubId, + transactionExecutor, + 0, + collectLimit, + referralFee, + followerOnly, + currentTimestamp, + endTimestamp, + recipient, + collectorProfileOwner + ); + + balancesAfter = Balances({ + creator: bonsai.balanceOf(defaultAccount.owner), + protocol: bonsai.balanceOf(hub.getTreasury()), + creatorFrontend: bonsai.balanceOf(creatorFrontendAddress), + executorFrontend: bonsai.balanceOf(executorFrontend), + collector: bonsai.balanceOf(collectorProfileOwner) + }); + + balancesChange = Balances({ + creator: balancesAfter.creator - balancesBefore.creator, + protocol: balancesAfter.protocol - balancesBefore.protocol, + creatorFrontend: balancesAfter.creatorFrontend - balancesBefore.creatorFrontend, + executorFrontend: balancesAfter.executorFrontend - balancesBefore.executorFrontend, + collector: balancesBefore.collector - balancesAfter.collector + }); + + uint256 expectedCreatorFee = (mintFee * mintFeeModule.getProtocolSharedRevenueDistribution().creatorSplit) / + BPS_MAX; + uint256 expectedProtocolFee = (mintFee * mintFeeModule.getProtocolSharedRevenueDistribution().protocolSplit) / + BPS_MAX; + uint256 expectedCreatorFrontendFee = (mintFee * + mintFeeModule.getProtocolSharedRevenueDistribution().creatorFrontendSplit) / BPS_MAX; + uint256 expectedExecutorFrontendFee = (mintFee * + mintFeeModule.getProtocolSharedRevenueDistribution().executorFrontendSplit) / BPS_MAX; + + assertEq( + balancesChange.creator, + expectedCreatorFee + expectedCreatorFrontendFee + expectedExecutorFrontendFee, + 'Creator balance change wrong' + ); + assertEq(balancesChange.protocol, expectedProtocolFee, 'Protocol balance change wrong'); + assertEq(balancesChange.creatorFrontend, 0, 'CreatorFrontend balance change wrong'); + assertEq(balancesChange.executorFrontend, 0, 'ExecutorFrontend balance change wrong'); + assertEq(balancesChange.collector, mintFee, 'Collector balance change wrong'); + } + + function testMintFeeDistribution_PaidPost( + uint256 pubId, + address transactionExecutor, + uint160 amount, + uint96 collectLimit, + uint16 referralFee, + bool followerOnly, + uint72 currentTimestamp, + uint72 endTimestamp, + address recipient + ) public { + vm.assume(amount > 0); + address collectorProfileOwner = makeAddr('COLLECTOR_PROFILE_OWNER'); + address executorFrontend = makeAddr('EXECUTOR_FRONTEND'); + + balancesBefore = Balances({ + creator: bonsai.balanceOf(defaultAccount.owner), + protocol: bonsai.balanceOf(hub.getTreasury()), + creatorFrontend: bonsai.balanceOf(creatorFrontendAddress), + executorFrontend: bonsai.balanceOf(executorFrontend), + collector: bonsai.balanceOf(collectorProfileOwner) + }); + + exampleExecutorFrontend = executorFrontend; + super.testCanCollectIfAllConditionsAreMet( + pubId, + transactionExecutor, + amount, + collectLimit, + referralFee, + followerOnly, + currentTimestamp, + endTimestamp, + recipient, + collectorProfileOwner + ); + + balancesAfter = Balances({ + creator: bonsai.balanceOf(defaultAccount.owner), + protocol: bonsai.balanceOf(hub.getTreasury()), + creatorFrontend: bonsai.balanceOf(creatorFrontendAddress), + executorFrontend: bonsai.balanceOf(executorFrontend), + collector: bonsai.balanceOf(collectorProfileOwner) + }); + + balancesChange = Balances({ + creator: balancesAfter.creator - balancesBefore.creator, + protocol: balancesAfter.protocol - balancesBefore.protocol, + creatorFrontend: balancesAfter.creatorFrontend - balancesBefore.creatorFrontend, + executorFrontend: balancesAfter.executorFrontend - balancesBefore.executorFrontend, + collector: balancesBefore.collector - balancesAfter.collector + }); + + assertEq(balancesChange.creator, 0, 'Creator balance change wrong'); + assertEq(balancesChange.protocol, 0, 'Protocol balance change wrong'); + assertEq(balancesChange.creatorFrontend, 0, 'CreatorFrontend balance change wrong'); + assertEq(balancesChange.executorFrontend, 0, 'ExecutorFrontend balance change wrong'); + assertEq(balancesChange.collector, 0, 'Collector balance change wrong'); + } +} + +////////////// +// Fee Distribution of ProtocolSharedRevenueMinFeeMintModule +// +contract ProtocolSharedRevenueMinFeeMintModule_FeeDistribution is ProtocolSharedRevenueMinFeeMintModuleBase { + function setUp() public override(ProtocolSharedRevenueMinFeeMintModuleBase) { + ProtocolSharedRevenueMinFeeMintModuleBase.setUp(); + } + + function getEncodedInitData() internal override(ProtocolSharedRevenueMinFeeMintModuleBase) returns (bytes memory) { + return ProtocolSharedRevenueMinFeeMintModuleBase.getEncodedInitData(); + } +} + +////////////// +// Fee Distribution of ProtocolSharedRevenueMinFeeMintModule +// +contract ProtocolSharedRevenueMinFeeMintModule_OwnerMethods is ProtocolSharedRevenueMinFeeMintModuleBase { + function setUp() public override(ProtocolSharedRevenueMinFeeMintModuleBase) { + ProtocolSharedRevenueMinFeeMintModuleBase.setUp(); + } + + // Negatives + + function testCannotSetMintFeeParams_ifNotOwner(address currency, uint256 mintFee, address notOwner) public { + if (mintFee == 0) currency = address(0); + if (currency == address(0)) mintFee = 0; + + vm.assume(notOwner != mintFeeModule.owner()); + + vm.expectRevert('Ownable: caller is not the owner'); + + vm.prank(notOwner); + mintFeeModule.setMintFeeParams(currency, mintFee); + } + + function testCannotSetProtocolSharedRevenueDistribution_ifNotOwner( + uint16 creatorSplit, + uint16 protocolSplit, + uint16 creatorFrontendSplit, + address notOwner + ) public { + vm.assume(notOwner != mintFeeModule.owner()); + creatorSplit = uint16(bound(uint256(creatorSplit), 0, BPS_MAX)); + protocolSplit = uint16(bound(uint256(protocolSplit), 0, BPS_MAX - creatorSplit)); + creatorFrontendSplit = uint16(bound(uint256(creatorFrontendSplit), 0, BPS_MAX - creatorSplit - protocolSplit)); + uint16 executorFrontendSplit = BPS_MAX - creatorSplit - protocolSplit - creatorFrontendSplit; + + vm.expectRevert('Ownable: caller is not the owner'); + + vm.prank(notOwner); + mintFeeModule.setProtocolSharedRevenueDistribution( + ProtocolSharedRevenueDistribution({ + creatorSplit: creatorSplit, + protocolSplit: protocolSplit, + creatorFrontendSplit: creatorFrontendSplit, + executorFrontendSplit: executorFrontendSplit + }) + ); + } + + function testCannotSetMintFeeParams_ifCurrencyZero_and_amountNotZero(uint256 mintFee) public { + vm.assume(mintFee > 0); + + vm.prank(mintFeeModule.owner()); + vm.expectRevert(ModuleErrors.InvalidParams.selector); + mintFeeModule.setMintFeeParams(address(0), mintFee); + } + + function testCannotSetProtocolSharedRevenueDistribution_ifSplitsDontAddUpToBPS_MAX( + uint16 creatorSplit, + uint16 protocolSplit, + uint16 creatorFrontendSplit, + uint16 executorFrontendSplit + ) public { + vm.assume( + uint256(creatorSplit) + + uint256(protocolSplit) + + uint256(creatorFrontendSplit) + + uint256(executorFrontendSplit) != + BPS_MAX + ); + + vm.startPrank(mintFeeModule.owner()); + if ( + uint256(creatorSplit) + + uint256(protocolSplit) + + uint256(creatorFrontendSplit) + + uint256(executorFrontendSplit) > + type(uint16).max + ) { + vm.expectRevert(stdError.arithmeticError); + } else { + vm.expectRevert(ModuleErrors.InvalidParams.selector); + } + + mintFeeModule.setProtocolSharedRevenueDistribution( + ProtocolSharedRevenueDistribution({ + creatorSplit: creatorSplit, + protocolSplit: protocolSplit, + creatorFrontendSplit: creatorFrontendSplit, + executorFrontendSplit: executorFrontendSplit + }) + ); + vm.stopPrank(); + } + + // Scenarios + + function testSetMintFeeParams(uint256 mintFee, address currency) public { + if (mintFee > 0) { + vm.assume(currency != address(0)); + } else { + currency = address(0); + } + + vm.prank(mintFeeModule.owner()); + mintFeeModule.setMintFeeParams(currency, mintFee); + + (address actualCurrency, uint256 actualMintFee) = mintFeeModule.getMintFeeParams(); + + assertEq(actualCurrency, currency, 'Currency mismatch'); + assertEq(actualMintFee, mintFee, 'Mint fee mismatch'); + } + + function testSetProtocolSharedRevenueDistribution( + uint16 creatorSplit, + uint16 protocolSplit, + uint16 creatorFrontendSplit, + uint16 executorFrontendSplit + ) public { + creatorSplit = uint16(bound(uint256(creatorSplit), 0, BPS_MAX)); + protocolSplit = uint16(bound(uint256(protocolSplit), 0, BPS_MAX - creatorSplit)); + creatorFrontendSplit = uint16(bound(uint256(creatorFrontendSplit), 0, BPS_MAX - creatorSplit - protocolSplit)); + uint16 executorFrontendSplit = BPS_MAX - creatorSplit - protocolSplit - creatorFrontendSplit; + + ProtocolSharedRevenueDistribution memory expectedDistribution = ProtocolSharedRevenueDistribution({ + creatorSplit: creatorSplit, + protocolSplit: protocolSplit, + creatorFrontendSplit: creatorFrontendSplit, + executorFrontendSplit: executorFrontendSplit + }); + + vm.prank(mintFeeModule.owner()); + mintFeeModule.setProtocolSharedRevenueDistribution(expectedDistribution); + + ProtocolSharedRevenueDistribution memory actualDistribution = mintFeeModule + .getProtocolSharedRevenueDistribution(); + + assertEq(actualDistribution.creatorSplit, expectedDistribution.creatorSplit, 'Creator split mismatch'); + assertEq(actualDistribution.protocolSplit, expectedDistribution.protocolSplit, 'Protocol split mismatch'); + assertEq( + actualDistribution.creatorFrontendSplit, + expectedDistribution.creatorFrontendSplit, + 'CreatorFrontend split mismatch' + ); + assertEq( + actualDistribution.executorFrontendSplit, + expectedDistribution.executorFrontendSplit, + 'ExecutorFrontend split mismatch' + ); + } +}