/
BuyCrowdfundBase.sol
192 lines (180 loc) · 7.12 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
// SPDX-License-Identifier: Beta Software
// http://ipfs.io/ipfs/QmbGX2MFCaMAsMNMugRFND6DtYygRkwkvrqEyTKhTdBLo5
pragma solidity 0.8.17;
import "../tokens/IERC721.sol";
import "../party/Party.sol";
import "../utils/Implementation.sol";
import "../utils/LibSafeERC721.sol";
import "../utils/LibRawResult.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;
using LibRawResult for bytes;
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 bid on 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;
// 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;
// Governance options.
FixedGovernanceOpts governanceOpts;
}
event Won(Party party, IERC721 token, uint256 tokenId, uint256 settledPrice);
event Lost();
error MaximumPriceError(uint96 callValue, uint96 maximumPrice);
error NoContributionsError();
error FailedToBuyNFTError(IERC721 token, uint256 tokenId);
error InvalidCallTargetError(address callTarget);
/// @notice When this crowdfund expires.
uint40 public expiry;
/// @notice Maximum amount this crowdfund will pay for the NFT. If zero, no maximum.
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,
gateKeeper: opts.gateKeeper,
gateKeeperId: opts.gateKeeperId,
governanceOpts: opts.governanceOpts
}));
}
// 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 calldata callData,
FixedGovernanceOpts memory governanceOpts,
bool isValidatedGovernanceOpts
)
internal
onlyDelegateCall
returns (Party party_)
{
// Ensure the call target isn't trying to reenter
if (callTarget == address(this)) {
revert InvalidCallTargetError(callTarget);
}
// Check that the crowdfund is still active.
CrowdfundLifecycle lc = getCrowdfundLifecycle();
if (lc != CrowdfundLifecycle.Active) {
revert WrongLifecycleError(lc);
}
uint96 totalContributions_ = totalContributions;
// Prevent unaccounted ETH from being used to inflate the price and
// create "ghost shares" in voting power.
if (callValue > totalContributions_) {
revert ExceedsTotalContributionsError(callValue, totalContributions_);
}
// Check that the call value is under the maximum price.
{
uint96 maximumPrice_ = maximumPrice;
if (callValue > maximumPrice_) {
revert MaximumPriceError(callValue, maximumPrice_);
}
}
// Temporarily set to non-zero as a reentrancy guard.
settledPrice = type(uint96).max;
{
// Execute the call to buy the NFT.
(bool s, bytes memory r) = callTarget.call{ value: callValue }(callData);
if (!s) {
r.rawRevert();
}
}
// Make sure we acquired the NFT we want.
if (token.safeOwnerOf(tokenId) == address(this)) {
if (address(this).balance >= totalContributions_) {
// If the purchase was free or the NFT was "gifted" to us,
// refund all contributors by declaring we lost.
settledPrice = 0;
expiry = 0;
emit Lost();
} else {
settledPrice = callValue;
emit Won(
// Create a party around the newly bought NFT.
party_ = _createParty(
governanceOpts,
isValidatedGovernanceOpts,
token,
tokenId
),
token,
tokenId,
callValue
);
}
} else {
revert FailedToBuyNFTError(token, tokenId);
}
}
/// @inheritdoc Crowdfund
function getCrowdfundLifecycle() public override view returns (CrowdfundLifecycle) {
// If there is a settled price then we tried to buy the NFT.
if (settledPrice != 0) {
return address(party) != address(0)
// If we have a party, then we succeeded buying the NFT.
? CrowdfundLifecycle.Won
// Otherwise we're in the middle of the `buy()`.
: CrowdfundLifecycle.Busy;
}
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
override
view
returns (uint256)
{
return settledPrice;
}
}