Merge pull request #167 from lens-protocol/feat/protocolSharedRevenueMinFeeMintModule

Feat/protocol shared revenue min fee mint module
This commit is contained in:
Alan
2024-05-30 12:18:44 -03:00
committed by GitHub
20 changed files with 4692 additions and 10 deletions

View File

@@ -171,6 +171,10 @@
{
"symbol": "USDC",
"addy": "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582"
},
{
"symbol": "BONSAI",
"addy": "0x9f98f838e3db42830ef488a92c8ed9671268000e"
}
],
"Modules": {
@@ -204,6 +208,10 @@
{
"name": "MultirecipientFeeCollectModule",
"addy": "0xC13ACcCe5cDb32bED1Af0B11cdb637E3966BCB45"
},
{
"name": "ProtocolSharedRevenueMinFeeMintModule",
"addy": "0x56174f7ff96C82004D3cEC42e9A99b5A44e76C77"
}
],
"reference": [
@@ -232,7 +240,7 @@
"CollectNFT": "0xC7B6faDeCE0345E60ffa46BD3100094815aeB428",
"LitAccessControlImpl": "0xae17Edd1CbCE05394575192006893Af9cB1AFc05",
"LitAccessControl": "0x9Ddad77aD520d02D2566563b446935C6edD970fC",
"PublicActProxy": "0x88c8fa7C470d9d94aDfA40187157917B26A548d3",
"PublicActProxy": "0x01B282B6e55F8B20c17680aAE87FBDf9f0364F72",
"GovernanceContract": "0x17e37DC312934108DaC5905897559C4c4A81b994",
"GovernanceContractAdmin": "0x532BbA5445e306cB83cF26Ef89842d4701330A45",
"ProxyAdminContract": "0xF71926E6487D465A307Bc0AcB5da50Ab7A15DA27",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -10,6 +10,10 @@ import {CollectPublicationAction} from 'contracts/modules/act/collect/CollectPub
import {BaseProfilePublicationData, IBaseFeeCollectModule} from 'contracts/modules/interfaces/IBaseFeeCollectModule.sol';
import {MetaTxLib} from 'contracts/libraries/MetaTxLib.sol';
interface IProtocolSharedRevenueMinFeeMintModule {
function getMintFeeParams() external view returns (address, uint256);
}
/// @title PublicActProxy
/// @author LensProtocol
/// @notice This contract allows anyone to Act on a publication without holding a profile
@@ -71,6 +75,18 @@ contract PublicActProxy {
_publicCollect(publicationActionParams, signature.signer);
}
function publicSharedRevenueCollect(Types.PublicationActionParams calldata publicationActionParams) external {
_publicSharedRevenueCollect(publicationActionParams, msg.sender);
}
function publicSharedRevenueCollectWithSig(
Types.PublicationActionParams calldata publicationActionParams,
Types.EIP712Signature calldata signature
) external {
MetaTxLib.validateActSignature(signature, publicationActionParams);
_publicSharedRevenueCollect(publicationActionParams, signature.signer);
}
function nonces(address signer) public view returns (uint256) {
return _nonces[signer];
}
@@ -103,6 +119,35 @@ contract PublicActProxy {
HUB.act(publicationActionParams);
}
function _publicSharedRevenueCollect(
Types.PublicationActionParams calldata publicationActionParams,
address transactionExecutor
) internal {
address collectModule = COLLECT_PUBLICATION_ACTION
.getCollectData(
publicationActionParams.publicationActedProfileId,
publicationActionParams.publicationActedId
)
.collectModule;
BaseProfilePublicationData memory collectData = IBaseFeeCollectModule(collectModule).getBasePublicationData(
publicationActionParams.publicationActedProfileId,
publicationActionParams.publicationActedId
);
if (collectData.amount > 0) {
IERC20(collectData.currency).safeTransferFrom(transactionExecutor, address(this), collectData.amount);
IERC20(collectData.currency).safeIncreaseAllowance(collectModule, collectData.amount);
} else {
(address currency, uint256 amount) = IProtocolSharedRevenueMinFeeMintModule(collectModule)
.getMintFeeParams();
IERC20(currency).safeTransferFrom(transactionExecutor, address(this), amount);
IERC20(currency).safeIncreaseAllowance(collectModule, amount);
}
HUB.act(publicationActionParams);
}
function name() external pure returns (string memory) {
return 'PublicActProxy';
}

View File

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

View File

@@ -0,0 +1,269 @@
// 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 {LensModuleMetadataInitializable} from 'contracts/modules/LensModuleMetadataInitializable.sol';
import {LensModule} from 'contracts/modules/LensModule.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;
address recipient;
uint16 referralFee;
bool followerOnly;
uint72 endTimestamp;
address creatorClient;
}
struct ProtocolSharedRevenueMinFeeMintModulePublicationData {
uint160 amount;
uint96 collectLimit;
address currency;
uint96 currentCollects;
address recipient;
uint16 referralFee;
bool followerOnly;
uint72 endTimestamp;
address creatorClient;
}
// Splits (in BPS)
struct ProtocolSharedRevenueDistribution {
uint16 creatorSplit;
uint16 protocolSplit;
uint16 creatorClientSplit;
uint16 executorClientSplit;
}
/**
* @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, LensModuleMetadataInitializable {
using SafeERC20 for IERC20;
address mintFeeToken;
uint256 mintFeeAmount;
ProtocolSharedRevenueDistribution protocolSharedRevenueDistribution;
event MintFeeParamsSet(address token, uint256 amount, uint256 timestamp);
event ProtocolSharedRevenueDistributionSet(ProtocolSharedRevenueDistribution distribution, uint256 timestamp);
mapping(uint256 profileId => mapping(uint256 pubId => address creatorClient))
internal _creatorClientByPublicationByProfile;
constructor(
address hub,
address actionModule,
address moduleRegistry,
address moduleOwner
) BaseFeeCollectModule(hub, actionModule, moduleRegistry) LensModuleMetadataInitializable(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.creatorClient != address(0)) {
_creatorClientByPublicationByProfile[profileId][pubId] = initData.creatorClient;
}
_validateBaseInitData(baseInitData);
_storeBasePublicationCollectParameters(profileId, pubId, baseInitData);
return data;
}
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 creatorClient = _creatorClientByPublicationByProfile[
processCollectParams.publicationCollectedProfileId
][processCollectParams.publicationCollectedId];
uint256 creatorClientAmount = (mintFeeAmount * protocolSharedRevenueDistribution.creatorClientSplit) / 10000;
if (creatorClient != address(0)) {
IERC20(mintFeeToken).safeTransferFrom(
processCollectParams.transactionExecutor,
creatorClient,
creatorClientAmount
);
} else {
// If there's no creatorClient specified - we give that amount to the publication creator
creatorAmount += creatorClientAmount;
}
(, , address executorClient) = abi.decode(processCollectParams.data, (address, uint256, address));
uint256 executorClientAmount = (mintFeeAmount * protocolSharedRevenueDistribution.executorClientSplit) / 10000;
if (executorClient != address(0)) {
IERC20(mintFeeToken).safeTransferFrom(
processCollectParams.transactionExecutor,
executorClient,
executorClientAmount
);
} else {
// If there's no executorClient specified - we give that amount to the publication creator
creatorAmount += executorClientAmount;
}
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;
emit MintFeeParamsSet(token, amount, block.timestamp);
}
function setProtocolSharedRevenueDistribution(
ProtocolSharedRevenueDistribution memory distribution
) external onlyOwner {
if (
distribution.creatorSplit +
distribution.protocolSplit +
distribution.creatorClientSplit +
distribution.executorClientSplit !=
BPS_MAX
) {
revert Errors.InvalidParams();
}
protocolSharedRevenueDistribution = distribution;
emit ProtocolSharedRevenueDistributionSet(distribution, block.timestamp);
}
// 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 creatorClient = _creatorClientByPublicationByProfile[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,
creatorClient: creatorClient
});
}
function supportsInterface(
bytes4 interfaceID
) public pure override(BaseFeeCollectModule, LensModule) returns (bool) {
return BaseFeeCollectModule.supportsInterface(interfaceID) || LensModule.supportsInterface(interfaceID);
}
}

View File

@@ -533,6 +533,8 @@ contract DeployFreshLensV2 is Script, ForkManagement, ArrayHelpers {
vm.writeLine(addressesFile, string.concat('AnonymousProfileId :', vm.toString(anonymousProfileId)));
console.log('\n* * * Anonymous profile created with id: ', anonymousProfileId);
saveValue('AnonymousProfileId', vm.toString(anonymousProfileId));
// TODO: Add PublicActProxy as a delegatedExecutor of anonymousProfileId
}
vm.stopBroadcast();

View File

@@ -0,0 +1,273 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {ForkManagement} from 'script/helpers/ForkManagement.sol';
import 'forge-std/Script.sol';
import {ProtocolSharedRevenueDistribution, ProtocolSharedRevenueMinFeeMintModule} from 'contracts/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.sol';
import {TransparentUpgradeableProxy} from '@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol';
import {CollectPublicationAction} from 'contracts/modules/act/collect/CollectPublicationAction.sol';
import {IModuleRegistry} from 'contracts/interfaces/IModuleRegistry.sol';
import {LibString} from 'solady/utils/LibString.sol';
contract DeployMintFeeModule is Script, ForkManagement {
using stdJson for string;
struct LensAccount {
uint256 ownerPk;
address owner;
uint256 profileId;
}
LensAccount _deployer;
LensAccount governance;
LensAccount proxyAdmin;
string mnemonic;
address lensHub;
address collectPublicationAction;
address moduleRegistry;
address governanceOwner;
address proxyAdminContractAdmin;
address bonsai;
ProtocolSharedRevenueMinFeeMintModule mintFeeModule;
address mintFeeModuleImpl;
address payable mintFeeModuleProxyAddr;
function loadPrivateKeys() internal {
if (isEnvSet('MNEMONIC')) {
mnemonic = vm.envString('MNEMONIC');
}
if (bytes(mnemonic).length == 0) {
revert('Missing mnemonic');
}
console.log('\n');
(_deployer.owner, _deployer.ownerPk) = deriveRememberKey(mnemonic, 0);
console.log('Deployer address: %s', address(_deployer.owner));
(governance.owner, governance.ownerPk) = deriveRememberKey(mnemonic, 1);
console.log('\n- - - GOVERNANCE: %s', governance.owner);
(proxyAdmin.owner, proxyAdmin.ownerPk) = deriveRememberKey(mnemonic, 2);
console.log('\n- - - PROXY ADMIN: %s', proxyAdmin.owner);
console.log('\n');
console.log('Current block:', block.number);
}
struct Currency {
address addy;
string symbol;
}
// TODO: Use from test/ContractAddresses
struct Module {
address addy;
string name;
}
// TODO: Move this somewhere common (also in UpgradeForkTest)
function findModuleHelper(
Module[] memory modules,
string memory moduleNameToFind
) internal pure returns (Module memory) {
for (uint256 i = 0; i < modules.length; i++) {
if (LibString.eq(modules[i].name, moduleNameToFind)) {
return modules[i];
}
}
revert('Module not found');
}
function findModuleHelper_noFail(
Module[] memory modules,
string memory moduleNameToFind
) internal pure returns (Module memory) {
for (uint256 i = 0; i < modules.length; i++) {
if (LibString.eq(modules[i].name, moduleNameToFind)) {
return modules[i];
}
}
return Module(address(0), '');
}
function saveModule(
string memory moduleName,
address moduleAddress,
string memory lensVersion,
string memory moduleType
) internal {
// console.log('Saving %s (%s) into addresses under %s environment', moduleName, moduleAddress, targetEnv);
string[] memory inputs = new string[](7);
inputs[0] = 'node';
inputs[1] = 'script/helpers/saveAddress.js';
inputs[2] = targetEnv;
inputs[3] = moduleName;
inputs[4] = vm.toString(moduleAddress);
inputs[5] = lensVersion;
inputs[6] = moduleType;
// bytes memory res =
vm.ffi(inputs);
// string memory output = abi.decode(res, (string));
// console.log(output);
}
function _logDeployedModule(address deployedAddress, string memory moduleName, string memory moduleType) internal {
string memory lensVersion = 'v2';
console.log('\n+ + + ', moduleName, ': ', deployedAddress);
saveModule(moduleName, deployedAddress, lensVersion, moduleType);
}
function loadBaseAddresses() internal override {
lensHub = json.readAddress(string(abi.encodePacked('.', targetEnv, '.LensHub')));
vm.label(lensHub, 'LensHub');
console.log('Lens Hub Proxy: %s', lensHub);
Module[] memory actModules = abi.decode(
vm.parseJson(json, string(abi.encodePacked('.', targetEnv, '.Modules.v2.act'))),
(Module[])
);
collectPublicationAction = findModuleHelper(actModules, 'CollectPublicationAction').addy;
vm.label(collectPublicationAction, 'CollectPublicationAction');
console.log('CollectPublicationAction: %s', collectPublicationAction);
Module[] memory collectModules = abi.decode(
vm.parseJson(json, string(abi.encodePacked('.', targetEnv, '.Modules.v2.collect'))),
(Module[])
);
mintFeeModuleProxyAddr = payable(
findModuleHelper_noFail(collectModules, 'ProtocolSharedRevenueMinFeeMintModule').addy
);
moduleRegistry = json.readAddress(string(abi.encodePacked('.', targetEnv, '.ModuleRegistry')));
vm.label(moduleRegistry, 'ModuleRegistry');
console.log('ModuleRegistry: %s', moduleRegistry);
governanceOwner = json.readAddress(string(abi.encodePacked('.', targetEnv, '.GovernanceContractAdmin')));
vm.label(governanceOwner, 'GovernanceOwner');
console.log('Governance Owner: %s', governanceOwner);
proxyAdminContractAdmin = json.readAddress(
string(abi.encodePacked('.', targetEnv, '.ProxyAdminContractAdmin'))
);
vm.label(proxyAdminContractAdmin, 'ProxyAdminContractAdmin');
console.log('Proxy Admin Contract Admin: %s', proxyAdminContractAdmin);
Currency[] memory currencies = abi.decode(
vm.parseJson(json, string(abi.encodePacked('.', targetEnv, '.Currencies'))),
(Currency[])
);
for (uint256 i = 0; i < currencies.length; i++) {
if (LibString.eq(currencies[i].symbol, 'BONSAI')) {
bonsai = currencies[i].addy;
}
}
vm.label(bonsai, 'BONSAI');
console.log('BONSAI: %s', bonsai);
}
function run(string memory targetEnv_) external {
targetEnv = targetEnv_;
loadJson();
checkNetworkParams();
loadBaseAddresses();
loadPrivateKeys();
deploy();
moduleRegistryActions();
governanceActions();
}
function deploy() internal {
vm.startBroadcast(_deployer.ownerPk);
{
mintFeeModuleImpl = address(
new ProtocolSharedRevenueMinFeeMintModule({
hub: lensHub,
actionModule: collectPublicationAction,
moduleRegistry: moduleRegistry,
moduleOwner: governanceOwner
})
);
console.log(
'\n* * * Deployed ProtocolSharedRevenueMinFeeMintModule implementation at: ',
mintFeeModuleImpl
);
console.log('With parameters:');
console.log('\tHub: ', lensHub);
console.log('\tAction Module: ', collectPublicationAction);
console.log('\tModule Registry: ', moduleRegistry);
console.log('\tModule Owner: ', governanceOwner);
}
vm.stopBroadcast();
if (mintFeeModuleProxyAddr == address(0)) {
console.log('\n* * * MintFeeModule proxy not found - deploying from scratch...');
vm.startBroadcast(_deployer.ownerPk);
{
mintFeeModule = ProtocolSharedRevenueMinFeeMintModule(
address(
new TransparentUpgradeableProxy(
mintFeeModuleImpl,
proxyAdminContractAdmin,
abi.encodeCall(mintFeeModule.initialize, (governanceOwner))
)
)
);
}
vm.stopBroadcast();
_logDeployedModule(address(mintFeeModule), 'ProtocolSharedRevenueMinFeeMintModule', 'collect');
} else {
console.log('\n* * * MintFeeModule proxy found - upgrading...');
console.log('\tProxyAdminContractAdmin: ', proxyAdminContractAdmin);
console.log('\tproxyAdmin.owner: ', proxyAdmin.owner);
vm.startBroadcast(proxyAdmin.ownerPk);
{
TransparentUpgradeableProxy(mintFeeModuleProxyAddr).upgradeTo(mintFeeModuleImpl);
}
vm.stopBroadcast();
mintFeeModule = ProtocolSharedRevenueMinFeeMintModule(mintFeeModuleProxyAddr);
console.log('\n* * * MintFeeModule upgraded to new implementation: ', mintFeeModuleImpl);
}
}
function moduleRegistryActions() internal {
vm.startBroadcast(_deployer.ownerPk);
IModuleRegistry(moduleRegistry).registerErc20Currency(bonsai);
console.log('\n* * * BONSAI registered as currency: ', bonsai);
CollectPublicationAction(collectPublicationAction).registerCollectModule(address(mintFeeModule));
console.log('\n* * * MintFeeModule registered as collect module in CollectPublicationAction');
vm.stopBroadcast();
}
function governanceActions() internal {
console.log('\n* * * Owner of mintFeeModule:', mintFeeModule.owner());
vm.startBroadcast(governance.ownerPk);
{
mintFeeModule.setMintFeeParams(bonsai, 10 ether);
console.log('\n* * * MintFeeModule mint fee set to 10 BONSAI');
mintFeeModule.setProtocolSharedRevenueDistribution(
ProtocolSharedRevenueDistribution({
creatorSplit: 5000,
protocolSplit: 2000,
creatorClientSplit: 1500,
executorClientSplit: 1500
})
);
console.log('\n* * * MintFeeModule revenue distribution set to:');
console.log('\tCreator: 50%');
console.log('\tProtocol: 20%');
console.log('\tCreator Client: 15%');
console.log('\tExecutor Client: 15%');
}
vm.stopBroadcast();
}
}

View File

@@ -0,0 +1,143 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import {ForkManagement} from 'script/helpers/ForkManagement.sol';
import 'forge-std/Script.sol';
import {ArrayHelpers} from 'script/helpers/ArrayHelpers.sol';
import {LibString} from 'solady/utils/LibString.sol';
import {PublicActProxy} from 'contracts/misc/PublicActProxy.sol';
import {ILensHub} from 'contracts/interfaces/ILensHub.sol';
contract DeployPublicActProxy is Script, ForkManagement, ArrayHelpers {
using stdJson for string;
string addressesFile = 'addressesV2.txt';
struct LensAccount {
uint256 ownerPk;
address owner;
uint256 profileId;
}
// TODO: Use from test/ContractAddresses
struct Module {
address addy;
string name;
}
LensAccount deployer;
LensAccount governance;
LensAccount proxyAdmin;
string mnemonic;
address lensHub;
address collectPublicationAction;
address publicActProxy;
function findModuleHelper(
Module[] memory modules,
string memory moduleNameToFind
) internal pure returns (Module memory) {
for (uint256 i = 0; i < modules.length; i++) {
if (LibString.eq(modules[i].name, moduleNameToFind)) {
return modules[i];
}
}
revert('Module not found');
}
function saveContractAddress(string memory contractName, address deployedAddress) internal {
// console.log('Saving %s (%s) into addresses under %s environment', contractName, deployedAddress, targetEnv);
string[] memory inputs = new string[](5);
inputs[0] = 'node';
inputs[1] = 'script/helpers/saveAddress.js';
inputs[2] = targetEnv;
inputs[3] = contractName;
inputs[4] = vm.toString(deployedAddress);
// bytes memory res =
vm.ffi(inputs);
// string memory output = abi.decode(res, (string));
// console.log(output);
}
function _logDeployedAddress(address deployedAddress, string memory addressLabel) internal {
console.log('\n+ + + ', addressLabel, ': ', deployedAddress);
vm.writeLine(addressesFile, string.concat(addressLabel, string.concat(': ', vm.toString(deployedAddress))));
saveContractAddress(addressLabel, deployedAddress);
}
function loadPrivateKeys() internal {
if (isEnvSet('MNEMONIC')) {
mnemonic = vm.envString('MNEMONIC');
}
if (bytes(mnemonic).length == 0) {
revert('Missing mnemonic');
}
console.log('\n');
(deployer.owner, deployer.ownerPk) = deriveRememberKey(mnemonic, 0);
console.log('\n- - - DEPLOYER: %s', deployer.owner);
(governance.owner, governance.ownerPk) = deriveRememberKey(mnemonic, 1);
console.log('\n- - - GOVERNANCE: %s', governance.owner);
(proxyAdmin.owner, proxyAdmin.ownerPk) = deriveRememberKey(mnemonic, 2);
console.log('\n- - - PROXYADMIN: %s', proxyAdmin.owner);
console.log('\n');
console.log('Current block:', block.number);
}
function loadBaseAddresses() internal override {
lensHub = json.readAddress(string(abi.encodePacked('.', targetEnv, '.LensHub')));
vm.label(lensHub, 'LensHub');
console.log('Lens Hub Proxy: %s', lensHub);
Module[] memory actModules = abi.decode(
vm.parseJson(json, string(abi.encodePacked('.', targetEnv, '.Modules.v2.act'))),
(Module[])
);
collectPublicationAction = findModuleHelper(actModules, 'CollectPublicationAction').addy;
vm.label(collectPublicationAction, 'CollectPublicationAction');
console.log('CollectPublicationAction: %s', collectPublicationAction);
}
function run(string memory targetEnv_) external {
targetEnv = targetEnv_;
loadJson();
checkNetworkParams();
loadBaseAddresses();
loadPrivateKeys();
deploy();
governanceActions();
}
function deploy() internal {
vm.startBroadcast(deployer.ownerPk);
{
publicActProxy = address(
new PublicActProxy({lensHub: lensHub, collectPublicationAction: collectPublicationAction})
);
_logDeployedAddress(publicActProxy, 'PublicActProxy');
}
vm.stopBroadcast();
}
function governanceActions() internal {
uint256 anonymousProfileId = json.readUint(string(abi.encodePacked('.', targetEnv, '.AnonymousProfileId')));
console.log('Anonymous Profile Id: %s', anonymousProfileId);
vm.startBroadcast(deployer.ownerPk);
{
ILensHub(lensHub).changeDelegatedExecutorsConfig(
anonymousProfileId,
_toAddressArray(publicActProxy),
_toBoolArray(true)
);
}
vm.stopBroadcast();
console.log('PublicActProxy added as DelegatedExecutor of AnonymousProfileId: %s', publicActProxy);
}
}

View File

@@ -10,6 +10,7 @@ import {CollectNFT} from 'contracts/modules/act/collect/CollectNFT.sol';
import {SimpleFeeCollectModule} from 'contracts/modules/act/collect/SimpleFeeCollectModule.sol';
import {BaseFeeCollectModuleInitData} from 'contracts/modules/interfaces/IBaseFeeCollectModule.sol';
import {MockCurrency} from 'test/mocks/MockCurrency.sol';
import {ProtocolSharedRevenueDistribution, ProtocolSharedRevenueMinFeeMintModuleInitData, ProtocolSharedRevenueMinFeeMintModule} from 'contracts/modules/act/collect/ProtocolSharedRevenueMinFeeMintModule.sol';
contract PublicActProxyTest is BaseTest {
using stdJson for string;
@@ -216,4 +217,321 @@ contract PublicActProxyTest is BaseTest {
assertTrue(collectNFT.balanceOf(nftRecipient) > 0, 'NFT recipient balance is 0');
}
function testCanPublicSharedRevenueCollectPaid() public {
vm.prank(deployer);
address revenueShareModule = address(
new ProtocolSharedRevenueMinFeeMintModule(
address(hub),
address(collectPublicationAction),
address(moduleRegistry),
address(this)
)
);
collectPublicationAction.registerCollectModule(revenueShareModule);
MockCurrency currency = new MockCurrency();
currency.mint(payer, 10 ether);
ProtocolSharedRevenueMinFeeMintModuleInitData memory exampleInitData;
exampleInitData.amount = 1 ether;
exampleInitData.collectLimit = 0;
exampleInitData.currency = address(currency);
exampleInitData.referralFee = 0;
exampleInitData.followerOnly = false;
exampleInitData.endTimestamp = 0;
exampleInitData.recipient = defaultAccount.owner;
exampleInitData.creatorClient = address(0);
Types.PostParams memory postParams = _getDefaultPostParams();
postParams.actionModules[0] = address(collectPublicationAction);
postParams.actionModulesInitDatas[0] = abi.encode(revenueShareModule, abi.encode(exampleInitData));
vm.prank(defaultAccount.owner);
defaultPubId = hub.post(postParams);
collectActionParams = Types.PublicationActionParams({
publicationActedProfileId: defaultAccount.profileId,
publicationActedId: defaultPubId,
actorProfileId: publicProfile.profileId,
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
actionModuleAddress: address(collectPublicationAction),
actionModuleData: abi.encode(nftRecipient, abi.encode(currency, exampleInitData.amount, address(0)))
});
vm.startPrank(payer);
currency.approve(address(publicActProxy), exampleInitData.amount);
publicActProxy.publicSharedRevenueCollect(collectActionParams);
vm.stopPrank();
CollectNFT collectNFT = CollectNFT(
CollectPublicationAction(collectPublicationAction)
.getCollectData(defaultAccount.profileId, defaultPubId)
.collectNFT
);
assertTrue(collectNFT.balanceOf(nftRecipient) > 0, 'NFT recipient balance is 0');
}
function testCanPublicSharedRevenueCollectPaidWithSig() public {
vm.prank(deployer);
address revenueShareModule = address(
new ProtocolSharedRevenueMinFeeMintModule(
address(hub),
address(collectPublicationAction),
address(moduleRegistry),
address(this)
)
);
collectPublicationAction.registerCollectModule(revenueShareModule);
MockCurrency currency = new MockCurrency();
currency.mint(payer, 10 ether);
ProtocolSharedRevenueMinFeeMintModuleInitData memory exampleInitData;
exampleInitData.amount = 1 ether;
exampleInitData.collectLimit = 0;
exampleInitData.currency = address(currency);
exampleInitData.referralFee = 0;
exampleInitData.followerOnly = false;
exampleInitData.endTimestamp = 0;
exampleInitData.recipient = defaultAccount.owner;
exampleInitData.creatorClient = address(0);
Types.PostParams memory postParams = _getDefaultPostParams();
postParams.actionModules[0] = address(collectPublicationAction);
postParams.actionModulesInitDatas[0] = abi.encode(revenueShareModule, abi.encode(exampleInitData));
vm.prank(defaultAccount.owner);
defaultPubId = hub.post(postParams);
collectActionParams = Types.PublicationActionParams({
publicationActedProfileId: defaultAccount.profileId,
publicationActedId: defaultPubId,
actorProfileId: publicProfile.profileId,
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
actionModuleAddress: address(collectPublicationAction),
actionModuleData: abi.encode(nftRecipient, abi.encode(currency, exampleInitData.amount, address(0)))
});
vm.prank(payer);
currency.approve(address(publicActProxy), exampleInitData.amount);
domainSeparator = keccak256(
abi.encode(
Typehash.EIP712_DOMAIN,
keccak256('PublicActProxy'),
MetaTxLib.EIP712_DOMAIN_VERSION_HASH,
block.chainid,
address(publicActProxy)
)
);
publicActProxy.publicSharedRevenueCollectWithSig({
publicationActionParams: collectActionParams,
signature: _getSigStruct({
pKey: payerPk,
digest: _getActTypedDataHash(collectActionParams, publicActProxy.nonces(payer), type(uint256).max),
deadline: type(uint256).max
})
});
CollectNFT collectNFT = CollectNFT(
CollectPublicationAction(collectPublicationAction)
.getCollectData(defaultAccount.profileId, defaultPubId)
.collectNFT
);
assertTrue(collectNFT.balanceOf(nftRecipient) > 0, 'NFT recipient balance is 0');
}
function testCanPublicSharedRevenueCollectFree() public {
address creatorClient = makeAddr('CREATOR_CLIENT');
address executorClient = makeAddr('EXECUTOR_CLIENT');
uint256 mintFee = 10 ether;
vm.prank(deployer);
address revenueShareModule = address(
new ProtocolSharedRevenueMinFeeMintModule(
address(hub),
address(collectPublicationAction),
address(moduleRegistry),
address(this)
)
);
collectPublicationAction.registerCollectModule(revenueShareModule);
MockCurrency currency = new MockCurrency();
currency.mint(payer, mintFee);
ProtocolSharedRevenueMinFeeMintModule(revenueShareModule).setMintFeeParams(address(currency), mintFee);
ProtocolSharedRevenueMinFeeMintModule(revenueShareModule).setProtocolSharedRevenueDistribution(
ProtocolSharedRevenueDistribution({
creatorSplit: 5000,
protocolSplit: 2000,
creatorClientSplit: 1500,
executorClientSplit: 1500
})
);
ProtocolSharedRevenueMinFeeMintModuleInitData memory exampleInitData;
exampleInitData.amount = 0 ether;
exampleInitData.collectLimit = 0;
exampleInitData.currency = address(0);
exampleInitData.referralFee = 0;
exampleInitData.followerOnly = false;
exampleInitData.endTimestamp = 0;
exampleInitData.recipient = defaultAccount.owner;
exampleInitData.creatorClient = creatorClient;
Types.PostParams memory postParams = _getDefaultPostParams();
postParams.actionModules[0] = address(collectPublicationAction);
postParams.actionModulesInitDatas[0] = abi.encode(revenueShareModule, abi.encode(exampleInitData));
vm.prank(defaultAccount.owner);
defaultPubId = hub.post(postParams);
collectActionParams = Types.PublicationActionParams({
publicationActedProfileId: defaultAccount.profileId,
publicationActedId: defaultPubId,
actorProfileId: publicProfile.profileId,
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
actionModuleAddress: address(collectPublicationAction),
actionModuleData: abi.encode(
nftRecipient,
abi.encode(exampleInitData.currency, exampleInitData.amount, executorClient)
)
});
vm.startPrank(payer);
currency.approve(address(publicActProxy), mintFee);
publicActProxy.publicSharedRevenueCollect(collectActionParams);
vm.stopPrank();
CollectNFT collectNFT = CollectNFT(
CollectPublicationAction(collectPublicationAction)
.getCollectData(defaultAccount.profileId, defaultPubId)
.collectNFT
);
assertTrue(collectNFT.balanceOf(nftRecipient) > 0, 'NFT recipient balance is 0');
assertEq(currency.balanceOf(defaultAccount.owner), (mintFee * 5000) / 10000, 'Creator balance is incorrect');
assertEq(
currency.balanceOf(hub.getTreasury()),
(mintFee * 2000) / 10000,
'Protocol treasury client balance is incorrect'
);
assertEq(currency.balanceOf(creatorClient), (mintFee * 1500) / 10000, 'Creator client balance is incorrect');
assertEq(currency.balanceOf(executorClient), (mintFee * 1500) / 10000, 'Executor client balance is incorrect');
}
function testCanPublicSharedRevenueCollectFreeWithSig() public {
address creatorClient = makeAddr('CREATOR_CLIENT');
address executorClient = makeAddr('EXECUTOR_CLIENT');
uint256 mintFee = 10 ether;
vm.prank(deployer);
address revenueShareModule = address(
new ProtocolSharedRevenueMinFeeMintModule(
address(hub),
address(collectPublicationAction),
address(moduleRegistry),
address(this)
)
);
collectPublicationAction.registerCollectModule(revenueShareModule);
MockCurrency currency = new MockCurrency();
currency.mint(payer, mintFee);
ProtocolSharedRevenueMinFeeMintModule(revenueShareModule).setMintFeeParams(address(currency), mintFee);
ProtocolSharedRevenueMinFeeMintModule(revenueShareModule).setProtocolSharedRevenueDistribution(
ProtocolSharedRevenueDistribution({
creatorSplit: 5000,
protocolSplit: 2000,
creatorClientSplit: 1500,
executorClientSplit: 1500
})
);
ProtocolSharedRevenueMinFeeMintModuleInitData memory exampleInitData;
exampleInitData.amount = 0 ether;
exampleInitData.collectLimit = 0;
exampleInitData.currency = address(0);
exampleInitData.referralFee = 0;
exampleInitData.followerOnly = false;
exampleInitData.endTimestamp = 0;
exampleInitData.recipient = defaultAccount.owner;
exampleInitData.creatorClient = creatorClient;
Types.PostParams memory postParams = _getDefaultPostParams();
postParams.actionModules[0] = address(collectPublicationAction);
postParams.actionModulesInitDatas[0] = abi.encode(revenueShareModule, abi.encode(exampleInitData));
vm.prank(defaultAccount.owner);
defaultPubId = hub.post(postParams);
collectActionParams = Types.PublicationActionParams({
publicationActedProfileId: defaultAccount.profileId,
publicationActedId: defaultPubId,
actorProfileId: publicProfile.profileId,
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
actionModuleAddress: address(collectPublicationAction),
actionModuleData: abi.encode(
nftRecipient,
abi.encode(exampleInitData.currency, exampleInitData.amount, executorClient)
)
});
vm.startPrank(payer);
currency.approve(address(publicActProxy), mintFee);
vm.stopPrank();
domainSeparator = keccak256(
abi.encode(
Typehash.EIP712_DOMAIN,
keccak256('PublicActProxy'),
MetaTxLib.EIP712_DOMAIN_VERSION_HASH,
block.chainid,
address(publicActProxy)
)
);
publicActProxy.publicSharedRevenueCollectWithSig({
publicationActionParams: collectActionParams,
signature: _getSigStruct({
pKey: payerPk,
digest: _getActTypedDataHash(collectActionParams, publicActProxy.nonces(payer), type(uint256).max),
deadline: type(uint256).max
})
});
CollectNFT collectNFT = CollectNFT(
CollectPublicationAction(collectPublicationAction)
.getCollectData(defaultAccount.profileId, defaultPubId)
.collectNFT
);
assertTrue(collectNFT.balanceOf(nftRecipient) > 0, 'NFT recipient balance is 0');
assertEq(currency.balanceOf(defaultAccount.owner), (mintFee * 5000) / 10000, 'Creator balance is incorrect');
assertEq(
currency.balanceOf(hub.getTreasury()),
(mintFee * 2000) / 10000,
'Protocol treasury client balance is incorrect'
);
assertEq(currency.balanceOf(creatorClient), (mintFee * 1500) / 10000, 'Creator client balance is incorrect');
assertEq(currency.balanceOf(executorClient), (mintFee * 1500) / 10000, 'Executor client balance is incorrect');
}
}

View File

@@ -246,7 +246,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(),
data: abi.encode(currency, passedAmount)
data: _getCollectParamsData(address(currency), passedAmount)
})
);
}
@@ -292,7 +292,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(),
data: abi.encode(passedCurrency, amount)
data: _getCollectParamsData(passedCurrency, exampleInitData.amount)
})
);
}
@@ -335,7 +335,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(),
data: abi.encode(currency, exampleInitData.amount)
data: _getCollectParamsData(address(currency), exampleInitData.amount)
})
);
}
@@ -379,7 +379,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(),
data: abi.encode(currency, exampleInitData.amount)
data: _getCollectParamsData(address(currency), exampleInitData.amount)
})
);
}
@@ -423,7 +423,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(),
data: abi.encode(currency, exampleInitData.amount)
data: _getCollectParamsData(address(currency), exampleInitData.amount)
})
);
}
@@ -440,7 +440,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(),
data: abi.encode(currency, exampleInitData.amount)
data: _getCollectParamsData(address(currency), exampleInitData.amount)
})
);
}
@@ -518,7 +518,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(),
data: abi.encode(currency, amount)
data: _getCollectParamsData(address(currency), amount)
})
);
}
@@ -564,7 +564,7 @@ contract BaseFeeCollectModule_ProcessCollect is BaseFeeCollectModuleBase {
referrerProfileIds: _emptyUint256Array(),
referrerPubIds: _emptyUint256Array(),
referrerPubTypes: _emptyPubTypesArray(),
data: abi.encode(currency, exampleInitData.amount)
data: _getCollectParamsData(address(currency), exampleInitData.amount)
})
);
@@ -572,6 +572,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 {

View File

@@ -29,6 +29,7 @@ contract MultirecipientCollectModule_Initialization is
return MultirecipientCollectModuleBase.getEncodedInitData();
}
// TODO: WTF?
function testCannotInitializeWithNonWhitelistedCurrency(
uint256 profileId,
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 creatorClientAddress = makeAddr('CREATOR_CLIENT');
address executorClientAddress = makeAddr('EXECUTOR_CLIENT');
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,
creatorClientSplit: 1500,
executorClientSplit: 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.creatorClient = creatorClientAddress;
return abi.encode(mintFeeModuleExampleInitData);
}
}

View File

@@ -0,0 +1,621 @@
// 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 creatorClient
) 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.creatorClient = creatorClient;
vm.prank(collectPublicationAction);
bytes memory returnedData = 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.creatorClient,
mintFeeModuleExampleInitData.creatorClient,
'CreatorClient initialization mismatch'
);
assertEq(keccak256(returnedData), keccak256(getEncodedInitData()), 'Returned data mismatch');
}
}
//////////////
// Collect with ProtocolSharedRevenueMinFeeMintModule
//
contract ProtocolSharedRevenueMinFeeMintModule_ProcessCollect is
ProtocolSharedRevenueMinFeeMintModuleBase,
BaseFeeCollectModule_ProcessCollect
{
function testProtocolSharedRevenueMinFeeMintModule_Collect() public {
// Prevents being counted in Foundry Coverage
}
address exampleExecutorClient = executorClientAddress;
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 view override returns (bytes memory) {
return abi.encode(currency, amount, exampleExecutorClient);
}
// 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 executorClient = makeAddr('EXECUTOR_CLIENT');
exampleExecutorClient = executorClient;
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 creatorClient;
uint256 executorClient;
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 executorClient = makeAddr('EXECUTOR_CLIENT');
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()),
creatorClient: bonsai.balanceOf(creatorClientAddress),
executorClient: bonsai.balanceOf(executorClient),
collector: bonsai.balanceOf(collectorProfileOwner)
});
exampleExecutorClient = executorClient;
super.testCanCollectIfAllConditionsAreMet(
pubId,
transactionExecutor,
0,
collectLimit,
referralFee,
followerOnly,
currentTimestamp,
endTimestamp,
recipient,
collectorProfileOwner
);
balancesAfter = Balances({
creator: bonsai.balanceOf(defaultAccount.owner),
protocol: bonsai.balanceOf(hub.getTreasury()),
creatorClient: bonsai.balanceOf(creatorClientAddress),
executorClient: bonsai.balanceOf(executorClient),
collector: bonsai.balanceOf(collectorProfileOwner)
});
balancesChange = Balances({
creator: balancesAfter.creator - balancesBefore.creator,
protocol: balancesAfter.protocol - balancesBefore.protocol,
creatorClient: balancesAfter.creatorClient - balancesBefore.creatorClient,
executorClient: balancesAfter.executorClient - balancesBefore.executorClient,
collector: balancesBefore.collector - balancesAfter.collector
});
uint256 expectedCreatorFee = (mintFee * mintFeeModule.getProtocolSharedRevenueDistribution().creatorSplit) /
BPS_MAX;
uint256 expectedProtocolFee = (mintFee * mintFeeModule.getProtocolSharedRevenueDistribution().protocolSplit) /
BPS_MAX;
uint256 expectedCreatorClientFee = (mintFee *
mintFeeModule.getProtocolSharedRevenueDistribution().creatorClientSplit) / BPS_MAX;
uint256 expectedExecutorClientFee = (mintFee *
mintFeeModule.getProtocolSharedRevenueDistribution().executorClientSplit) / BPS_MAX;
assertEq(balancesChange.creator, expectedCreatorFee, 'Creator balance change wrong');
assertEq(balancesChange.protocol, expectedProtocolFee, 'Protocol balance change wrong');
assertEq(balancesChange.creatorClient, expectedCreatorClientFee, 'CreatorClient balance change wrong');
assertEq(balancesChange.executorClient, expectedExecutorClientFee, 'ExecutorClient balance change wrong');
assertEq(balancesChange.collector, mintFee, 'Collector balance change wrong');
}
function testMintFeeDistribution_FreePost_WithoutClients(
uint256 pubId,
address transactionExecutor,
uint96 collectLimit,
uint16 referralFee,
bool followerOnly,
uint72 currentTimestamp,
uint72 endTimestamp,
address recipient
) public {
address collectorProfileOwner = makeAddr('COLLECTOR_PROFILE_OWNER');
address executorClient = address(0);
creatorClientAddress = 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()),
creatorClient: bonsai.balanceOf(creatorClientAddress),
executorClient: bonsai.balanceOf(executorClient),
collector: bonsai.balanceOf(collectorProfileOwner)
});
console.log('creatorClient balance before: %s', balancesBefore.creatorClient);
exampleExecutorClient = executorClient;
super.testCanCollectIfAllConditionsAreMet(
pubId,
transactionExecutor,
0,
collectLimit,
referralFee,
followerOnly,
currentTimestamp,
endTimestamp,
recipient,
collectorProfileOwner
);
balancesAfter = Balances({
creator: bonsai.balanceOf(defaultAccount.owner),
protocol: bonsai.balanceOf(hub.getTreasury()),
creatorClient: bonsai.balanceOf(creatorClientAddress),
executorClient: bonsai.balanceOf(executorClient),
collector: bonsai.balanceOf(collectorProfileOwner)
});
balancesChange = Balances({
creator: balancesAfter.creator - balancesBefore.creator,
protocol: balancesAfter.protocol - balancesBefore.protocol,
creatorClient: balancesAfter.creatorClient - balancesBefore.creatorClient,
executorClient: balancesAfter.executorClient - balancesBefore.executorClient,
collector: balancesBefore.collector - balancesAfter.collector
});
uint256 expectedCreatorFee = (mintFee * mintFeeModule.getProtocolSharedRevenueDistribution().creatorSplit) /
BPS_MAX;
uint256 expectedProtocolFee = (mintFee * mintFeeModule.getProtocolSharedRevenueDistribution().protocolSplit) /
BPS_MAX;
uint256 expectedCreatorClientFee = (mintFee *
mintFeeModule.getProtocolSharedRevenueDistribution().creatorClientSplit) / BPS_MAX;
uint256 expectedExecutorClientFee = (mintFee *
mintFeeModule.getProtocolSharedRevenueDistribution().executorClientSplit) / BPS_MAX;
assertEq(
balancesChange.creator,
expectedCreatorFee + expectedCreatorClientFee + expectedExecutorClientFee,
'Creator balance change wrong'
);
assertEq(balancesChange.protocol, expectedProtocolFee, 'Protocol balance change wrong');
assertEq(balancesChange.creatorClient, 0, 'CreatorClient balance change wrong');
assertEq(balancesChange.executorClient, 0, 'ExecutorClient 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 executorClient = makeAddr('EXECUTOR_CLIENT');
balancesBefore = Balances({
creator: bonsai.balanceOf(defaultAccount.owner),
protocol: bonsai.balanceOf(hub.getTreasury()),
creatorClient: bonsai.balanceOf(creatorClientAddress),
executorClient: bonsai.balanceOf(executorClient),
collector: bonsai.balanceOf(collectorProfileOwner)
});
exampleExecutorClient = executorClient;
super.testCanCollectIfAllConditionsAreMet(
pubId,
transactionExecutor,
amount,
collectLimit,
referralFee,
followerOnly,
currentTimestamp,
endTimestamp,
recipient,
collectorProfileOwner
);
balancesAfter = Balances({
creator: bonsai.balanceOf(defaultAccount.owner),
protocol: bonsai.balanceOf(hub.getTreasury()),
creatorClient: bonsai.balanceOf(creatorClientAddress),
executorClient: bonsai.balanceOf(executorClient),
collector: bonsai.balanceOf(collectorProfileOwner)
});
balancesChange = Balances({
creator: balancesAfter.creator - balancesBefore.creator,
protocol: balancesAfter.protocol - balancesBefore.protocol,
creatorClient: balancesAfter.creatorClient - balancesBefore.creatorClient,
executorClient: balancesAfter.executorClient - balancesBefore.executorClient,
collector: balancesBefore.collector - balancesAfter.collector
});
assertEq(balancesChange.creator, 0, 'Creator balance change wrong');
assertEq(balancesChange.protocol, 0, 'Protocol balance change wrong');
assertEq(balancesChange.creatorClient, 0, 'CreatorClient balance change wrong');
assertEq(balancesChange.executorClient, 0, 'ExecutorClient 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();
}
event MintFeeParamsSet(address token, uint256 amount, uint256 timestamp);
event ProtocolSharedRevenueDistributionSet(ProtocolSharedRevenueDistribution distribution, uint256 timestamp);
// 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 creatorClientSplit,
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));
creatorClientSplit = uint16(bound(uint256(creatorClientSplit), 0, BPS_MAX - creatorSplit - protocolSplit));
uint16 executorClientSplit = BPS_MAX - creatorSplit - protocolSplit - creatorClientSplit;
vm.expectRevert('Ownable: caller is not the owner');
vm.prank(notOwner);
mintFeeModule.setProtocolSharedRevenueDistribution(
ProtocolSharedRevenueDistribution({
creatorSplit: creatorSplit,
protocolSplit: protocolSplit,
creatorClientSplit: creatorClientSplit,
executorClientSplit: executorClientSplit
})
);
}
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 creatorClientSplit,
uint16 executorClientSplit
) public {
vm.assume(
uint256(creatorSplit) +
uint256(protocolSplit) +
uint256(creatorClientSplit) +
uint256(executorClientSplit) !=
BPS_MAX
);
vm.startPrank(mintFeeModule.owner());
if (
uint256(creatorSplit) +
uint256(protocolSplit) +
uint256(creatorClientSplit) +
uint256(executorClientSplit) >
type(uint16).max
) {
vm.expectRevert(stdError.arithmeticError);
} else {
vm.expectRevert(ModuleErrors.InvalidParams.selector);
}
mintFeeModule.setProtocolSharedRevenueDistribution(
ProtocolSharedRevenueDistribution({
creatorSplit: creatorSplit,
protocolSplit: protocolSplit,
creatorClientSplit: creatorClientSplit,
executorClientSplit: executorClientSplit
})
);
vm.stopPrank();
}
// Scenarios
function testSetMintFeeParams(uint256 mintFee, address currency) public {
if (mintFee > 0) {
vm.assume(currency != address(0));
} else {
currency = address(0);
}
vm.expectEmit(true, true, true, true, address(mintFeeModule));
emit MintFeeParamsSet(currency, mintFee, block.timestamp);
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 creatorClientSplit
) public {
creatorSplit = uint16(bound(uint256(creatorSplit), 0, BPS_MAX));
protocolSplit = uint16(bound(uint256(protocolSplit), 0, BPS_MAX - creatorSplit));
creatorClientSplit = uint16(bound(uint256(creatorClientSplit), 0, BPS_MAX - creatorSplit - protocolSplit));
uint16 executorClientSplit = BPS_MAX - creatorSplit - protocolSplit - creatorClientSplit;
ProtocolSharedRevenueDistribution memory expectedDistribution = ProtocolSharedRevenueDistribution({
creatorSplit: creatorSplit,
protocolSplit: protocolSplit,
creatorClientSplit: creatorClientSplit,
executorClientSplit: executorClientSplit
});
vm.expectEmit(true, true, true, true, address(mintFeeModule));
emit ProtocolSharedRevenueDistributionSet(expectedDistribution, block.timestamp);
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.creatorClientSplit,
expectedDistribution.creatorClientSplit,
'CreatorClient split mismatch'
);
assertEq(
actualDistribution.executorClientSplit,
expectedDistribution.executorClientSplit,
'ExecutorClient split mismatch'
);
}
}