@crisp-e3/contracts 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/contracts/CRISPOnchainVerifier.sol +2493 -0
- package/contracts/CRISPProgram.sol +203 -44
- package/contracts/CRISPVerifier.sol +45 -45
- package/contracts/Mocks/MockInterfold.sol +29 -0
- package/contracts/Mocks/MockVotesToken.sol +48 -0
- package/contracts/interfaces/IERC6372Clock.sol +16 -0
- package/contracts/interfaces/IHonkVerifier.sol +19 -0
- package/contracts/interfaces/IVotesToken.sol +17 -0
- package/package.json +3 -3
|
@@ -12,9 +12,12 @@ import { IInterfold } from "@interfold/contracts/contracts/interfaces/IInterfold
|
|
|
12
12
|
import { E3 } from "@interfold/contracts/contracts/interfaces/IE3.sol";
|
|
13
13
|
import { Risc0ComputeProof } from "@interfold/contracts/contracts/lib/Risc0ComputeProof.sol";
|
|
14
14
|
import { LazyIMTData, InternalLazyIMT } from "@zk-kit/lazy-imt.sol/InternalLazyIMT.sol";
|
|
15
|
-
import {
|
|
15
|
+
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
|
|
16
|
+
import { IHonkVerifier } from "./interfaces/IHonkVerifier.sol";
|
|
17
|
+
import { IVotesToken } from "./interfaces/IVotesToken.sol";
|
|
18
|
+
import { IERC6372Clock } from "./interfaces/IERC6372Clock.sol";
|
|
16
19
|
|
|
17
|
-
contract CRISPProgram is IE3Program, Ownable {
|
|
20
|
+
contract CRISPProgram is IE3Program, Ownable, EIP712 {
|
|
18
21
|
using InternalLazyIMT for LazyIMTData;
|
|
19
22
|
|
|
20
23
|
/// @notice Enum to represent credit modes
|
|
@@ -43,7 +46,12 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
43
46
|
/// @notice Derived from token balances by the coordinator. The default.
|
|
44
47
|
TOKEN,
|
|
45
48
|
/// @notice Supplied by the requester via `getCensus(uint256 e3Id) returns (address[])`.
|
|
46
|
-
BY_REQUESTER
|
|
49
|
+
BY_REQUESTER,
|
|
50
|
+
/// @notice Read from the token by this contract, one input at a time. No list is enumerated
|
|
51
|
+
/// and no root is posted, so there is no census producer to trust. `publishInput` calls
|
|
52
|
+
/// `getPastVotes` for the slot and passes the result to the circuit as a public input, which
|
|
53
|
+
/// is why this mode uses the `crisp_onchain` verifier rather than the `crisp` one.
|
|
54
|
+
ONCHAIN
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/// @notice Struct to store all data related to a voting round
|
|
@@ -55,6 +63,16 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
55
63
|
uint256 numOptions;
|
|
56
64
|
CreditMode creditMode;
|
|
57
65
|
CensusMode censusMode;
|
|
66
|
+
/// @notice The token that voting power is read from. Only used by `CensusMode.ONCHAIN`.
|
|
67
|
+
address token;
|
|
68
|
+
/// @notice The smallest voting power that may cast an input. Only used by
|
|
69
|
+
/// `CensusMode.ONCHAIN`, where eligibility is checked per input instead of by a census.
|
|
70
|
+
uint256 minVotingPower;
|
|
71
|
+
/// @notice Credits given to each eligible voter under `CreditMode.CONSTANT`.
|
|
72
|
+
uint256 credits;
|
|
73
|
+
/// @notice The timepoint that voting power is read at, in the ERC-6372 clock units of the
|
|
74
|
+
/// token. Recorded when the round is requested, so it is the same for every input.
|
|
75
|
+
uint48 snapshot;
|
|
58
76
|
}
|
|
59
77
|
|
|
60
78
|
// Constants
|
|
@@ -75,7 +93,17 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
75
93
|
IInterfold public interfold;
|
|
76
94
|
IRiscZeroVerifier public risc0Verifier;
|
|
77
95
|
bytes32 public imageId;
|
|
78
|
-
|
|
96
|
+
/// @notice Verifies ballots for the census modes that prove membership of a Merkle tree.
|
|
97
|
+
IHonkVerifier private immutable honkVerifier;
|
|
98
|
+
/// @notice Verifies ballots for `CensusMode.ONCHAIN`, whose circuit has no Merkle inputs and
|
|
99
|
+
/// takes voting power as a public input instead.
|
|
100
|
+
IHonkVerifier private immutable onchainHonkVerifier;
|
|
101
|
+
|
|
102
|
+
/// @notice The EIP-712 type of the message a voter signs to authorise one ballot.
|
|
103
|
+
/// @dev The digest binds the signature to the round, the slot, and this exact ciphertext. The
|
|
104
|
+
/// chain id and this contract address come from the EIP-712 domain, so a signature cannot be
|
|
105
|
+
/// replayed onto another round, another slot, another ballot, or another deployment.
|
|
106
|
+
bytes32 private constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 e3Id,address slot,bytes32 ciphertextCommitment)");
|
|
79
107
|
|
|
80
108
|
// Mappings
|
|
81
109
|
mapping(uint256 e3Id => RoundData) e3Data;
|
|
@@ -94,6 +122,16 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
94
122
|
/// @notice A requester-supplied census names who may vote, not how much each vote weighs, so it
|
|
95
123
|
/// only has meaning when every voter carries the same credits.
|
|
96
124
|
error CensusModeRequiresConstantCredits();
|
|
125
|
+
/// @notice `CensusMode.ONCHAIN` reads voting power from a token, so it cannot run without one
|
|
126
|
+
/// that answers `getPastVotes`.
|
|
127
|
+
error CensusModeRequiresToken();
|
|
128
|
+
/// @notice An `ONCHAIN` round with constant credits must grant a non-zero allowance, or it
|
|
129
|
+
/// bounds every ballot to zero and can only accept masks.
|
|
130
|
+
error InvalidCredits();
|
|
131
|
+
/// @notice The slot holds less voting power than the round requires, so it cannot be written to.
|
|
132
|
+
/// @dev Raised for mask inputs as well as real votes. Under `CensusMode.ONCHAIN` this check
|
|
133
|
+
/// replaces the Merkle membership proof that gates both branches in the other modes.
|
|
134
|
+
error SlotNotEligible();
|
|
97
135
|
error InvalidCensusMode();
|
|
98
136
|
error SlotIsEmpty();
|
|
99
137
|
error MerkleRootNotSet();
|
|
@@ -111,17 +149,38 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
111
149
|
/// @param _risc0Verifier The RISC Zero verifier address
|
|
112
150
|
/// @param _honkVerifier The honk verifier address
|
|
113
151
|
/// @param _imageId The image ID for the guest program
|
|
114
|
-
constructor(
|
|
152
|
+
constructor(
|
|
153
|
+
IInterfold _interfold,
|
|
154
|
+
IRiscZeroVerifier _risc0Verifier,
|
|
155
|
+
IHonkVerifier _honkVerifier,
|
|
156
|
+
IHonkVerifier _onchainHonkVerifier,
|
|
157
|
+
bytes32 _imageId
|
|
158
|
+
) Ownable(msg.sender) EIP712("CRISP", "1") {
|
|
115
159
|
if (address(_interfold) == address(0)) revert InterfoldAddressZero();
|
|
116
160
|
if (address(_risc0Verifier) == address(0)) revert Risc0VerifierAddressZero();
|
|
117
161
|
if (address(_honkVerifier) == address(0)) revert InvalidHonkVerifier();
|
|
162
|
+
if (address(_onchainHonkVerifier) == address(0)) revert InvalidHonkVerifier();
|
|
118
163
|
|
|
119
164
|
interfold = _interfold;
|
|
120
165
|
risc0Verifier = _risc0Verifier;
|
|
121
166
|
honkVerifier = _honkVerifier;
|
|
167
|
+
onchainHonkVerifier = _onchainHonkVerifier;
|
|
122
168
|
imageId = _imageId;
|
|
123
169
|
}
|
|
124
170
|
|
|
171
|
+
/// @notice The digest a voter signs to authorise one ballot.
|
|
172
|
+
/// @dev Computed for every input, and for mask inputs as well as real votes. The circuit ignores
|
|
173
|
+
/// it on the mask branch, but the contract must not skip it: a digest that were computed only
|
|
174
|
+
/// for real votes would let an observer tell the two apart on-chain, which is exactly what mask
|
|
175
|
+
/// inputs exist to prevent.
|
|
176
|
+
/// @param e3Id The E3 the ballot belongs to.
|
|
177
|
+
/// @param slot The slot address the ballot is written to.
|
|
178
|
+
/// @param ciphertextCommitment The commitment to the ballot ciphertext.
|
|
179
|
+
/// @return The EIP-712 digest.
|
|
180
|
+
function ballotDigest(uint256 e3Id, address slot, bytes32 ciphertextCommitment) public view returns (bytes32) {
|
|
181
|
+
return _hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, e3Id, slot, ciphertextCommitment)));
|
|
182
|
+
}
|
|
183
|
+
|
|
125
184
|
/// @notice Sets the Merkle root for an E3 program. Can only be set once.
|
|
126
185
|
/// @param _e3Id The E3 program ID
|
|
127
186
|
/// @param _root The Merkle root to set.
|
|
@@ -199,34 +258,9 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
199
258
|
if (msg.sender != address(interfold) && msg.sender != owner()) revert CallerNotAuthorized();
|
|
200
259
|
if (e3Data[e3Id].paramsHash != bytes32(0)) revert E3AlreadyInitialized();
|
|
201
260
|
|
|
202
|
-
//
|
|
203
|
-
// stack limit that holding
|
|
204
|
-
|
|
205
|
-
// One decode, every field required. `censusMode` is read as a uint and range-checked rather
|
|
206
|
-
// than decoded straight into the enum, so an unrecognised value gives a named error instead
|
|
207
|
-
// of a bare panic.
|
|
208
|
-
(, , uint256 numOptions, CreditMode creditMode, , uint256 rawCensusMode) = abi.decode(
|
|
209
|
-
customParams,
|
|
210
|
-
(address, uint256, uint256, CreditMode, uint256, uint256)
|
|
211
|
-
);
|
|
212
|
-
// The circuit asserts `num_options <= MAX_OPTIONS`, so a round configured above it accepts no
|
|
213
|
-
// ballot at all. Reject at request time rather than stranding a round nobody can vote in.
|
|
214
|
-
if (numOptions < 2 || numOptions > MAX_VOTE_OPTIONS) revert InvalidNumOptions();
|
|
215
|
-
if (rawCensusMode > uint256(type(CensusMode).max)) revert InvalidCensusMode();
|
|
216
|
-
|
|
217
|
-
// Rejected here rather than by the coordinator, so a combination that can never work costs
|
|
218
|
-
// nothing: this reverts in the same transaction that requests the E3, before any fee is paid.
|
|
219
|
-
if (CensusMode(rawCensusMode) == CensusMode.BY_REQUESTER && creditMode != CreditMode.CONSTANT) {
|
|
220
|
-
revert CensusModeRequiresConstantCredits();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// we need to know the number of options for decoding the tally
|
|
224
|
-
e3Data[e3Id].numOptions = numOptions;
|
|
225
|
-
// we want to save the credit mode so it can be verified on chain by everyone
|
|
226
|
-
e3Data[e3Id].creditMode = creditMode;
|
|
227
|
-
// recorded so anyone can verify which electorate the round was requested against
|
|
228
|
-
e3Data[e3Id].censusMode = CensusMode(rawCensusMode);
|
|
229
|
-
}
|
|
261
|
+
// Delegated to its own frame rather than scoped inline: `validate` is close enough to the
|
|
262
|
+
// stack limit that holding the six decoded values alongside the parameters exceeds it.
|
|
263
|
+
_initRound(e3Id, customParams);
|
|
230
264
|
|
|
231
265
|
e3Data[e3Id].paramsHash = keccak256(e3ProgramParams);
|
|
232
266
|
|
|
@@ -236,6 +270,79 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
236
270
|
return ENCRYPTION_SCHEME_ID;
|
|
237
271
|
}
|
|
238
272
|
|
|
273
|
+
/// @notice Decode the round configuration and record it.
|
|
274
|
+
/// @dev One decode, every field required. `censusMode` is read as a uint and range-checked
|
|
275
|
+
/// rather than decoded straight into the enum, so an unrecognised value gives a named error
|
|
276
|
+
/// instead of a bare panic.
|
|
277
|
+
/// @param e3Id The E3 being configured.
|
|
278
|
+
/// @param customParams The ABI-encoded round configuration.
|
|
279
|
+
function _initRound(uint256 e3Id, bytes calldata customParams) internal {
|
|
280
|
+
(address token, uint256 minVotingPower, uint256 numOptions, CreditMode creditMode, uint256 credits, uint256 rawCensusMode) = abi.decode(
|
|
281
|
+
customParams,
|
|
282
|
+
(address, uint256, uint256, CreditMode, uint256, uint256)
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// The circuit asserts `num_options <= MAX_OPTIONS`, so a round configured above it accepts no
|
|
286
|
+
// ballot at all. Reject at request time rather than stranding a round nobody can vote in.
|
|
287
|
+
if (numOptions < 2 || numOptions > MAX_VOTE_OPTIONS) revert InvalidNumOptions();
|
|
288
|
+
if (rawCensusMode > uint256(type(CensusMode).max)) revert InvalidCensusMode();
|
|
289
|
+
|
|
290
|
+
// Rejected here rather than by the coordinator, so a combination that can never work costs
|
|
291
|
+
// nothing: this reverts in the same transaction that requests the E3, before any fee is paid.
|
|
292
|
+
if (CensusMode(rawCensusMode) == CensusMode.BY_REQUESTER && creditMode != CreditMode.CONSTANT) {
|
|
293
|
+
revert CensusModeRequiresConstantCredits();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ONCHAIN reads every voter's power from this token, so a round without one accepts no ballot
|
|
297
|
+
// at all. Same reasoning as the numOptions bound: fail before the fee is paid.
|
|
298
|
+
if (CensusMode(rawCensusMode) == CensusMode.ONCHAIN && token == address(0)) {
|
|
299
|
+
revert CensusModeRequiresToken();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// An ONCHAIN round hands `credits` to the circuit as the voting-power bound, so zero credits
|
|
303
|
+
// bound every ballot to zero: only a mask would be accepted, and the round would tally
|
|
304
|
+
// nothing. Checked for ONCHAIN only — the Merkle modes take the bound from the census leaf,
|
|
305
|
+
// where `credits` never reaches the circuit and the contract has nothing to check.
|
|
306
|
+
if (CensusMode(rawCensusMode) == CensusMode.ONCHAIN && creditMode == CreditMode.CONSTANT && credits == 0) {
|
|
307
|
+
revert InvalidCredits();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
RoundData storage round = e3Data[e3Id];
|
|
311
|
+
// we need to know the number of options for decoding the tally
|
|
312
|
+
round.numOptions = numOptions;
|
|
313
|
+
// we want to save the credit mode so it can be verified on chain by everyone
|
|
314
|
+
round.creditMode = creditMode;
|
|
315
|
+
// recorded so anyone can verify which electorate the round was requested against
|
|
316
|
+
round.censusMode = CensusMode(rawCensusMode);
|
|
317
|
+
round.token = token;
|
|
318
|
+
round.minVotingPower = minVotingPower;
|
|
319
|
+
round.credits = credits;
|
|
320
|
+
|
|
321
|
+
// The snapshot is taken here rather than supplied by the requester. This function runs in the
|
|
322
|
+
// transaction that requests the E3, so `clock() - 1` is the last finalized timepoint of the
|
|
323
|
+
// round, and a requester cannot name a timepoint that suits it. Recording it once also makes
|
|
324
|
+
// every input of the round read the same electorate.
|
|
325
|
+
if (CensusMode(rawCensusMode) == CensusMode.ONCHAIN) {
|
|
326
|
+
// Checked before any call is attempted. A call to an address with no code succeeds and
|
|
327
|
+
// returns nothing, so `clock()` fails while decoding the empty return data rather than
|
|
328
|
+
// inside the call — and a decode failure is not what `try/catch` is there to catch. An EOA
|
|
329
|
+
// would otherwise be refused by a bare panic instead of a named error.
|
|
330
|
+
if (token.code.length == 0) revert CensusModeRequiresToken();
|
|
331
|
+
|
|
332
|
+
uint48 snapshot = _previousTimepoint(token);
|
|
333
|
+
|
|
334
|
+
// Probe the exact call every input will make. `_previousTimepoint` swallows a missing
|
|
335
|
+
// `clock()` and falls back to block numbers, which is right for a token that predates
|
|
336
|
+
// ERC-6372 but also lets an address that is not a votes token pass validation — and then
|
|
337
|
+
// every `publishInput` reverts inside `getPastVotes`, after the fee is paid.
|
|
338
|
+
try IVotesToken(token).getPastVotes(address(0), snapshot) returns (uint256) {} catch {
|
|
339
|
+
revert CensusModeRequiresToken();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
round.snapshot = snapshot;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
239
346
|
/// @inheritdoc IE3Program
|
|
240
347
|
function publishInput(uint256 e3Id, bytes memory data) external {
|
|
241
348
|
E3 memory e3 = interfold.getE3(e3Id);
|
|
@@ -256,9 +363,6 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
256
363
|
revert E3NotAcceptingInputs(e3Id);
|
|
257
364
|
}
|
|
258
365
|
|
|
259
|
-
// We need to ensure that the CRISP admin set the merkle root of the census.
|
|
260
|
-
if (e3Data[e3Id].merkleRoot == 0) revert MerkleRootNotSet();
|
|
261
|
-
|
|
262
366
|
if (data.length == 0) revert EmptyInputData();
|
|
263
367
|
|
|
264
368
|
(bytes memory noirProof, address slotAddress, bytes32 encryptedVoteCommitment, bytes memory encryptedVote) = abi.decode(
|
|
@@ -266,26 +370,81 @@ contract CRISPProgram is IE3Program, Ownable {
|
|
|
266
370
|
(bytes, address, bytes32, bytes)
|
|
267
371
|
);
|
|
268
372
|
|
|
373
|
+
// The two census families differ here and nowhere else. A Merkle round proves membership
|
|
374
|
+
// inside the circuit against a posted root. An ONCHAIN round reads the power from the token
|
|
375
|
+
// and gives it to the circuit, so the eligibility check has to happen here instead.
|
|
376
|
+
(bytes32 eligibility, IHonkVerifier verifier) = _eligibility(e3Id, slotAddress);
|
|
377
|
+
|
|
269
378
|
(uint40 voteIndex, bytes32 previousEncryptedVoteCommitment) = _processVote(e3Id, slotAddress, encryptedVoteCommitment);
|
|
270
379
|
|
|
271
380
|
// Set the public inputs for the proof. Order must match Noir circuit.
|
|
272
|
-
bytes32[] memory noirPublicInputs = new bytes32[](
|
|
381
|
+
bytes32[] memory noirPublicInputs = new bytes32[](9);
|
|
273
382
|
noirPublicInputs[0] = previousEncryptedVoteCommitment;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
383
|
+
// A Keccak digest does not fit in one field element, so it enters the circuit as its two
|
|
384
|
+
// 16-byte halves. The circuit rebuilds the 32 bytes with `digest_from_halves`.
|
|
385
|
+
{
|
|
386
|
+
uint256 digest = uint256(ballotDigest(e3Id, slotAddress, encryptedVoteCommitment));
|
|
387
|
+
noirPublicInputs[1] = bytes32(digest >> 128);
|
|
388
|
+
noirPublicInputs[2] = bytes32(digest & type(uint128).max);
|
|
389
|
+
}
|
|
390
|
+
noirPublicInputs[3] = bytes32(uint256(uint160(slotAddress)));
|
|
391
|
+
noirPublicInputs[4] = eligibility;
|
|
392
|
+
noirPublicInputs[5] = bytes32(uint256(previousEncryptedVoteCommitment == bytes32(0) ? 1 : 0));
|
|
393
|
+
noirPublicInputs[6] = bytes32(e3Data[e3Id].numOptions);
|
|
394
|
+
noirPublicInputs[7] = encryptedVoteCommitment;
|
|
395
|
+
noirPublicInputs[8] = e3.committeePublicKey;
|
|
280
396
|
|
|
281
397
|
// Check if the ciphertext was encrypted correctly
|
|
282
|
-
if (!
|
|
398
|
+
if (!verifier.verify(noirProof, noirPublicInputs)) {
|
|
283
399
|
revert InvalidNoirProof();
|
|
284
400
|
}
|
|
285
401
|
|
|
286
402
|
emit InputPublished(e3Id, encryptedVote, voteIndex);
|
|
287
403
|
}
|
|
288
404
|
|
|
405
|
+
/// @notice Resolve the eligibility public input and the verifier for a round.
|
|
406
|
+
/// @dev Returns the value that occupies index 4 of the circuit public inputs. The `crisp` and
|
|
407
|
+
/// `crisp_onchain` circuits agree on every other position, so this one value and the verifier
|
|
408
|
+
/// address are the whole difference between the two paths.
|
|
409
|
+
/// @param e3Id The E3 the input belongs to.
|
|
410
|
+
/// @param slotAddress The slot the input is written to.
|
|
411
|
+
/// @return eligibility The Merkle root of the census, or the voting power of the slot.
|
|
412
|
+
/// @return verifier The verifier that matches the circuit of this round.
|
|
413
|
+
function _eligibility(uint256 e3Id, address slotAddress) internal view returns (bytes32 eligibility, IHonkVerifier verifier) {
|
|
414
|
+
RoundData storage round = e3Data[e3Id];
|
|
415
|
+
|
|
416
|
+
if (round.censusMode != CensusMode.ONCHAIN) {
|
|
417
|
+
// We need to ensure that the CRISP admin set the merkle root of the census.
|
|
418
|
+
if (round.merkleRoot == 0) revert MerkleRootNotSet();
|
|
419
|
+
return (bytes32(round.merkleRoot), honkVerifier);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
uint256 power = IVotesToken(round.token).getPastVotes(slotAddress, round.snapshot);
|
|
423
|
+
|
|
424
|
+
// A slot with no power is never writable, whatever the round configured. Without this floor a
|
|
425
|
+
// round with `minVotingPower == 0` would let anyone mask any address, including addresses that
|
|
426
|
+
// never held the token.
|
|
427
|
+
uint256 threshold = round.minVotingPower == 0 ? 1 : round.minVotingPower;
|
|
428
|
+
if (power < threshold) revert SlotNotEligible();
|
|
429
|
+
|
|
430
|
+
// Eligibility comes from the power at the snapshot. The weight the circuit enforces comes from
|
|
431
|
+
// the credit mode, so a CONSTANT round gives every eligible slot the same credits.
|
|
432
|
+
return (bytes32(round.creditMode == CreditMode.CONSTANT ? round.credits : power), onchainHonkVerifier);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/// @notice The last finalized timepoint of a token, in its ERC-6372 clock units.
|
|
436
|
+
/// @dev Falls back to block numbers when the token has no `clock()`, which matches the default
|
|
437
|
+
/// ERC20Votes clock.
|
|
438
|
+
/// @param token The token to read the clock of.
|
|
439
|
+
/// @return The timepoint before the current one.
|
|
440
|
+
function _previousTimepoint(address token) internal view returns (uint48) {
|
|
441
|
+
try IERC6372Clock(token).clock() returns (uint48 current) {
|
|
442
|
+
return current == 0 ? 0 : current - 1;
|
|
443
|
+
} catch {
|
|
444
|
+
return uint48(block.number - 1);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
289
448
|
/// @notice Decode the tally from the plaintext output
|
|
290
449
|
/// @param e3Id The E3 program ID
|
|
291
450
|
/// @return votes - an array of vote counts for each option
|
|
@@ -7,85 +7,85 @@ pragma solidity >=0.8.21;
|
|
|
7
7
|
|
|
8
8
|
uint256 constant N = 2097152;
|
|
9
9
|
uint256 constant LOG_N = 21;
|
|
10
|
-
uint256 constant NUMBER_OF_PUBLIC_INPUTS =
|
|
11
|
-
uint256 constant VK_HASH =
|
|
10
|
+
uint256 constant NUMBER_OF_PUBLIC_INPUTS = 17;
|
|
11
|
+
uint256 constant VK_HASH = 0x1d0e6ec8f2fe720a1f528f02a66b66c1479c1648afaaedec06d89b889a0ff0c2;
|
|
12
12
|
library HonkVerificationKey {
|
|
13
13
|
function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) {
|
|
14
14
|
Honk.VerificationKey memory vk = Honk.VerificationKey({
|
|
15
15
|
circuitSize: uint256(2097152),
|
|
16
16
|
logCircuitSize: uint256(21),
|
|
17
|
-
publicInputsSize: uint256(
|
|
17
|
+
publicInputsSize: uint256(17),
|
|
18
18
|
ql: Honk.G1Point({
|
|
19
|
-
x: uint256(
|
|
20
|
-
y: uint256(
|
|
19
|
+
x: uint256(0x2089549d27c8b4fa58dd4ed5276c42ccc9830ee5cd64cf877df0301a02d54e62),
|
|
20
|
+
y: uint256(0x01137e39f6b1ec6101fad7a4472102bfe11e7534e55e58109734c4b927efe6a6)
|
|
21
21
|
}),
|
|
22
22
|
qr: Honk.G1Point({
|
|
23
|
-
x: uint256(
|
|
24
|
-
y: uint256(
|
|
23
|
+
x: uint256(0x0c9d534606fb88b2814caaec28c8f0ccd9f903b2236fe399ce723cb7a89cff54),
|
|
24
|
+
y: uint256(0x12d1f3a930e1c9be768e98d5ed2c5c74b3bd1bce2f50ce042cde7270f8032df1)
|
|
25
25
|
}),
|
|
26
26
|
qo: Honk.G1Point({
|
|
27
|
-
x: uint256(
|
|
28
|
-
y: uint256(
|
|
27
|
+
x: uint256(0x18424976826978ddb710bfdc21c64a296884c27b0d8dadf6ac7681445dde8d8c),
|
|
28
|
+
y: uint256(0x1690625dd2a2a6afa259cfe9dc7601058bd3275e8fcea82e008caa319c2710b4)
|
|
29
29
|
}),
|
|
30
30
|
q4: Honk.G1Point({
|
|
31
|
-
x: uint256(
|
|
32
|
-
y: uint256(
|
|
31
|
+
x: uint256(0x025aeec2f923a237df2c5ddfe9bc1af74bdaf41d2fe47aa4c0cb12dc4cafac76),
|
|
32
|
+
y: uint256(0x0a3ecdb966ed757ad3acc3e2ad670b804b582686b85bdad85ebe18713cddd85e)
|
|
33
33
|
}),
|
|
34
34
|
qm: Honk.G1Point({
|
|
35
|
-
x: uint256(
|
|
36
|
-
y: uint256(
|
|
35
|
+
x: uint256(0x1b562d7113fa8ac2dec528dfcec26f0c0b1290f9a0fa7cd08ee8492b7f8ed657),
|
|
36
|
+
y: uint256(0x188c0bdd795dda2f441f73f4a93919cf28417e039c491966c4305582edfcf63f)
|
|
37
37
|
}),
|
|
38
38
|
qc: Honk.G1Point({
|
|
39
|
-
x: uint256(
|
|
40
|
-
y: uint256(
|
|
39
|
+
x: uint256(0x0b824186ff62956e929ad489895b6f94e44fedde51b9c7ada98f43339fa875e2),
|
|
40
|
+
y: uint256(0x288402862aac013f44d9af847a2a1b318e8f3c026d50818ed5467c252505f8b8)
|
|
41
41
|
}),
|
|
42
42
|
qLookup: Honk.G1Point({
|
|
43
43
|
x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000),
|
|
44
44
|
y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000)
|
|
45
45
|
}),
|
|
46
46
|
qArith: Honk.G1Point({
|
|
47
|
-
x: uint256(
|
|
48
|
-
y: uint256(
|
|
47
|
+
x: uint256(0x1eec43a65b0287ccdadfdee7b5c7f1edd764f78af3be146380f9285dc23935ac),
|
|
48
|
+
y: uint256(0x00334d21a1e3fdb81573ecfedf177b7a266b994f7c03e603d22c1d50a14c1177)
|
|
49
49
|
}),
|
|
50
50
|
qDeltaRange: Honk.G1Point({
|
|
51
|
-
x: uint256(
|
|
52
|
-
y: uint256(
|
|
51
|
+
x: uint256(0x1fbf72c051dd975693c7581ba456e27c8779526d6226300b89fdcb2f46236846),
|
|
52
|
+
y: uint256(0x15c7e84bd025a07f602a5ced777d9a6dc519fa77e263fd6ae739a872ced3403b)
|
|
53
53
|
}),
|
|
54
54
|
qElliptic: Honk.G1Point({
|
|
55
55
|
x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000),
|
|
56
56
|
y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000)
|
|
57
57
|
}),
|
|
58
58
|
qMemory: Honk.G1Point({
|
|
59
|
-
x: uint256(
|
|
60
|
-
y: uint256(
|
|
59
|
+
x: uint256(0x047852a9a4fee8460c8a70000bd59b1e9fa7b3a8ebe89a4a9bd74f8b667ec0e3),
|
|
60
|
+
y: uint256(0x206a2850fbba93adfaaf19b47f64be4050e7e3d0934e3bb3c152c6d0ebf9bc6e)
|
|
61
61
|
}),
|
|
62
62
|
qNnf: Honk.G1Point({
|
|
63
|
-
x: uint256(
|
|
64
|
-
y: uint256(
|
|
63
|
+
x: uint256(0x1bbbf2123e3d8fb40a55063fc262031ed631ba08b2403fc66be28284d8bf81f0),
|
|
64
|
+
y: uint256(0x22ed7e7b495a40b67a9131d0ec78104425c718cfcb4a1d539cc0efb438863481)
|
|
65
65
|
}),
|
|
66
66
|
qPoseidon2External: Honk.G1Point({
|
|
67
|
-
x: uint256(
|
|
68
|
-
y: uint256(
|
|
67
|
+
x: uint256(0x0545a1bebaefe1aed53b541c02ff5f8dc6ea9fcf3790df43723cec27fad71211),
|
|
68
|
+
y: uint256(0x0f6e3454a6b2e0f9ef6e3c3c420d2212f9b9611dcc56c93cb3f7b3a843b8b061)
|
|
69
69
|
}),
|
|
70
70
|
qPoseidon2Internal: Honk.G1Point({
|
|
71
|
-
x: uint256(
|
|
72
|
-
y: uint256(
|
|
71
|
+
x: uint256(0x1dc8084a81e94401164ab0cc7dd600fdcccf393611a00c881b100f125a7c2cae),
|
|
72
|
+
y: uint256(0x1b45db8582b61bd1f9938b966fd2c62007a4e6b29bd12d93300fc87fc248541c)
|
|
73
73
|
}),
|
|
74
74
|
s1: Honk.G1Point({
|
|
75
|
-
x: uint256(
|
|
76
|
-
y: uint256(
|
|
75
|
+
x: uint256(0x0055c092383ecaa42ba772ccd0dc288af43851c2230e18ce204b0b921c565ebb),
|
|
76
|
+
y: uint256(0x29586aa1c092712c4d7337d3017e2ca1c0b460b5020366e3a218f5dbdb65dac3)
|
|
77
77
|
}),
|
|
78
78
|
s2: Honk.G1Point({
|
|
79
|
-
x: uint256(
|
|
80
|
-
y: uint256(
|
|
79
|
+
x: uint256(0x03c15d176f3295a287398c9bd47ebcbae4206b787ae7f99a0b4b845ba42c5b8f),
|
|
80
|
+
y: uint256(0x06046a4ecd73c3a079fdd9b1aadb3ec8a10d81bbdfff4fe64491b5d73850c951)
|
|
81
81
|
}),
|
|
82
82
|
s3: Honk.G1Point({
|
|
83
|
-
x: uint256(
|
|
84
|
-
y: uint256(
|
|
83
|
+
x: uint256(0x2ab36d90282916edcc8344a83329989d6ab32ce72e50a8c5fb71bdc96cc13bba),
|
|
84
|
+
y: uint256(0x1f91bde663a11f2dd1c2189801544154219b96fc15c228a2afaf94c2fd525614)
|
|
85
85
|
}),
|
|
86
86
|
s4: Honk.G1Point({
|
|
87
|
-
x: uint256(
|
|
88
|
-
y: uint256(
|
|
87
|
+
x: uint256(0x20e8b44228a8581f3b89daf0afd122fb5eb9b54235e5f3006f4da3ced24a4bff),
|
|
88
|
+
y: uint256(0x121da33cdfeb99d9117a23a70ca53f0409a753f28e8217d6297f2d890507fa06)
|
|
89
89
|
}),
|
|
90
90
|
t1: Honk.G1Point({
|
|
91
91
|
x: uint256(0x0000000000000000000000000000000000000000000000000000000000000000),
|
|
@@ -104,28 +104,28 @@ library HonkVerificationKey {
|
|
|
104
104
|
y: uint256(0x0000000000000000000000000000000000000000000000000000000000000000)
|
|
105
105
|
}),
|
|
106
106
|
id1: Honk.G1Point({
|
|
107
|
-
x: uint256(
|
|
108
|
-
y: uint256(
|
|
107
|
+
x: uint256(0x165a0d0053cad2cfaf860a8dffc7b857ae52a0830a5d0ff389f126736c8a68af),
|
|
108
|
+
y: uint256(0x1230c74e80658090adff46f20a5ba230b00a36fa1647bbee26dcd4f61c81ac74)
|
|
109
109
|
}),
|
|
110
110
|
id2: Honk.G1Point({
|
|
111
|
-
x: uint256(
|
|
112
|
-
y: uint256(
|
|
111
|
+
x: uint256(0x0cde9685338036c896bf8031dfb4a27df07170d25a17a6f3487ff9dbc2d58d97),
|
|
112
|
+
y: uint256(0x2ba321fb18bb8416deb007188ba7eaf7d9acc32649feb97b99b61a385503e561)
|
|
113
113
|
}),
|
|
114
114
|
id3: Honk.G1Point({
|
|
115
|
-
x: uint256(
|
|
116
|
-
y: uint256(
|
|
115
|
+
x: uint256(0x1803faf5351782f647e62d18cffbd667c89844b034d38e49c4317b6c6c262efc),
|
|
116
|
+
y: uint256(0x00d296c60b3f1ad4f263c6c0cf1c931573e621b89604b5a18add83543af491fb)
|
|
117
117
|
}),
|
|
118
118
|
id4: Honk.G1Point({
|
|
119
|
-
x: uint256(
|
|
120
|
-
y: uint256(
|
|
119
|
+
x: uint256(0x22136f96d01a39d6cd8808212c2d90f8f539d0c302bbfbe0856ec5858d14026e),
|
|
120
|
+
y: uint256(0x0c8e6eb560ae1fe67f9341f70bc50ad204ee5da5814a40a811d777be4d796ad7)
|
|
121
121
|
}),
|
|
122
122
|
lagrangeFirst: Honk.G1Point({
|
|
123
123
|
x: uint256(0x2a56ce41f6b0be13b9c26747621b821eee81b23a887f299049b14c11e98460d6),
|
|
124
124
|
y: uint256(0x1aa98f2de3ddda547d8f6de4e725ded5827d6338c78656c0d12ca1aea6ef2c7c)
|
|
125
125
|
}),
|
|
126
126
|
lagrangeLast: Honk.G1Point({
|
|
127
|
-
x: uint256(
|
|
128
|
-
y: uint256(
|
|
127
|
+
x: uint256(0x054b6343cdc2405ed0b8e6603bd33aad161e52d8b252ff7916e72f9474fb5c92),
|
|
128
|
+
y: uint256(0x0d9a27acd4b305126491136fbce26d1bc29ecf5b067fe121b87d7347cdf3baf0)
|
|
129
129
|
})
|
|
130
130
|
});
|
|
131
131
|
return vk;
|
|
@@ -30,6 +30,35 @@ contract MockInterfold {
|
|
|
30
30
|
_request(program, numOptions);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/// @notice Request an E3 with caller-supplied program params.
|
|
34
|
+
/// @dev `_request` hardcodes a TOKEN-census round. `CensusMode.ONCHAIN` needs a token, a credit
|
|
35
|
+
/// mode and a census mode that only the caller knows, and the snapshot is taken during
|
|
36
|
+
/// `validate`, so the params have to reach it here rather than being patched afterwards.
|
|
37
|
+
function requestWithParams(address program, uint256 numOptions, bytes memory params) external {
|
|
38
|
+
e3s[nextE3Id] = E3({
|
|
39
|
+
seed: 0,
|
|
40
|
+
committeeSize: IInterfold.CommitteeSize.Minimum,
|
|
41
|
+
requestBlock: 0,
|
|
42
|
+
inputWindow: [uint256(0), uint256(0)],
|
|
43
|
+
encryptionSchemeId: ENCRYPTION_SCHEME_ID,
|
|
44
|
+
e3Program: IE3Program(address(0)),
|
|
45
|
+
paramSet: 0, // Insecure512
|
|
46
|
+
customParams: params,
|
|
47
|
+
decryptionVerifier: IDecryptionVerifier(address(0)),
|
|
48
|
+
pkVerifier: IPkVerifier(address(0)),
|
|
49
|
+
committeePublicKey: committeePublicKey,
|
|
50
|
+
ciphertextOutput: bytes32(0),
|
|
51
|
+
plaintextOutput: plaintextOutput,
|
|
52
|
+
requester: address(0),
|
|
53
|
+
ciphertextCommitment: bytes32(0)
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
IE3Program(program).validate(nextE3Id, 0, bytes(""), bytes(""), params);
|
|
57
|
+
|
|
58
|
+
nextE3Id++;
|
|
59
|
+
numOptions; // silence unused-parameter warning; the count travels inside `params`
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
function _request(address program, uint256 numOptions) internal {
|
|
34
63
|
e3s[nextE3Id] = E3({
|
|
35
64
|
seed: 0,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-only
|
|
2
|
+
//
|
|
3
|
+
// This file is provided WITHOUT ANY WARRANTY;
|
|
4
|
+
// without even the implied warranty of MERCHANTABILITY
|
|
5
|
+
// or FITNESS FOR A PARTICULAR PURPOSE.
|
|
6
|
+
pragma solidity ^0.8.27;
|
|
7
|
+
|
|
8
|
+
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
9
|
+
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
|
|
10
|
+
import { ERC20Votes } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
|
|
11
|
+
import { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol";
|
|
12
|
+
|
|
13
|
+
/// @title MockVotesToken
|
|
14
|
+
/// @notice An ERC20Votes token for exercising `CensusMode.ONCHAIN`.
|
|
15
|
+
/// @dev `MockVotingToken` is a plain ERC20 with no `getPastVotes`, which makes it the negative
|
|
16
|
+
/// case for the token probe in `CRISPProgram._initRound`. This one is the positive case.
|
|
17
|
+
///
|
|
18
|
+
/// Uses the timestamp clock, matching `InterfoldToken`. A round records its snapshot in whatever
|
|
19
|
+
/// units the token reports, so a mock on the default block-number clock would exercise a
|
|
20
|
+
/// different path from the token this mode is built for.
|
|
21
|
+
contract MockVotesToken is ERC20, ERC20Permit, ERC20Votes {
|
|
22
|
+
constructor() ERC20("Mock Votes Token", "MVOTE") ERC20Permit("Mock Votes Token") {
|
|
23
|
+
_mint(msg.sender, 1e24);
|
|
24
|
+
_delegate(msg.sender, msg.sender);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mint(address to, uint256 amount) external {
|
|
28
|
+
_mint(to, amount);
|
|
29
|
+
if (delegates(to) == address(0)) _delegate(to, to);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clock() public view override returns (uint48) {
|
|
33
|
+
return uint48(block.timestamp);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// @dev Declared pure per ERC-6372, so it cannot read `clock()`.
|
|
37
|
+
function CLOCK_MODE() public pure override returns (string memory) {
|
|
38
|
+
return "mode=timestamp";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) {
|
|
42
|
+
super._update(from, to, value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) {
|
|
46
|
+
return super.nonces(owner);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-only
|
|
2
|
+
//
|
|
3
|
+
// This file is provided WITHOUT ANY WARRANTY;
|
|
4
|
+
// without even the implied warranty of MERCHANTABILITY
|
|
5
|
+
// or FITNESS FOR A PARTICULAR PURPOSE.
|
|
6
|
+
pragma solidity >=0.8.27;
|
|
7
|
+
|
|
8
|
+
/// @notice The ERC-6372 clock of a token, used to record the snapshot of a round.
|
|
9
|
+
/// @dev Kept separate from `IVotesToken` because it is the optional half of the token surface.
|
|
10
|
+
/// A token that predates ERC-6372 still answers `getPastVotes`, so `CRISPProgram` falls back to
|
|
11
|
+
/// block numbers when this call reverts rather than refusing the round.
|
|
12
|
+
interface IERC6372Clock {
|
|
13
|
+
/// @notice The current timepoint, in the clock units of the token.
|
|
14
|
+
/// @return The timepoint.
|
|
15
|
+
function clock() external view returns (uint48);
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-only
|
|
2
|
+
//
|
|
3
|
+
// This file is provided WITHOUT ANY WARRANTY;
|
|
4
|
+
// without even the implied warranty of MERCHANTABILITY
|
|
5
|
+
// or FITNESS FOR A PARTICULAR PURPOSE.
|
|
6
|
+
pragma solidity >=0.8.27;
|
|
7
|
+
|
|
8
|
+
/// @notice The subset of a generated Honk verifier that `CRISPProgram` calls.
|
|
9
|
+
/// @dev Declared as an interface so the census paths can hold verifiers generated from different
|
|
10
|
+
/// circuits. `CRISPVerifier.sol` and `CRISPOnchainVerifier.sol` both declare a contract named
|
|
11
|
+
/// `HonkVerifier`, so importing both concrete types would collide.
|
|
12
|
+
interface IHonkVerifier {
|
|
13
|
+
/// @notice Verify a folded ballot proof against its public inputs.
|
|
14
|
+
/// @dev Reverts rather than returning false when the public inputs do not match the proof.
|
|
15
|
+
/// @param proof The folded proof.
|
|
16
|
+
/// @param publicInputs The public inputs, in the order the fold circuit declares them.
|
|
17
|
+
/// @return True when the proof is valid.
|
|
18
|
+
function verify(bytes calldata proof, bytes32[] calldata publicInputs) external view returns (bool);
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-only
|
|
2
|
+
//
|
|
3
|
+
// This file is provided WITHOUT ANY WARRANTY;
|
|
4
|
+
// without even the implied warranty of MERCHANTABILITY
|
|
5
|
+
// or FITNESS FOR A PARTICULAR PURPOSE.
|
|
6
|
+
pragma solidity >=0.8.27;
|
|
7
|
+
|
|
8
|
+
/// @notice The subset of an ERC20Votes token that `CensusMode.ONCHAIN` reads.
|
|
9
|
+
/// @dev Required, not optional. A round that names a token which cannot answer this is rejected
|
|
10
|
+
/// at request time, because every input would otherwise revert here after the fee is paid.
|
|
11
|
+
interface IVotesToken {
|
|
12
|
+
/// @notice The voting power of an account at a past timepoint.
|
|
13
|
+
/// @param account The account to read.
|
|
14
|
+
/// @param timepoint The timepoint, in the ERC-6372 clock units of the token.
|
|
15
|
+
/// @return The voting power at that timepoint.
|
|
16
|
+
function getPastVotes(address account, uint256 timepoint) external view returns (uint256);
|
|
17
|
+
}
|