Skip to content

Commit d444015

Browse files
Feat: FeeCollectModule V2 (#13)
* feat: Create base FeeCollect v2 contract * feat: New data structs, in progress refactor * fix: Natspec comment fixes * feat: Added recipients array to init params * feat: Added recipient validation and data storing * feat: Draft FeeCollect V2 contract completed * feat: Started module test file in Foundry * fix: FeeCollectModuleV2 - add more checks and recipients.push to fix out of bounds * test: FeeCollectModuleV2 - add minimal collect module foundry tests * fix: Unchecked increments in loops * test: FeeCollectModule - simplify getting IDs - without recording events * fix: Added endTimestamp == 0 to tests * test: Added more fuzzing tests for multi recipients * fix: Smol cleanup in fuzz tests * fix: FeeCollectModuleV2 bug when checking for zero recipient * test: FeeCollectModule - more init params test - improve coverage * feat: Added overridable calculateFee view function to module * fix: FeeCollectModuleV2 - use calculateFee for amount in processCollect * feat: BaseFeeCollectModule - Initial commit * fix: BaseFeeCollectModule test fix * fix: BaseFeeCollectModule refactor * test: BaseFeeCollectModule tests * test: Inherited foundry tests * fix: BaseFeeCollectModule - refactor to save gas * feat: BaseFeeCollectV2 - probably better inheritance approach * feat: AbstractCollectModule refactoring * feat: AbstractCollect and MultirecipientCollect modules refactoring, natspec, tests * feat: AbstractCollectModule - more refactor * feat: AbstractCollectModule - refactor collectsAfter * feat: AbstractCollectModule refactor * feat: BaseFeeCollect module renamed to SimpleFeeCollect * feat: Rename AbstractCollect module into BaseCollect, and refactor * fix: Removal of FeeCollectModuleV2 files * fix: Refactor MultirecipientFeeCollect * fix: Removed TODO in SimpleFeeCollectModule * feat: remove foundry lib files * forge install: forge-std * test: Use Foundry bound for fuzzing * fix: Rename to BaseFeeCollectModule * feat: Update readme - add BaseFeeCollect, SimpleFeeCollect, MultirecipientFeeCollect modules Co-authored-by: vicnaum <[email protected]>
1 parent d7c7a3e commit d444015

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2138
-8267
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "lib/forge-std"]
2+
path = lib/forge-std
3+
url = https://github.com/foundry-rs/forge-std

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@ This repository contains both - Hardhat and Foundry tests. Foundry will be used
3232

3333
- [**Aave Fee Collect Module**](./contracts/collect/AaveFeeCollectModule.sol): Extend the LimitedFeeCollectModule to deposit all received fees into the Aave Polygon Market (if applicable for the asset) and send the resulting aTokens to the beneficiary.
3434
- [**Auction Collect Module**](./contracts/collect/AuctionCollectModule.sol): This module works by creating an English auction for the underlying publication. After the auction ends, only the auction winner is allowed to collect the publication.
35+
- [**Base Fee Collect Module**](./contracts/collect/base/BaseFeeCollectModule.sol): An abstract base fee collect module contract which can be used to construct flexible fee collect modules using inheritance.
36+
- [**Multirecipient Fee Collect Module**](./contracts/collect/MultirecipientFeeCollectModule.sol): Fee Collect module that allows multiple recipients (up to 5) with different proportions of fees payout.
37+
- [**Simple Fee Collect Module**](./contracts/collect/SimpleFeeCollectModule.sol): A simple fee collect module implementation, as an example of using base fee collect module abstract contract.
3538
- [**Updatable Ownable Fee Collect Module**](./contracts/collect/UpdatableOwnableFeeCollectModule.sol): A fee collect module that, for each publication that uses it, mints an ERC-721 ownership-NFT to its author. Whoever owns the ownership-NFT has the rights to update the parameters required to do a successful collect operation over its underlying publication.
3639

3740
## Follow modules
3841

3942
## Reference modules
4043

41-
- [**Degrees Of Separation Reference Module**](./contracts/reference/DegreesOfSeparationReferenceModule.sol): This reference module allows to set a degree of separation `n`, and then allows to comment/mirror only to profiles that are at most at `n` degrees of separation from the author of the root publication.
44+
- [**Degrees Of Separation Reference Module**](./contracts/reference/DegreesOfSeparationReferenceModule.sol): This reference module allows to set a degree of separation `n`, and then allows to comment/mirror only to profiles that are at most at `n` degrees of separation from the author of the root publication.
4245
- [**Token Gated Reference Module**](./contracts/reference/TokenGatedReferenceModule.sol): A reference module that validates that the user who tries to reference has a required minimum balance of ERC20/ERC721 token.
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity 0.8.10;
4+
5+
import {Errors} from '@aave/lens-protocol/contracts/libraries/Errors.sol';
6+
import {BaseFeeCollectModule} from './base/BaseFeeCollectModule.sol';
7+
import {BaseProfilePublicationData, BaseFeeCollectModuleInitData} from './base/IBaseFeeCollectModule.sol';
8+
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
9+
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
10+
import {ICollectModule} from '@aave/lens-protocol/contracts/interfaces/ICollectModule.sol';
11+
12+
struct RecipientData {
13+
address recipient;
14+
uint16 split; // fraction of BPS_MAX (10 000)
15+
}
16+
17+
/**
18+
* @notice A struct containing the necessary data to initialize MultirecipientFeeCollectModule.
19+
*
20+
* @param amount The collecting cost associated with this publication. 0 for free collect.
21+
* @param collectLimit The maximum number of collects for this publication. 0 for no limit.
22+
* @param currency The currency associated with this publication.
23+
* @param referralFee The referral fee associated with this publication.
24+
* @param followerOnly True if only followers of publisher may collect the post.
25+
* @param endTimestamp The end timestamp after which collecting is impossible. 0 for no expiry.
26+
* @param recipients Array of RecipientData items to split collect fees across multiple recipients.
27+
*/
28+
struct MultirecipientFeeCollectModuleInitData {
29+
uint160 amount;
30+
uint96 collectLimit;
31+
address currency;
32+
uint16 referralFee;
33+
bool followerOnly;
34+
uint72 endTimestamp;
35+
RecipientData[] recipients;
36+
}
37+
38+
/**
39+
* @notice A struct containing the necessary data to execute collect actions on a publication.
40+
*
41+
* @param amount The collecting cost associated with this publication. 0 for free collect.
42+
* @param collectLimit The maximum number of collects for this publication. 0 for no limit.
43+
* @param currency The currency associated with this publication.
44+
* @param currentCollects The current number of collects for this publication.
45+
* @param referralFee The referral fee associated with this publication.
46+
* @param followerOnly True if only followers of publisher may collect the post.
47+
* @param endTimestamp The end timestamp after which collecting is impossible. 0 for no expiry.
48+
* @param recipients Array of RecipientData items to split collect fees across multiple recipients.
49+
*/
50+
struct MultirecipientFeeCollectProfilePublicationData {
51+
uint160 amount;
52+
uint96 collectLimit;
53+
address currency;
54+
uint96 currentCollects;
55+
uint16 referralFee;
56+
bool followerOnly;
57+
uint72 endTimestamp;
58+
RecipientData[] recipients;
59+
}
60+
61+
error TooManyRecipients();
62+
error InvalidRecipientSplits();
63+
error RecipientSplitCannotBeZero();
64+
65+
/**
66+
* @title MultirecipientCollectModule
67+
* @author Lens Protocol
68+
*
69+
* @notice This is a simple Lens CollectModule implementation, allowing customization of time to collect, number of collects,
70+
* splitting collect fee across multiple recipients, and whether only followers can collect.
71+
* It is charging a fee for collect (if enabled) and distributing it among Receivers/Referral/Treasury.
72+
*/
73+
contract MultirecipientFeeCollectModule is BaseFeeCollectModule {
74+
using SafeERC20 for IERC20;
75+
76+
uint256 internal constant MAX_RECIPIENTS = 5;
77+
78+
mapping(uint256 => mapping(uint256 => RecipientData[]))
79+
internal _recipientsByPublicationByProfile;
80+
81+
constructor(address hub, address moduleGlobals) BaseFeeCollectModule(hub, moduleGlobals) {}
82+
83+
/**
84+
* @inheritdoc ICollectModule
85+
*/
86+
function initializePublicationCollectModule(
87+
uint256 profileId,
88+
uint256 pubId,
89+
bytes calldata data
90+
) external override onlyHub returns (bytes memory) {
91+
MultirecipientFeeCollectModuleInitData memory initData = abi.decode(
92+
data,
93+
(MultirecipientFeeCollectModuleInitData)
94+
);
95+
96+
BaseFeeCollectModuleInitData memory baseInitData = BaseFeeCollectModuleInitData({
97+
amount: initData.amount,
98+
collectLimit: initData.collectLimit,
99+
currency: initData.currency,
100+
referralFee: initData.referralFee,
101+
followerOnly: initData.followerOnly,
102+
endTimestamp: initData.endTimestamp,
103+
recipient: address(0)
104+
});
105+
106+
_validateBaseInitData(baseInitData);
107+
_validateAndStoreRecipients(initData.recipients, profileId, pubId);
108+
_storeBasePublicationCollectParameters(profileId, pubId, baseInitData);
109+
return data;
110+
}
111+
112+
/**
113+
* @dev Validates the recipients array and stores them to (a separate from Base) storage.
114+
*
115+
* @param recipients An array of recipients
116+
* @param profileId The profile ID who is publishing the publication.
117+
* @param pubId The associated publication's LensHub publication ID.
118+
*/
119+
function _validateAndStoreRecipients(
120+
RecipientData[] memory recipients,
121+
uint256 profileId,
122+
uint256 pubId
123+
) internal {
124+
uint256 len = recipients.length;
125+
126+
// Check number of recipients is supported
127+
if (len > MAX_RECIPIENTS) revert TooManyRecipients();
128+
if (len == 0) revert Errors.InitParamsInvalid();
129+
130+
// Skip loop check if only 1 recipient in the array
131+
if (len == 1) {
132+
if (recipients[0].recipient == address(0)) revert Errors.InitParamsInvalid();
133+
if (recipients[0].split != BPS_MAX) revert InvalidRecipientSplits();
134+
135+
// If single recipient passes check above, store and return
136+
_recipientsByPublicationByProfile[profileId][pubId].push(recipients[0]);
137+
} else {
138+
// Check recipient splits sum to 10 000 BPS (100%)
139+
uint256 totalSplits;
140+
for (uint256 i = 0; i < len; ) {
141+
if (recipients[i].recipient == address(0)) revert Errors.InitParamsInvalid();
142+
if (recipients[i].split == 0) revert RecipientSplitCannotBeZero();
143+
totalSplits += recipients[i].split;
144+
145+
// Store each recipient while looping - avoids extra gas costs in successful cases
146+
_recipientsByPublicationByProfile[profileId][pubId].push(recipients[i]);
147+
148+
unchecked {
149+
++i;
150+
}
151+
}
152+
153+
if (totalSplits != BPS_MAX) revert InvalidRecipientSplits();
154+
}
155+
}
156+
157+
/**
158+
* @dev Transfers the fee to multiple recipients.
159+
*
160+
* @inheritdoc BaseFeeCollectModule
161+
*/
162+
function _transferToRecipients(
163+
address currency,
164+
address collector,
165+
uint256 profileId,
166+
uint256 pubId,
167+
uint256 amount
168+
) internal override {
169+
RecipientData[] memory recipients = _recipientsByPublicationByProfile[profileId][pubId];
170+
uint256 len = recipients.length;
171+
172+
// If only 1 recipient, transfer full amount and skip split calculations
173+
if (len == 1 && amount != 0) {
174+
IERC20(currency).safeTransferFrom(collector, recipients[0].recipient, amount);
175+
} else {
176+
uint256 splitAmount;
177+
for (uint256 i = 0; i < len; ) {
178+
splitAmount = (amount * recipients[i].split) / BPS_MAX;
179+
if (splitAmount != 0)
180+
IERC20(currency).safeTransferFrom(
181+
collector,
182+
recipients[i].recipient,
183+
splitAmount
184+
);
185+
186+
unchecked {
187+
++i;
188+
}
189+
}
190+
}
191+
}
192+
193+
/**
194+
* @notice Returns the publication data for a given publication, or an empty struct if that publication was not
195+
* initialized with this module.
196+
*
197+
* @param profileId The token ID of the profile mapped to the publication to query.
198+
* @param pubId The publication ID of the publication to query.
199+
*
200+
* @return The BaseProfilePublicationData struct mapped to that publication.
201+
*/
202+
function getPublicationData(uint256 profileId, uint256 pubId)
203+
external
204+
view
205+
returns (MultirecipientFeeCollectProfilePublicationData memory)
206+
{
207+
BaseProfilePublicationData memory baseData = getBasePublicationData(profileId, pubId);
208+
RecipientData[] memory recipients = _recipientsByPublicationByProfile[profileId][pubId];
209+
210+
return
211+
MultirecipientFeeCollectProfilePublicationData({
212+
amount: baseData.amount,
213+
collectLimit: baseData.collectLimit,
214+
currency: baseData.currency,
215+
currentCollects: baseData.currentCollects,
216+
referralFee: baseData.referralFee,
217+
followerOnly: baseData.followerOnly,
218+
endTimestamp: baseData.endTimestamp,
219+
recipients: recipients
220+
});
221+
}
222+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.10;
3+
4+
import {BaseFeeCollectModule} from './base/BaseFeeCollectModule.sol';
5+
import {BaseFeeCollectModuleInitData, BaseProfilePublicationData} from './base/IBaseFeeCollectModule.sol';
6+
7+
/**
8+
* @title SimpleFeeCollectModule
9+
* @author Lens Protocol
10+
*
11+
* @notice This is a simple Lens CollectModule implementation, allowing customization of time to collect,
12+
* number of collects and whether only followers can collect.
13+
*
14+
* You can build your own collect modules by inheriting from BaseFeeCollectModule and adding your
15+
* functionality along with getPublicationData function.
16+
*/
17+
contract SimpleFeeCollectModule is BaseFeeCollectModule {
18+
constructor(address hub, address moduleGlobals) BaseFeeCollectModule(hub, moduleGlobals) {}
19+
20+
/**
21+
* @notice This collect module levies a fee on collects and supports referrals. Thus, we need to decode data.
22+
* @param data The arbitrary data parameter, decoded into:
23+
* amount: The collecting cost associated with this publication. 0 for free collect.
24+
* collectLimit: The maximum number of collects for this publication. 0 for no limit.
25+
* currency: The currency associated with this publication.
26+
* referralFee: The referral fee associated with this publication.
27+
* followerOnly: True if only followers of publisher may collect the post.
28+
* endTimestamp: The end timestamp after which collecting is impossible. 0 for no expiry.
29+
* recipient: Recipient of collect fees.
30+
*
31+
* @return An abi encoded bytes parameter, which is the same as the passed data parameter.
32+
*/
33+
function initializePublicationCollectModule(
34+
uint256 profileId,
35+
uint256 pubId,
36+
bytes calldata data
37+
) external virtual onlyHub returns (bytes memory) {
38+
BaseFeeCollectModuleInitData memory baseInitData = abi.decode(
39+
data,
40+
(BaseFeeCollectModuleInitData)
41+
);
42+
_validateBaseInitData(baseInitData);
43+
_storeBasePublicationCollectParameters(profileId, pubId, baseInitData);
44+
return data;
45+
}
46+
47+
/**
48+
* @notice Returns the publication data for a given publication, or an empty struct if that publication was not
49+
* initialized with this module.
50+
*
51+
* @param profileId The token ID of the profile mapped to the publication to query.
52+
* @param pubId The publication ID of the publication to query.
53+
*
54+
* @return The BaseProfilePublicationData struct mapped to that publication.
55+
*/
56+
function getPublicationData(uint256 profileId, uint256 pubId)
57+
external
58+
view
59+
virtual
60+
returns (BaseProfilePublicationData memory)
61+
{
62+
return getBasePublicationData(profileId, pubId);
63+
}
64+
}

0 commit comments

Comments
 (0)