-
Notifications
You must be signed in to change notification settings - Fork 862
/
LimitedFeeCollectModule.sol
205 lines (183 loc) · 8.31 KB
/
LimitedFeeCollectModule.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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;
import {ICollectModule} from '../../../interfaces/ICollectModule.sol';
import {Errors} from '../../../libraries/Errors.sol';
import {FeeModuleBase} from '../FeeModuleBase.sol';
import {ModuleBase} from '../ModuleBase.sol';
import {FollowValidationModuleBase} from '../FollowValidationModuleBase.sol';
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
/**
* @notice A struct containing the necessary data to execute collect actions on a publication.
*
* @param collectLimit The maximum number of collects for this publication.
* @param currentCollects The current number of collects for this publication.
* @param amount The collecting cost associated with this publication.
* @param currency The currency associated with this publication.
* @param recipient The recipient address associated with this publication.
* @param referralFee The referral fee associated with this publication.
* @param followerOnly Whether only followers should be able to collect.
*/
struct ProfilePublicationData {
uint256 collectLimit;
uint256 currentCollects;
uint256 amount;
address currency;
address recipient;
uint16 referralFee;
bool followerOnly;
}
/**
* @title LimitedFeeCollectModule
* @author Lens Protocol
*
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface and
* the FeeCollectModuleBase abstract contract.
*
* This module works by allowing limited collects for a publication indefinitely.
*/
contract LimitedFeeCollectModule is FeeModuleBase, FollowValidationModuleBase, ICollectModule {
using SafeERC20 for IERC20;
mapping(uint256 => mapping(uint256 => ProfilePublicationData))
internal _dataByPublicationByProfile;
constructor(address hub, address moduleGlobals) FeeModuleBase(moduleGlobals) ModuleBase(hub) {}
/**
* @notice This collect module levies a fee on collects and supports referrals. Thus, we need to decode data.
*
* @param profileId The profile ID of the publication to initialize this module for's publishing profile.
* @param pubId The publication ID of the publication to initialize this module for.
* @param data The arbitrary data parameter, decoded into:
* uint256 collectLimit: The maximum amount of collects.
* uint256 amount: The currency total amount to levy.
* address currency: The currency address, must be internally whitelisted.
* address recipient: The custom recipient address to direct earnings to.
* uint16 referralFee: The referral fee to set.
* bool followerOnly: Whether only followers should be able to collect.
*
* @return bytes An abi encoded bytes parameter, which is the same as the passed data parameter.
*/
function initializePublicationCollectModule(
uint256 profileId,
uint256 pubId,
bytes calldata data
) external override onlyHub returns (bytes memory) {
(
uint256 collectLimit,
uint256 amount,
address currency,
address recipient,
uint16 referralFee,
bool followerOnly
) = abi.decode(data, (uint256, uint256, address, address, uint16, bool));
if (
collectLimit == 0 ||
!_currencyWhitelisted(currency) ||
recipient == address(0) ||
referralFee > BPS_MAX ||
amount == 0
) revert Errors.InitParamsInvalid();
_dataByPublicationByProfile[profileId][pubId].collectLimit = collectLimit;
_dataByPublicationByProfile[profileId][pubId].amount = amount;
_dataByPublicationByProfile[profileId][pubId].currency = currency;
_dataByPublicationByProfile[profileId][pubId].recipient = recipient;
_dataByPublicationByProfile[profileId][pubId].referralFee = referralFee;
_dataByPublicationByProfile[profileId][pubId].followerOnly = followerOnly;
return data;
}
/**
* @dev Processes a collect by:
* 1. Ensuring the collector is a follower
* 2. Ensuring the collect does not pass the collect limit
* 3. Charging a fee
*/
function processCollect(
uint256 referrerProfileId,
address collector,
uint256 profileId,
uint256 pubId,
bytes calldata data
) external override onlyHub {
if (_dataByPublicationByProfile[profileId][pubId].followerOnly)
_checkFollowValidity(profileId, collector);
if (
_dataByPublicationByProfile[profileId][pubId].currentCollects >=
_dataByPublicationByProfile[profileId][pubId].collectLimit
) {
revert Errors.MintLimitExceeded();
} else {
++_dataByPublicationByProfile[profileId][pubId].currentCollects;
if (referrerProfileId == profileId) {
_processCollect(collector, profileId, pubId, data);
} else {
_processCollectWithReferral(referrerProfileId, collector, profileId, pubId, data);
}
}
}
/**
* @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 ProfilePublicationData The ProfilePublicationData struct mapped to that publication.
*/
function getPublicationData(uint256 profileId, uint256 pubId)
external
view
returns (ProfilePublicationData memory)
{
return _dataByPublicationByProfile[profileId][pubId];
}
function _processCollect(
address collector,
uint256 profileId,
uint256 pubId,
bytes calldata data
) internal {
uint256 amount = _dataByPublicationByProfile[profileId][pubId].amount;
address currency = _dataByPublicationByProfile[profileId][pubId].currency;
_validateDataIsExpected(data, currency, amount);
(address treasury, uint16 treasuryFee) = _treasuryData();
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
uint256 treasuryAmount = (amount * treasuryFee) / BPS_MAX;
uint256 adjustedAmount = amount - treasuryAmount;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
function _processCollectWithReferral(
uint256 referrerProfileId,
address collector,
uint256 profileId,
uint256 pubId,
bytes calldata data
) internal {
uint256 amount = _dataByPublicationByProfile[profileId][pubId].amount;
address currency = _dataByPublicationByProfile[profileId][pubId].currency;
_validateDataIsExpected(data, currency, amount);
uint256 referralFee = _dataByPublicationByProfile[profileId][pubId].referralFee;
address treasury;
uint256 treasuryAmount;
// Avoids stack too deep
{
uint16 treasuryFee;
(treasury, treasuryFee) = _treasuryData();
treasuryAmount = (amount * treasuryFee) / BPS_MAX;
}
uint256 adjustedAmount = amount - treasuryAmount;
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.
uint256 referralAmount = (adjustedAmount * referralFee) / BPS_MAX;
adjustedAmount = adjustedAmount - referralAmount;
address referralRecipient = IERC721(HUB).ownerOf(referrerProfileId);
IERC20(currency).safeTransferFrom(collector, referralRecipient, referralAmount);
}
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
}