-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathCrowdfund.sol
494 lines (464 loc) · 20 KB
/
Crowdfund.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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
// SPDX-License-Identifier: Beta Software
pragma solidity ^0.8;
import "../utils/LibAddress.sol";
import "../utils/LibRawResult.sol";
import "../utils/LibSafeCast.sol";
import "../tokens/ERC721Receiver.sol";
import "../party/Party.sol";
import "../globals/IGlobals.sol";
import "../gatekeepers/IGateKeeper.sol";
import "./CrowdfundNFT.sol";
// Base contract for AuctionCrowdfund/BuyCrowdfund.
// Holds post-win/loss logic. E.g., burning contribution NFTs and creating a
// party after winning.
abstract contract Crowdfund is ERC721Receiver, CrowdfundNFT {
using LibRawResult for bytes;
using LibSafeCast for uint256;
using LibAddress for address payable;
enum CrowdfundLifecycle {
Invalid,
Active,
Expired,
Busy, // Temporary. mid-settlement state
Lost,
Won
}
// PartyGovernance options that must be known and fixed at crowdfund creation.
// This is a subset of PartyGovernance.GovernanceOpts.
struct FixedGovernanceOpts {
// Address of initial party hosts.
address[] hosts;
// How long people can vote on a proposal.
uint40 voteDuration;
// How long to wait after a proposal passes before it can be
// executed.
uint40 executionDelay;
// Minimum ratio of accept votes to consider a proposal passed,
// in bps, where 10,000 == 100%.
uint16 passThresholdBps;
// Fee bps for governance distributions.
uint16 feeBps;
// Fee recipeint for governance distributions.
address payable feeRecipient;
}
// Options to be passed into `_initialize()` when the crowdfund is created.
struct CrowdfundOptions {
string name;
string symbol;
address payable splitRecipient;
uint16 splitBps;
address initialContributor;
address initialDelegate;
IGateKeeper gateKeeper;
bytes12 gateKeeperId;
FixedGovernanceOpts governanceOpts;
}
// A record of a single contribution made by a user.
// Stored in `_contributionsByContributor`.
struct Contribution {
// The value of `Crowdfund.totalContributions` when this contribution was made.
uint96 previousTotalContributions;
// How much was this contribution.
uint96 amount;
}
error PartyAlreadyExistsError(Party party);
error WrongLifecycleError(CrowdfundLifecycle lc);
error InvalidGovernanceOptionsError(bytes32 actualHash, bytes32 expectedHash);
error InvalidDelegateError();
error NoPartyError();
error OnlyContributorAllowedError();
error NotAllowedByGateKeeperError(address contributor, IGateKeeper gateKeeper, bytes12 gateKeeperId, bytes gateData);
error SplitRecipientAlreadyBurnedError();
error InvalidBpsError(uint16 bps);
event Burned(address contributor, uint256 ethUsed, uint256 ethOwed, uint256 votingPower);
event Contributed(address contributor, uint256 amount, address delegate, uint256 previousTotalContributions);
// The `Globals` contract storing global configuration values. This contract
// is immutable and it’s address will never change.
IGlobals private immutable _GLOBALS;
/// @notice The party instance created by `_createParty()`, if any after a
/// successful crowdfund.
Party public party;
/// @notice The total (recorded) ETH contributed to this crowdfund.
uint96 public totalContributions;
/// @notice The gatekeeper contract to use (if non-null) to restrict who can
/// contribute to the party.
IGateKeeper public gateKeeper;
/// @notice The ID of the gatekeeper strategy to use.
bytes12 public gateKeeperId;
/// @notice Who will receive a reserved portion of governance power when
/// the governance party is created.
address payable public splitRecipient;
/// @notice How much governance power to reserve for `splitRecipient`,
/// in bps, where 10,000 = 100%.
uint16 public splitBps;
// Whether the share for split recipient has been claimed through `burn()`.
bool private _splitRecipientHasBurned;
/// @notice Hash of party governance options passed into `initialize()`.
/// Used to check whether the `GovernanceOpts` passed into
/// `_createParty()` matches.
bytes32 public governanceOptsHash;
/// @notice Who a contributor last delegated to.
mapping(address => address) public delegationsByContributor;
// Array of contributions by a contributor.
// One is created for every nonzero contribution made.
mapping (address => Contribution[]) private _contributionsByContributor;
// Set the `Globals` contract.
constructor(IGlobals globals) CrowdfundNFT(globals) {
_GLOBALS = globals;
}
// Initialize storage for proxy contracts, credit initial contribution (if
// any), and setup gatekeeper.
function _initialize(CrowdfundOptions memory opts)
internal
{
CrowdfundNFT._initialize(opts.name, opts.symbol);
// Check that BPS values do not exceed the max.
if (opts.governanceOpts.feeBps > 1e4) {
revert InvalidBpsError(opts.governanceOpts.feeBps);
}
if (opts.governanceOpts.passThresholdBps > 1e4) {
revert InvalidBpsError(opts.governanceOpts.passThresholdBps);
}
if (opts.splitBps > 1e4) {
revert InvalidBpsError(opts.splitBps);
}
governanceOptsHash = _hashFixedGovernanceOpts(opts.governanceOpts);
splitRecipient = opts.splitRecipient;
splitBps = opts.splitBps;
// If the deployer passed in some ETH during deployment, credit them
// for the initial contribution.
uint96 initialBalance = address(this).balance.safeCastUint256ToUint96();
if (initialBalance > 0) {
// If this contract has ETH, either passed in during deployment or
// pre-existing, credit it to the `initialContributor`.
_contribute(opts.initialContributor, initialBalance, opts.initialDelegate, 0, "");
}
// Set up gatekeeper after initial contribution (initial always gets in).
gateKeeper = opts.gateKeeper;
gateKeeperId = opts.gateKeeperId;
}
/// @notice Burn the participation NFT for `contributor`, potentially
/// minting voting power and/or refunding unused ETH. `contributor`
/// may also be the split recipient, regardless of whether they are
/// also a contributor or not. This can be called by anyone on a
/// contributor's behalf to unlock their voting power in the
/// governance stage ensuring delegates receive their voting
/// power and governance is not stalled.
/// @dev If the party has won, someone needs to call `_createParty()` first. After
/// which, `burn()` will refund unused ETH and mint governance tokens for the
/// given `contributor`.
/// If the party has lost, this will only refund unused ETH (all of it) for
/// the given `contributor`.
/// @param contributor The contributor whose NFT to burn for.
function burn(address payable contributor)
public
{
return _burn(contributor, getCrowdfundLifecycle(), party);
}
/// @notice `burn()` in batch form.
/// @param contributors The contributors whose NFT to burn for.
function batchBurn(address payable[] calldata contributors)
external
{
Party party_ = party;
CrowdfundLifecycle lc = getCrowdfundLifecycle();
for (uint256 i = 0; i < contributors.length; ++i) {
_burn(contributors[i], lc, party_);
}
}
/// @notice Contribute to this crowdfund and/or update your delegation for the
/// governance phase should the crowdfund succeed.
/// For restricted crowdfunds, `gateData` can be provided to prove
/// membership to the gatekeeper.
/// @param delegate The address to delegate to for the governance phase.
/// @param gateData Data to pass to the gatekeeper to prove eligibility.
function contribute(address delegate, bytes memory gateData)
public
payable
{
_contribute(
msg.sender,
msg.value.safeCastUint256ToUint96(),
delegate,
// We cannot use `address(this).balance - msg.value` as the previous
// total contributions in case someone forces (suicides) ETH into this
// contract. This wouldn't be such a big deal for open crowdfunds
// but private ones (protected by a gatekeeper) could be griefed
// because it would ultimately result in governance power that
// is unattributed/unclaimable, meaning that party will never be
// able to reach 100% consensus.
totalContributions,
gateData
);
}
/// @inheritdoc EIP165
function supportsInterface(bytes4 interfaceId)
public
override(ERC721Receiver, CrowdfundNFT)
pure
returns (bool)
{
return ERC721Receiver.supportsInterface(interfaceId) ||
CrowdfundNFT.supportsInterface(interfaceId);
}
/// @notice Retrieve info about a participant's contributions.
/// @dev This will only be called off-chain so doesn't have to be optimal.
/// @param contributor The contributor to retrieve contributions for.
/// @return ethContributed The total ETH contributed by `contributor`.
/// @return ethUsed The total ETH used by `contributor` to acquire the NFT.
/// @return ethOwed The total ETH refunded back to `contributor`.
/// @return votingPower The total voting power minted to `contributor`.
function getContributorInfo(address contributor)
external
view
returns (
uint256 ethContributed,
uint256 ethUsed,
uint256 ethOwed,
uint256 votingPower
)
{
CrowdfundLifecycle lc = getCrowdfundLifecycle();
Contribution[] storage contributions = _contributionsByContributor[contributor];
uint256 numContributions = contributions.length;
for (uint256 i = 0; i < numContributions; ++i) {
ethContributed += contributions[i].amount;
}
if (lc == CrowdfundLifecycle.Won || lc == CrowdfundLifecycle.Lost) {
(ethUsed, ethOwed, votingPower) = _getFinalContribution(contributor);
}
}
/// @notice Get the current lifecycle of the crowdfund.
function getCrowdfundLifecycle() public virtual view returns (CrowdfundLifecycle);
// Get the final sale price of the bought assets. This will also be the total
// voting power of the governance party.
function _getFinalPrice() internal virtual view returns (uint256);
// Can be called after a party has won.
// Deploys and initializes a a `Party` instance via the `PartyFactory`
// and transfers the bought NFT to it.
// After calling this, anyone can burn CF tokens on a contributor's behalf
// with the `burn()` function.
function _createParty(
IPartyFactory partyFactory,
FixedGovernanceOpts memory governanceOpts,
IERC721[] memory preciousTokens,
uint256[] memory preciousTokenIds
)
internal
returns (Party party_)
{
if (party != Party(payable(0))) {
revert PartyAlreadyExistsError(party);
}
{
bytes16 governanceOptsHash_ = _hashFixedGovernanceOpts(governanceOpts);
if (governanceOptsHash_ != governanceOptsHash) {
revert InvalidGovernanceOptionsError(governanceOptsHash_, governanceOptsHash);
}
}
party = party_ = partyFactory
.createParty(
address(this),
Party.PartyOptions({
name: name,
symbol: symbol,
governance: PartyGovernance.GovernanceOpts({
hosts: governanceOpts.hosts,
voteDuration: governanceOpts.voteDuration,
executionDelay: governanceOpts.executionDelay,
passThresholdBps: governanceOpts.passThresholdBps,
totalVotingPower: _getFinalPrice().safeCastUint256ToUint96(),
feeBps: governanceOpts.feeBps,
feeRecipient: governanceOpts.feeRecipient
})
}),
preciousTokens,
preciousTokenIds
);
// Transfer the acquired NFTs to the new party.
for (uint256 i = 0; i < preciousTokens.length; ++i) {
preciousTokens[i].transferFrom(address(this), address(party_), preciousTokenIds[i]);
}
}
// Overloaded single token wrapper for _createParty()
function _createParty(
IPartyFactory partyFactory,
FixedGovernanceOpts memory governanceOpts,
IERC721 preciousToken,
uint256 preciousTokenId
)
internal
returns (Party party_)
{
IERC721[] memory tokens = new IERC721[](1);
tokens[0] = preciousToken;
uint256[] memory tokenIds = new uint256[](1);
tokenIds[0] = preciousTokenId;
return _createParty(partyFactory, governanceOpts, tokens, tokenIds);
}
function _hashFixedGovernanceOpts(FixedGovernanceOpts memory opts)
internal
pure
returns (bytes16 h)
{
// Hash in place.
assembly {
// Replace the address[] hosts field with its hash temporarily.
let oldHostsFieldValue := mload(opts)
mstore(opts, keccak256(add(mload(opts), 0x20), mul(mload(mload(opts)), 32)))
// Hash the entire struct.
h := keccak256(opts, 0xC0)
// Restore old hosts field value.
mstore(opts, oldHostsFieldValue)
}
}
function _getFinalContribution(address contributor)
internal
view
returns (uint256 ethUsed, uint256 ethOwed, uint256 votingPower)
{
uint256 totalEthUsed = _getFinalPrice();
{
Contribution[] storage contributions = _contributionsByContributor[contributor];
uint256 numContributions = contributions.length;
for (uint256 i = 0; i < numContributions; ++i) {
Contribution memory c = contributions[i];
if (c.previousTotalContributions >= totalEthUsed) {
// This entire contribution was not used.
ethOwed += c.amount;
} else if (c.previousTotalContributions + c.amount <= totalEthUsed) {
// This entire contribution was used.
ethUsed += c.amount;
} else {
// This contribution was partially used.
uint256 partialEthUsed = totalEthUsed - c.previousTotalContributions;
ethUsed += partialEthUsed;
ethOwed = c.amount - partialEthUsed;
}
}
}
// one SLOAD with optimizer on
address splitRecipient_ = splitRecipient;
uint256 splitBps_ = splitBps;
if (splitRecipient_ == address(0)) {
splitBps_ = 0;
}
votingPower = ((1e4 - splitBps_) * ethUsed) / 1e4;
if (splitRecipient_ == contributor) {
// Split recipient is also the contributor so just add the split
// voting power.
votingPower += (splitBps_ * totalEthUsed + (1e4 - 1)) / 1e4; // round up
}
}
function _contribute(
address contributor,
uint96 amount,
address delegate,
uint96 previousTotalContributions,
bytes memory gateData
)
internal
{
// Require a non-null delegate.
if (delegate == address(0)) {
revert InvalidDelegateError();
}
// Must not be blocked by gatekeeper.
if (gateKeeper != IGateKeeper(address(0))) {
if (!gateKeeper.isAllowed(contributor, gateKeeperId, gateData)) {
revert NotAllowedByGateKeeperError(
contributor,
gateKeeper,
gateKeeperId,
gateData
);
}
}
// Update delegate.
// OK if this happens out of cycle.
delegationsByContributor[contributor] = delegate;
emit Contributed(contributor, amount, delegate, previousTotalContributions);
// OK to contribute with zero just to update delegate.
if (amount != 0) {
// Increase total contributions.
totalContributions += amount;
// Only allow contributions while the crowdfund is active.
{
CrowdfundLifecycle lc = getCrowdfundLifecycle();
if (lc != CrowdfundLifecycle.Active) {
revert WrongLifecycleError(lc);
}
}
// Create contributions entry for this contributor.
Contribution[] storage contributions = _contributionsByContributor[contributor];
uint256 numContributions = contributions.length;
if (numContributions >= 1) {
Contribution memory lastContribution = contributions[numContributions - 1];
if (lastContribution.previousTotalContributions == previousTotalContributions) {
// No one else has contributed since so just reuse the last entry.
lastContribution.amount += amount;
contributions[numContributions - 1] = lastContribution;
return;
}
}
// Add a new contribution entry.
contributions.push(Contribution({
previousTotalContributions: previousTotalContributions,
amount: amount
}));
// Mint a participation NFT if this is their first contribution.
if (numContributions == 0) {
_mint(contributor);
}
}
}
function _burn(address payable contributor, CrowdfundLifecycle lc, Party party_)
private
{
// If the CF has won, a party must have been created prior.
if (lc == CrowdfundLifecycle.Won) {
if (party_ == Party(payable(0))) {
revert NoPartyError();
}
} else if (lc != CrowdfundLifecycle.Lost) {
// Otherwise it must have lost.
revert WrongLifecycleError(lc);
}
// Split recipient can burn even if they don't have a token.
if (contributor == splitRecipient) {
if (_splitRecipientHasBurned) {
revert SplitRecipientAlreadyBurnedError();
}
_splitRecipientHasBurned = true;
}
// Revert if already burned or does not exist.
if (splitRecipient != contributor || _doesTokenExistFor(contributor)) {
CrowdfundNFT._burn(contributor);
}
// Compute the contributions used and owed to the contributor, along
// with the voting power they'll have in the governance stage.
(uint256 ethUsed, uint256 ethOwed, uint256 votingPower) =
_getFinalContribution(contributor);
if (votingPower > 0) {
// Get the address to delegate voting power to. If null, delegate to self.
address delegate = delegationsByContributor[contributor];
if (delegate == address(0)) {
// Delegate can be unset for the split recipient if they never
// contribute. Self-delegate if this occurs.
delegate = contributor;
}
// Mint governance NFT for the contributor.
party_.mint(
contributor,
votingPower,
delegate
);
}
// Refund any ETH owed back to the contributor.
contributor.transferEth(ethOwed);
emit Burned(contributor, ethUsed, ethOwed, votingPower);
}
function _getPartyFactory() internal view returns (IPartyFactory) {
return IPartyFactory(_GLOBALS.getAddress(LibGlobals.GLOBAL_PARTY_FACTORY));
}
}