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

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/README.md CHANGED
@@ -63,7 +63,7 @@ Per-platform ERC-4337 paymaster cloned from a shared implementation.
63
63
  Deploys `PlatformPaymaster` clones deterministically.
64
64
 
65
65
  - `deployPlatformPaymaster(platformAddress, dailyLimit, salt)` — clones the implementation and calls `initialize`
66
- - `computePaymasterAddress(salt)` — predict the clone address before deployment
66
+ - `computePaymasterAddress(deployer, platformAddress, salt)` — predict the clone address before deployment. The CREATE2 salt is `keccak256(abi.encode(deployer, platformAddress, salt))`, so another caller cannot take the predicted address.
67
67
  - `updateTdocDeployer(addr)` — update TDoc deployer (`onlyOwner`)
68
68
  - `updatePaymasterImplementation(addr)` — upgrade the implementation for future clones (`onlyOwner`)
69
69
 
@@ -43,7 +43,12 @@ contract PlatformAccountFactory is Ownable {
43
43
  uint256 dailyLimit,
44
44
  bytes32 salt
45
45
  ) external returns (address paymaster) {
46
- paymaster = Clones.cloneDeterministic(paymasterImplementation, salt);
46
+ // Bind the CREATE2 salt to the caller and platform so a third party who
47
+ // only knows `salt` cannot deploy at the address this caller would get.
48
+ paymaster = Clones.cloneDeterministic(
49
+ paymasterImplementation,
50
+ _paymasterSalt(msg.sender, platformAddress, salt)
51
+ );
47
52
  PlatformPaymaster(payable(paymaster)).initialize(
48
53
  platformAddress,
49
54
  dailyLimit,
@@ -52,15 +57,26 @@ contract PlatformAccountFactory is Ownable {
52
57
  emit PlatformOnboarded(platformAddress, paymaster);
53
58
  }
54
59
 
55
- // Address is determined solely by implementation + salt (not by constructor args).
60
+ // Predict the clone address for a specific deployer and platform.
61
+ // `deployer` must be the account that will call deployPlatformPaymaster.
56
62
  function computePaymasterAddress(
63
+ address deployer,
64
+ address platformAddress,
57
65
  bytes32 salt
58
66
  ) external view returns (address) {
59
67
  return
60
68
  Clones.predictDeterministicAddress(
61
69
  paymasterImplementation,
62
- salt,
70
+ _paymasterSalt(deployer, platformAddress, salt),
63
71
  address(this)
64
72
  );
65
73
  }
74
+
75
+ function _paymasterSalt(
76
+ address deployer,
77
+ address platformAddress,
78
+ bytes32 salt
79
+ ) internal pure returns (bytes32) {
80
+ return keccak256(abi.encode(deployer, platformAddress, salt));
81
+ }
66
82
  }
@@ -36,7 +36,7 @@ interface IAccessControl {
36
36
  * Access control is enforced by the Storage contract itself:
37
37
  * require(msg.sender == beneficiary || msg.sender == holder)
38
38
  * so non-beneficiary/holder callers are rejected at execution time.
39
- * If the call reverts, _postOp skips the spend update.
39
+ * Reverted ops are still charged to dailySpend in _postOp (gas was paid).
40
40
  */
41
41
  contract PlatformPaymaster is BasePaymaster {
42
42
  bytes4 private constant EXECUTE_SEL =
@@ -70,6 +70,10 @@ contract PlatformPaymaster is BasePaymaster {
70
70
  mapping(address => uint256) public lastReset;
71
71
  uint256 public dailyLimit;
72
72
 
73
+ // Set once by initialize() (or the constructor for the implementation).
74
+ // Kept separate from owner() so renounceOwnership() cannot re-open initialize().
75
+ bool private _initialized;
76
+
73
77
  event RegistryAdded(address indexed registry);
74
78
  event RegistryRemoved(address indexed registry);
75
79
  event TitleEscrowLinked(
@@ -89,19 +93,22 @@ contract PlatformPaymaster is BasePaymaster {
89
93
  event UserOpRejected(address indexed user, string reason);
90
94
  event DailyLimitUpdated(uint256 newLimit);
91
95
 
92
- // Deployed once as the shared implementation; BasePaymaster sets owner = msg.sender.
93
- // That non-zero owner prevents initialize() from running on the implementation itself.
94
- constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {}
96
+ // Deployed once as the shared implementation; lock it so initialize() can never run on it.
97
+ constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {
98
+ _initialized = true;
99
+ }
95
100
 
96
101
  // Called by the factory immediately after cloneDeterministic().
97
- // A fresh clone has all-zero storage, so owner() == address(0) exactly once.
102
+ // Initialization state is tracked separately from ownership, so renounceOwnership()
103
+ // cannot make an initialized clone look fresh and re-open initialize().
98
104
  function initialize(
99
105
  address _owner,
100
106
  uint256 _dailyLimit,
101
107
  address _tdocDeployer
102
108
  ) external {
103
- require(owner() == address(0), "Already initialized");
109
+ require(!_initialized, "Already initialized");
104
110
  require(_owner != address(0), "Zero owner");
111
+ _initialized = true;
105
112
  _transferOwnership(_owner);
106
113
  dailyLimit = _dailyLimit;
107
114
  tdocDeployer = ITDocDeployer(_tdocDeployer);
@@ -278,6 +285,10 @@ contract PlatformPaymaster is BasePaymaster {
278
285
  // Double-spend safe: EntryPoint sequential nonces allow only one
279
286
  // pending UserOp per sender in the mempool at a time.
280
287
  if (target == address(this)) {
288
+ if (innerData.length < 4) {
289
+ emit UserOpRejected(sender, "inner callData too short");
290
+ return ("", _packValidationData(true, 0, 0));
291
+ }
281
292
  bytes4 innerSel;
282
293
  assembly {
283
294
  innerSel := mload(add(innerData, 32))
@@ -289,7 +300,15 @@ contract PlatformPaymaster is BasePaymaster {
289
300
  emit UserOpRejected(sender, "not whitelisted");
290
301
  return ("", _packValidationData(true, 0, 0));
291
302
  }
292
- // credits consumed inside deployRegistry; flag as deployment to skip daily spend
303
+ if (address(tdocDeployer) == address(0)) {
304
+ emit UserOpRejected(sender, "TdocDeployer not set");
305
+ return ("", _packValidationData(true, 0, 0));
306
+ }
307
+ // Reverts on malformed ABI args, so only well-formed
308
+ // deployRegistry(address,string,string) payloads are sponsored.
309
+ _decodeDeployArgs(innerData);
310
+ // credits consumed inside deployRegistry; flag as deployment to skip daily spend.
311
+ // _postOp falls back to normal accounting if execution reverts.
293
312
  return (
294
313
  abi.encode(sender, maxCost, true),
295
314
  _packValidationData(false, 0, 0)
@@ -336,18 +355,38 @@ contract PlatformPaymaster is BasePaymaster {
336
355
  (address, uint256, bool)
337
356
  );
338
357
 
339
- // Deployment ops are credit-gated; skip daily spend tracking for them
340
- if (!isDeployment) {
358
+ // Successful deployments are credit-gated; skip daily spend tracking for them.
359
+ // A reverted deployment rolls back the credit decrement inside deployRegistry,
360
+ // so charge it like a normal op and burn the credit here instead — otherwise a
361
+ // sender could replay failing deployments to drain the deposit for free.
362
+ if (!isDeployment || mode != PostOpMode.opSucceeded) {
341
363
  if (block.timestamp > lastReset[sender] + 1 days) {
342
364
  dailySpend[sender] = 0;
343
365
  lastReset[sender] = block.timestamp;
344
366
  }
345
367
  dailySpend[sender] += actualGasCost;
346
368
  }
369
+ if (isDeployment && mode == PostOpMode.opReverted) {
370
+ uint256 credits = userWhitelist[sender];
371
+ if (credits > 0) {
372
+ userWhitelist[sender] = credits - 1;
373
+ emit UserWhitelistUpdated(sender, credits - 1);
374
+ }
375
+ }
347
376
 
348
377
  emit UserOpSponsored(sender, actualGasCost);
349
378
  }
350
379
 
380
+ // Strips the 4-byte selector and ABI-decodes deployRegistry's arguments.
381
+ function _decodeDeployArgs(bytes memory innerData) private pure {
382
+ uint256 argsLength = innerData.length - 4;
383
+ bytes memory args = new bytes(argsLength);
384
+ assembly ("memory-safe") {
385
+ mcopy(add(args, 32), add(innerData, 36), argsLength)
386
+ }
387
+ abi.decode(args, (address, string, string));
388
+ }
389
+
351
390
  // v0.8 contracts on Etherspot EntryPoint — skip interface mismatch check
352
391
  function _validateEntryPointInterface(IEntryPoint) internal pure override {}
353
392
 
package/dist/index.d.mts CHANGED
@@ -322,6 +322,14 @@ declare const platformAccountFactoryAbi: readonly [{
322
322
  readonly type: "event";
323
323
  }, {
324
324
  readonly inputs: readonly [{
325
+ readonly internalType: "address";
326
+ readonly name: "deployer";
327
+ readonly type: "address";
328
+ }, {
329
+ readonly internalType: "address";
330
+ readonly name: "platformAddress";
331
+ readonly type: "address";
332
+ }, {
325
333
  readonly internalType: "bytes32";
326
334
  readonly name: "salt";
327
335
  readonly type: "bytes32";
package/dist/index.d.ts CHANGED
@@ -322,6 +322,14 @@ declare const platformAccountFactoryAbi: readonly [{
322
322
  readonly type: "event";
323
323
  }, {
324
324
  readonly inputs: readonly [{
325
+ readonly internalType: "address";
326
+ readonly name: "deployer";
327
+ readonly type: "address";
328
+ }, {
329
+ readonly internalType: "address";
330
+ readonly name: "platformAddress";
331
+ readonly type: "address";
332
+ }, {
325
333
  readonly internalType: "bytes32";
326
334
  readonly name: "salt";
327
335
  readonly type: "bytes32";
package/dist/index.js CHANGED
@@ -440,6 +440,16 @@ var platformAccountFactoryAbi = [
440
440
  },
441
441
  {
442
442
  "inputs": [
443
+ {
444
+ "internalType": "address",
445
+ "name": "deployer",
446
+ "type": "address"
447
+ },
448
+ {
449
+ "internalType": "address",
450
+ "name": "platformAddress",
451
+ "type": "address"
452
+ },
443
453
  {
444
454
  "internalType": "bytes32",
445
455
  "name": "salt",
package/dist/index.mjs CHANGED
@@ -438,6 +438,16 @@ var platformAccountFactoryAbi = [
438
438
  },
439
439
  {
440
440
  "inputs": [
441
+ {
442
+ "internalType": "address",
443
+ "name": "deployer",
444
+ "type": "address"
445
+ },
446
+ {
447
+ "internalType": "address",
448
+ "name": "platformAddress",
449
+ "type": "address"
450
+ },
441
451
  {
442
452
  "internalType": "bytes32",
443
453
  "name": "salt",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustvc/eip7702",
3
- "version": "1.1.0-beta.2",
3
+ "version": "1.1.0-beta.3",
4
4
  "description": "EIP-7702 smart account, platform paymaster, and factory contracts for TrustVC",
5
5
  "author": "DexTech",
6
6
  "license": "MIT",