Skip to content

Commit 4159ac2

Browse files
committed
Merge branch 'aa/erc7913-signer' into feature/zk-email-7913
2 parents 1486330 + 87119a0 commit 4159ac2

30 files changed

+1044
-220
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
- `SignerERC7913`: Abstract signer that verifies signatures using the ERC-7913 workflow.
55
- `ERC7913SignatureVerifierP256` and `ERC7913SignatureVerifierRSA`: Ready to use ERC-7913 verifiers that implement key verification for P256 (secp256r1) and RSA keys.
66

7+
## 03-04-2025
8+
9+
- `PaymasterNFT`: Extension of `PaymasterCore` that approves sponsoring of user operation based on ownership of an ERC-721 NFT.
10+
- `PaymasterERC20`: Extension of `PaymasterCore` that sponsors user operations against payment in ERC-20 tokens.
11+
712
## 28-03-2025
813

914
- Deprecate `Account` and rename `AccountCore` to `Account`.

contracts/account/extensions/AccountERC7579.sol

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-License-Identifier: MIT
22

3-
pragma solidity ^0.8.24;
3+
pragma solidity ^0.8.27;
44

55
import {PackedUserOperation} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol";
66
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
@@ -19,7 +19,7 @@ import {Account} from "../Account.sol";
1919
* To comply with the ERC-1271 support requirement, this contract defers signature validation to
2020
* installed validator modules by calling {IERC7579Validator-isValidSignatureWithSender}.
2121
*
22-
* This contract does not implement validation logic for user operations since these functionality
22+
* This contract does not implement validation logic for user operations since this functionality
2323
* is often delegated to self-contained validation modules. Developers must install a validator module
2424
* upon initialization (or any other mechanism to enable execution from the account):
2525
*
@@ -40,7 +40,12 @@ import {Account} from "../Account.sol";
4040
* internal virtual functions {_extractUserOpValidator} and {_extractSignatureValidator}. Both are implemented
4141
* following common practices. However, this part is not standardized in ERC-7579 (or in any follow-up ERC). Some
4242
* accounts may want to override these internal functions.
43+
* * When combined with {ERC7739}, resolution ordering of {isValidSignature} may have an impact ({ERC7739} does not
44+
* call super). Manual resolution might be necessary.
45+
* * Static calls (using callType `0xfe`) are currently NOT supported.
4346
* ====
47+
*
48+
* WARNING: Removing all validator modules will render the account inoperable, as no user operations can be validated thereafter.
4449
*/
4550
abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC7579AccountConfig, IERC7579ModuleConfig {
4651
using Bytes for *;
@@ -163,8 +168,8 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
163168
* @dev Implement ERC-1271 through IERC7579Validator modules. If module based validation fails, fallback to
164169
* "native" validation by the abstract signer.
165170
*
166-
* NOTE: when combined with {ERC7739} (for example through {Account}), resolution ordering may have an impact
167-
* ({ERC7739} does not call super). Manual resolution might be necessary.
171+
* NOTE: when combined with {ERC7739}, resolution ordering may have an impact ({ERC7739} does not call super).
172+
* Manual resolution might be necessary.
168173
*/
169174
function isValidSignature(bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) {
170175
// check signature length is enough for extraction
@@ -173,10 +178,10 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
173178
// if module is not installed, skip
174179
if (isModuleInstalled(MODULE_TYPE_VALIDATOR, module, Calldata.emptyBytes())) {
175180
// try validation, skip any revert
176-
try IERC7579Validator(module).isValidSignatureWithSender(address(this), hash, innerSignature) returns (
181+
try IERC7579Validator(module).isValidSignatureWithSender(msg.sender, hash, innerSignature) returns (
177182
bytes4 magic
178183
) {
179-
if (magic == IERC1271.isValidSignature.selector) return magic;
184+
return magic;
180185
} catch {}
181186
}
182187
}
@@ -220,8 +225,8 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
220225
/**
221226
* @dev Installs a module of the given type with the given initialization data.
222227
*
223-
* For the fallback module type, the `initData` is expected to be a tuple of a 4-byte selector and the
224-
* rest of the data to be sent to the handler when calling {IERC7579Module-onInstall}.
228+
* For the fallback module type, the `initData` is expected to be the (packed) concatenation of a 4-byte
229+
* selector and the rest of the data to be sent to the handler when calling {IERC7579Module-onInstall}.
225230
*
226231
* Requirements:
227232
*
@@ -259,14 +264,16 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
259264
/**
260265
* @dev Uninstalls a module of the given type with the given de-initialization data.
261266
*
262-
* For the fallback module type, the `deInitData` is expected to be a tuple of a 4-byte selector and the
263-
* rest of the data to be sent to the handler when calling {IERC7579Module-onUninstall}.
267+
* For the fallback module type, the `deInitData` is expected to be the (packed) concatenation of a 4-byte
268+
* selector and the rest of the data to be sent to the handler when calling {IERC7579Module-onUninstall}.
264269
*
265270
* Requirements:
266271
*
267272
* * Module must be already installed. Reverts with {ERC7579UninstalledModule} otherwise.
268273
*/
269274
function _uninstallModule(uint256 moduleTypeId, address module, bytes memory deInitData) internal virtual {
275+
require(supportsModule(moduleTypeId), ERC7579Utils.ERC7579UnsupportedModuleType(moduleTypeId));
276+
270277
if (moduleTypeId == MODULE_TYPE_VALIDATOR) {
271278
require(_validators.remove(module), ERC7579Utils.ERC7579UninstalledModule(moduleTypeId, module));
272279
} else if (moduleTypeId == MODULE_TYPE_EXECUTOR) {
@@ -336,9 +343,10 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
336343
* ```
337344
* <module address (20 bytes)> | <key (4 bytes)> | <nonce (8 bytes)>
338345
* ```
339-
* NOTE: The default behavior of this function replicated the behavior of
340-
* https://github.com/rhinestonewtf/safe7579/blob/bb29e8b1a66658790c4169e72608e27d220f79be/src/Safe7579.sol#L266[Safe adapter] and
341-
* https://github.com/etherspot/etherspot-prime-contracts/blob/cfcdb48c4172cea0d66038324c0bae3288aa8caa/src/modular-etherspot-wallet/wallet/ModularEtherspotWallet.sol#L227[Etherspot's Prime Account].
346+
* NOTE: The default behavior of this function replicates the behavior of
347+
* https://github.com/rhinestonewtf/safe7579/blob/bb29e8b1a66658790c4169e72608e27d220f79be/src/Safe7579.sol#L266[Safe adapter],
348+
* https://github.com/etherspot/etherspot-prime-contracts/blob/cfcdb48c4172cea0d66038324c0bae3288aa8caa/src/modular-etherspot-wallet/wallet/ModularEtherspotWallet.sol#L227[Etherspot's Prime Account], and
349+
* https://github.com/erc7579/erc7579-implementation/blob/16138d1afd4e9711f6c1425133538837bd7787b5/src/MSAAdvanced.sol#L247[ERC7579 reference implementation].
342350
*
343351
* This is not standardized in ERC-7579 (or in any follow-up ERC). Some accounts may want to override these internal functions.
344352
*
@@ -359,10 +367,11 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
359367
* <module address (20 bytes)> | <signature data>
360368
* ```
361369
*
362-
* NOTE: The default behavior of this function replicated the behavior of
370+
* NOTE: The default behavior of this function replicates the behavior of
363371
* https://github.com/rhinestonewtf/safe7579/blob/bb29e8b1a66658790c4169e72608e27d220f79be/src/Safe7579.sol#L350[Safe adapter],
364-
* https://github.com/bcnmy/nexus/blob/54f4e19baaff96081a8843672977caf712ef19f4/contracts/Nexus.sol#L239[Biconomy's Nexus] and
365-
* https://github.com/etherspot/etherspot-prime-contracts/blob/cfcdb48c4172cea0d66038324c0bae3288aa8caa/src/modular-etherspot-wallet/wallet/ModularEtherspotWallet.sol#L252[Etherspot's Prime Account]
372+
* https://github.com/bcnmy/nexus/blob/54f4e19baaff96081a8843672977caf712ef19f4/contracts/Nexus.sol#L239[Biconomy's Nexus],
373+
* https://github.com/etherspot/etherspot-prime-contracts/blob/cfcdb48c4172cea0d66038324c0bae3288aa8caa/src/modular-etherspot-wallet/wallet/ModularEtherspotWallet.sol#L252[Etherspot's Prime Account], and
374+
* https://github.com/erc7579/erc7579-implementation/blob/16138d1afd4e9711f6c1425133538837bd7787b5/src/MSAAdvanced.sol#L296[ERC7579 reference implementation].
366375
*
367376
* This is not standardized in ERC-7579 (or in any follow-up ERC). Some accounts may want to override these internal functions.
368377
*/
@@ -375,10 +384,10 @@ abstract contract AccountERC7579 is Account, IERC1271, IERC7579Execution, IERC75
375384
/**
376385
* @dev Extract the function selector from initData/deInitData for MODULE_TYPE_FALLBACK
377386
*
378-
* NOTE: If we had calldata here, we would could use calldata slice which are cheaper to manipulate and don't
379-
* require actual copy. However, this would require `_installModule` to get a calldata bytes object instead of a
380-
* memory bytes object. This would prevent calling `_installModule` from a contract constructor and would force
381-
* the use of external initializers. That may change in the future, as most accounts will probably be deployed as
387+
* NOTE: If we had calldata here, we could use calldata slice which are cheaper to manipulate and don't require
388+
* actual copy. However, this would require `_installModule` to get a calldata bytes object instead of a memory
389+
* bytes object. This would prevent calling `_installModule` from a contract constructor and would force the use
390+
* of external initializers. That may change in the future, as most accounts will probably be deployed as
382391
* clones/proxy/ERC-7702 delegates and therefore rely on initializers anyway.
383392
*/
384393
function _decodeFallbackData(

contracts/account/extensions/AccountERC7579Hooked.sol

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-License-Identifier: MIT
22

3-
pragma solidity ^0.8.24;
3+
pragma solidity ^0.8.27;
44

55
import {IERC7579Hook, MODULE_TYPE_HOOK} from "@openzeppelin/contracts/interfaces/draft-IERC7579.sol";
66
import {ERC7579Utils, Mode} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol";
@@ -21,6 +21,9 @@ import {AccountERC7579} from "./AccountERC7579.sol";
2121
abstract contract AccountERC7579Hooked is AccountERC7579 {
2222
address private _hook;
2323

24+
/// @dev A hook module is already present. This contract only supports one hook module.
25+
error ERC7579HookModuleAlreadyPresent(address hook);
26+
2427
/**
2528
* @dev Calls {IERC7579Hook-preCheck} before executing the modified function and {IERC7579Hook-postCheck}
2629
* thereafter.
@@ -35,6 +38,12 @@ abstract contract AccountERC7579Hooked is AccountERC7579 {
3538
if (hook_ != address(0)) IERC7579Hook(hook_).postCheck(hookData);
3639
}
3740

41+
/// @inheritdoc AccountERC7579
42+
function accountId() public view virtual override returns (string memory) {
43+
// vendorname.accountname.semver
44+
return "@openzeppelin/community-contracts.AccountERC7579Hooked.v0.0.0";
45+
}
46+
3847
/// @dev Returns the hook module address if installed, or `address(0)` otherwise.
3948
function hook() public view virtual returns (address) {
4049
return _hook;
@@ -63,7 +72,7 @@ abstract contract AccountERC7579Hooked is AccountERC7579 {
6372
bytes memory initData
6473
) internal virtual override withHook {
6574
if (moduleTypeId == MODULE_TYPE_HOOK) {
66-
require(_hook == address(0), ERC7579Utils.ERC7579AlreadyInstalledModule(moduleTypeId, module));
75+
require(_hook == address(0), ERC7579HookModuleAlreadyPresent(_hook));
6776
_hook = module;
6877
}
6978
super._installModule(moduleTypeId, module, initData);

contracts/account/extensions/ERC7821.sol

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {IERC7821} from "../../interfaces/IERC7821.sol";
77
import {Account} from "../Account.sol";
88

99
/**
10-
* @dev Minimal batch executor following ERC-7821. Only supports basic mode (no optional "opData").
10+
* @dev Minimal batch executor following ERC-7821.
11+
*
12+
* Only supports supports single batch mode (`0x01000000000000000000`). Does not support optional "opData".
1113
*/
1214
abstract contract ERC7821 is IERC7821 {
1315
using ERC7579Utils for *;
@@ -18,7 +20,7 @@ abstract contract ERC7821 is IERC7821 {
1820
* @dev Executes the calls in `executionData` with no optional `opData` support.
1921
*
2022
* NOTE: Access to this function is controlled by {_erc7821AuthorizedExecutor}. Changing access permissions, for
21-
* example to approve calls by the ERC-4337 entrypoint, should be implement by overriding it.
23+
* example to approve calls by the ERC-4337 entrypoint, should be implemented by overriding it.
2224
*
2325
* Reverts and bubbles up error if any call fails.
2426
*/
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {ERC4337Utils, PackedUserOperation} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol";
6+
import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7+
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
8+
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
9+
import {PaymasterCore} from "./PaymasterCore.sol";
10+
11+
/**
12+
* @dev Extension of {PaymasterCore} that enables users to pay gas with ERC-20 tokens.
13+
*
14+
* To enable this feature, developers must implement the {fetchDetails} function:
15+
*
16+
* ```solidity
17+
* function _fetchDetails(
18+
* PackedUserOperation calldata userOp,
19+
* bytes32 userOpHash
20+
* ) internal view override returns (uint256 validationData, IERC20 token, uint256 tokenPrice, address guarantor) {
21+
* // Implement logic to fetch the token, token price, and guarantor address from the userOp
22+
* }
23+
* ```
24+
*/
25+
abstract contract PaymasterERC20 is PaymasterCore {
26+
using ERC4337Utils for *;
27+
using Math for *;
28+
using SafeERC20 for IERC20;
29+
30+
event UserOperationSponsored(
31+
bytes32 indexed userOpHash,
32+
address indexed user,
33+
address indexed guarantor,
34+
uint256 tokenAmount,
35+
uint256 tokenPrice,
36+
bool paidByGuarantor
37+
);
38+
39+
// Over-estimations: ERC-20 balances/allowances may be cold and contracts may not be optimized
40+
uint256 private constant POST_OP_COST = 30_000;
41+
uint256 private constant POST_OP_COST_WITH_GUARANTOR = 45_000;
42+
43+
/// @inheritdoc PaymasterCore
44+
function _validatePaymasterUserOp(
45+
PackedUserOperation calldata userOp,
46+
bytes32 userOpHash,
47+
uint256 maxCost
48+
) internal virtual override returns (bytes memory context, uint256 validationData) {
49+
(uint256 validationData_, IERC20 token, uint256 tokenPrice, address guarantor) = _fetchDetails(
50+
userOp,
51+
userOpHash
52+
);
53+
54+
uint256 prefundAmount = (maxCost +
55+
(guarantor == address(0)).ternary(POST_OP_COST, POST_OP_COST_WITH_GUARANTOR) *
56+
userOp.maxFeePerGas()).mulDiv(tokenPrice, _tokenPriceDenominator());
57+
58+
// if validation is obviously failed, don't even try to do the ERC-20 transfer
59+
return
60+
(validationData_ != ERC4337Utils.SIG_VALIDATION_FAILED &&
61+
token.trySafeTransferFrom(
62+
guarantor == address(0) ? userOp.sender : guarantor,
63+
address(this),
64+
prefundAmount
65+
))
66+
? (
67+
abi.encodePacked(userOpHash, token, prefundAmount, tokenPrice, userOp.sender, guarantor),
68+
validationData_
69+
)
70+
: (bytes(""), ERC4337Utils.SIG_VALIDATION_FAILED);
71+
}
72+
73+
/// @inheritdoc PaymasterCore
74+
function _postOp(
75+
PostOpMode /* mode */,
76+
bytes calldata context,
77+
uint256 actualGasCost,
78+
uint256 actualUserOpFeePerGas
79+
) internal virtual override {
80+
bytes32 userOpHash = bytes32(context[0x00:0x20]);
81+
IERC20 token = IERC20(address(bytes20(context[0x20:0x34])));
82+
uint256 prefundAmount = uint256(bytes32(context[0x34:0x54]));
83+
uint256 tokenPrice = uint256(bytes32(context[0x54:0x74]));
84+
address user = address(bytes20(context[0x74:0x88]));
85+
address guarantor = address(bytes20(context[0x88:0x9C]));
86+
87+
uint256 actualAmount = (actualGasCost +
88+
(guarantor == address(0)).ternary(POST_OP_COST, POST_OP_COST_WITH_GUARANTOR) *
89+
actualUserOpFeePerGas).mulDiv(tokenPrice, _tokenPriceDenominator());
90+
91+
if (guarantor == address(0)) {
92+
token.safeTransfer(user, prefundAmount - actualAmount);
93+
emit UserOperationSponsored(userOpHash, user, address(0), actualAmount, tokenPrice, false);
94+
} else if (token.trySafeTransferFrom(user, address(this), actualAmount)) {
95+
token.safeTransfer(guarantor, prefundAmount);
96+
emit UserOperationSponsored(userOpHash, user, guarantor, actualAmount, tokenPrice, false);
97+
} else {
98+
token.safeTransfer(guarantor, prefundAmount - actualAmount);
99+
emit UserOperationSponsored(userOpHash, user, guarantor, actualAmount, tokenPrice, true);
100+
}
101+
}
102+
103+
/**
104+
* @dev Retrieves payment details for a user operation
105+
*
106+
* The values returned by this internal function are:
107+
* * `validationData`: ERC-4337 validation data, indicating success/failure and optional time validity (`validAfter`, `validUntil`).
108+
* * `token`: Address of the ERC-20 token used for payment to the paymaster.
109+
* * `tokenPrice`: Price of the token in native currency, scaled by `_tokenPriceDenominator()`.
110+
* * `guarantor`: Address of an entity advancing funds if the user lacks them; receives tokens during execution or pays if the user can't.
111+
*
112+
* ==== Calculating the token price
113+
*
114+
* Given gas fees are paid in native currency, developers can use the `ERC20 price unit / native price unit` ratio to
115+
* calculate the price of an ERC20 token price in native currency. However, the token may have a different number of decimals
116+
* than the native currency. For a a generalized formula considering prices in USD and decimals, consider using:
117+
*
118+
* `(<ERC-20 token price in $> / 10**<ERC-20 decimals>) / (<Native token price in $> / 1e18) * _tokenPriceDenominator()`
119+
*
120+
* For example, suppose token is USDC ($1 with 6 decimals) and native currency is ETH (assuming $2524.86 with 18 decimals),
121+
* then each unit (1e-6) of USDC is worth `(1 / 1e6) / ((252486 / 1e2) / 1e18) = 396061563.8094785` wei. The `_tokenPriceDenominator()`
122+
* ensures precision by avoiding fractional value loss. (i.e. the 0.8094785 part).
123+
*
124+
* ==== Guarantor
125+
*
126+
* To support a guarantor, developers can use the `paymasterData` field to store the guarantor's address. Developers can disable
127+
* support for a guarantor by returning `address(0)`. If supported, ensure explicit consent (e.g., signature verification) to prevent
128+
* unauthorized use.
129+
*/
130+
function _fetchDetails(
131+
PackedUserOperation calldata userOp,
132+
bytes32 userOpHash
133+
) internal view virtual returns (uint256 validationData, IERC20 token, uint256 tokenPrice, address guarantor);
134+
135+
/// @dev Denominator used for interpreting the `tokenPrice` returned by {_fetchDetails} as "fixed point".
136+
function _tokenPriceDenominator() internal view virtual returns (uint256) {
137+
return 1e18;
138+
}
139+
140+
/// @dev Public function that allows the withdrawer to extract ERC-20 tokens resulting from gas payments.
141+
function withdrawTokens(IERC20 token, address recipient, uint256 amount) public virtual onlyWithdrawer {
142+
if (amount == type(uint256).max) amount = token.balanceOf(address(this));
143+
token.safeTransfer(recipient, amount);
144+
}
145+
}

0 commit comments

Comments
 (0)