feat: ProtocolSharedRevenueMinFeeMintModule

This commit is contained in:
vicnaum
2024-04-17 15:31:02 +02:00
parent 67dcc74638
commit 8ca03d5491
6 changed files with 969 additions and 9 deletions

View File

@@ -34,7 +34,7 @@ abstract contract FeeModuleBase {
return HUB.getTreasuryData(); 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)); (address decodedCurrency, uint256 decodedAmount) = abi.decode(data, (address, uint256));
if (decodedAmount != amount || decodedCurrency != currency) { if (decodedAmount != amount || decodedCurrency != currency) {
revert Errors.ModuleDataMismatch(); revert Errors.ModuleDataMismatch();

View File

@@ -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);
}
}

View File

@@ -245,7 +245,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(), referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(), referrerPubTypes: _emptyPubTypesArray(),
data: abi.encode(currency, passedAmount) data: _getCollectParamsData(address(currency), passedAmount)
}) })
); );
} }
@@ -291,7 +291,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(), referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(), referrerPubTypes: _emptyPubTypesArray(),
data: abi.encode(passedCurrency, amount) data: _getCollectParamsData(passedCurrency, exampleInitData.amount)
}) })
); );
} }
@@ -334,7 +334,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(), referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(), 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(), referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(), 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(), referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(), 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(), referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(), 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(), referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(), referrerPubTypes: _emptyPubTypesArray(),
data: abi.encode(currency, amount) data: _getCollectParamsData(address(currency), amount)
}) })
); );
} }
@@ -563,7 +563,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(), referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(), referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(), 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); 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 { contract BaseFeeCollectModule_FeeDistribution is BaseFeeCollectModuleBase {

View File

@@ -29,6 +29,7 @@ contract MultirecipientCollectModule_Initialization is
return MultirecipientCollectModuleBase.getEncodedInitData(); return MultirecipientCollectModuleBase.getEncodedInitData();
} }
// TODO: WTF?
function testCannotInitializeWithNonWhitelistedCurrency( function testCannotInitializeWithNonWhitelistedCurrency(
uint256 profileId, uint256 profileId,
uint256 pubId, uint256 pubId,

View File

@@ -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);
}
}

View File

@@ -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'
);
}
}