-
Notifications
You must be signed in to change notification settings - Fork 8
/
BaseFeeCollectModule.sol
296 lines (263 loc) · 12.4 KB
/
BaseFeeCollectModule.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
import {Errors} from 'contracts/modules/constants/Errors.sol';
import {FeeModuleBase} from 'contracts/modules/FeeModuleBase.sol';
import {ICollectModule} from 'contracts/interfaces/ICollectModule.sol';
import {ActionRestricted} from 'contracts/modules/ActionRestricted.sol';
import {Types} from 'contracts/libraries/constants/Types.sol';
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {FollowValidationLib} from 'contracts/modules/libraries/FollowValidationLib.sol';
import {BaseFeeCollectModuleInitData, BaseProfilePublicationData, IBaseFeeCollectModule} from 'contracts/modules/interfaces/IBaseFeeCollectModule.sol';
/**
* @title BaseFeeCollectModule
* @author Lens Protocol
*
* @notice This is base Lens CollectModule implementation, allowing customization of time to collect, number of collects
* and Followers-only restriction. Charges a fee for collect and distributing it among Receiver/Referrals/Treasury.
* @dev Here we use "Base" terminology to anything that represents this base functionality (base structs,
* base functions, base storage). Other collect modules can be built on top of the "Base" by inheriting from this
* contract and overriding functions.
* This contract is marked "abstract" as it requires you to implement initializePublicationCollectModule and
* getPublicationData functions when you inherit from it. See SimpleFeeCollectModule as an example implementation.
*/
abstract contract BaseFeeCollectModule is FeeModuleBase, ActionRestricted, IBaseFeeCollectModule {
using SafeERC20 for IERC20;
address immutable HUB;
mapping(uint256 => mapping(uint256 => BaseProfilePublicationData)) internal _dataByPublicationByProfile;
constructor(
address hub,
address actionModule,
address moduleGlobals
) ActionRestricted(actionModule) FeeModuleBase(moduleGlobals) {
HUB = hub;
}
/**
* @inheritdoc ICollectModule
* @notice Processes a collect by:
* 1. Validating that collect action meets all needed criteria
* 2. Processing the collect action either with or without referral
*
* @param processCollectParams Collect action parameters (see Types.ProcessCollectParams struct)
*/
function processCollect(Types.ProcessCollectParams calldata processCollectParams)
external
virtual
onlyActionModule
returns (bytes memory)
{
_validateAndStoreCollect(processCollectParams);
if (processCollectParams.referrerProfileIds.length == 0) {
_processCollect(processCollectParams);
} else {
_processCollectWithReferral(processCollectParams);
}
return '';
}
/// @inheritdoc IBaseFeeCollectModule
function getBasePublicationData(uint256 profileId, uint256 pubId)
public
view
virtual
returns (BaseProfilePublicationData memory)
{
return _dataByPublicationByProfile[profileId][pubId];
}
/// @inheritdoc IBaseFeeCollectModule
function calculateFee(Types.ProcessCollectParams calldata processCollectParams)
public
view
virtual
returns (uint160)
{
return
_dataByPublicationByProfile[processCollectParams.publicationCollectedProfileId][
processCollectParams.publicationCollectedId
].amount;
}
/**
* @dev Validates the Base parameters like:
* 1) Is the currency whitelisted
* 2) Is the referralFee in valid range
* 3) Is the end of collects timestamp in valid range
*
* This should be called during initializePublicationCollectModule()
*
* @param baseInitData Module initialization data (see BaseFeeCollectModuleInitData struct)
*/
function _validateBaseInitData(BaseFeeCollectModuleInitData memory baseInitData) internal virtual {
if (
!_currencyWhitelisted(baseInitData.currency) ||
baseInitData.referralFee > BPS_MAX ||
(baseInitData.endTimestamp != 0 && baseInitData.endTimestamp < block.timestamp)
) revert Errors.InitParamsInvalid();
}
/**
* @dev Stores the initial module parameters
*
* This should be called during initializePublicationCollectModule()
*
* @param profileId The token ID of the profile publishing the publication.
* @param pubId The publication ID.
* @param baseInitData Module initialization data (see BaseFeeCollectModuleInitData struct)
*/
function _storeBasePublicationCollectParameters(
uint256 profileId,
uint256 pubId,
BaseFeeCollectModuleInitData memory baseInitData
) internal virtual {
_dataByPublicationByProfile[profileId][pubId].amount = baseInitData.amount;
_dataByPublicationByProfile[profileId][pubId].collectLimit = baseInitData.collectLimit;
_dataByPublicationByProfile[profileId][pubId].currency = baseInitData.currency;
_dataByPublicationByProfile[profileId][pubId].recipient = baseInitData.recipient;
_dataByPublicationByProfile[profileId][pubId].referralFee = baseInitData.referralFee;
_dataByPublicationByProfile[profileId][pubId].followerOnly = baseInitData.followerOnly;
_dataByPublicationByProfile[profileId][pubId].endTimestamp = baseInitData.endTimestamp;
}
/**
* @dev Validates the collect action by checking that:
* 1) the collector is a follower (if enabled)
* 2) the number of collects after the action doesn't surpass the collect limit (if enabled)
* 3) the current block timestamp doesn't surpass the end timestamp (if enabled)
*
* This should be called during processCollect()
*/
function _validateAndStoreCollect(Types.ProcessCollectParams calldata processCollectParams) internal virtual {
uint96 collectsAfter = ++_dataByPublicationByProfile[processCollectParams.publicationCollectedProfileId][
processCollectParams.publicationCollectedId
].currentCollects;
if (
_dataByPublicationByProfile[processCollectParams.publicationCollectedProfileId][
processCollectParams.publicationCollectedId
].followerOnly
) {
FollowValidationLib.validateIsFollowing({
hub: HUB,
followerProfileId: processCollectParams.collectorProfileId,
followedProfileId: processCollectParams.publicationCollectedProfileId
});
}
uint256 endTimestamp = _dataByPublicationByProfile[processCollectParams.publicationCollectedProfileId][
processCollectParams.publicationCollectedId
].endTimestamp;
uint256 collectLimit = _dataByPublicationByProfile[processCollectParams.publicationCollectedProfileId][
processCollectParams.publicationCollectedId
].collectLimit;
if (collectLimit != 0 && collectsAfter > collectLimit) {
revert Errors.MintLimitExceeded();
}
if (endTimestamp != 0 && block.timestamp > endTimestamp) {
revert Errors.CollectExpired();
}
}
/**
* @dev Internal processing of a collect:
* 1. Calculation of fees
* 2. Validation that fees are what collector expected
* 3. Transfer of fees to recipient(-s) and treasury
*
* @param processCollectParams Parameters of the collect
*/
function _processCollect(Types.ProcessCollectParams calldata processCollectParams) internal virtual {
uint256 amount = calculateFee(processCollectParams);
address currency = _dataByPublicationByProfile[processCollectParams.publicationCollectedProfileId][
processCollectParams.publicationCollectedId
].currency;
_validateDataIsExpected(processCollectParams.data, currency, amount);
(address treasury, uint16 treasuryFee) = _treasuryData();
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
if (treasuryAmount > 0) {
IERC20(currency).safeTransferFrom(processCollectParams.transactionExecutor, treasury, treasuryAmount);
}
// Send amount after treasury cut, to all recipients
_transferToRecipients(processCollectParams, currency, amount - treasuryAmount);
}
/**
* @dev Internal processing of a collect with a referrals (if any).
*
* Same as _processCollect, but also includes transfer to referrals (if any):
* 1. Calculation of fees
* 2. Validation that fees are what collector expected
* 3. Transfer of fees to treasury, referrals (if any) and recipients
*
* @param processCollectParams Parameters of the collect
*/
function _processCollectWithReferral(Types.ProcessCollectParams calldata processCollectParams) internal virtual {
uint256 amount = calculateFee(processCollectParams);
address currency = _dataByPublicationByProfile[processCollectParams.publicationCollectedProfileId][
processCollectParams.publicationCollectedId
].currency;
_validateDataIsExpected(processCollectParams.data, currency, amount);
(address treasury, uint16 treasuryFee) = _treasuryData();
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
if (treasuryAmount > 0) {
IERC20(currency).safeTransferFrom(processCollectParams.transactionExecutor, treasury, treasuryAmount);
}
uint256 amountAfterReferrals = _transferToReferrals(processCollectParams, currency, amount - treasuryAmount);
_transferToRecipients(processCollectParams, currency, amountAfterReferrals);
}
/**
* @dev Tranfers the fee to recipient(-s)
*
* Override this to add additional functionality (e.g. multiple recipients)
*
* @param processCollectParams Parameters of the collect
* @param currency Currency of the transaction
* @param amount Amount to transfer to recipient(-s)
*/
function _transferToRecipients(
Types.ProcessCollectParams calldata processCollectParams,
address currency,
uint256 amount
) internal virtual {
address recipient = _dataByPublicationByProfile[processCollectParams.publicationCollectedProfileId][
processCollectParams.publicationCollectedId
].recipient;
if (amount > 0) {
IERC20(currency).safeTransferFrom(processCollectParams.transactionExecutor, recipient, amount);
}
}
/**
* @dev Tranfers the part of fee to referral(-s)
*
* Override this to add additional functionality (e.g. different amounts to different referrals, etc)
*
* @param processCollectParams Parameters of the collect
* @param currency Currency of the transaction
* @param amount Amount of the fee after subtracting the Treasury part.
*/
function _transferToReferrals(
Types.ProcessCollectParams calldata processCollectParams,
address currency,
uint256 amount
) internal virtual returns (uint256) {
uint256 referralFee = _dataByPublicationByProfile[processCollectParams.publicationCollectedProfileId][
processCollectParams.publicationCollectedId
].referralFee;
uint256 totalReferralsAmount;
if (referralFee != 0) {
// The reason we levy the referral fee on the adjusted amount is so that referral fees
// don't bypass the treasury fee, in essence referrals pay their fair share to the treasury.
totalReferralsAmount = (amount * referralFee) / BPS_MAX;
uint256 numberOfReferrals = processCollectParams.referrerProfileIds.length;
uint256 amountPerReferral = totalReferralsAmount / numberOfReferrals;
if (amountPerReferral > 0) {
uint256 i;
while (i < numberOfReferrals) {
address referralRecipient = IERC721(HUB).ownerOf(processCollectParams.referrerProfileIds[i]);
// Send referral fee in ERC20 tokens
IERC20(currency).safeTransferFrom(
processCollectParams.transactionExecutor,
referralRecipient,
amountPerReferral
);
unchecked {
++i;
}
}
}
}
return amount - totalReferralsAmount;
}
}