-
Notifications
You must be signed in to change notification settings - Fork 33
/
BuyCrowdfundBase.sol
241 lines (225 loc) · 9.31 KB
/
BuyCrowdfundBase.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
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;
import "../tokens/IERC721.sol";
import "../party/Party.sol";
import "../utils/LibSafeERC721.sol";
import "../globals/IGlobals.sol";
import "../gatekeepers/IGateKeeper.sol";
import "./Crowdfund.sol";
// Base for BuyCrowdfund and CollectionBuyCrowdfund
abstract contract BuyCrowdfundBase is Crowdfund {
using LibSafeERC721 for IERC721;
using LibSafeCast for uint256;
struct BuyCrowdfundBaseOptions {
// The name of the crowdfund.
// This will also carry over to the governance party.
string name;
// The token symbol for both the crowdfund and the governance NFTs.
string symbol;
// Customization preset ID to use for the crowdfund and governance NFTs.
uint256 customizationPresetId;
// How long this crowdfund has to buy the NFT, in seconds.
uint40 duration;
// Maximum amount this crowdfund will pay for the NFT.
uint96 maximumPrice;
// An address that receives an extra share of the final voting power
// when the party transitions into governance.
address payable splitRecipient;
// What percentage (in bps) of the final total voting power `splitRecipient`
// receives.
uint16 splitBps;
// If ETH is attached during deployment, it will be interpreted
// as a contribution. This is who gets credit for that contribution.
address initialContributor;
// If there is an initial contribution, this is who they will delegate their
// voting power to when the crowdfund transitions to governance.
address initialDelegate;
// Minimum amount of ETH that can be contributed to this crowdfund per address.
uint96 minContribution;
// Maximum amount of ETH that can be contributed to this crowdfund per address.
uint96 maxContribution;
// The gatekeeper contract to use (if non-null) to restrict who can
// contribute to this crowdfund.
IGateKeeper gateKeeper;
// The gatekeeper contract to use (if non-null).
bytes12 gateKeeperId;
// Fixed governance options (i.e. cannot be changed) that the governance
// `Party` will be created with if the crowdfund succeeds.
FixedGovernanceOpts governanceOpts;
// Options for the proposal engine that the governance `Party` will be
// created with if the crowdfund succeeds.
ProposalStorage.ProposalEngineOpts proposalEngineOpts;
}
event Won(Party party, IERC721[] tokens, uint256[] tokenIds, uint256 settledPrice);
event Lost();
error MaximumPriceError(uint96 callValue, uint96 maximumPrice);
error NoContributionsError();
error CallProhibitedError(address target, bytes data);
error FailedToBuyNFTError(IERC721 token, uint256 tokenId);
/// @notice When this crowdfund expires.
uint40 public expiry;
/// @notice Maximum amount this crowdfund will pay for the NFT.
uint96 public maximumPrice;
/// @notice What the NFT was actually bought for.
uint96 public settledPrice;
// Set the `Globals` contract.
constructor(IGlobals globals) Crowdfund(globals) {}
// Initialize storage for proxy contracts.
function _initialize(BuyCrowdfundBaseOptions memory opts) internal {
expiry = uint40(opts.duration + block.timestamp);
maximumPrice = opts.maximumPrice;
Crowdfund._initialize(
CrowdfundOptions({
name: opts.name,
symbol: opts.symbol,
customizationPresetId: opts.customizationPresetId,
splitRecipient: opts.splitRecipient,
splitBps: opts.splitBps,
initialContributor: opts.initialContributor,
initialDelegate: opts.initialDelegate,
minContribution: opts.minContribution,
maxContribution: opts.maxContribution,
gateKeeper: opts.gateKeeper,
gateKeeperId: opts.gateKeeperId,
governanceOpts: opts.governanceOpts,
proposalEngineOpts: opts.proposalEngineOpts
})
);
}
// Execute arbitrary calldata to perform a buy, creating a party
// if it successfully buys the NFT.
function _buy(
IERC721 token,
uint256 tokenId,
address payable callTarget,
uint96 callValue,
bytes memory callData
) internal returns (bool success, bytes memory revertData) {
// Check that the call is not prohibited.
if (!_isCallAllowed(callTarget, callData, token)) {
revert CallProhibitedError(callTarget, callData);
}
// Check that the call value is under the maximum price.
{
uint96 maximumPrice_ = maximumPrice;
if (callValue > maximumPrice_) {
revert MaximumPriceError(callValue, maximumPrice_);
}
}
// Execute the call to buy the NFT.
(bool s, bytes memory r) = callTarget.call{ value: callValue }(callData);
if (!s) {
return (false, r);
}
// Return whether the NFT was successfully bought.
return (token.safeOwnerOf(tokenId) == address(this), "");
}
function _finalize(
IERC721[] memory tokens,
uint256[] memory tokenIds,
uint96 totalEthUsed,
FixedGovernanceOpts memory governanceOpts,
ProposalStorage.ProposalEngineOpts memory proposalEngineOpts,
bool isValidatedGovernanceOpts
) internal returns (Party party_) {
{
// Prevent unaccounted ETH from being used to inflate the price and
// create "ghost shares" in voting power.
uint96 totalContributions_ = totalContributions;
if (totalEthUsed > totalContributions_) {
revert ExceedsTotalContributionsError(totalEthUsed, totalContributions_);
}
}
if (totalEthUsed != 0) {
// Create a party around the newly bought NFTs and finalize a win.
settledPrice = totalEthUsed;
party_ = _createParty(
governanceOpts,
proposalEngineOpts,
isValidatedGovernanceOpts,
tokens,
tokenIds
);
emit Won(party_, tokens, tokenIds, totalEthUsed);
} else {
// If all NFTs were purchased for free or were all "gifted" to us,
// refund all contributors by finalizing a loss.
settledPrice = 0;
expiry = uint40(block.timestamp);
emit Lost();
}
// Notify third-party platforms that the crowdfund NFT metadata has
// updated for all tokens.
emit BatchMetadataUpdate(0, type(uint256).max);
}
function _finalize(
IERC721 token,
uint256 tokenId,
uint96 totalEthUsed,
FixedGovernanceOpts memory governanceOpts,
ProposalStorage.ProposalEngineOpts memory proposalEngineOpts,
bool isValidatedGovernanceOpts
) internal returns (Party party_) {
IERC721[] memory tokens = new IERC721[](1);
tokens[0] = token;
uint256[] memory tokenIds = new uint256[](1);
tokenIds[0] = tokenId;
return
_finalize(
tokens,
tokenIds,
totalEthUsed,
governanceOpts,
proposalEngineOpts,
isValidatedGovernanceOpts
);
}
/// @inheritdoc Crowdfund
function getCrowdfundLifecycle() public view override returns (CrowdfundLifecycle) {
// If there is a settled price then we tried to buy the NFT.
if (settledPrice != 0) {
return
address(party) != address(0)
? CrowdfundLifecycle.Won // If we have a party, then we succeeded buying the NFT.
: CrowdfundLifecycle.Busy; // Otherwise we're in the middle of the `buy()`.
}
if (block.timestamp >= expiry) {
// Expired, but nothing to do so skip straight to lost, or NFT was
// acquired for free so refund contributors and trigger lost.
return CrowdfundLifecycle.Lost;
}
return CrowdfundLifecycle.Active;
}
function _getFinalPrice() internal view override returns (uint256) {
return settledPrice;
}
function _isCallAllowed(
address payable callTarget,
bytes memory callData,
IERC721 token
) private view returns (bool isAllowed) {
// Ensure the call target isn't trying to reenter
if (callTarget == address(this)) {
return false;
}
if (callTarget == address(token) && callData.length >= 4) {
// Get the function selector of the call (first 4 bytes of calldata).
bytes4 selector;
assembly {
selector := and(
mload(add(callData, 32)),
0xffffffff00000000000000000000000000000000000000000000000000000000
)
}
// Prevent approving the NFT to be transferred out from the crowdfund.
if (
selector == IERC721.approve.selector ||
selector == IERC721.setApprovalForAll.selector
) {
return false;
}
}
// All other calls are allowed.
return true;
}
}