@trustvc/eip7702 1.1.0-beta.2 → 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.
- package/README.md +1 -1
- package/contracts/Factory.sol +19 -3
- package/contracts/PlatformPaymaster.sol +100 -36
- package/dist/index.d.mts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/dist/index.mjs +10 -0
- package/package.json +1 -1
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
|
|
package/contracts/Factory.sol
CHANGED
|
@@ -43,7 +43,12 @@ contract PlatformAccountFactory is Ownable {
|
|
|
43
43
|
uint256 dailyLimit,
|
|
44
44
|
bytes32 salt
|
|
45
45
|
) external returns (address paymaster) {
|
|
46
|
-
|
|
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
|
-
//
|
|
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
|
}
|
|
@@ -23,20 +23,23 @@ 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 (
|
|
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.
|
|
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)
|
|
38
41
|
* so non-beneficiary/holder callers are rejected at execution time.
|
|
39
|
-
*
|
|
42
|
+
* Reverted ops are still charged to dailySpend in _postOp (gas was paid).
|
|
40
43
|
*/
|
|
41
44
|
contract PlatformPaymaster is BasePaymaster {
|
|
42
45
|
bytes4 private constant EXECUTE_SEL =
|
|
@@ -70,6 +73,10 @@ contract PlatformPaymaster is BasePaymaster {
|
|
|
70
73
|
mapping(address => uint256) public lastReset;
|
|
71
74
|
uint256 public dailyLimit;
|
|
72
75
|
|
|
76
|
+
// Set once by initialize() (or the constructor for the implementation).
|
|
77
|
+
// Kept separate from owner() so renounceOwnership() cannot re-open initialize().
|
|
78
|
+
bool private _initialized;
|
|
79
|
+
|
|
73
80
|
event RegistryAdded(address indexed registry);
|
|
74
81
|
event RegistryRemoved(address indexed registry);
|
|
75
82
|
event TitleEscrowLinked(
|
|
@@ -89,19 +96,22 @@ contract PlatformPaymaster is BasePaymaster {
|
|
|
89
96
|
event UserOpRejected(address indexed user, string reason);
|
|
90
97
|
event DailyLimitUpdated(uint256 newLimit);
|
|
91
98
|
|
|
92
|
-
// Deployed once as the shared implementation;
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
// Deployed once as the shared implementation; lock it so initialize() can never run on it.
|
|
100
|
+
constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {
|
|
101
|
+
_initialized = true;
|
|
102
|
+
}
|
|
95
103
|
|
|
96
104
|
// Called by the factory immediately after cloneDeterministic().
|
|
97
|
-
//
|
|
105
|
+
// Initialization state is tracked separately from ownership, so renounceOwnership()
|
|
106
|
+
// cannot make an initialized clone look fresh and re-open initialize().
|
|
98
107
|
function initialize(
|
|
99
108
|
address _owner,
|
|
100
109
|
uint256 _dailyLimit,
|
|
101
110
|
address _tdocDeployer
|
|
102
111
|
) external {
|
|
103
|
-
require(
|
|
112
|
+
require(!_initialized, "Already initialized");
|
|
104
113
|
require(_owner != address(0), "Zero owner");
|
|
114
|
+
_initialized = true;
|
|
105
115
|
_transferOwnership(_owner);
|
|
106
116
|
dailyLimit = _dailyLimit;
|
|
107
117
|
tdocDeployer = ITDocDeployer(_tdocDeployer);
|
|
@@ -161,7 +171,7 @@ contract PlatformPaymaster is BasePaymaster {
|
|
|
161
171
|
|
|
162
172
|
// Mints a TradeTrust document, captures the TitleEscrow, and auto-authorizes it.
|
|
163
173
|
// Paymaster must hold MINTER_ROLE on the registry.
|
|
164
|
-
//
|
|
174
|
+
// Caller must hold MINTER_ROLE on the registry (the deployer, or whoever they granted it to).
|
|
165
175
|
function mintDocument(
|
|
166
176
|
address registry,
|
|
167
177
|
address beneficiary,
|
|
@@ -170,11 +180,10 @@ contract PlatformPaymaster is BasePaymaster {
|
|
|
170
180
|
bytes calldata remark
|
|
171
181
|
) external returns (address titleEscrow) {
|
|
172
182
|
require(authorizedRegistries[registry], "registry not authorized");
|
|
183
|
+
// The registry only sees the paymaster as minter, so check the real caller here
|
|
173
184
|
require(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
msg.sender == owner(),
|
|
177
|
-
"caller not authorized"
|
|
185
|
+
IAccessControl(registry).hasRole(MINTER_ROLE, msg.sender),
|
|
186
|
+
"not registry minter"
|
|
178
187
|
);
|
|
179
188
|
|
|
180
189
|
titleEscrow = ITradeTrustToken(registry).mint(
|
|
@@ -264,20 +273,17 @@ contract PlatformPaymaster is BasePaymaster {
|
|
|
264
273
|
emit UserOpRejected(sender, "caller not authorized");
|
|
265
274
|
return ("", _packValidationData(true, 0, 0));
|
|
266
275
|
}
|
|
267
|
-
|
|
268
|
-
emit UserOpRejected(sender, "daily limit exceeded");
|
|
269
|
-
return ("", _packValidationData(true, 0, 0));
|
|
270
|
-
}
|
|
271
|
-
return (
|
|
272
|
-
abi.encode(sender, maxCost, false),
|
|
273
|
-
_packValidationData(false, 0, 0)
|
|
274
|
-
);
|
|
276
|
+
return _sponsorWithinDailyLimit(sender, maxCost);
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
// Path B — gasless paymaster call (deployRegistry or mintDocument)
|
|
278
280
|
// Double-spend safe: EntryPoint sequential nonces allow only one
|
|
279
281
|
// pending UserOp per sender in the mempool at a time.
|
|
280
282
|
if (target == address(this)) {
|
|
283
|
+
if (innerData.length < 4) {
|
|
284
|
+
emit UserOpRejected(sender, "inner callData too short");
|
|
285
|
+
return ("", _packValidationData(true, 0, 0));
|
|
286
|
+
}
|
|
281
287
|
bytes4 innerSel;
|
|
282
288
|
assembly {
|
|
283
289
|
innerSel := mload(add(innerData, 32))
|
|
@@ -289,7 +295,15 @@ contract PlatformPaymaster is BasePaymaster {
|
|
|
289
295
|
emit UserOpRejected(sender, "not whitelisted");
|
|
290
296
|
return ("", _packValidationData(true, 0, 0));
|
|
291
297
|
}
|
|
292
|
-
|
|
298
|
+
if (address(tdocDeployer) == address(0)) {
|
|
299
|
+
emit UserOpRejected(sender, "TdocDeployer not set");
|
|
300
|
+
return ("", _packValidationData(true, 0, 0));
|
|
301
|
+
}
|
|
302
|
+
// Reverts on malformed ABI args, so only well-formed
|
|
303
|
+
// deployRegistry(address,string,string) payloads are sponsored.
|
|
304
|
+
_decodeDeployArgs(innerData);
|
|
305
|
+
// credits consumed inside deployRegistry; flag as deployment to skip daily spend.
|
|
306
|
+
// _postOp falls back to normal accounting if execution reverts.
|
|
293
307
|
return (
|
|
294
308
|
abi.encode(sender, maxCost, true),
|
|
295
309
|
_packValidationData(false, 0, 0)
|
|
@@ -297,22 +311,25 @@ contract PlatformPaymaster is BasePaymaster {
|
|
|
297
311
|
}
|
|
298
312
|
|
|
299
313
|
if (innerSel == MINT_DOCUMENT_SEL) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
sender
|
|
304
|
-
) {
|
|
305
|
-
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");
|
|
306
318
|
return ("", _packValidationData(true, 0, 0));
|
|
307
319
|
}
|
|
308
|
-
|
|
309
|
-
|
|
320
|
+
address registry;
|
|
321
|
+
assembly {
|
|
322
|
+
registry := mload(add(innerData, 36))
|
|
323
|
+
}
|
|
324
|
+
if (!authorizedRegistries[registry]) {
|
|
325
|
+
emit UserOpRejected(sender, "registry not authorized");
|
|
310
326
|
return ("", _packValidationData(true, 0, 0));
|
|
311
327
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
_packValidationData(
|
|
315
|
-
|
|
328
|
+
if (!IAccessControl(registry).hasRole(MINTER_ROLE, sender)) {
|
|
329
|
+
emit UserOpRejected(sender, "not registry minter");
|
|
330
|
+
return ("", _packValidationData(true, 0, 0));
|
|
331
|
+
}
|
|
332
|
+
return _sponsorWithinDailyLimit(sender, maxCost);
|
|
316
333
|
}
|
|
317
334
|
|
|
318
335
|
emit UserOpRejected(sender, "unauthorized paymaster call");
|
|
@@ -323,6 +340,33 @@ contract PlatformPaymaster is BasePaymaster {
|
|
|
323
340
|
return ("", _packValidationData(true, 0, 0));
|
|
324
341
|
}
|
|
325
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
|
+
|
|
326
370
|
function _postOp(
|
|
327
371
|
PostOpMode mode,
|
|
328
372
|
bytes calldata context,
|
|
@@ -336,18 +380,38 @@ contract PlatformPaymaster is BasePaymaster {
|
|
|
336
380
|
(address, uint256, bool)
|
|
337
381
|
);
|
|
338
382
|
|
|
339
|
-
//
|
|
340
|
-
|
|
383
|
+
// Successful deployments are credit-gated; skip daily spend tracking for them.
|
|
384
|
+
// A reverted deployment rolls back the credit decrement inside deployRegistry,
|
|
385
|
+
// so charge it like a normal op and burn the credit here instead — otherwise a
|
|
386
|
+
// sender could replay failing deployments to drain the deposit for free.
|
|
387
|
+
if (!isDeployment || mode != PostOpMode.opSucceeded) {
|
|
341
388
|
if (block.timestamp > lastReset[sender] + 1 days) {
|
|
342
389
|
dailySpend[sender] = 0;
|
|
343
390
|
lastReset[sender] = block.timestamp;
|
|
344
391
|
}
|
|
345
392
|
dailySpend[sender] += actualGasCost;
|
|
346
393
|
}
|
|
394
|
+
if (isDeployment && mode == PostOpMode.opReverted) {
|
|
395
|
+
uint256 credits = userWhitelist[sender];
|
|
396
|
+
if (credits > 0) {
|
|
397
|
+
userWhitelist[sender] = credits - 1;
|
|
398
|
+
emit UserWhitelistUpdated(sender, credits - 1);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
347
401
|
|
|
348
402
|
emit UserOpSponsored(sender, actualGasCost);
|
|
349
403
|
}
|
|
350
404
|
|
|
405
|
+
// Strips the 4-byte selector and ABI-decodes deployRegistry's arguments.
|
|
406
|
+
function _decodeDeployArgs(bytes memory innerData) private pure {
|
|
407
|
+
uint256 argsLength = innerData.length - 4;
|
|
408
|
+
bytes memory args = new bytes(argsLength);
|
|
409
|
+
assembly ("memory-safe") {
|
|
410
|
+
mcopy(add(args, 32), add(innerData, 36), argsLength)
|
|
411
|
+
}
|
|
412
|
+
abi.decode(args, (address, string, string));
|
|
413
|
+
}
|
|
414
|
+
|
|
351
415
|
// v0.8 contracts on Etherspot EntryPoint — skip interface mismatch check
|
|
352
416
|
function _validateEntryPointInterface(IEntryPoint) internal pure override {}
|
|
353
417
|
|
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",
|