@trustvc/eip7702 1.0.0-beta.1
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 +156 -0
- package/contracts/EIP7702Implementation.sol +98 -0
- package/contracts/Factory.sol +50 -0
- package/contracts/PlatformPaymaster.sol +347 -0
- package/dist/index.d.mts +1156 -0
- package/dist/index.d.ts +1156 -0
- package/dist/index.js +1503 -0
- package/dist/index.mjs +1500 -0
- package/package.json +86 -0
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# TrustVC EIP-7702 Smart Account
|
|
2
|
+
|
|
3
|
+
Gasless trade document operations on TrustVC via **EIP-7702** (smart account delegation) and **ERC-4337** (account abstraction). Deployed on Sepolia testnet.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides three contracts:
|
|
8
|
+
|
|
9
|
+
| Contract | Role |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `EIP7702Implementation` | Shared smart account logic that EOAs delegate to via EIP-7702 |
|
|
12
|
+
| `PlatformPaymaster` | Per-platform ERC-4337 paymaster — sponsors gas for registry and title escrow operations |
|
|
13
|
+
| `PlatformAccountFactory` | Deploys `PlatformPaymaster` clones cheaply via EIP-1167 (`Clones.cloneDeterministic`) |
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
1. **Delegate** — An EOA signs an EIP-7702 authorization pointing to `EIP7702Implementation`. The EOA's code becomes `0xef0100 || impl_address`, giving it full smart-account capabilities while keeping its original private key.
|
|
18
|
+
2. **Deploy paymaster** — A platform calls `PlatformAccountFactory.deployPlatformPaymaster()`. A minimal-proxy clone (~55k gas vs ~1.5M for a full deploy) is initialized with the platform's owner address, daily ETH limit, and TDoc deployer.
|
|
19
|
+
3. **Gasless ops** — Users submit `UserOperation`s through a bundler (Pimlico). The paymaster validates and sponsors:
|
|
20
|
+
- **Path A** — Calls to authorized registries or title escrows (beneficiary/holder/owner only, daily limit enforced)
|
|
21
|
+
- **Path B** — `deployRegistry` (credit-gated) and `mintDocument` (MINTER\_ROLE gated) on the paymaster itself
|
|
22
|
+
|
|
23
|
+
## Contracts
|
|
24
|
+
|
|
25
|
+
### EIP7702Implementation
|
|
26
|
+
|
|
27
|
+
EIP-7702 smart account using OpenZeppelin's `Account`, `SignerEIP7702`, and `ERC7821`. Deployed once; EOAs point to it via authorization.
|
|
28
|
+
|
|
29
|
+
- `entryPoint` — immutable, baked into bytecode at deploy time
|
|
30
|
+
- Execution via `ERC7821` batch execute
|
|
31
|
+
- Signature validation: signer must equal `address(this)` (the delegating EOA)
|
|
32
|
+
|
|
33
|
+
### PlatformPaymaster
|
|
34
|
+
|
|
35
|
+
Per-platform ERC-4337 paymaster cloned from a shared implementation.
|
|
36
|
+
|
|
37
|
+
#### State
|
|
38
|
+
|
|
39
|
+
| Variable | Description |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `tdocDeployer` | TDocDeployer contract that mints registry clones |
|
|
42
|
+
| `authorizedRegistries` | Registries whose calls this paymaster will sponsor |
|
|
43
|
+
| `authorizedTitleEscrows` | Title escrows whose calls this paymaster will sponsor |
|
|
44
|
+
| `authorizedCallers` | Addresses (beneficiary/holder) allowed on Path A |
|
|
45
|
+
| `userWhitelist` | Deployment credits per user (max 3) |
|
|
46
|
+
| `documentsMinted` | Count of documents minted per caller |
|
|
47
|
+
| `dailyLimit` | Max ETH sponsored per user per day |
|
|
48
|
+
|
|
49
|
+
#### Key functions
|
|
50
|
+
|
|
51
|
+
- `initialize(owner, dailyLimit, tdocDeployer)` — called once by the factory after clone; guarded by `owner() == address(0)`
|
|
52
|
+
- `deployRegistry(impl, name, symbol)` — deploys a TradeTrust registry, consumes one credit, auto-authorizes it
|
|
53
|
+
- `mintDocument(registry, beneficiary, holder, tokenId, remark)` — mints a document, auto-authorizes beneficiary + holder + title escrow
|
|
54
|
+
- `setUserWhitelist / removeUserFromWhitelist` — manage deployment credits (`onlyOwner`)
|
|
55
|
+
- `addRegistry / removeRegistry` — manage authorized registries (`onlyOwner`)
|
|
56
|
+
- `addTitleEscrow / removeTitleEscrow` — manage authorized title escrows (`onlyOwner`)
|
|
57
|
+
- `addAuthorizedCaller / removeAuthorizedCaller` — manage Path A callers (`onlyOwner`)
|
|
58
|
+
- `setDailyLimit` — update daily spend cap (`onlyOwner`)
|
|
59
|
+
- `getUserDailySpend(user)` — returns `(spent, limit, resetsAt)`
|
|
60
|
+
|
|
61
|
+
### PlatformAccountFactory
|
|
62
|
+
|
|
63
|
+
Deploys `PlatformPaymaster` clones deterministically.
|
|
64
|
+
|
|
65
|
+
- `deployPlatformPaymaster(platformAddress, dailyLimit, salt)` — clones the implementation and calls `initialize`
|
|
66
|
+
- `computePaymasterAddress(salt)` — predict the clone address before deployment
|
|
67
|
+
- `updateTdocDeployer(addr)` — update TDoc deployer (`onlyOwner`)
|
|
68
|
+
- `updatePaymasterImplementation(addr)` — upgrade the implementation for future clones (`onlyOwner`)
|
|
69
|
+
|
|
70
|
+
## Deployed addresses (Sepolia)
|
|
71
|
+
|
|
72
|
+
| Contract | Address |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| EIP7702Implementation | `0xa46EC3920Ac5fc54F4bA33185A91ae250aDF59B8` |
|
|
75
|
+
| PlatformPaymaster (implementation) | `0xa24695178ea881ab7d4d105106e4906a8da4752b` |
|
|
76
|
+
| PlatformAccountFactory | `0x7e9ef6363180baa744eb32ceab367a44f52adc9f` |
|
|
77
|
+
| TDocDeployer | `0x64bc665056DC8bE4092e569ED13a7F273Be28cD2` |
|
|
78
|
+
| EntryPoint v0.8 | `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108` |
|
|
79
|
+
|
|
80
|
+
## Installation
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install @trustvc/eip7702
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
### Prerequisites
|
|
89
|
+
|
|
90
|
+
- Node.js 18+
|
|
91
|
+
- A Sepolia RPC URL (e.g. Infura)
|
|
92
|
+
- A Pimlico API key for bundler access
|
|
93
|
+
|
|
94
|
+
### Setup
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm install
|
|
98
|
+
cp .env.example .env # fill in PRIVATE_KEY, SEPOLIA_RPC_URL, PIMLICO_API_KEY
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Build
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npm run build # compile contracts + generate ABIs + bundle JS
|
|
105
|
+
npm run build:sol # compile contracts only
|
|
106
|
+
npm run build:abis # generate ABI TypeScript files only
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Test
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm test # runs all 45 hardhat tests
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Deploy (Sepolia)
|
|
116
|
+
|
|
117
|
+
Deploy in order:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# 1. Deploy the shared PlatformPaymaster implementation
|
|
121
|
+
npx hardhat run scripts/deployImplementation.ts --network sepolia
|
|
122
|
+
|
|
123
|
+
# 2. Deploy the factory (add PAYMASTER_IMPLEMENTATION to .env first)
|
|
124
|
+
npx hardhat run scripts/deployFactory.ts --network sepolia
|
|
125
|
+
|
|
126
|
+
# 3. Clone a paymaster for your platform (add FACTORY_ADDRESS to .env first)
|
|
127
|
+
npx hardhat run scripts/deployPlatformPaymaster.ts --network sepolia
|
|
128
|
+
|
|
129
|
+
# 4. Stake the paymaster on the EntryPoint
|
|
130
|
+
npx hardhat run scripts/stakePlatformPaymaster.ts --network sepolia
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Mint a document gaslessly
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npx hardhat run scripts/mintDocumentGasless.ts --network sepolia
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Environment variables
|
|
140
|
+
|
|
141
|
+
| Variable | Description |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `PRIVATE_KEY` | Deployer wallet private key |
|
|
144
|
+
| `SEPOLIA_RPC_URL` | Sepolia RPC endpoint |
|
|
145
|
+
| `PIMLICO_API_KEY` | Pimlico bundler API key |
|
|
146
|
+
| `TDOC_DEPLOYER_ADDRESS` | Deployed TDocDeployer address |
|
|
147
|
+
| `PAYMASTER_IMPLEMENTATION` | PlatformPaymaster implementation address |
|
|
148
|
+
| `FACTORY_ADDRESS` | PlatformAccountFactory address |
|
|
149
|
+
| `PAYMASTER_ADDRESS` | Deployed paymaster clone address |
|
|
150
|
+
| `EIP7702_IMPL_ADDRESS` | EIP7702Implementation address |
|
|
151
|
+
|
|
152
|
+
## Tech stack
|
|
153
|
+
|
|
154
|
+
- Solidity `^0.8.28` · OpenZeppelin Contracts v5 · `@account-abstraction/contracts` v0.8
|
|
155
|
+
- Hardhat · hardhat-toolbox-viem · Viem · Pimlico bundler
|
|
156
|
+
- EIP-7702 · ERC-4337 · EIP-1167 minimal proxy · Sepolia testnet
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Account} from "@openzeppelin/contracts/account/Account.sol";
|
|
5
|
+
import {ERC7821} from "@openzeppelin/contracts/account/extensions/draft-ERC7821.sol";
|
|
6
|
+
import {SignerEIP7702} from "@openzeppelin/contracts/utils/cryptography/signers/SignerEIP7702.sol";
|
|
7
|
+
import {EIP7702Utils} from "@openzeppelin/contracts/account/utils/EIP7702Utils.sol";
|
|
8
|
+
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
9
|
+
import {IEntryPoint} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* EIP-7702 smart account implementation using OpenZeppelin base contracts.
|
|
13
|
+
*
|
|
14
|
+
* Inheritance stack:
|
|
15
|
+
* Account — ERC-4337 validateUserOp, onlyEntryPoint guards, pre-fund payment
|
|
16
|
+
* SignerEIP7702 — _rawSignatureValidation: recovers signer and checks == address(this)
|
|
17
|
+
* ERC7821 — standardized batch execute(bytes32 mode, bytes executionData)
|
|
18
|
+
* ERC165 — supportsInterface
|
|
19
|
+
*
|
|
20
|
+
* EIP-7702 flow: deploy this contract once, then have EOAs sign an authorization
|
|
21
|
+
* pointing to this address. The EOA's code becomes `0xef0100 || address(this)`,
|
|
22
|
+
* giving it full smart-account capabilities while keeping the original private key.
|
|
23
|
+
*/
|
|
24
|
+
contract EIP7702Implementation is Account, SignerEIP7702, ERC7821, ERC165 {
|
|
25
|
+
// Baked into bytecode at deploy time so EIP-7702 delegating EOAs inherit
|
|
26
|
+
// the correct EntryPoint without any storage reads.
|
|
27
|
+
IEntryPoint private immutable _entryPoint;
|
|
28
|
+
|
|
29
|
+
// Takes address so callers don't need OZ's IEntryPoint type in scope.
|
|
30
|
+
constructor(address entryPoint_) {
|
|
31
|
+
_entryPoint = IEntryPoint(entryPoint_);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function entryPoint() public view virtual override returns (IEntryPoint) {
|
|
35
|
+
return _entryPoint;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// -------------------------------------------------------------------------
|
|
39
|
+
// Execution
|
|
40
|
+
// -------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @dev Allow the EntryPoint (in addition to address(this)) to call the
|
|
44
|
+
* ERC-7821 batch execute function. Without this override only the account
|
|
45
|
+
* itself can trigger execution.
|
|
46
|
+
*/
|
|
47
|
+
function _erc7821AuthorizedExecutor(
|
|
48
|
+
address caller,
|
|
49
|
+
bytes32 mode,
|
|
50
|
+
bytes calldata executionData
|
|
51
|
+
) internal view virtual override returns (bool) {
|
|
52
|
+
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @dev Convenience single-call path. Only callable by the EntryPoint or the
|
|
57
|
+
* account itself — prevents arbitrary callers from draining funds.
|
|
58
|
+
*
|
|
59
|
+
* For batch calls use the ERC-7821 `execute(bytes32 mode, bytes executionData)`
|
|
60
|
+
* inherited from ERC7821 with mode = 0x0100000000000000000000000000000000000000000000000000000000000000
|
|
61
|
+
* and executionData = abi.encode(calls) where calls is (address,uint256,bytes)[].
|
|
62
|
+
*/
|
|
63
|
+
function execute(
|
|
64
|
+
address to,
|
|
65
|
+
uint256 value,
|
|
66
|
+
bytes calldata data
|
|
67
|
+
) external onlyEntryPointOrSelf returns (bytes memory) {
|
|
68
|
+
(bool ok, bytes memory result) = to.call{value: value}(data);
|
|
69
|
+
require(ok, "execution failed");
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// -------------------------------------------------------------------------
|
|
74
|
+
// ERC-165
|
|
75
|
+
// -------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @dev Advertise ERC-7821 (batch executor) and ERC-165 support.
|
|
79
|
+
* The ERC-7821 interface ID 0x4e49c5c7 is the XOR of:
|
|
80
|
+
* execute(bytes32,bytes).selector ^ supportsExecutionMode(bytes32).selector
|
|
81
|
+
*/
|
|
82
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
|
83
|
+
return interfaceId == 0x4e49c5c7 // ERC-7821 batch executor
|
|
84
|
+
|| super.supportsInterface(interfaceId); // covers 0x01ffc9a7 (ERC-165)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// -------------------------------------------------------------------------
|
|
88
|
+
// Utilities
|
|
89
|
+
// -------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @dev Returns the implementation address this EOA has delegated to via
|
|
93
|
+
* EIP-7702, or address(0) if no delegation is active.
|
|
94
|
+
*/
|
|
95
|
+
function getDelegate() external view returns (address) {
|
|
96
|
+
return EIP7702Utils.fetchDelegate(address(this));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import {PlatformPaymaster} from "./PlatformPaymaster.sol";
|
|
5
|
+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
6
|
+
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
|
|
7
|
+
|
|
8
|
+
contract PlatformAccountFactory is Ownable {
|
|
9
|
+
address public tdocDeployer;
|
|
10
|
+
address public paymasterImplementation;
|
|
11
|
+
|
|
12
|
+
event PlatformOnboarded(address indexed platformAddress, address indexed paymaster);
|
|
13
|
+
event TdocDeployerUpdated(address indexed newDeployer);
|
|
14
|
+
event ImplementationUpdated(address indexed newImplementation);
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
address _tdocDeployer,
|
|
18
|
+
address _paymasterImplementation
|
|
19
|
+
) Ownable(msg.sender) {
|
|
20
|
+
tdocDeployer = _tdocDeployer;
|
|
21
|
+
paymasterImplementation = _paymasterImplementation;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function updateTdocDeployer(address _tdocDeployer) external onlyOwner {
|
|
25
|
+
require(_tdocDeployer != address(0), "Zero address");
|
|
26
|
+
tdocDeployer = _tdocDeployer;
|
|
27
|
+
emit TdocDeployerUpdated(_tdocDeployer);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function updatePaymasterImplementation(address _impl) external onlyOwner {
|
|
31
|
+
require(_impl != address(0), "Zero address");
|
|
32
|
+
paymasterImplementation = _impl;
|
|
33
|
+
emit ImplementationUpdated(_impl);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function deployPlatformPaymaster(
|
|
37
|
+
address platformAddress,
|
|
38
|
+
uint256 dailyLimit,
|
|
39
|
+
bytes32 salt
|
|
40
|
+
) external returns (address paymaster) {
|
|
41
|
+
paymaster = Clones.cloneDeterministic(paymasterImplementation, salt);
|
|
42
|
+
PlatformPaymaster(payable(paymaster)).initialize(platformAddress, dailyLimit, tdocDeployer);
|
|
43
|
+
emit PlatformOnboarded(platformAddress, paymaster);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Address is determined solely by implementation + salt (not by constructor args).
|
|
47
|
+
function computePaymasterAddress(bytes32 salt) external view returns (address) {
|
|
48
|
+
return Clones.predictDeterministicAddress(paymasterImplementation, salt, address(this));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.28;
|
|
3
|
+
|
|
4
|
+
import "@account-abstraction/contracts/core/BasePaymaster.sol";
|
|
5
|
+
import "@account-abstraction/contracts/core/Helpers.sol";
|
|
6
|
+
|
|
7
|
+
interface ITDocDeployer {
|
|
8
|
+
function deploy(
|
|
9
|
+
address implementation,
|
|
10
|
+
bytes memory params
|
|
11
|
+
) external returns (address);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ITradeTrustToken {
|
|
15
|
+
function mint(
|
|
16
|
+
address beneficiary,
|
|
17
|
+
address holder,
|
|
18
|
+
uint256 tokenId,
|
|
19
|
+
bytes calldata remark
|
|
20
|
+
) external returns (address);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface IAccessControl {
|
|
24
|
+
function grantRole(bytes32 role, address account) external;
|
|
25
|
+
function renounceRole(bytes32 role, address callerConfirmation) external;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* PlatformPaymaster — validates by inspecting the target contract in callData.
|
|
30
|
+
*
|
|
31
|
+
* Validation strategy (no third-party storage reads → bundler compliant):
|
|
32
|
+
* 1. Decode userOp.callData → must be execute(address to, uint256, bytes)
|
|
33
|
+
* 2. Check `to` is in authorizedRegistries (paymaster's own storage)
|
|
34
|
+
* 3. Check daily spend (paymaster's own storage — always allowed)
|
|
35
|
+
*
|
|
36
|
+
* Access control is enforced by the Storage contract itself:
|
|
37
|
+
* require(msg.sender == beneficiary || msg.sender == holder)
|
|
38
|
+
* so non-beneficiary/holder callers are rejected at execution time.
|
|
39
|
+
* If the call reverts, _postOp skips the spend update.
|
|
40
|
+
*/
|
|
41
|
+
contract PlatformPaymaster is BasePaymaster {
|
|
42
|
+
bytes4 private constant EXECUTE_SEL =
|
|
43
|
+
bytes4(keccak256("execute(address,uint256,bytes)"));
|
|
44
|
+
bytes4 private constant DEPLOY_REGISTRY_SEL =
|
|
45
|
+
bytes4(keccak256("deployRegistry(address,string,string)"));
|
|
46
|
+
bytes4 private constant MINT_DOCUMENT_SEL =
|
|
47
|
+
bytes4(
|
|
48
|
+
keccak256("mintDocument(address,address,address,uint256,bytes)")
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
bytes32 private constant DEFAULT_ADMIN_ROLE = bytes32(0);
|
|
52
|
+
|
|
53
|
+
ITDocDeployer public tdocDeployer;
|
|
54
|
+
|
|
55
|
+
// Paymaster's own storage — always allowed under ERC-7562
|
|
56
|
+
mapping(address => bool) public authorizedRegistries;
|
|
57
|
+
mapping(address => bool) public authorizedTitleEscrows;
|
|
58
|
+
// remaining deployment credits per user; max 3 per user
|
|
59
|
+
mapping(address => uint256) public userWhitelist;
|
|
60
|
+
// Addresses permitted to call title escrow functions via this paymaster
|
|
61
|
+
mapping(address => bool) public authorizedCallers;
|
|
62
|
+
// Count of documents minted per user
|
|
63
|
+
mapping(address => uint256) public documentsMinted;
|
|
64
|
+
|
|
65
|
+
// Daily spend tracking — paymaster's own storage, always allowed
|
|
66
|
+
mapping(address => uint256) public dailySpend;
|
|
67
|
+
mapping(address => uint256) public lastReset;
|
|
68
|
+
uint256 public dailyLimit;
|
|
69
|
+
|
|
70
|
+
event RegistryAdded(address indexed registry);
|
|
71
|
+
event RegistryRemoved(address indexed registry);
|
|
72
|
+
event TitleEscrowLinked(
|
|
73
|
+
address indexed titleEscrow,
|
|
74
|
+
address indexed registry
|
|
75
|
+
);
|
|
76
|
+
event TitleEscrowAdded(address indexed titleEscrow);
|
|
77
|
+
event TitleEscrowRemoved(address indexed titleEscrow);
|
|
78
|
+
event UserWhitelistUpdated(address indexed user, uint256 credits);
|
|
79
|
+
event RegistryDeployed(
|
|
80
|
+
address indexed user,
|
|
81
|
+
address indexed deployed,
|
|
82
|
+
uint256 creditsLeft
|
|
83
|
+
);
|
|
84
|
+
event AuthorizedCallerUpdated(address indexed caller, bool authorized);
|
|
85
|
+
event UserOpSponsored(address indexed user, uint256 gasCost);
|
|
86
|
+
event UserOpRejected(address indexed user, string reason);
|
|
87
|
+
event DailyLimitUpdated(uint256 newLimit);
|
|
88
|
+
|
|
89
|
+
// Deployed once as the shared implementation; BasePaymaster sets owner = msg.sender.
|
|
90
|
+
// That non-zero owner prevents initialize() from running on the implementation itself.
|
|
91
|
+
constructor(IEntryPoint _entryPoint) BasePaymaster(_entryPoint) {}
|
|
92
|
+
|
|
93
|
+
// Called by the factory immediately after cloneDeterministic().
|
|
94
|
+
// A fresh clone has all-zero storage, so owner() == address(0) exactly once.
|
|
95
|
+
function initialize(
|
|
96
|
+
address _owner,
|
|
97
|
+
uint256 _dailyLimit,
|
|
98
|
+
address _tdocDeployer
|
|
99
|
+
) external {
|
|
100
|
+
require(owner() == address(0), "Already initialized");
|
|
101
|
+
require(_owner != address(0), "Zero owner");
|
|
102
|
+
_transferOwnership(_owner);
|
|
103
|
+
dailyLimit = _dailyLimit;
|
|
104
|
+
tdocDeployer = ITDocDeployer(_tdocDeployer);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Grant/update deployment credits for a user; credits cannot exceed 3
|
|
108
|
+
function setUserWhitelist(
|
|
109
|
+
address user,
|
|
110
|
+
uint256 credits
|
|
111
|
+
) external onlyOwner {
|
|
112
|
+
require(user != address(0), "Zero address");
|
|
113
|
+
require(credits <= 3, "Exceeds max credits of 3");
|
|
114
|
+
userWhitelist[user] = credits;
|
|
115
|
+
emit UserWhitelistUpdated(user, credits);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// add function to remove user from whitelist
|
|
119
|
+
function removeUserFromWhitelist(address user) external onlyOwner {
|
|
120
|
+
require(user != address(0), "Zero address");
|
|
121
|
+
userWhitelist[user] = 0;
|
|
122
|
+
emit UserWhitelistUpdated(user, 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Deploys a TDoc clone on behalf of a whitelisted user; consumes one credit.
|
|
126
|
+
// Role setup (atomic, no follow-up txs needed):
|
|
127
|
+
// - msg.sender (EOA) gets DEFAULT_ADMIN_ROLE
|
|
128
|
+
// - paymaster gets MINTER_ROLE + RESTORER_ROLE + ACCEPTER_ROLE
|
|
129
|
+
function deployRegistry(
|
|
130
|
+
address implementation,
|
|
131
|
+
string memory name,
|
|
132
|
+
string memory symbol
|
|
133
|
+
) external returns (address deployed) {
|
|
134
|
+
require(address(tdocDeployer) != address(0), "TdocDeployer not set");
|
|
135
|
+
uint256 credits = userWhitelist[msg.sender];
|
|
136
|
+
require(credits > 0, "No deployment credits");
|
|
137
|
+
|
|
138
|
+
userWhitelist[msg.sender] = credits - 1;
|
|
139
|
+
|
|
140
|
+
// Paymaster is temporary admin so it can configure roles after deploy
|
|
141
|
+
bytes memory params = abi.encode(name, symbol, address(this));
|
|
142
|
+
deployed = tdocDeployer.deploy(implementation, params);
|
|
143
|
+
|
|
144
|
+
// Hand admin to the calling EOA
|
|
145
|
+
IAccessControl(deployed).grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
|
|
146
|
+
|
|
147
|
+
// Paymaster keeps only the operational roles it needs
|
|
148
|
+
// (MINTER_ROLE, RESTORER_ROLE, ACCEPTER_ROLE were granted to address(this) via initialize)
|
|
149
|
+
|
|
150
|
+
// Relinquish admin — EOA is now sole admin
|
|
151
|
+
IAccessControl(deployed).renounceRole(
|
|
152
|
+
DEFAULT_ADMIN_ROLE,
|
|
153
|
+
address(this)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
authorizedRegistries[deployed] = true;
|
|
157
|
+
emit RegistryAdded(deployed);
|
|
158
|
+
emit RegistryDeployed(msg.sender, deployed, credits - 1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Mints a TradeTrust document, captures the TitleEscrow, and auto-authorizes it.
|
|
162
|
+
// Paymaster must hold MINTER_ROLE on the registry.
|
|
163
|
+
// Callable gaslessly by userWhitelist users (credits not consumed — only deployRegistry uses credits).
|
|
164
|
+
function mintDocument(
|
|
165
|
+
address registry,
|
|
166
|
+
address beneficiary,
|
|
167
|
+
address holder,
|
|
168
|
+
uint256 tokenId,
|
|
169
|
+
bytes calldata remark
|
|
170
|
+
) external returns (address titleEscrow) {
|
|
171
|
+
require(authorizedRegistries[registry], "registry not authorized");
|
|
172
|
+
|
|
173
|
+
titleEscrow = ITradeTrustToken(registry).mint(
|
|
174
|
+
beneficiary,
|
|
175
|
+
holder,
|
|
176
|
+
tokenId,
|
|
177
|
+
remark
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Auto-authorize beneficiary and holder to call title escrow functions
|
|
181
|
+
authorizedCallers[beneficiary] = true;
|
|
182
|
+
authorizedCallers[holder] = true;
|
|
183
|
+
|
|
184
|
+
authorizedTitleEscrows[titleEscrow] = true;
|
|
185
|
+
documentsMinted[msg.sender]++;
|
|
186
|
+
emit TitleEscrowLinked(titleEscrow, registry);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function addRegistry(address registry) external onlyOwner {
|
|
190
|
+
require(registry != address(0), "Zero address");
|
|
191
|
+
authorizedRegistries[registry] = true;
|
|
192
|
+
emit RegistryAdded(registry);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function removeRegistry(address registry) external onlyOwner {
|
|
196
|
+
authorizedRegistries[registry] = false;
|
|
197
|
+
emit RegistryRemoved(registry);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function addTitleEscrow(address titleEscrow) external onlyOwner {
|
|
201
|
+
require(titleEscrow != address(0), "Zero address");
|
|
202
|
+
authorizedTitleEscrows[titleEscrow] = true;
|
|
203
|
+
emit TitleEscrowAdded(titleEscrow);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function removeTitleEscrow(address titleEscrow) external onlyOwner {
|
|
207
|
+
authorizedTitleEscrows[titleEscrow] = false;
|
|
208
|
+
emit TitleEscrowRemoved(titleEscrow);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function addAuthorizedCaller(address caller) external onlyOwner {
|
|
212
|
+
require(caller != address(0), "Zero address");
|
|
213
|
+
authorizedCallers[caller] = true;
|
|
214
|
+
emit AuthorizedCallerUpdated(caller, true);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function removeAuthorizedCaller(address caller) external onlyOwner {
|
|
218
|
+
authorizedCallers[caller] = false;
|
|
219
|
+
emit AuthorizedCallerUpdated(caller, false);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function setDailyLimit(uint256 _dailyLimit) external onlyOwner {
|
|
223
|
+
dailyLimit = _dailyLimit;
|
|
224
|
+
emit DailyLimitUpdated(_dailyLimit);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function _validatePaymasterUserOp(
|
|
228
|
+
PackedUserOperation calldata userOp,
|
|
229
|
+
bytes32, // userOpHash — not needed
|
|
230
|
+
uint256 maxCost
|
|
231
|
+
) internal override returns (bytes memory context, uint256 validationData) {
|
|
232
|
+
address sender = userOp.sender;
|
|
233
|
+
|
|
234
|
+
// 1. callData must be long enough for selector + one address arg
|
|
235
|
+
if (userOp.callData.length < 36) {
|
|
236
|
+
emit UserOpRejected(sender, "callData too short");
|
|
237
|
+
return ("", _packValidationData(true, 0, 0));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 2. Must call execute(address,uint256,bytes)
|
|
241
|
+
bytes4 sel = bytes4(userOp.callData[:4]);
|
|
242
|
+
if (sel != EXECUTE_SEL) {
|
|
243
|
+
emit UserOpRejected(sender, "wrong selector");
|
|
244
|
+
return ("", _packValidationData(true, 0, 0));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 3. Decode target and inner calldata
|
|
248
|
+
(address target, , bytes memory innerData) = abi.decode(
|
|
249
|
+
userOp.callData[4:],
|
|
250
|
+
(address, uint256, bytes)
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Path A — calling an authorized registry or title escrow (regular sponsored op)
|
|
254
|
+
// Only authorizedCallers or the platform owner may be sponsored here.
|
|
255
|
+
if (authorizedRegistries[target] || authorizedTitleEscrows[target]) {
|
|
256
|
+
if (!authorizedCallers[sender] && sender != owner()) {
|
|
257
|
+
emit UserOpRejected(sender, "caller not authorized");
|
|
258
|
+
return ("", _packValidationData(true, 0, 0));
|
|
259
|
+
}
|
|
260
|
+
if (dailyLimit > 0 && dailySpend[sender] + maxCost > dailyLimit) {
|
|
261
|
+
emit UserOpRejected(sender, "daily limit exceeded");
|
|
262
|
+
return ("", _packValidationData(true, 0, 0));
|
|
263
|
+
}
|
|
264
|
+
return (
|
|
265
|
+
abi.encode(sender, maxCost, false),
|
|
266
|
+
_packValidationData(false, 0, 0)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Path B — gasless paymaster call (deployRegistry or mintDocument)
|
|
271
|
+
// Double-spend safe: EntryPoint sequential nonces allow only one
|
|
272
|
+
// pending UserOp per sender in the mempool at a time.
|
|
273
|
+
if (target == address(this)) {
|
|
274
|
+
bytes4 innerSel;
|
|
275
|
+
assembly {
|
|
276
|
+
innerSel := mload(add(innerData, 32))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (innerSel == DEPLOY_REGISTRY_SEL) {
|
|
280
|
+
// deployRegistry requires whitelist credits — platform-level gate
|
|
281
|
+
if (userWhitelist[sender] == 0) {
|
|
282
|
+
emit UserOpRejected(sender, "not whitelisted");
|
|
283
|
+
return ("", _packValidationData(true, 0, 0));
|
|
284
|
+
}
|
|
285
|
+
// credits consumed inside deployRegistry; flag as deployment to skip daily spend
|
|
286
|
+
return (
|
|
287
|
+
abi.encode(sender, maxCost, true),
|
|
288
|
+
_packValidationData(false, 0, 0)
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (innerSel == MINT_DOCUMENT_SEL) {
|
|
293
|
+
// mintDocument: registry enforces MINTER_ROLE — no extra whitelist needed
|
|
294
|
+
return (
|
|
295
|
+
abi.encode(sender, maxCost, false),
|
|
296
|
+
_packValidationData(false, 0, 0)
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
emit UserOpRejected(sender, "unauthorized paymaster call");
|
|
301
|
+
return ("", _packValidationData(true, 0, 0));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
emit UserOpRejected(sender, "unauthorized target");
|
|
305
|
+
return ("", _packValidationData(true, 0, 0));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function _postOp(
|
|
309
|
+
PostOpMode mode,
|
|
310
|
+
bytes calldata context,
|
|
311
|
+
uint256 actualGasCost,
|
|
312
|
+
uint256 /*actualUserOpFeePerGas*/
|
|
313
|
+
) internal override {
|
|
314
|
+
if (mode == PostOpMode.postOpReverted) return;
|
|
315
|
+
|
|
316
|
+
(address sender, , bool isDeployment) = abi.decode(
|
|
317
|
+
context,
|
|
318
|
+
(address, uint256, bool)
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// Deployment ops are credit-gated; skip daily spend tracking for them
|
|
322
|
+
if (!isDeployment) {
|
|
323
|
+
if (block.timestamp > lastReset[sender] + 1 days) {
|
|
324
|
+
dailySpend[sender] = 0;
|
|
325
|
+
lastReset[sender] = block.timestamp;
|
|
326
|
+
}
|
|
327
|
+
dailySpend[sender] += actualGasCost;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
emit UserOpSponsored(sender, actualGasCost);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// v0.8 contracts on Etherspot EntryPoint — skip interface mismatch check
|
|
334
|
+
function _validateEntryPointInterface(IEntryPoint) internal pure override {}
|
|
335
|
+
|
|
336
|
+
receive() external payable {
|
|
337
|
+
deposit();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getUserDailySpend(
|
|
341
|
+
address user
|
|
342
|
+
) external view returns (uint256 spent, uint256 limit, uint256 resetsAt) {
|
|
343
|
+
spent = dailySpend[user];
|
|
344
|
+
limit = dailyLimit;
|
|
345
|
+
resetsAt = lastReset[user] + 1 days;
|
|
346
|
+
}
|
|
347
|
+
}
|