-
Notifications
You must be signed in to change notification settings - Fork 423
/
NounsDAOData.sol
380 lines (329 loc) · 16.6 KB
/
NounsDAOData.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
// SPDX-License-Identifier: GPL-3.0
/// @title Nouns DAO Data Contract
/*********************************
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░█████████░░█████████░░░ *
* ░░░░░░██░░░████░░██░░░████░░░ *
* ░░██████░░░████████░░░████░░░ *
* ░░██░░██░░░████░░██░░░████░░░ *
* ░░██░░██░░░████░░██░░░████░░░ *
* ░░░░░░█████████░░█████████░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *
*********************************/
pragma solidity ^0.8.19;
import { OwnableUpgradeable } from '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
import { NounsDAOProposals } from '../NounsDAOProposals.sol';
import { NounsTokenLike, NounsDAOTypes } from '../NounsDAOInterfaces.sol';
import { SignatureChecker } from '../../external/openzeppelin/SignatureChecker.sol';
import { UUPSUpgradeable } from '@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol';
import { NounsDAODataEvents } from './NounsDAODataEvents.sol';
interface INounsDAO {
function proposalsV3(uint256 proposalId) external view returns (NounsDAOTypes.ProposalCondensedV3 memory);
}
contract NounsDAOData is OwnableUpgradeable, UUPSUpgradeable, NounsDAODataEvents {
/**
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
* ERRORS
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
*/
error MustBeNounerOrPaySufficientFee();
error SlugAlreadyUsed();
error SlugDoesNotExist();
error AmountExceedsBalance();
error FailedWithdrawingETH(bytes data);
error InvalidSignature();
error InvalidSupportValue();
error ProposalToUpdateMustBeUpdatable();
error OnlyProposerCanCreateUpdateCandidate();
error UpdateProposalCandidatesOnlyWorkWithProposalsBySigs();
/**
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
* EVENTS
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
*/
/**
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
* STATE
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
*/
/// @notice The number of blocks before the current block where account votes are counted.
uint256 public constant PRIOR_VOTES_BLOCKS_AGO = 1;
/// @notice The Nouns token contract.
NounsTokenLike public immutable nounsToken;
/// @notice The Nouns DAO contract.
address public immutable nounsDao;
/// @notice The cost non-Nouners must pay in ETH in order to emit a new proposal candidate event from this contract.
uint256 public createCandidateCost;
/// @notice The cost non-Nouners must pay in ETH in order to emit a proposal candidate update event from this contract.
uint256 public updateCandidateCost;
/// @notice The state of which (proposer,slug) pairs have been used to create a proposal candidate.
mapping(address => mapping(bytes32 => bool)) public propCandidates;
/// @notice The account to send ETH fees to.
address payable public feeRecipient;
constructor(address nounsToken_, address nounsDao_) initializer {
nounsToken = NounsTokenLike(nounsToken_);
nounsDao = nounsDao_;
}
/**
* @notice Initialize this data availability contract.
* @param admin the account that can set config state like `createCandidateCost` and `updateCandidateCost`
* @param createCandidateCost_ the cost non-Nouners must pay in ETH in order to emit a new proposal candidate event from this contract.
* @param updateCandidateCost_ the cost non-Nouners must pay in ETH in order to emit a proposal candidate update event from this contract.
*/
function initialize(
address admin,
uint256 createCandidateCost_,
uint256 updateCandidateCost_,
address payable feeRecipient_
) external initializer {
_transferOwnership(admin);
createCandidateCost = createCandidateCost_;
updateCandidateCost = updateCandidateCost_;
feeRecipient = feeRecipient_;
}
/**
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
* PUBLIC/EXTERNAL
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
*/
/**
* @notice Create a new proposal candidate by emitting an event. Nouners can post for free, while non-Nouners must pay `createCandidateCost` in ETH.
* When used to update a proposal created by signatures, `proposalIdToUpdate` should be the ID of the proposal to update,
* and no fee is required.
* Also in this case, the following conditions must be met:
* 1. The proposal must be in the Updatable state.
* 2. `msg.sender` must be the same as the proposer of the proposal to update.
* 3. The proposal must have at least one signer.
* @dev Reverts if the proposer (msg.sender) has already created a candidate with the same slug.
* @param targets the candidate proposal targets.
* @param values the candidate proposal values.
* @param signatures the candidate proposal signatures.
* @param calldatas the candidate proposal calldatas.
* @param description the candidate proposal description.
* @param slug the candidate proposal slug string, used alognside the proposer to uniquely identify candidates.
* @param proposalIdToUpdate if this is an update to an existing proposal, the ID of the proposal to update, otherwise 0.
*/
function createProposalCandidate(
address[] memory targets,
uint256[] memory values,
string[] memory signatures,
bytes[] memory calldatas,
string memory description,
string memory slug,
uint256 proposalIdToUpdate
) external payable {
if (proposalIdToUpdate > 0) {
INounsDAO dao = INounsDAO(nounsDao);
NounsDAOTypes.ProposalCondensedV3 memory propInfo = dao.proposalsV3(proposalIdToUpdate);
if (block.number > propInfo.updatePeriodEndBlock) revert ProposalToUpdateMustBeUpdatable();
if (propInfo.proposer != msg.sender) revert OnlyProposerCanCreateUpdateCandidate();
if (propInfo.signers.length == 0) revert UpdateProposalCandidatesOnlyWorkWithProposalsBySigs();
} else {
if (!isNouner(msg.sender) && msg.value < createCandidateCost) revert MustBeNounerOrPaySufficientFee();
}
if (propCandidates[msg.sender][keccak256(bytes(slug))]) revert SlugAlreadyUsed();
NounsDAOProposals.checkProposalTxs(NounsDAOProposals.ProposalTxs(targets, values, signatures, calldatas));
propCandidates[msg.sender][keccak256(bytes(slug))] = true;
bytes memory encodedProp = NounsDAOProposals.calcProposalEncodeData(
msg.sender,
NounsDAOProposals.ProposalTxs(targets, values, signatures, calldatas),
description
);
if (proposalIdToUpdate > 0) {
encodedProp = abi.encodePacked(proposalIdToUpdate, encodedProp);
}
sendValueToRecipient();
emit ProposalCandidateCreated(
msg.sender,
targets,
values,
signatures,
calldatas,
description,
slug,
proposalIdToUpdate,
keccak256(encodedProp)
);
}
/**
* @notice Update a proposal candidate by emitting an event. Nouners can update for free, while non-Nouners must pay `updateCandidateCost` in ETH.
* @dev Reverts if the proposer (msg.sender) has not already created a candidate with the same slug.
* @param targets the candidate proposal targets.
* @param values the candidate proposal values.
* @param signatures the candidate proposal signatures.
* @param calldatas the candidate proposal calldatas.
* @param description the candidate proposal description.
* @param slug the candidate proposal slug string, used alognside the proposer to uniquely identify candidates.
* @param proposalIdToUpdate if this is an update to an existing proposal, the ID of the proposal to update, otherwise 0.
* @param reason the free text reason and context for the update.
*/
function updateProposalCandidate(
address[] memory targets,
uint256[] memory values,
string[] memory signatures,
bytes[] memory calldatas,
string memory description,
string memory slug,
uint256 proposalIdToUpdate,
string memory reason
) external payable {
if (!isNouner(msg.sender) && msg.value < updateCandidateCost) revert MustBeNounerOrPaySufficientFee();
if (!propCandidates[msg.sender][keccak256(bytes(slug))]) revert SlugDoesNotExist();
NounsDAOProposals.checkProposalTxs(NounsDAOProposals.ProposalTxs(targets, values, signatures, calldatas));
bytes memory encodedProp = NounsDAOProposals.calcProposalEncodeData(
msg.sender,
NounsDAOProposals.ProposalTxs(targets, values, signatures, calldatas),
description
);
if (proposalIdToUpdate > 0) {
encodedProp = abi.encodePacked(proposalIdToUpdate, encodedProp);
}
sendValueToRecipient();
emit ProposalCandidateUpdated(
msg.sender,
targets,
values,
signatures,
calldatas,
description,
slug,
proposalIdToUpdate,
keccak256(encodedProp),
reason
);
}
/**
* @notice Cancel a proposal candidate by emitting an event.
* @dev Reverts if the proposer (msg.sender) has not already created a candidate with the same slug.
* @param slug the candidate proposal slug string, used alognside the proposer to uniquely identify candidates.
*/
function cancelProposalCandidate(string memory slug) external {
if (!propCandidates[msg.sender][keccak256(bytes(slug))]) revert SlugDoesNotExist();
emit ProposalCandidateCanceled(msg.sender, slug);
}
/**
* @notice Add a signature supporting a new candidate to be proposed to the DAO using `proposeBySigs`, by emitting an event with the signature data.
* Only lets the signer account submit their signature, to minimize potential spam.
* @param sig the signature bytes.
* @param expirationTimestamp the signature's expiration timestamp.
* @param proposer the proposer account that posted the candidate proposal with the provided slug.
* @param slug the slug of the proposal candidate signer signed on.
* @param proposalIdToUpdate if this is an update to an existing proposal, the ID of the proposal to update, otherwise 0.
* @param encodedProp the abi encoding of the candidate version signed; should be identical to the output of
* the `NounsDAOProposals.calcProposalEncodeData` function.
* @param reason signer's reason free text.
*/
function addSignature(
bytes memory sig,
uint256 expirationTimestamp,
address proposer,
string memory slug,
uint256 proposalIdToUpdate,
bytes memory encodedProp,
string memory reason
) external {
if (!propCandidates[proposer][keccak256(bytes(slug))]) revert SlugDoesNotExist();
bytes32 typeHash = proposalIdToUpdate == 0
? NounsDAOProposals.PROPOSAL_TYPEHASH
: NounsDAOProposals.UPDATE_PROPOSAL_TYPEHASH;
bytes32 sigDigest = NounsDAOProposals.sigDigest(typeHash, encodedProp, expirationTimestamp, nounsDao);
if (!SignatureChecker.isValidSignatureNow(msg.sender, sigDigest, sig)) revert InvalidSignature();
emit SignatureAdded(
msg.sender,
sig,
expirationTimestamp,
proposer,
slug,
proposalIdToUpdate,
keccak256(encodedProp),
sigDigest,
reason
);
}
/**
* @notice Send feedback on a proposal. Meant to be used during a proposal's Updatable period, to help proposers receive
* valuable feedback and update their proposal in time.
* @param proposalId the ID of the proposal.
* @param support msg.sender's vote-like feedback: 0 is against, 1 is for, 2 is abstain.
* @param reason their free text feedback.
*/
function sendFeedback(uint256 proposalId, uint8 support, string memory reason) external {
if (support > 2) revert InvalidSupportValue();
emit FeedbackSent(msg.sender, proposalId, support, reason);
}
/**
* @notice Send feedback on a proposal candidate. Meant to be used prior to submitting the candidate as a proposal,
* to help proposers refine their candidate.
* @param proposer the proposer of the candidate.
* @param slug the slug of the candidate.
* @param support msg.sender's vote-like feedback: 0 is against, 1 is for, 2 is abstain.
* @param reason their free text feedback.
*/
function sendCandidateFeedback(address proposer, string memory slug, uint8 support, string memory reason) external {
if (!propCandidates[proposer][keccak256(bytes(slug))]) revert SlugDoesNotExist();
if (support > 2) revert InvalidSupportValue();
emit CandidateFeedbackSent(msg.sender, proposer, slug, support, reason);
}
/**
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
* ADMIN (OWNER) FUNCTIONS
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
*/
function setCreateCandidateCost(uint256 newCreateCandidateCost) external onlyOwner {
uint256 oldCreateCandidateCost = createCandidateCost;
createCandidateCost = newCreateCandidateCost;
emit CreateCandidateCostSet(oldCreateCandidateCost, newCreateCandidateCost);
}
function setUpdateCandidateCost(uint256 newUpdateCandidateCost) external onlyOwner {
uint256 oldUpdateCandidateCost = updateCandidateCost;
updateCandidateCost = newUpdateCandidateCost;
emit UpdateCandidateCostSet(oldUpdateCandidateCost, newUpdateCandidateCost);
}
function setFeeRecipient(address payable newFeeRecipient) external onlyOwner {
address oldFeeRecipient = feeRecipient;
feeRecipient = newFeeRecipient;
emit FeeRecipientSet(oldFeeRecipient, newFeeRecipient);
}
/**
* @notice Withdraw ETH from this contract's balance. Only owner can call this function.
* @param to the recipient.
* @param amount the ETH amount to send.
*/
function withdrawETH(address to, uint256 amount) external onlyOwner {
if (amount > address(this).balance) revert AmountExceedsBalance();
(bool sent, bytes memory data) = to.call{ value: amount }('');
if (!sent) {
revert FailedWithdrawingETH(data);
}
emit ETHWithdrawn(to, amount);
}
/**
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
* INTERNAL
* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
*/
function sendValueToRecipient() internal {
address feeRecipient_ = feeRecipient;
if (msg.value > 0 && feeRecipient_ != address(0)) {
// choosing to not revert upon failure here because owner can always use
// the withdraw function instead.
feeRecipient_.call{ value: msg.value }('');
}
}
function isNouner(address account) internal view returns (bool) {
return nounsToken.getPriorVotes(account, block.number - PRIOR_VOTES_BLOCKS_AGO) > 0;
}
/**
* @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by
* {upgradeTo} and {upgradeToAndCall}.
*
* Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}.
*
* ```solidity
* function _authorizeUpgrade(address) internal override onlyOwner {}
* ```
*/
function _authorizeUpgrade(address) internal view override onlyOwner {}
}