@coordination-network/contracts 0.0.1 → 0.0.2

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.
@@ -96,9 +96,9 @@ contract ApprovedIssuerSet is IApprovedIssuerSet {
96
96
  uint256 public rootDuration;
97
97
 
98
98
  /// @notice The instant at or before which no superseded root is ever acceptable again.
99
- /// @dev Bumped by {retireAllIssuerKeys} on every removal of a member that ever held a key, so
100
- /// removing one member closes off every root superseded in the window before removal, not just the
101
- /// one root the removal directly replaces.
99
+ /// @dev Bumped by {retireIssuerKeyImmediately}, and by {retireAllIssuerKeys} on every removal of a
100
+ /// member that ever held a key, so either closes off every root superseded in the window before it,
101
+ /// not just the one root it directly replaces.
102
102
  uint256 public rootsInvalidatedAt;
103
103
 
104
104
  /// @dev Whether a member has ever published a live key. Set once, never cleared, so
@@ -257,7 +257,7 @@ contract ApprovedIssuerSet is IApprovedIssuerSet {
257
257
  /// @param memberNetworkId The member the key belongs to.
258
258
  /// @param leaf The commitment to retire.
259
259
  function retireIssuerKey(bytes32 memberNetworkId, uint256 leaf)
260
- external
260
+ public
261
261
  onlyAuthAddressOrGovernance(memberNetworkId)
262
262
  {
263
263
  _detachFromMember(memberNetworkId, leaf);
@@ -279,6 +279,16 @@ contract ApprovedIssuerSet is IApprovedIssuerSet {
279
279
  emit IssuerKeyRetired(memberNetworkId, leaf, newRoot);
280
280
  }
281
281
 
282
+ /// @notice Retires a signing key immediately and stops every superseded root being accepted at once.
283
+ /// @dev Useful in cases of key theft so prevents attack using key during grace window. In-flight
284
+ /// proofs against old root must be rebuilt.
285
+ /// @param memberNetworkId The network member the key belongs to.
286
+ /// @param leaf The commitment to retire.
287
+ function retireIssuerKeyImmediately(bytes32 memberNetworkId, uint256 leaf) external {
288
+ rootsInvalidatedAt = block.timestamp;
289
+ retireIssuerKey(memberNetworkId, leaf);
290
+ }
291
+
282
292
  /// @inheritdoc IApprovedIssuerSet
283
293
  /// @dev Callable only by the registry, which invokes it while removing a member so that removal
284
294
  /// and the loss of signing power are one transaction. Two transactions would leave a window in
@@ -594,7 +604,7 @@ contract ApprovedIssuerSet is IApprovedIssuerSet {
594
604
  }
595
605
  }
596
606
 
597
- /// @dev Rejects a public key that is not a Baby Jubjub point of full order.
607
+ /// @dev Rejects a public key that is outside the field, off the curve, or of low order.
598
608
  ///
599
609
  /// The tree would accept any field element as a leaf, so without the curve check the set could
600
610
  /// hold a commitment to a point that does not exist.
@@ -607,6 +617,12 @@ contract ApprovedIssuerSet is IApprovedIssuerSet {
607
617
  /// verify as issued by an approved member, and issuer anonymity means no verifier could say whose
608
618
  /// key to retire.
609
619
  ///
620
+ /// It does not reject a mixed-order key, a prime-order point plus a nonzero torsion point. The
621
+ /// circuit's EdDSA (noir-lang/eddsa 7e206c95) multiplies the key by 8 before verifying, so only the
622
+ /// holder of its prime-order part's secret can sign under such a key. The only effect is that one
623
+ /// secret can take several leaves, at most eight live ones, one per torsion point, possibly under
624
+ /// different members. The TypeScript `issuer-set` refuses to publish them.
625
+ ///
610
626
  /// @param publicKeyX The x coordinate.
611
627
  /// @param publicKeyY The y coordinate.
612
628
  function _assertOnCurve(uint256 publicKeyX, uint256 publicKeyY) private pure {
@@ -3,10 +3,9 @@ pragma solidity ^0.8.34;
3
3
 
4
4
  import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
5
5
  import { BasePaymaster } from "@account-abstraction/contracts/core/BasePaymaster.sol";
6
- import { IPaymaster } from "@account-abstraction/contracts/interfaces/IPaymaster.sol";
7
6
  import { PackedUserOperation } from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol";
8
7
  import { _packValidationData } from "@account-abstraction/contracts/core/Helpers.sol";
9
- import { UserOperationLib } from "@account-abstraction/contracts/core/UserOperationLib.sol";
8
+ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
10
9
  import { Semaphore } from "@semaphore-protocol/contracts/Semaphore.sol";
11
10
  import { ISemaphore } from "@semaphore-protocol/contracts/interfaces/ISemaphore.sol";
12
11
  import { ISemaphoreVerifier } from "@semaphore-protocol/contracts/interfaces/ISemaphoreVerifier.sol";
@@ -17,8 +16,6 @@ import { MIN_DEPTH, MAX_DEPTH } from "@semaphore-protocol/contracts/base/Constan
17
16
  * @notice A paymaster that validates user operations in a privacy preserving way using a single semaphore group
18
17
  */
19
18
  contract SemaphorePaymaster is BasePaymaster, Semaphore {
20
- using UserOperationLib for PackedUserOperation;
21
-
22
19
  /**
23
20
  * The paymaster uses a single Semaphore group
24
21
  */
@@ -30,46 +27,17 @@ contract SemaphorePaymaster is BasePaymaster, Semaphore {
30
27
  uint256 private constant ENCODED_PROOF_LENGTH = 13 * 32;
31
28
 
32
29
  /**
33
- * Sponsorship stops for the rest of the epoch once spend reaches this cap, bounding what a
34
- * stolen identity can drain before the owner can revoke or withdraw
35
- */
36
- uint256 public immutable SPEND_CAP_PER_EPOCH;
37
- uint256 public immutable EPOCH_DURATION;
38
-
39
- /**
40
- * Smaller than epoch cap, so exhausting the cap takes many userOps rather than one.
41
30
  * `preVerificationGas` is self declared and never refunded, so `maxCost` can be used to drain the paymaster
42
31
  */
43
32
  uint256 public immutable MAX_COST_PER_OP;
44
33
 
45
- /**
46
- * Enough for `_postOp` to record spend. The EntryPoint charges the paymaster even when `postOp`
47
- * runs out of gas, so validation requires the limit up front
48
- */
49
- uint256 public constant MIN_POSTOP_GAS = 40_000;
50
-
51
- /**
52
- * Validation cannot read `block.timestamp` (banned by ERC-7562), so the epoch lives in
53
- * storage and is advanced by `updateEpoch`
54
- */
55
- uint256 public currentEpoch;
56
-
57
- mapping(uint256 epoch => uint256 spent) public spentInEpoch;
58
-
59
34
  error ZeroOwner();
60
35
 
61
- constructor(
62
- address _entryPoint,
63
- address _verifier,
64
- address _owner,
65
- uint256 _spendCapPerEpoch,
66
- uint256 _epochDuration,
67
- uint256 _maxCostPerOp
68
- ) BasePaymaster(IEntryPoint(_entryPoint)) Semaphore(ISemaphoreVerifier(_verifier)) {
69
- SPEND_CAP_PER_EPOCH = _spendCapPerEpoch;
70
- EPOCH_DURATION = _epochDuration;
36
+ constructor(address _entryPoint, address _verifier, address _owner, uint256 _maxCostPerOp)
37
+ BasePaymaster(IEntryPoint(_entryPoint))
38
+ Semaphore(ISemaphoreVerifier(_verifier))
39
+ {
71
40
  MAX_COST_PER_OP = _maxCostPerOp;
72
- currentEpoch = block.timestamp / _epochDuration;
73
41
 
74
42
  // _transferOwnership skips zero-address guard
75
43
  if (_owner == address(0)) revert ZeroOwner();
@@ -101,11 +69,7 @@ contract SemaphorePaymaster is BasePaymaster, Semaphore {
101
69
  override
102
70
  returns (bytes memory context, uint256 validationData)
103
71
  {
104
- if (maxCost > MAX_COST_PER_OP || spentInEpoch[currentEpoch] + maxCost > SPEND_CAP_PER_EPOCH) {
105
- return ("", _packValidationData(true, 0, 0));
106
- }
107
-
108
- if (userOp.unpackPostOpGasLimit() < MIN_POSTOP_GAS) {
72
+ if (maxCost > MAX_COST_PER_OP) {
109
73
  return ("", _packValidationData(true, 0, 0));
110
74
  }
111
75
 
@@ -156,10 +120,7 @@ contract SemaphorePaymaster is BasePaymaster, Semaphore {
156
120
  return ("", _packValidationData(true, 0, 0));
157
121
  }
158
122
 
159
- // merkleRootCreationDate is block.timestamp. Overflow would require owner
160
- // setting very high merkleTreeDuration
161
- // forge-lint: disable-next-line(unsafe-typecast)
162
- validUntil = uint48(merkleRootCreationDate + merkleTreeDuration);
123
+ validUntil = SafeCast.toUint48(merkleRootCreationDate + merkleTreeDuration);
163
124
  }
164
125
 
165
126
  // Call `verifier.verifyProof` directly. `this.verifyProof` reads `block.timestamp` when the
@@ -175,20 +136,7 @@ contract SemaphorePaymaster is BasePaymaster, Semaphore {
175
136
  return ("", _packValidationData(true, 0, 0));
176
137
  }
177
138
 
178
- // a non empty context is what makes the EntryPoint call `postOp`
179
- return (hex"01", _packValidationData(false, validUntil, 0));
180
- }
181
-
182
- function _postOp(IPaymaster.PostOpMode, bytes calldata, uint256 actualGasCost, uint256) internal override {
183
- updateEpoch();
184
- spentInEpoch[currentEpoch] += actualGasCost;
185
- }
186
-
187
- /**
188
- * Advance to the current epoch, freeing the spend cap again.
189
- */
190
- function updateEpoch() public {
191
- currentEpoch = block.timestamp / EPOCH_DURATION;
139
+ return ("", _packValidationData(false, validUntil, 0));
192
140
  }
193
141
 
194
142
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coordination-network/contracts",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "The coordination network's Solidity contracts: the network member registry and its governance, the identity registry, the approved-issuer set, the revocation registries and the Semaphore paymaster.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -83,4 +83,5 @@ yarn test:contracts
83
83
  - There is deliberately no on-chain Merkle proof verifier. Verifying a plaintext proof would reveal the commitment, and a caller willing to do that can just call `isRevoked`, which is cheaper and answers the same question. The private path is a zero-knowledge proof verified elsewhere, whose root is checked with `isValidRoot`.
84
84
  - Registries are isolated. One deployment serves many issuers, and an admin can only write to its own registry.
85
85
  - Admin transfers are two-step, so a typo cannot strand a registry.
86
+ - `unrevoke` and `updateRevocationCommitment` are for correcting mistakes, such as a commitment revoked in error or an invalid value written to the tree. Like every write here, they are visible to anyone reading the chain. `unrevoke` shows that a commitment is no longer revoked. `updateRevocationCommitment` shows the old and new commitments together, so an observer can tell one replaced the other. Whether that link reveals anything depends on how the commitments are generated and handled in your use case. If two commitments should not appear related, use `unrevoke` and a separate `revoke` instead, spaced apart in time, since two writes landing back to back can be linked by their timing alone.
86
87
  - `test/integration/CrossLanguageRoot.t.sol` pins the contract's roots against the TypeScript package. Any drift in the hashing or the insertion rules fails a test on both sides.