@trustvc/eip7702 1.1.0-beta.3 → 1.1.0-beta.4

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.
@@ -23,15 +23,18 @@ interface ITradeTrustToken {
23
23
  interface IAccessControl {
24
24
  function grantRole(bytes32 role, address account) external;
25
25
  function renounceRole(bytes32 role, address callerConfirmation) external;
26
+ function hasRole(bytes32 role, address account) external view returns (bool);
26
27
  }
27
28
 
28
29
  /**
29
30
  * PlatformPaymaster — validates by inspecting the target contract in callData.
30
31
  *
31
- * Validation strategy (no third-party storage reads → bundler compliant):
32
+ * Validation strategy (paymaster must be staked → bundler compliant):
32
33
  * 1. Decode userOp.callData → must be execute(address to, uint256, bytes)
33
34
  * 2. Check `to` is in authorizedRegistries (paymaster's own storage)
34
- * 3. Check daily spend (paymaster's own storage — always allowed)
35
+ * 3. mintDocument only: sender holds MINTER_ROLE on the registry
36
+ * (registry storage associated with sender)
37
+ * 4. Check daily spend (paymaster's own storage)
35
38
  *
36
39
  * Access control is enforced by the Storage contract itself:
37
40
  * require(msg.sender == beneficiary || msg.sender == holder)
@@ -168,7 +171,7 @@ contract PlatformPaymaster is BasePaymaster {
168
171
 
169
172
  // Mints a TradeTrust document, captures the TitleEscrow, and auto-authorizes it.
170
173
  // Paymaster must hold MINTER_ROLE on the registry.
171
- // Callable gaslessly by userWhitelist users (credits not consumed — only deployRegistry uses credits).
174
+ // Caller must hold MINTER_ROLE on the registry (the deployer, or whoever they granted it to).
172
175
  function mintDocument(
173
176
  address registry,
174
177
  address beneficiary,
@@ -177,11 +180,10 @@ contract PlatformPaymaster is BasePaymaster {
177
180
  bytes calldata remark
178
181
  ) external returns (address titleEscrow) {
179
182
  require(authorizedRegistries[registry], "registry not authorized");
183
+ // The registry only sees the paymaster as minter, so check the real caller here
180
184
  require(
181
- authorizedCallers[msg.sender] ||
182
- userWhitelist[msg.sender] > 0 ||
183
- msg.sender == owner(),
184
- "caller not authorized"
185
+ IAccessControl(registry).hasRole(MINTER_ROLE, msg.sender),
186
+ "not registry minter"
185
187
  );
186
188
 
187
189
  titleEscrow = ITradeTrustToken(registry).mint(
@@ -271,14 +273,7 @@ contract PlatformPaymaster is BasePaymaster {
271
273
  emit UserOpRejected(sender, "caller not authorized");
272
274
  return ("", _packValidationData(true, 0, 0));
273
275
  }
274
- if (dailyLimit > 0 && dailySpend[sender] + maxCost > dailyLimit) {
275
- emit UserOpRejected(sender, "daily limit exceeded");
276
- return ("", _packValidationData(true, 0, 0));
277
- }
278
- return (
279
- abi.encode(sender, maxCost, false),
280
- _packValidationData(false, 0, 0)
281
- );
276
+ return _sponsorWithinDailyLimit(sender, maxCost);
282
277
  }
283
278
 
284
279
  // Path B — gasless paymaster call (deployRegistry or mintDocument)
@@ -316,22 +311,25 @@ contract PlatformPaymaster is BasePaymaster {
316
311
  }
317
312
 
318
313
  if (innerSel == MINT_DOCUMENT_SEL) {
319
- if (
320
- !authorizedCallers[sender] &&
321
- userWhitelist[sender] == 0 &&
322
- sender != owner()
323
- ) {
324
- emit UserOpRejected(sender, "caller not authorized");
314
+ // Mirror mintDocument's checks so non-minters can't get failing ops sponsored.
315
+ // hasRole reads _roles[role][sender] — storage associated with sender (ERC-7562).
316
+ if (innerData.length < 36) {
317
+ emit UserOpRejected(sender, "mint calldata too short");
318
+ return ("", _packValidationData(true, 0, 0));
319
+ }
320
+ address registry;
321
+ assembly {
322
+ registry := mload(add(innerData, 36))
323
+ }
324
+ if (!authorizedRegistries[registry]) {
325
+ emit UserOpRejected(sender, "registry not authorized");
325
326
  return ("", _packValidationData(true, 0, 0));
326
327
  }
327
- if (dailyLimit > 0 && dailySpend[sender] + maxCost > dailyLimit) {
328
- emit UserOpRejected(sender, "daily limit exceeded");
328
+ if (!IAccessControl(registry).hasRole(MINTER_ROLE, sender)) {
329
+ emit UserOpRejected(sender, "not registry minter");
329
330
  return ("", _packValidationData(true, 0, 0));
330
331
  }
331
- return (
332
- abi.encode(sender, maxCost, false),
333
- _packValidationData(false, 0, 0)
334
- );
332
+ return _sponsorWithinDailyLimit(sender, maxCost);
335
333
  }
336
334
 
337
335
  emit UserOpRejected(sender, "unauthorized paymaster call");
@@ -342,6 +340,33 @@ contract PlatformPaymaster is BasePaymaster {
342
340
  return ("", _packValidationData(true, 0, 0));
343
341
  }
344
342
 
343
+ // Approves a daily-limit-tracked op. If the sender is over today's limit, the op
344
+ // is approved with validAfter = window end, so it only runs after the window rolls
345
+ // over and _postOp resets the counter. block.timestamp is banned in validation
346
+ // (ERC-7562), so the EntryPoint enforces the time via validAfter.
347
+ function _sponsorWithinDailyLimit(
348
+ address sender,
349
+ uint256 maxCost
350
+ ) internal returns (bytes memory context, uint256 validationData) {
351
+ context = abi.encode(sender, maxCost, false);
352
+
353
+ if (dailyLimit > 0 && dailySpend[sender] + maxCost > dailyLimit) {
354
+ // Even a fresh window can't fit this op
355
+ if (maxCost > dailyLimit) {
356
+ emit UserOpRejected(sender, "maxCost exceeds daily limit");
357
+ return ("", _packValidationData(true, 0, 0));
358
+ }
359
+ // EntryPoint runs the op only when block.timestamp > validAfter, which is
360
+ // exactly when _postOp's reset condition becomes true
361
+ return (
362
+ context,
363
+ _packValidationData(false, 0, uint48(lastReset[sender] + 1 days))
364
+ );
365
+ }
366
+
367
+ return (context, _packValidationData(false, 0, 0));
368
+ }
369
+
345
370
  function _postOp(
346
371
  PostOpMode mode,
347
372
  bytes calldata context,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustvc/eip7702",
3
- "version": "1.1.0-beta.3",
3
+ "version": "1.1.0-beta.4",
4
4
  "description": "EIP-7702 smart account, platform paymaster, and factory contracts for TrustVC",
5
5
  "author": "DexTech",
6
6
  "license": "MIT",