Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start initial work on ERC4337 interface and helpers #4991

Draft
wants to merge 55 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
5e82076
Add P256 implementation and testing
Amxx Feb 7, 2024
da0f27e
enable optimizations by default
Amxx Feb 7, 2024
aa59c67
test recovering address
Amxx Feb 7, 2024
9512947
improved testing
Amxx Feb 7, 2024
a60bf48
spelling
Amxx Feb 7, 2024
9185026
fix lint
Amxx Feb 7, 2024
025e360
expose imports tick
Amxx Feb 7, 2024
803e735
fix lint
Amxx Feb 7, 2024
57fcecd
fix lint
Amxx Feb 7, 2024
4dae298
add changeset
Amxx Feb 7, 2024
6cf039d
improve doc
Amxx Feb 7, 2024
c094fa1
add envvar to force allowUnlimitedContractSize
Amxx Feb 7, 2024
20a03df
fix lint
Amxx Feb 7, 2024
15f1a6b
fix stack too deep error in coverage
Amxx Feb 7, 2024
e2040e4
reoder arguments to match ecrecover and EIP-7212
Amxx Feb 13, 2024
695b732
reduce diff
Amxx Mar 13, 2024
41aaf71
Merge branch 'master' into feature/P256
Amxx Mar 13, 2024
f36f183
Start initial work on ERC4337 interface and helpers
Amxx Apr 4, 2024
a1532d0
Packing library
Amxx Apr 4, 2024
00e0eea
reorder UserOperationUtils.sol
Amxx Apr 5, 2024
8fa363e
4337 account wip
Amxx Apr 8, 2024
9309f71
wip
Amxx Apr 10, 2024
1616a96
wip
Amxx Apr 11, 2024
aef2168
refactor
Amxx Apr 11, 2024
04a6fb0
refactor
Amxx Apr 12, 2024
0f3c3fa
entrypoint deploys account
Amxx Apr 12, 2024
3bf4557
Update contracts/utils/cryptography/P256.sol
Amxx Apr 24, 2024
3cbf426
Merge branch 'master' into feature/P256
Amxx Apr 25, 2024
bba7fa3
update pseudocode reference
Amxx Apr 25, 2024
2812ed8
Update contracts/utils/cryptography/P256.sol
Amxx Apr 25, 2024
e0ef63b
refactor neutral element in jAdd
Amxx Apr 26, 2024
a13ad48
refactor entrypoint
Amxx Apr 26, 2024
342256c
erc4337 js helper
Amxx Apr 26, 2024
30c2d12
update 4337 helper
Amxx Apr 29, 2024
b1f3b60
Merge remote-tracking branch 'origin' into erc4337/interfaces-and-hel…
Amxx Apr 30, 2024
2f07188
Working on abstract account primitives
Amxx Apr 30, 2024
a129a45
improve tests
Amxx Apr 30, 2024
57b4fb2
use getBytes
Amxx Apr 30, 2024
c641f0a
update Account
Amxx Apr 30, 2024
3124b88
Merge branch 'feature/P256' into erc4337/interfaces-and-helpers
Amxx Apr 30, 2024
618b563
Add ECDSA and P256 variants of SimpleAccount
Amxx Apr 30, 2024
14474fc
move AccountECDSA and AccountP256 to a "modules" subfolder
Amxx Apr 30, 2024
7babdf1
Merge branch 'master' into erc4337/interfaces-and-helpers
Amxx Apr 30, 2024
6a5c91b
fix comments
Amxx May 1, 2024
1487fdb
inline documentation
Amxx May 2, 2024
6373a08
Inline documentation
Amxx May 2, 2024
4512481
add AccountMultisig module
Amxx May 3, 2024
ee47efc
update
Amxx May 3, 2024
003c232
refactor signature processing
Amxx May 4, 2024
318c372
add AccountAllSignatures.sol that support ECDSA & P256 identities in …
Amxx May 13, 2024
03dfa71
up
Amxx May 15, 2024
82d6bde
Add Account7702
Amxx May 20, 2024
612cadc
AccountCommon
Amxx May 21, 2024
b81817a
rename
Amxx May 21, 2024
fbf4ca5
Merge branch 'erc4337/EIP-7702-account' into erc4337/interfaces-and-h…
Amxx May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
76 changes: 76 additions & 0 deletions contracts/abstraction/Account.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {PackedUserOperation, IAccount, IEntryPoint} from "../interfaces/IERC4337.sol";
import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol";
import {SignatureChecker} from "../utils/cryptography/SignatureChecker.sol";
import {SafeCast} from "../utils/math/SafeCast.sol";

abstract contract Account is IAccount {
using SafeCast for bool;

error AccountEntryPointRestricted();
error AccountUserRestricted();
error AccountInvalidBatchLength();

// Modifiers
modifier onlyEntryPoint() {
if (msg.sender != address(entryPoint())) {
revert AccountEntryPointRestricted();
}
_;
}

modifier onlyAuthorizedOrSelf() {
if (msg.sender != address(this) && !_isAuthorized(msg.sender)) {
revert AccountUserRestricted();
}
_;
}

// Virtual pure (not implemented) hooks
function entryPoint() public view virtual returns (IEntryPoint);

function _isAuthorized(address) internal view virtual returns (bool);

// Public interface
function getNonce() public view virtual returns (uint256) {
return entryPoint().getNonce(address(this), 0);
}

function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external virtual override onlyEntryPoint returns (uint256 validationData) {
validationData = _validateSignature(userOp, userOpHash);
_validateNonce(userOp.nonce);
_payPrefund(missingAccountFunds);
}

function _validateSignature(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal virtual returns (uint256 validationData) {
return
(_isAuthorized(userOp.sender) &&
SignatureChecker.isValidSignatureNow(
userOp.sender,
MessageHashUtils.toEthSignedMessageHash(userOpHash),
userOp.signature
)).toUint();
}

function _validateNonce(uint256 nonce) internal view virtual {
// TODO ?
}

function _payPrefund(uint256 missingAccountFunds) internal virtual {
if (missingAccountFunds != 0) {
(bool success, ) = payable(msg.sender).call{value: missingAccountFunds}("");
success;
//ignore failure (its EntryPoint's job to verify, not account.)
}
}
}
247 changes: 247 additions & 0 deletions contracts/abstraction/ERC4337Utils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {IEntryPoint, PackedUserOperation} from "../interfaces/IERC4337.sol";
import {Math} from "../utils/math/Math.sol";
import {Call} from "../utils/Call.sol";
import {Packing} from "../utils/Packing.sol";

library ERC4337Utils {
using Packing for *;
/*
* For simulation purposes, validateUserOp (and validatePaymasterUserOp)
* return this value on success.
*/
uint256 internal constant SIG_VALIDATION_SUCCESS = 0;

/*
* For simulation purposes, validateUserOp (and validatePaymasterUserOp)
* must return this value in case of signature failure, instead of revert.
*/
uint256 internal constant SIG_VALIDATION_FAILED = 1;

// Create sender from initcode
function createSender(bytes calldata initCode, uint256 gas) internal returns (address sender) {
return
Call.call(address(bytes20(initCode[0:20])), 0, initCode[20:], gas) && Call.getReturnDataSize() >= 0x20
? abi.decode(Call.getReturnData(0x20), (address))
: address(0);
}

// Validation data
function parseValidationData(
uint256 validationData
) internal pure returns (address aggregator, uint48 validAfter, uint48 validUntil) {
aggregator = address(uint160(validationData));
validUntil = uint48(validationData >> 160);
validAfter = uint48(validationData >> 208);
if (validUntil == 0) validUntil = type(uint48).max;
}

function packValidationData(
address aggregator,
uint48 validAfter,
uint48 validUntil
) internal pure returns (uint256) {
return uint160(aggregator) | (uint256(validUntil) << 160) | (uint256(validAfter) << 208);
}

function packValidationData(bool sigFailed, uint48 validUntil, uint48 validAfter) internal pure returns (uint256) {
return
(sigFailed ? SIG_VALIDATION_FAILED : SIG_VALIDATION_SUCCESS) |
(uint256(validUntil) << 160) |
(uint256(validAfter) << 208);
}

function getValidationData(uint256 validationData) internal view returns (address aggregator, bool outOfTimeRange) {
if (validationData == 0) {
return (address(0), false);
} else {
(address agregator, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData);

Check failure on line 61 in contracts/abstraction/ERC4337Utils.sol

View workflow job for this annotation

GitHub Actions / codespell

agregator ==> aggregator
return (agregator, block.timestamp > validUntil || block.timestamp < validAfter);

Check failure on line 62 in contracts/abstraction/ERC4337Utils.sol

View workflow job for this annotation

GitHub Actions / codespell

agregator ==> aggregator
}
}

/*
enum ErrorCodes {
AA10_SENDER_ALREADY_CONSTRUCTED,
AA13_INITCODE_FAILLED,
AA14_INITCODE_WRONG_SENDER,
AA15_INITCODE_NO_DEPLOYMENT,
// Account
AA21_MISSING_FUNDS,
AA22_EXPIRED_OR_NOT_DUE,
AA23_REVERTED,
AA24_SIGNATURE_ERROR,
AA25_INVALID_NONCE,
AA26_OVER_VERIFICATION_GAS_LIMIT,
// Paymaster
AA31_MISSING_FUNDS,
AA32_EXPIRED_OR_NOT_DUE,
AA33_REVERTED,
AA34_SIGNATURE_ERROR,
AA36_OVER_VERIFICATION_GAS_LIMIT,
// other
AA95_OUT_OF_GAS
}

function toString(ErrorCodes err) internal pure returns (string memory) {
if (err == ErrorCodes.AA10_SENDER_ALREADY_CONSTRUCTED) {
return "AA10 sender already constructed";
} else if (err == ErrorCodes.AA13_INITCODE_FAILLED) {
return "AA13 initCode failed or OOG";
} else if (err == ErrorCodes.AA14_INITCODE_WRONG_SENDER) {
return "AA14 initCode must return sender";
} else if (err == ErrorCodes.AA15_INITCODE_NO_DEPLOYMENT) {
return "AA15 initCode must create sender";
} else if (err == ErrorCodes.AA21_MISSING_FUNDS) {
return "AA21 didn't pay prefund";
} else if (err == ErrorCodes.AA22_EXPIRED_OR_NOT_DUE) {
return "AA22 expired or not due";
} else if (err == ErrorCodes.AA23_REVERTED) {
return "AA23 reverted";
} else if (err == ErrorCodes.AA24_SIGNATURE_ERROR) {
return "AA24 signature error";
} else if (err == ErrorCodes.AA25_INVALID_NONCE) {
return "AA25 invalid account nonce";
} else if (err == ErrorCodes.AA26_OVER_VERIFICATION_GAS_LIMIT) {
return "AA26 over verificationGasLimit";
} else if (err == ErrorCodes.AA31_MISSING_FUNDS) {
return "AA31 paymaster deposit too low";
} else if (err == ErrorCodes.AA32_EXPIRED_OR_NOT_DUE) {
return "AA32 paymaster expired or not due";
} else if (err == ErrorCodes.AA33_REVERTED) {
return "AA33 reverted";
} else if (err == ErrorCodes.AA34_SIGNATURE_ERROR) {
return "AA34 signature error";
} else if (err == ErrorCodes.AA36_OVER_VERIFICATION_GAS_LIMIT) {
return "AA36 over paymasterVerificationGasLimit";
} else if (err == ErrorCodes.AA95_OUT_OF_GAS) {
return "AA95 out of gas";
} else {
return "Unknown error code";
}
}

function failedOp(uint256 index, ErrorCodes err) internal pure {
revert IEntryPoint.FailedOp(index, toString(err));
}

function failedOp(uint256 index, ErrorCodes err, bytes memory extraData) internal pure {
revert IEntryPoint.FailedOpWithRevert(index, toString(err), extraData);
}
*/

// Packed user operation
function hash(PackedUserOperation calldata self) internal pure returns (bytes32) {
return keccak256(encode(self));
}

function encode(PackedUserOperation calldata self) internal pure returns (bytes memory ret) {
return
abi.encode(
self.sender,
self.nonce,
keccak256(self.initCode),
keccak256(self.callData),
self.accountGasLimits,
self.preVerificationGas,
self.gasFees,
keccak256(self.paymasterAndData)
);
}

function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
return self.accountGasLimits.asUint128x2().first();
}

function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
return self.accountGasLimits.asUint128x2().second();
}

function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) {
return self.gasFees.asUint128x2().first();
}

function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) {
return self.gasFees.asUint128x2().second();
}

function gasPrice(PackedUserOperation calldata self) internal view returns (uint256) {
unchecked {
// Following values are "per gas"
(uint256 maxPriorityFee, uint256 maxFee) = self.gasFees.asUint128x2().split();
return maxFee == maxPriorityFee ? maxFee : Math.min(maxFee, maxPriorityFee + block.basefee);
}
}

function paymaster(PackedUserOperation calldata self) internal pure returns (address) {
return address(bytes20(self.paymasterAndData[0:20]));
}

function paymasterVerificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
return uint128(bytes16(self.paymasterAndData[20:36]));
}

function paymasterPostOpGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
return uint128(bytes16(self.paymasterAndData[36:52]));
}

struct UserOpInfo {
address sender;
uint256 nonce;
uint256 verificationGasLimit;
uint256 callGasLimit;
uint256 paymasterVerificationGasLimit;
uint256 paymasterPostOpGasLimit;
uint256 preVerificationGas;
address paymaster;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes32 userOpHash;
uint256 prefund;
uint256 preOpGas;
bytes context;
}

function load(UserOpInfo memory self, PackedUserOperation calldata source) internal view {
self.sender = source.sender;
self.nonce = source.nonce;
(self.verificationGasLimit, self.callGasLimit) = source.accountGasLimits.asUint128x2().split();
self.preVerificationGas = source.preVerificationGas;
(self.maxPriorityFeePerGas, self.maxFeePerGas) = source.gasFees.asUint128x2().split();

if (source.paymasterAndData.length > 0) {
require(source.paymasterAndData.length >= 52, "AA93 invalid paymasterAndData");
self.paymaster = paymaster(source);
self.paymasterVerificationGasLimit = paymasterVerificationGasLimit(source);
self.paymasterPostOpGasLimit = paymasterPostOpGasLimit(source);
} else {
self.paymaster = address(0);
self.paymasterVerificationGasLimit = 0;
self.paymasterPostOpGasLimit = 0;
}
self.userOpHash = keccak256(abi.encode(hash(source), address(this), block.chainid));
self.prefund = 0;
self.preOpGas = 0;
self.context = "";
}

function requiredPrefund(UserOpInfo memory self) internal pure returns (uint256) {
return
(self.verificationGasLimit +
self.callGasLimit +
self.paymasterVerificationGasLimit +
self.paymasterPostOpGasLimit +
self.preVerificationGas) * self.maxFeePerGas;
}

function gasPrice(UserOpInfo memory self) internal view returns (uint256) {
unchecked {
uint256 maxFee = self.maxFeePerGas;
uint256 maxPriorityFee = self.maxPriorityFeePerGas;
return maxFee == maxPriorityFee ? maxFee : Math.min(maxFee, maxPriorityFee + block.basefee);
}
}
}