mirror of
https://github.com/getwax/zk-account-abstraction.git
synced 2026-01-09 20:47:58 -05:00
AA-61 rename wallet to account (#134)
* rename IWallet to IAccount (and all other contracts, e.g. SimpleWallet to SimpleAccount, etc..)
This commit is contained in:
@@ -1,22 +1,22 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
import "../samples/SimpleWallet.sol";
|
||||
import "./IBLSWallet.sol";
|
||||
import "../samples/SimpleAccount.sol";
|
||||
import "./IBLSAccount.sol";
|
||||
|
||||
/**
|
||||
* Minimal BLS-based wallet that uses an aggregated signature.
|
||||
* The wallet must maintain its own BLS public-key, and expose its trusted signature aggregator.
|
||||
* Note that unlike the "standard" SimpleWallet, this wallet can't be called directly
|
||||
* (normal SimpleWallet uses its "signer" address as both the ecrecover signer, and as a legitimate
|
||||
* Minimal BLS-based account that uses an aggregated signature.
|
||||
* The account must maintain its own BLS public-key, and expose its trusted signature aggregator.
|
||||
* Note that unlike the "standard" SimpleAccount, this account can't be called directly
|
||||
* (normal SimpleAccount uses its "signer" address as both the ecrecover signer, and as a legitimate
|
||||
* Ethereum sender address. Obviously, a BLS public is not a valid Ethereum sender address.)
|
||||
*/
|
||||
contract BLSWallet is SimpleWallet, IBLSWallet {
|
||||
contract BLSAccount is SimpleAccount, IBLSAccount {
|
||||
address public immutable aggregator;
|
||||
uint256[4] private publicKey;
|
||||
|
||||
constructor(IEntryPoint anEntryPoint, address anAggregator, uint256[4] memory aPublicKey)
|
||||
SimpleWallet(anEntryPoint, address(0)) {
|
||||
SimpleAccount(anEntryPoint, address(0)) {
|
||||
publicKey = aPublicKey;
|
||||
aggregator = anAggregator;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ contract BLSWallet is SimpleWallet, IBLSWallet {
|
||||
internal override view returns (uint256 deadline) {
|
||||
|
||||
(userOp, userOpHash);
|
||||
require(userOpAggregator == aggregator, "BLSWallet: wrong aggregator");
|
||||
require(userOpAggregator == aggregator, "BLSAccount: wrong aggregator");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@ contract BLSWallet is SimpleWallet, IBLSWallet {
|
||||
}
|
||||
|
||||
|
||||
contract BLSWalletDeployer {
|
||||
contract BLSAccountDeployer {
|
||||
|
||||
function deployWallet(IEntryPoint anEntryPoint, address anAggregator, uint salt, uint256[4] memory aPublicKey) public returns (BLSWallet) {
|
||||
return new BLSWallet{salt : bytes32(salt)}(anEntryPoint, anAggregator, aPublicKey);
|
||||
function deployAccount(IEntryPoint anEntryPoint, address anAggregator, uint salt, uint256[4] memory aPublicKey) public returns (BLSAccount) {
|
||||
return new BLSAccount{salt : bytes32(salt)}(anEntryPoint, anAggregator, aPublicKey);
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ pragma abicoder v2;
|
||||
import "../interfaces/IAggregator.sol";
|
||||
import "../interfaces/IEntryPoint.sol";
|
||||
import {BLSOpen} from "./lib/BLSOpen.sol";
|
||||
import "./IBLSWallet.sol";
|
||||
import "./IBLSAccount.sol";
|
||||
import "./BLSHelper.sol";
|
||||
import "hardhat/console.sol";
|
||||
|
||||
/**
|
||||
* A BLS-based signature aggregator, to validate aggregated signature of multiple UserOps if BLSWallet
|
||||
* A BLS-based signature aggregator, to validate aggregated signature of multiple UserOps if BLSAccount
|
||||
*/
|
||||
contract BLSSignatureAggregator is IAggregator {
|
||||
using UserOperationLib for UserOperation;
|
||||
@@ -22,7 +22,7 @@ contract BLSSignatureAggregator is IAggregator {
|
||||
if (initCode.length > 0) {
|
||||
publicKey = getTrailingPublicKey(initCode);
|
||||
} else {
|
||||
return IBLSWallet(userOp.sender).getBlsPublicKey();
|
||||
return IBLSAccount(userOp.sender).getBlsPublicKey();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ contract BLSSignatureAggregator is IAggregator {
|
||||
for (uint256 i = 0; i < userOpsLen; i++) {
|
||||
|
||||
UserOperation memory userOp = userOps[i];
|
||||
IBLSWallet blsWallet = IBLSWallet(userOp.sender);
|
||||
IBLSAccount blsAccount = IBLSAccount(userOp.sender);
|
||||
|
||||
blsPublicKeys[i] = blsWallet.getBlsPublicKey{gas : 30000}();
|
||||
blsPublicKeys[i] = blsAccount.getBlsPublicKey{gas : 30000}();
|
||||
|
||||
messages[i] = _userOpToMessage(userOp, keccak256(abi.encode(blsPublicKeys[i])));
|
||||
}
|
||||
@@ -86,7 +86,7 @@ contract BLSSignatureAggregator is IAggregator {
|
||||
|
||||
/**
|
||||
* return the BLS "message" for the given UserOp.
|
||||
* the wallet should sign this value using its public-key
|
||||
* the account checks the signature over this value using its public-key
|
||||
*/
|
||||
function userOpToMessage(UserOperation memory userOp) public view returns (uint256[2] memory) {
|
||||
bytes32 hashPublicKey = _getUserOpPubkeyHash(userOp);
|
||||
@@ -94,8 +94,8 @@ contract BLSSignatureAggregator is IAggregator {
|
||||
}
|
||||
|
||||
function _userOpToMessage(UserOperation memory userOp, bytes32 publicKeyHash) internal view returns (uint256[2] memory) {
|
||||
bytes32 requestId = _getUserOpHash(userOp, publicKeyHash);
|
||||
return BLSOpen.hashToPoint(BLS_DOMAIN, abi.encodePacked(requestId));
|
||||
bytes32 userOpHash = _getUserOpHash(userOp, publicKeyHash);
|
||||
return BLSOpen.hashToPoint(BLS_DOMAIN, abi.encodePacked(userOpHash));
|
||||
}
|
||||
|
||||
//return the public-key hash of a userOp.
|
||||
@@ -118,7 +118,7 @@ contract BLSSignatureAggregator is IAggregator {
|
||||
* First it validates the signature over the userOp. then it return data to be used when creating the handleOps:
|
||||
* @param userOp the userOperation received from the user.
|
||||
* @return sigForUserOp the value to put into the signature field of the userOp when calling handleOps.
|
||||
* (usually empty, unless wallet and aggregator support some kind of "multisig"
|
||||
* (usually empty, unless account and aggregator support some kind of "multisig"
|
||||
*/
|
||||
function validateUserOpSignature(UserOperation calldata userOp)
|
||||
external view returns (bytes memory sigForUserOp) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
pragma solidity >=0.7.6;
|
||||
|
||||
import "../interfaces/IAggregatedWallet.sol";
|
||||
import "../interfaces/IAggregatedAccount.sol";
|
||||
|
||||
/**
|
||||
* a BLS wallet should expose its own public key.
|
||||
* a BLS account should expose its own public key.
|
||||
*/
|
||||
interface IBLSWallet is IAggregatedWallet {
|
||||
interface IBLSAccount is IAggregatedAccount {
|
||||
function getBlsPublicKey() external view returns (uint256[4] memory);
|
||||
}
|
||||
@@ -5,26 +5,26 @@ pragma solidity ^0.8.12;
|
||||
/* solhint-disable no-inline-assembly */
|
||||
/* solhint-disable reason-string */
|
||||
|
||||
import "../interfaces/IWallet.sol";
|
||||
import "../interfaces/IAccount.sol";
|
||||
import "../interfaces/IEntryPoint.sol";
|
||||
|
||||
/**
|
||||
* Basic wallet implementation.
|
||||
* this contract provides the basic logic for implementing the IWallet interface - validateUserOp
|
||||
* specific wallet implementation should inherit it and provide the wallet-specific logic
|
||||
* Basic account implementation.
|
||||
* this contract provides the basic logic for implementing the IAccount interface - validateUserOp
|
||||
* specific account implementation should inherit it and provide the account-specific logic
|
||||
*/
|
||||
abstract contract BaseWallet is IWallet {
|
||||
abstract contract BaseAccount is IAccount {
|
||||
using UserOperationLib for UserOperation;
|
||||
|
||||
/**
|
||||
* return the wallet nonce.
|
||||
* return the account nonce.
|
||||
* subclass should return a nonce value that is used both by _validateAndUpdateNonce, and by the external provider (to read the current nonce)
|
||||
*/
|
||||
function nonce() public view virtual returns (uint256);
|
||||
|
||||
/**
|
||||
* return the entryPoint used by this wallet.
|
||||
* subclass should return the current entryPoint used by this wallet.
|
||||
* return the entryPoint used by this account.
|
||||
* subclass should return the current entryPoint used by this account.
|
||||
*/
|
||||
function entryPoint() public view virtual returns (IEntryPoint);
|
||||
|
||||
@@ -32,21 +32,21 @@ abstract contract BaseWallet is IWallet {
|
||||
* Validate user's signature and nonce.
|
||||
* subclass doesn't need to override this method. Instead, it should override the specific internal validation methods.
|
||||
*/
|
||||
function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, address aggregator, uint256 missingWalletFunds)
|
||||
function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, address aggregator, uint256 missingAccountFunds)
|
||||
external override virtual returns (uint256 deadline) {
|
||||
_requireFromEntryPoint();
|
||||
deadline = _validateSignature(userOp, userOpHash, aggregator);
|
||||
if (userOp.initCode.length == 0) {
|
||||
_validateAndUpdateNonce(userOp);
|
||||
}
|
||||
_payPrefund(missingWalletFunds);
|
||||
_payPrefund(missingAccountFunds);
|
||||
}
|
||||
|
||||
/**
|
||||
* ensure the request comes from the known entrypoint.
|
||||
*/
|
||||
function _requireFromEntryPoint() internal virtual view {
|
||||
require(msg.sender == address(entryPoint()), "wallet: not from EntryPoint");
|
||||
require(msg.sender == address(entryPoint()), "account: not from EntryPoint");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +54,7 @@ abstract contract BaseWallet is IWallet {
|
||||
* @param userOp validate the userOp.signature field
|
||||
* @param userOpHash convenient field: the hash of the request, to check the signature against
|
||||
* (also hashes the entrypoint and chain-id)
|
||||
* @param aggregator the current aggregator. can be ignored by wallets that don't use aggregators
|
||||
* @param aggregator the current aggregator. can be ignored by accounts that don't use aggregators
|
||||
* @return deadline the last block timestamp this operation is valid, or zero if it is valid indefinitely.
|
||||
* Note that the validation code cannot use block.timestamp (or block.number) directly.
|
||||
*/
|
||||
@@ -63,8 +63,8 @@ abstract contract BaseWallet is IWallet {
|
||||
|
||||
/**
|
||||
* validate the current nonce matches the UserOperation nonce.
|
||||
* then it should update the wallet's state to prevent replay of this UserOperation.
|
||||
* called only if initCode is empty (since "nonce" field is used as "salt" on wallet creation)
|
||||
* then it should update the account's state to prevent replay of this UserOperation.
|
||||
* called only if initCode is empty (since "nonce" field is used as "salt" on account creation)
|
||||
* @param userOp the op to validate.
|
||||
*/
|
||||
function _validateAndUpdateNonce(UserOperation calldata userOp) internal virtual;
|
||||
@@ -74,20 +74,20 @@ abstract contract BaseWallet is IWallet {
|
||||
* subclass MAY override this method for better funds management
|
||||
* (e.g. send to the entryPoint more than the minimum required, so that in future transactions
|
||||
* it will not be required to send again)
|
||||
* @param missingWalletFunds the minimum value this method should send the entrypoint.
|
||||
* @param missingAccountFunds the minimum value this method should send the entrypoint.
|
||||
* this value MAY be zero, in case there is enough deposit, or the userOp has a paymaster.
|
||||
*/
|
||||
function _payPrefund(uint256 missingWalletFunds) internal virtual {
|
||||
if (missingWalletFunds != 0) {
|
||||
(bool success,) = payable(msg.sender).call{value : missingWalletFunds, gas : type(uint256).max}("");
|
||||
function _payPrefund(uint256 missingAccountFunds) internal virtual {
|
||||
if (missingAccountFunds != 0) {
|
||||
(bool success,) = payable(msg.sender).call{value : missingAccountFunds, gas : type(uint256).max}("");
|
||||
(success);
|
||||
//ignore failure (its EntryPoint's job to verify, not wallet.)
|
||||
//ignore failure (its EntryPoint's job to verify, not account.)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* expose an api to modify the entryPoint.
|
||||
* must be called by current "admin" of the wallet.
|
||||
* must be called by current "admin" of the account.
|
||||
* @param newEntryPoint the new entrypoint to trust.
|
||||
*/
|
||||
function updateEntryPoint(address newEntryPoint) external {
|
||||
@@ -97,7 +97,7 @@ abstract contract BaseWallet is IWallet {
|
||||
|
||||
/**
|
||||
* ensure the caller is allowed "admin" operations (such as changing the entryPoint)
|
||||
* default implementation trust the wallet itself (or any signer that passes "validateUserOp")
|
||||
* default implementation trust the account itself (or any signer that passes "validateUserOp")
|
||||
* to be the "admin"
|
||||
*/
|
||||
function _requireFromAdmin() internal view virtual {
|
||||
@@ -10,10 +10,10 @@ pragma solidity ^0.8.12;
|
||||
/* solhint-disable reason-string */
|
||||
/* solhint-disable avoid-tx-origin */
|
||||
|
||||
import "../interfaces/IWallet.sol";
|
||||
import "../interfaces/IAccount.sol";
|
||||
import "../interfaces/IPaymaster.sol";
|
||||
|
||||
import "../interfaces/IAggregatedWallet.sol";
|
||||
import "../interfaces/IAggregatedAccount.sol";
|
||||
import "../interfaces/IEntryPoint.sol";
|
||||
import "../interfaces/ICreate2Deployer.sol";
|
||||
import "../utils/Exec.sol";
|
||||
@@ -62,7 +62,7 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
/**
|
||||
* Execute a batch of UserOperation.
|
||||
* no signature aggregator is used.
|
||||
* if any wallet requires an aggregator (that is, it returned an "actualAggregator" when
|
||||
* if any account requires an aggregator (that is, it returned an "actualAggregator" when
|
||||
* performing simulateValidation), then handleAggregatedOps() must be used instead.
|
||||
* @param ops the operations to execute
|
||||
* @param beneficiary the address to receive the fees
|
||||
@@ -89,7 +89,7 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
|
||||
/**
|
||||
* Execute a batch of UserOperation with Aggregators
|
||||
* @param opsPerAggregator the operations to execute, grouped by aggregator (or address(0) for no-aggregator wallets)
|
||||
* @param opsPerAggregator the operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts)
|
||||
* @param beneficiary the address to receive the fees
|
||||
*/
|
||||
function handleAggregatedOps(
|
||||
@@ -218,9 +218,9 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a call to wallet.validateUserOp and paymaster.validatePaymasterUserOp.
|
||||
* Simulate a call to account.validateUserOp and paymaster.validatePaymasterUserOp.
|
||||
* @dev this method always revert. Successful result is SimulationResult error. other errors are failures.
|
||||
* @dev The node must also verify it doesn't use banned opcodes, and that it doesn't reference storage outside the wallet's data.
|
||||
* @dev The node must also verify it doesn't use banned opcodes, and that it doesn't reference storage outside the account's data.
|
||||
* @param userOp the user operation to validate.
|
||||
*/
|
||||
function simulateValidation(UserOperation calldata userOp) external {
|
||||
@@ -277,31 +277,31 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* call wallet.validateUserOp.
|
||||
* revert (with FailedOp) in case validateUserOp reverts, or wallet didn't send required prefund.
|
||||
* decrement wallet's deposit if needed
|
||||
* call account.validateUserOp.
|
||||
* revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund.
|
||||
* decrement account's deposit if needed
|
||||
*/
|
||||
function _validateWalletPrepayment(uint256 opIndex, UserOperation calldata op, UserOpInfo memory opInfo, address aggregator, uint256 requiredPrefund)
|
||||
internal returns (uint256 gasUsedByValidateWalletPrepayment, address actualAggregator, uint256 deadline) {
|
||||
function _validateAccountPrepayment(uint256 opIndex, UserOperation calldata op, UserOpInfo memory opInfo, address aggregator, uint256 requiredPrefund)
|
||||
internal returns (uint256 gasUsedByValidateAccountPrepayment, address actualAggregator, uint256 deadline) {
|
||||
unchecked {
|
||||
uint256 preGas = gasleft();
|
||||
MemoryUserOp memory mUserOp = opInfo.mUserOp;
|
||||
_createSenderIfNeeded(opIndex, mUserOp, op.initCode);
|
||||
if (aggregator == SIMULATE_FIND_AGGREGATOR) {
|
||||
try IAggregatedWallet(mUserOp.sender).getAggregator() returns (address userOpAggregator) {
|
||||
try IAggregatedAccount(mUserOp.sender).getAggregator() returns (address userOpAggregator) {
|
||||
aggregator = actualAggregator = userOpAggregator;
|
||||
} catch {
|
||||
aggregator = actualAggregator = address(0);
|
||||
}
|
||||
}
|
||||
uint256 missingWalletFunds = 0;
|
||||
uint256 missingAccountFunds = 0;
|
||||
address sender = mUserOp.sender;
|
||||
address paymaster = mUserOp.paymaster;
|
||||
if (paymaster == address(0)) {
|
||||
uint256 bal = balanceOf(sender);
|
||||
missingWalletFunds = bal > requiredPrefund ? 0 : requiredPrefund - bal;
|
||||
missingAccountFunds = bal > requiredPrefund ? 0 : requiredPrefund - bal;
|
||||
}
|
||||
try IWallet(sender).validateUserOp{gas : mUserOp.verificationGasLimit}(op, opInfo.userOpHash, aggregator, missingWalletFunds) returns (uint256 _deadline) {
|
||||
try IAccount(sender).validateUserOp{gas : mUserOp.verificationGasLimit}(op, opInfo.userOpHash, aggregator, missingAccountFunds) returns (uint256 _deadline) {
|
||||
// solhint-disable-next-line not-rely-on-time
|
||||
if (_deadline != 0 && _deadline < block.timestamp) {
|
||||
revert FailedOp(opIndex, address(0), "expired");
|
||||
@@ -316,11 +316,11 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
DepositInfo storage senderInfo = deposits[sender];
|
||||
uint256 deposit = senderInfo.deposit;
|
||||
if (requiredPrefund > deposit) {
|
||||
revert FailedOp(opIndex, address(0), "wallet didn't pay prefund");
|
||||
revert FailedOp(opIndex, address(0), "account didn't pay prefund");
|
||||
}
|
||||
senderInfo.deposit = uint112(deposit - requiredPrefund);
|
||||
}
|
||||
gasUsedByValidateWalletPrepayment = preGas - gasleft();
|
||||
gasUsedByValidateAccountPrepayment = preGas - gasleft();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
* revert with proper FailedOp in case paymaster reverts.
|
||||
* decrement paymaster's deposit
|
||||
*/
|
||||
function _validatePaymasterPrepayment(uint256 opIndex, UserOperation calldata op, UserOpInfo memory opInfo, uint256 requiredPreFund, uint256 gasUsedByValidateWalletPrepayment) internal returns (bytes memory context, uint256 deadline) {
|
||||
function _validatePaymasterPrepayment(uint256 opIndex, UserOperation calldata op, UserOpInfo memory opInfo, uint256 requiredPreFund, uint256 gasUsedByValidateAccountPrepayment) internal returns (bytes memory context, uint256 deadline) {
|
||||
unchecked {
|
||||
MemoryUserOp memory mUserOp = opInfo.mUserOp;
|
||||
address paymaster = mUserOp.paymaster;
|
||||
@@ -345,7 +345,7 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
revert FailedOp(opIndex, paymaster, "paymaster deposit too low");
|
||||
}
|
||||
paymasterInfo.deposit = uint112(deposit - requiredPreFund);
|
||||
uint256 gas = mUserOp.verificationGasLimit - gasUsedByValidateWalletPrepayment;
|
||||
uint256 gas = mUserOp.verificationGasLimit - gasUsedByValidateAccountPrepayment;
|
||||
try IPaymaster(paymaster).validatePaymasterUserOp{gas : gas}(op, opInfo.userOpHash, requiredPreFund) returns (bytes memory _context, uint256 _deadline){
|
||||
// solhint-disable-next-line not-rely-on-time
|
||||
if (_deadline != 0 && _deadline < block.timestamp) {
|
||||
@@ -362,7 +362,7 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* validate wallet and paymaster (if defined).
|
||||
* validate account and paymaster (if defined).
|
||||
* also make sure total validation doesn't exceed verificationGasLimit
|
||||
* this method is called off-chain (simulateValidation()) and on-chain (from handleOps)
|
||||
* @param opIndex the index of this userOp into the "opInfos" array
|
||||
@@ -382,17 +382,17 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
userOp.maxFeePerGas | userOp.maxPriorityFeePerGas;
|
||||
require(maxGasValues <= type(uint120).max, "gas values overflow");
|
||||
|
||||
uint256 gasUsedByValidateWalletPrepayment;
|
||||
uint256 gasUsedByValidateAccountPrepayment;
|
||||
(uint256 requiredPreFund) = _getRequiredPrefund(mUserOp);
|
||||
(gasUsedByValidateWalletPrepayment, actualAggregator, deadline) = _validateWalletPrepayment(opIndex, userOp, outOpInfo, aggregator, requiredPreFund);
|
||||
//a "marker" where wallet opcode validation is done and paymaster opcode validation is about to start
|
||||
(gasUsedByValidateAccountPrepayment, actualAggregator, deadline) = _validateAccountPrepayment(opIndex, userOp, outOpInfo, aggregator, requiredPreFund);
|
||||
//a "marker" where account opcode validation is done and paymaster opcode validation is about to start
|
||||
// (used only by off-chain simulateValidation)
|
||||
numberMarker();
|
||||
|
||||
bytes memory context;
|
||||
if (mUserOp.paymaster != address(0)) {
|
||||
uint paymasterDeadline;
|
||||
(context, paymasterDeadline) = _validatePaymasterPrepayment(opIndex, userOp, outOpInfo, requiredPreFund, gasUsedByValidateWalletPrepayment);
|
||||
(context, paymasterDeadline) = _validatePaymasterPrepayment(opIndex, userOp, outOpInfo, requiredPreFund, gasUsedByValidateAccountPrepayment);
|
||||
if (paymasterDeadline != 0 && paymasterDeadline < deadline) {
|
||||
deadline = paymasterDeadline;
|
||||
}
|
||||
@@ -416,7 +416,7 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
* process post-operation.
|
||||
* called just after the callData is executed.
|
||||
* if a paymaster is defined and its validation returned a non-empty context, its postOp is called.
|
||||
* the excess amount is refunded to the wallet (or paymaster - if it is was used in the request)
|
||||
* the excess amount is refunded to the account (or paymaster - if it is was used in the request)
|
||||
* @param opIndex index in the batch
|
||||
* @param mode - whether is called from innerHandleOp, or outside (postOpReverted)
|
||||
* @param opInfo userOp fields and info collected during validation
|
||||
@@ -493,7 +493,7 @@ contract EntryPoint is IEntryPoint, StakeManager {
|
||||
|
||||
//place the NUMBER opcode in the code.
|
||||
// this is used as a marker during simulation, as this OP is completely banned from the simulated code of the
|
||||
// wallet and paymaster.
|
||||
// account and paymaster.
|
||||
function numberMarker() internal view {
|
||||
assembly {mstore(0, number())}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ pragma solidity ^0.8.12;
|
||||
contract SenderCreator {
|
||||
|
||||
/**
|
||||
* call the "initCode" factory to create and return the sender wallet address
|
||||
* call the "initCode" factory to create and return the sender account address
|
||||
* @param initCode the initCode value from a UserOp. contains 20 bytes of factory address, followed by calldata
|
||||
* @return sender the returned address of the created wallet, or zero address on failure.
|
||||
* @return sender the returned address of the created account, or zero address on failure.
|
||||
*/
|
||||
function createSender(bytes calldata initCode) external returns (address sender) {
|
||||
address initAddress = address(bytes20(initCode[0 : 20]));
|
||||
|
||||
@@ -7,7 +7,7 @@ import "../interfaces/IStakeManager.sol";
|
||||
/* solhint-disable not-rely-on-time */
|
||||
/**
|
||||
* manage deposits and stakes.
|
||||
* deposit is just a balance used to pay for UserOperations (either by a paymaster or a wallet)
|
||||
* deposit is just a balance used to pay for UserOperations (either by a paymaster or an account)
|
||||
* stake is value locked for at least "unstakeDelay" by a paymaster.
|
||||
*/
|
||||
abstract contract StakeManager is IStakeManager {
|
||||
|
||||
@@ -5,10 +5,10 @@ pragma solidity ^0.8.7;
|
||||
|
||||
import "@gnosis.pm/safe-contracts/contracts/handler/DefaultCallbackHandler.sol";
|
||||
import "@gnosis.pm/safe-contracts/contracts/GnosisSafe.sol";
|
||||
import "../interfaces/IWallet.sol";
|
||||
import "../interfaces/IAccount.sol";
|
||||
import "./EIP4337Manager.sol";
|
||||
|
||||
contract EIP4337Fallback is DefaultCallbackHandler, IWallet {
|
||||
contract EIP4337Fallback is DefaultCallbackHandler, IAccount {
|
||||
address immutable public eip4337manager;
|
||||
constructor(address _eip4337manager) {
|
||||
eip4337manager = _eip4337manager;
|
||||
|
||||
@@ -14,12 +14,12 @@ import "../core/EntryPoint.sol";
|
||||
|
||||
/**
|
||||
* Main EIP4337 module.
|
||||
* Called (through the fallback module) using "delegate" from the GnosisSafe as an "IWallet",
|
||||
* Called (through the fallback module) using "delegate" from the GnosisSafe as an "IAccount",
|
||||
* so must implement validateUserOp
|
||||
* holds an immutable reference to the EntryPoint
|
||||
* Inherits GnosisSafeStorage so that it can reference the memory storage
|
||||
*/
|
||||
contract EIP4337Manager is GnosisSafe, IWallet {
|
||||
contract EIP4337Manager is GnosisSafe, IAccount {
|
||||
|
||||
address public immutable eip4337Fallback;
|
||||
address public immutable entryPoint;
|
||||
@@ -32,26 +32,26 @@ contract EIP4337Manager is GnosisSafe, IWallet {
|
||||
/**
|
||||
* delegate-called (using execFromModule) through the fallback, so "real" msg.sender is attached as last 20 bytes
|
||||
*/
|
||||
function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, address /*aggregator*/, uint256 missingWalletFunds)
|
||||
function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, address /*aggregator*/, uint256 missingAccountFunds)
|
||||
external override returns (uint256 deadline) {
|
||||
address _msgSender = address(bytes20(msg.data[msg.data.length - 20 :]));
|
||||
require(_msgSender == entryPoint, "wallet: not from entrypoint");
|
||||
require(_msgSender == entryPoint, "account: not from entrypoint");
|
||||
|
||||
GnosisSafe pThis = GnosisSafe(payable(address(this)));
|
||||
bytes32 hash = userOpHash.toEthSignedMessageHash();
|
||||
address recovered = hash.recover(userOp.signature);
|
||||
require(threshold == 1, "wallet: only threshold 1");
|
||||
require(pThis.isOwner(recovered), "wallet: wrong signature");
|
||||
require(threshold == 1, "account: only threshold 1");
|
||||
require(pThis.isOwner(recovered), "account: wrong signature");
|
||||
|
||||
if (userOp.initCode.length == 0) {
|
||||
require(nonce++ == userOp.nonce, "wallet: invalid nonce");
|
||||
require(nonce++ == userOp.nonce, "account: invalid nonce");
|
||||
}
|
||||
|
||||
if (missingWalletFunds > 0) {
|
||||
if (missingAccountFunds > 0) {
|
||||
//TODO: MAY pay more than the minimum, to deposit for future transactions
|
||||
(bool success,) = payable(_msgSender).call{value : missingWalletFunds}("");
|
||||
(bool success,) = payable(_msgSender).call{value : missingAccountFunds}("");
|
||||
(success);
|
||||
//ignore failure (its EntryPoint's job to verify, not wallet.)
|
||||
//ignore failure (its EntryPoint's job to verify, not account.)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -131,7 +131,7 @@ contract EIP4337Manager is GnosisSafe, IWallet {
|
||||
try _entryPoint.handleOps(userOps, payable(msg.sender)) {
|
||||
revert("validateEip4337: handleOps must fail");
|
||||
} catch (bytes memory error) {
|
||||
if (keccak256(error) != keccak256(abi.encodeWithSignature("FailedOp(uint256,address,string)", 0, address(0), "wallet: wrong signature"))) {
|
||||
if (keccak256(error) != keccak256(abi.encodeWithSignature("FailedOp(uint256,address,string)", 0, address(0), "account: wrong signature"))) {
|
||||
revert(string(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ pragma solidity ^0.8.12;
|
||||
|
||||
import "./UserOperation.sol";
|
||||
|
||||
interface IWallet {
|
||||
interface IAccount {
|
||||
|
||||
/**
|
||||
* Validate user's signature and nonce
|
||||
@@ -13,8 +13,8 @@ interface IWallet {
|
||||
* Must validate the signature and nonce
|
||||
* @param userOp the operation that is about to be executed.
|
||||
* @param userOpHash hash of the user's request data. can be used as the basis for signature.
|
||||
* @param aggregator the aggregator used to validate the signature. NULL for non-aggregated signature wallets.
|
||||
* @param missingWalletFunds missing funds on the wallet's deposit in the entrypoint.
|
||||
* @param aggregator the aggregator used to validate the signature. NULL for non-aggregated signature accounts.
|
||||
* @param missingAccountFunds missing funds on the account's deposit in the entrypoint.
|
||||
* This is the minimum amount to transfer to the sender(entryPoint) to be able to make the call.
|
||||
* The excess is left as a deposit in the entrypoint, for future calls.
|
||||
* can be withdrawn anytime using "entryPoint.withdrawTo()"
|
||||
@@ -22,6 +22,6 @@ interface IWallet {
|
||||
* @return deadline the last block timestamp this operation is valid, or zero if it is valid indefinitely.
|
||||
* Note that the validation code cannot use block.timestamp (or block.number) directly.
|
||||
*/
|
||||
function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, address aggregator, uint256 missingWalletFunds)
|
||||
function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, address aggregator, uint256 missingAccountFunds)
|
||||
external returns (uint256 deadline);
|
||||
}
|
||||
@@ -2,18 +2,18 @@
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
import "./UserOperation.sol";
|
||||
import "./IWallet.sol";
|
||||
import "./IAccount.sol";
|
||||
import "./IAggregator.sol";
|
||||
|
||||
/**
|
||||
* Aggregated wallet, that support IAggregator.
|
||||
* - the validateUserOp will be called only after the aggregator validated this wallet (with all other wallets of this aggregator).
|
||||
* Aggregated account, that support IAggregator.
|
||||
* - the validateUserOp will be called only after the aggregator validated this account (with all other accounts of this aggregator).
|
||||
* - the validateUserOp MUST valiate the aggregator parameter, and MAY ignore the userOp.signature field.
|
||||
*/
|
||||
interface IAggregatedWallet is IWallet {
|
||||
interface IAggregatedAccount is IAccount {
|
||||
|
||||
/**
|
||||
* return the address of the signature aggregator the wallet supports.
|
||||
* return the address of the signature aggregator the account supports.
|
||||
*/
|
||||
function getAggregator() external view returns (address);
|
||||
}
|
||||
@@ -16,11 +16,11 @@ interface IAggregator {
|
||||
|
||||
/**
|
||||
* validate signature of a single userOp
|
||||
* This method is called by EntryPoint.simulateUserOperation() if the wallet has an aggregator.
|
||||
* This method is called by EntryPoint.simulateUserOperation() if the account has an aggregator.
|
||||
* First it validates the signature over the userOp. then it return data to be used when creating the handleOps:
|
||||
* @param userOp the userOperation received from the user.
|
||||
* @return sigForUserOp the value to put into the signature field of the userOp when calling handleOps.
|
||||
* (usually empty, unless wallet and aggregator support some kind of "multisig"
|
||||
* (usually empty, unless account and aggregator support some kind of "multisig"
|
||||
*/
|
||||
function validateUserOpSignature(UserOperation calldata userOp)
|
||||
external view returns (bytes memory sigForUserOp);
|
||||
|
||||
@@ -44,7 +44,7 @@ interface IEntryPoint is IStakeManager {
|
||||
* this value will be zero (since it failed before accessing the paymaster)
|
||||
* @param reason - revert reason
|
||||
* Should be caught in off-chain handleOps simulation and not happen on-chain.
|
||||
* Useful for mitigating DoS attempts against batchers or for troubleshooting of wallet/paymaster reverts.
|
||||
* Useful for mitigating DoS attempts against batchers or for troubleshooting of account/paymaster reverts.
|
||||
*/
|
||||
error FailedOp(uint256 opIndex, address paymaster, string reason);
|
||||
|
||||
@@ -66,7 +66,7 @@ interface IEntryPoint is IStakeManager {
|
||||
/**
|
||||
* Execute a batch of UserOperation.
|
||||
* no signature aggregator is used.
|
||||
* if any wallet requires an aggregator (that is, it returned an "actualAggregator" when
|
||||
* if any account requires an aggregator (that is, it returned an "actualAggregator" when
|
||||
* performing simulateValidation), then handleAggregatedOps() must be used instead.
|
||||
* @param ops the operations to execute
|
||||
* @param beneficiary the address to receive the fees
|
||||
@@ -75,7 +75,7 @@ interface IEntryPoint is IStakeManager {
|
||||
|
||||
/**
|
||||
* Execute a batch of UserOperation with Aggregators
|
||||
* @param opsPerAggregator the operations to execute, grouped by aggregator (or address(0) for no-aggregator wallets)
|
||||
* @param opsPerAggregator the operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts)
|
||||
* @param beneficiary the address to receive the fees
|
||||
*/
|
||||
function handleAggregatedOps(
|
||||
@@ -90,9 +90,9 @@ interface IEntryPoint is IStakeManager {
|
||||
function getUserOpHash(UserOperation calldata userOp) external view returns (bytes32);
|
||||
|
||||
/**
|
||||
* Simulate a call to wallet.validateUserOp and paymaster.validatePaymasterUserOp.
|
||||
* Simulate a call to account.validateUserOp and paymaster.validatePaymasterUserOp.
|
||||
* @dev this method always revert. Successful result is SimulationResult error. other errors are failures.
|
||||
* @dev The node must also verify it doesn't use banned opcodes, and that it doesn't reference storage outside the wallet's data.
|
||||
* @dev The node must also verify it doesn't use banned opcodes, and that it doesn't reference storage outside the account's data.
|
||||
* @param userOp the user operation to validate.
|
||||
*/
|
||||
function simulateValidation(UserOperation calldata userOp) external;
|
||||
@@ -101,7 +101,7 @@ interface IEntryPoint is IStakeManager {
|
||||
* Successful result from simulateValidation.
|
||||
* @param preOpGas the gas used for validation (including preValidationGas)
|
||||
* @param prefund the required prefund for this operation
|
||||
* @param deadline until what time this userOp is valid (the minimum value of wallet and paymaster's deadline)
|
||||
* @param deadline until what time this userOp is valid (the minimum value of account and paymaster's deadline)
|
||||
* @param paymasterInfo stake information about the paymaster (if any)
|
||||
*/
|
||||
error SimulationResult(uint256 preOpGas, uint256 prefund, uint256 deadline, PaymasterInfo paymasterInfo);
|
||||
@@ -118,19 +118,19 @@ interface IEntryPoint is IStakeManager {
|
||||
|
||||
|
||||
/**
|
||||
* Successful result from simulateValidation, if the wallet returns a signature aggregator
|
||||
* Successful result from simulateValidation, if the account returns a signature aggregator
|
||||
* @param preOpGas the gas used for validation (including preValidationGas)
|
||||
* @param prefund the required prefund for this operation
|
||||
* @param deadline until what time this userOp is valid (the minimum value of wallet and paymaster's deadline)
|
||||
* @param deadline until what time this userOp is valid (the minimum value of account and paymaster's deadline)
|
||||
* @param paymasterInfo stake information about the paymaster (if any)
|
||||
* @param aggregationInfo signature aggregation info (if the wallet requires signature aggregator)
|
||||
* @param aggregationInfo signature aggregation info (if the account requires signature aggregator)
|
||||
* bundler MUST use it to verify the signature, or reject the UserOperation
|
||||
*/
|
||||
error SimulationResultWithAggregation(uint256 preOpGas, uint256 prefund, uint256 deadline, PaymasterInfo paymasterInfo, AggregationInfo aggregationInfo);
|
||||
|
||||
/**
|
||||
* returned aggregated signature info.
|
||||
* the aggregator returned by the wallet, and its current stake.
|
||||
* the aggregator returned by the account, and its current stake.
|
||||
*/
|
||||
struct AggregationInfo {
|
||||
address actualAggregator;
|
||||
|
||||
@@ -3,7 +3,7 @@ pragma solidity ^0.8.12;
|
||||
|
||||
/**
|
||||
* manage deposits and stakes.
|
||||
* deposit is just a balance used to pay for UserOperations (either by a paymaster or a wallet)
|
||||
* deposit is just a balance used to pay for UserOperations (either by a paymaster or an account)
|
||||
* stake is value locked for at least "unstakeDelay" by a paymaster.
|
||||
*/
|
||||
interface IStakeManager {
|
||||
|
||||
@@ -5,16 +5,16 @@ pragma solidity ^0.8.12;
|
||||
/* solhint-disable no-inline-assembly */
|
||||
/* solhint-disable reason-string */
|
||||
|
||||
import "../core/BaseWallet.sol";
|
||||
import "../core/BaseAccount.sol";
|
||||
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
||||
|
||||
/**
|
||||
* minimal wallet.
|
||||
* this is sample minimal wallet.
|
||||
* minimal account.
|
||||
* this is sample minimal account.
|
||||
* has execute, eth handling methods
|
||||
* has a single signer that can send requests through the entryPoint.
|
||||
*/
|
||||
contract SimpleWallet is BaseWallet {
|
||||
contract SimpleAccount is BaseAccount {
|
||||
using ECDSA for bytes32;
|
||||
|
||||
//explicit sizes of nonce, to fit a single storage cell with "owner"
|
||||
@@ -77,7 +77,7 @@ contract SimpleWallet is BaseWallet {
|
||||
|
||||
/**
|
||||
* change entry-point:
|
||||
* a wallet must have a method for replacing the entryPoint, in case the the entryPoint is
|
||||
* an account must have a method for replacing the entryPoint, in case the the entryPoint is
|
||||
* upgraded to a newer version.
|
||||
*/
|
||||
function _updateEntryPoint(address newEntryPoint) internal override {
|
||||
@@ -98,7 +98,7 @@ contract SimpleWallet is BaseWallet {
|
||||
* - pay prefund, in case current deposit is not enough
|
||||
*/
|
||||
function _requireFromEntryPoint() internal override view {
|
||||
require(msg.sender == address(entryPoint()), "wallet: not from EntryPoint");
|
||||
require(msg.sender == address(entryPoint()), "account: not from EntryPoint");
|
||||
}
|
||||
|
||||
// called by entryPoint, only after validateUserOp succeeded.
|
||||
@@ -107,18 +107,18 @@ contract SimpleWallet is BaseWallet {
|
||||
_call(dest, value, func);
|
||||
}
|
||||
|
||||
/// implement template method of BaseWallet
|
||||
/// implement template method of BaseAccount
|
||||
function _validateAndUpdateNonce(UserOperation calldata userOp) internal override {
|
||||
require(_nonce++ == userOp.nonce, "wallet: invalid nonce");
|
||||
require(_nonce++ == userOp.nonce, "account: invalid nonce");
|
||||
}
|
||||
|
||||
/// implement template method of BaseWallet
|
||||
/// implement template method of BaseAccount
|
||||
function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash, address)
|
||||
internal override virtual returns (uint256 deadline) {
|
||||
bytes32 hash = userOpHash.toEthSignedMessageHash();
|
||||
//ignore signature mismatch of from==ZERO_ADDRESS (for eth_callUserOp validation purposes)
|
||||
// solhint-disable-next-line avoid-tx-origin
|
||||
require(owner == hash.recover(userOp.signature) || tx.origin == address(0), "wallet: wrong signature");
|
||||
require(owner == hash.recover(userOp.signature) || tx.origin == address(0), "account: wrong signature");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -132,14 +132,14 @@ contract SimpleWallet is BaseWallet {
|
||||
}
|
||||
|
||||
/**
|
||||
* check current wallet deposit in the entryPoint
|
||||
* check current account deposit in the entryPoint
|
||||
*/
|
||||
function getDeposit() public view returns (uint256) {
|
||||
return entryPoint().balanceOf(address(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* deposit more funds for this wallet in the entryPoint
|
||||
* deposit more funds for this account in the entryPoint
|
||||
*/
|
||||
function addDeposit() public payable {
|
||||
|
||||
@@ -148,7 +148,7 @@ contract SimpleWallet is BaseWallet {
|
||||
}
|
||||
|
||||
/**
|
||||
* withdraw value from the wallet's deposit
|
||||
* withdraw value from the account's deposit
|
||||
* @param withdrawAddress target to send to
|
||||
* @param amount to withdraw
|
||||
*/
|
||||
38
contracts/samples/SimpleAccountDeployer.sol
Normal file
38
contracts/samples/SimpleAccountDeployer.sol
Normal file
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
import "./SimpleAccount.sol";
|
||||
import "@openzeppelin/contracts/utils/Create2.sol";
|
||||
/**
|
||||
* A sampler deployer contract for SimpleAccount
|
||||
* A UserOperations "initCode" holds the address of the deployer, and a method call (to deployAccount, in this sample deployer).
|
||||
* The deployer's deployAccount returns the target account address even if it is already installed.
|
||||
* This way, the entryPoint.getSenderAddress() can be called either before or after the account is created.
|
||||
*/
|
||||
contract SimpleAccountDeployer {
|
||||
|
||||
/**
|
||||
* create an account, and return its address.
|
||||
* returns the address even if the account is already deployed.
|
||||
* Note that during UserOperation execution, this method is called only if the account is not deployed.
|
||||
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
|
||||
*/
|
||||
function deployAccount(IEntryPoint entryPoint, address owner, uint salt) public returns (SimpleAccount ret) {
|
||||
address addr = getAddress(entryPoint, owner, salt);
|
||||
uint codeSize = addr.code.length;
|
||||
if (codeSize > 0) {
|
||||
return SimpleAccount(payable(addr));
|
||||
}
|
||||
ret = new SimpleAccount{salt : bytes32(salt)}(entryPoint, owner);
|
||||
}
|
||||
|
||||
/**
|
||||
* calculate the counterfactual address of this account as it would be returned by deployAccount()
|
||||
*/
|
||||
function getAddress(IEntryPoint entryPoint, address owner, uint salt) public view returns (address) {
|
||||
return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked(
|
||||
type(SimpleAccount).creationCode,
|
||||
abi.encode(entryPoint, owner))
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
import "./SimpleWallet.sol";
|
||||
import "./SimpleAccount.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
|
||||
//in order to be created with tokens, the wallet has to have allowance to the paymaster in advance.
|
||||
//in order to be created with tokens, the account has to have allowance to the paymaster in advance.
|
||||
// the simplest strategy is assign the allowance in the constructor or init function
|
||||
contract SimpleWalletForTokens is SimpleWallet {
|
||||
contract SimpleAccountForTokens is SimpleAccount {
|
||||
|
||||
constructor(IEntryPoint _entryPoint, address _owner, IERC20 token, address paymaster) SimpleWallet(_entryPoint, _owner) {
|
||||
constructor(IEntryPoint _entryPoint, address _owner, IERC20 token, address paymaster) SimpleAccount(_entryPoint, _owner) {
|
||||
token.approve(paymaster, type(uint256).max);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
import "./SimpleWallet.sol";
|
||||
import "@openzeppelin/contracts/utils/Create2.sol";
|
||||
/**
|
||||
* A sampler deployer contract for SimpleWallet
|
||||
* A UserOperations "initCode" holds the address of the deployer, and a method call (to deployWallet, in this sample deployer).
|
||||
* The deployer's deployWallet returns the target wallet address even if it is already installed.
|
||||
* This way, the entryPoint.getSenderAddress() can be called either before or after the wallet is created.
|
||||
*/
|
||||
contract SimpleWalletDeployer {
|
||||
|
||||
/**
|
||||
* create a wallet, and return its address.
|
||||
* returns the address even if the wallet is already deployed.
|
||||
* Note that during UserOperation execution, this method is called only if the wallet is not deployed.
|
||||
* This method returns an existing wallet address so that entryPoint.getSenderAddress() would work even after wallet creation
|
||||
*/
|
||||
function deployWallet(IEntryPoint entryPoint, address owner, uint salt) public returns (SimpleWallet ret) {
|
||||
address addr = getAddress(entryPoint, owner, salt);
|
||||
uint codeSize = addr.code.length;
|
||||
if (codeSize > 0) {
|
||||
return SimpleWallet(payable(addr));
|
||||
}
|
||||
ret = new SimpleWallet{salt : bytes32(salt)}(entryPoint, owner);
|
||||
}
|
||||
|
||||
/**
|
||||
* calculate the counterfactual address of this wallet as it would be returned by deployWallet()
|
||||
*/
|
||||
function getAddress(IEntryPoint entryPoint, address owner, uint salt) public view returns (address) {
|
||||
return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked(
|
||||
type(SimpleWallet).creationCode,
|
||||
abi.encode(entryPoint, owner))
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
import "../interfaces/IAggregatedWallet.sol";
|
||||
import "../core/BaseWallet.sol";
|
||||
import "./SimpleWallet.sol";
|
||||
import "../interfaces/IAggregatedAccount.sol";
|
||||
import "../core/BaseAccount.sol";
|
||||
import "./SimpleAccount.sol";
|
||||
import "../interfaces/UserOperation.sol";
|
||||
|
||||
/**
|
||||
* test aggregated-signature wallet.
|
||||
* test aggregated-signature account.
|
||||
* works only with TestAggregatedSignature, which doesn't really check signature, but nonce sum
|
||||
* a true aggregated wallet should expose data (e.g. its public key) to the aggregator.
|
||||
* a true aggregated account should expose data (e.g. its public key) to the aggregator.
|
||||
*/
|
||||
contract TestAggregatedWallet is SimpleWallet, IAggregatedWallet {
|
||||
contract TestAggregatedAccount is SimpleAccount, IAggregatedAccount {
|
||||
address public immutable aggregator;
|
||||
|
||||
constructor(IEntryPoint anEntryPoint, address anAggregator)
|
||||
SimpleWallet(anEntryPoint, address(0)) {
|
||||
SimpleAccount(anEntryPoint, address(0)) {
|
||||
aggregator = anAggregator;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ pragma solidity ^0.8.12;
|
||||
|
||||
import "../interfaces/IAggregator.sol";
|
||||
import "hardhat/console.sol";
|
||||
import "./SimpleWallet.sol";
|
||||
import "./SimpleAccount.sol";
|
||||
import "../core/EntryPoint.sol";
|
||||
|
||||
/**
|
||||
@@ -19,7 +19,7 @@ contract TestSignatureAggregator is IAggregator {
|
||||
for (uint i = 0; i < userOps.length; i++) {
|
||||
uint nonce = userOps[i].nonce;
|
||||
sum += nonce;
|
||||
// console.log('%s validate sender=%s nonce %s', i, address(senderWallet), nonce);
|
||||
// console.log('%s validate sender=%s nonce %s', i, address(senderAccount), nonce);
|
||||
}
|
||||
require(signature.length == 32, "TestSignatureValidator: sig must be uint");
|
||||
(uint sig) = abi.decode(signature, (uint));
|
||||
|
||||
@@ -4,7 +4,7 @@ pragma solidity ^0.8.12;
|
||||
/* solhint-disable reason-string */
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "./SimpleWallet.sol";
|
||||
import "./SimpleAccount.sol";
|
||||
import "../core/BasePaymaster.sol";
|
||||
|
||||
/**
|
||||
@@ -12,12 +12,12 @@ import "../core/BasePaymaster.sol";
|
||||
* The paymaster IS the token to use, since a paymaster cannot use an external contract.
|
||||
* Also, the exchange rate has to be fixed, since it can't reference an external Uniswap or other exchange contract.
|
||||
* subclass should override "getTokenValueOfEth to provide actual token exchange rate, settable by the owner.
|
||||
* Known Limitation: this paymaster is exploitable when put into a batch with multiple ops (of different wallets):
|
||||
* Known Limitation: this paymaster is exploitable when put into a batch with multiple ops (of different accounts):
|
||||
* - while a single op can't exploit the paymaster (if postOp fails to withdraw the tokens, the user's op is reverted,
|
||||
* and then we know we can withdraw the tokens), multiple ops with different senders (all using this paymaster)
|
||||
* in a batch can withdraw funds from 2nd and further ops, forcing the paymaster itself to pay (from its deposit)
|
||||
* - Possible workarounds are either use a more complex paymaster scheme (e.g. the DepositPaymaster) or
|
||||
* to whitelist the wallet and the called method ids.
|
||||
* to whitelist the account and the called method ids.
|
||||
*/
|
||||
contract TokenPaymaster is BasePaymaster, ERC20 {
|
||||
|
||||
@@ -26,8 +26,8 @@ contract TokenPaymaster is BasePaymaster, ERC20 {
|
||||
|
||||
address public theDeployer;
|
||||
|
||||
constructor(address walletDeployer, string memory _symbol, IEntryPoint _entryPoint) ERC20(_symbol, _symbol) BasePaymaster(_entryPoint) {
|
||||
theDeployer = walletDeployer;
|
||||
constructor(address accountDeployer, string memory _symbol, IEntryPoint _entryPoint) ERC20(_symbol, _symbol) BasePaymaster(_entryPoint) {
|
||||
theDeployer = accountDeployer;
|
||||
//make it non-empty
|
||||
_mint(address(this), 1);
|
||||
|
||||
@@ -62,7 +62,7 @@ contract TokenPaymaster is BasePaymaster, ERC20 {
|
||||
|
||||
/**
|
||||
* validate the request:
|
||||
* if this is a constructor call, make sure it is a known wallet (that is, a contract that
|
||||
* if this is a constructor call, make sure it is a known account (that is, a contract that
|
||||
* we trust that in its constructor will set
|
||||
* verify the sender has enough tokens.
|
||||
* (since the paymaster is also the token, there is no notion of "approval")
|
||||
@@ -86,18 +86,18 @@ contract TokenPaymaster is BasePaymaster, ERC20 {
|
||||
return (abi.encode(userOp.sender), 0);
|
||||
}
|
||||
|
||||
// when constructing a wallet, validate constructor code and parameters
|
||||
// when constructing an account, validate constructor code and parameters
|
||||
// this code highly dependent on the deployer we use.
|
||||
// our deployer has a method deploy(bytes,salt)
|
||||
function _validateConstructor(UserOperation calldata userOp) internal virtual view {
|
||||
//we trust a specific deployer contract
|
||||
address deployer = address(bytes20(userOp.initCode[0 : 20]));
|
||||
require(deployer == theDeployer, "TokenPaymaster: wrong wallet deployer");
|
||||
require(deployer == theDeployer, "TokenPaymaster: wrong account deployer");
|
||||
}
|
||||
|
||||
/**
|
||||
* actual charge of user.
|
||||
* this method will be called just after the user's TX with mode==OpSucceeded|OpReverted (wallet pays in both cases)
|
||||
* this method will be called just after the user's TX with mode==OpSucceeded|OpReverted (account pays in both cases)
|
||||
* BUT: if the user changed its balance in a way that will cause postOp to revert, then it gets called again, after reverting
|
||||
* the user's TX , back to the state it was before the transaction started (before the validatePaymasterUserOp),
|
||||
* and the transaction should succeed there.
|
||||
|
||||
@@ -13,7 +13,7 @@ import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
||||
* whatever off-chain verification before signing the UserOp.
|
||||
* Note that this signature is NOT a replacement for wallet signature:
|
||||
* - the paymaster signs to agree to PAY for GAS.
|
||||
* - the wallet signs to prove identity and wallet ownership.
|
||||
* - the wallet signs to prove identity and account ownership.
|
||||
*/
|
||||
contract VerifyingPaymaster is BasePaymaster {
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
//sample "receiver" contract, for testing "exec" from wallet.
|
||||
//sample "receiver" contract, for testing "exec" from account.
|
||||
contract TestCounter {
|
||||
mapping(address => uint256) public counters;
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
pragma solidity ^0.8.12;
|
||||
|
||||
import "../samples/SimpleWallet.sol";
|
||||
import "../samples/SimpleAccount.sol";
|
||||
|
||||
/**
|
||||
* A test wallet, for testing expiry.
|
||||
* A test account, for testing expiry.
|
||||
* add "temporary" owners, each with a deadline time for each.
|
||||
* NOTE: this is not a full "session key" implementation: a real session key should probably limit
|
||||
* other things, like target contracts and methods to be called.
|
||||
*/
|
||||
contract TestExpiryWallet is SimpleWallet {
|
||||
contract TestExpiryAccount is SimpleAccount {
|
||||
using ECDSA for bytes32;
|
||||
|
||||
mapping(address => uint256) public ownerDeadlines;
|
||||
|
||||
constructor(IEntryPoint anEntryPoint, address anOwner) SimpleWallet(anEntryPoint, anOwner) {
|
||||
constructor(IEntryPoint anEntryPoint, address anOwner) SimpleAccount(anEntryPoint, anOwner) {
|
||||
addTemporaryOwner(anOwner, type(uint256).max);
|
||||
}
|
||||
|
||||
@@ -22,13 +22,13 @@ contract TestExpiryWallet is SimpleWallet {
|
||||
ownerDeadlines[owner] = deadline;
|
||||
}
|
||||
|
||||
/// implement template method of BaseWallet
|
||||
/// implement template method of BaseAccount
|
||||
function _validateSignature(UserOperation calldata userOp, bytes32 userOpHash, address)
|
||||
internal override view returns (uint256 deadline) {
|
||||
bytes32 hash = userOpHash.toEthSignedMessageHash();
|
||||
address signer = hash.recover(userOp.signature);
|
||||
deadline = ownerDeadlines[signer];
|
||||
require(deadline != 0, "wallet: wrong signature");
|
||||
require(deadline != 0, "account: wrong signature");
|
||||
//not testing deadline (since we can't). just return it.
|
||||
}
|
||||
}
|
||||
@@ -41,17 +41,17 @@ This proposal takes a different approach, avoiding any adjustments to the consen
|
||||
* **Sender** - the account contract sending a user operation.
|
||||
* **EntryPoint** - a singleton contract to execute bundles of UserOperations. Bundlers/Clients whitelist the supported entrypoint.
|
||||
* **Bundler** - a node (block builder) that bundles multiple UserOperations and create an EntryPoint.handleOps() transaction.
|
||||
* **Aggregator** - a helper contract trusted by wallets to validate an aggregated signature. Bundlers/Clients whitelist the supported aggregators.
|
||||
* **Aggregator** - a helper contract trusted by accounts to validate an aggregated signature. Bundlers/Clients whitelist the supported aggregators.
|
||||
|
||||
## Specification
|
||||
|
||||
To avoid Ethereum consensus changes, we do not attempt to create new transaction types for account-abstracted transactions. Instead, users package up the action they want their wallet to take in an ABI-encoded struct called a `UserOperation`:
|
||||
To avoid Ethereum consensus changes, we do not attempt to create new transaction types for account-abstracted transactions. Instead, users package up the action they want their account to take in an ABI-encoded struct called a `UserOperation`:
|
||||
|
||||
| Field | Type | Description
|
||||
| - | - | - |
|
||||
| `sender` | `address` | The wallet making the operation |
|
||||
| `nonce` | `uint256` | Anti-replay parameter; also used as the salt for first-time wallet creation |
|
||||
| `initCode` | `bytes` | The initCode of the wallet (needed if and only if the wallet is not yet on-chain and needs to be created) |
|
||||
| `sender` | `address` | The account making the operation |
|
||||
| `nonce` | `uint256` | Anti-replay parameter; also used as the salt for first-time account creation |
|
||||
| `initCode` | `bytes` | The initCode of the account (needed if and only if the account is not yet on-chain and needs to be created) |
|
||||
| `callData` | `bytes` | The data to pass to the `sender` during the main execution call |
|
||||
| `callGasLimit` | `uint256` | The amount of gas to allocate the main execution call |
|
||||
| `verificationGasLimit` | `uint256` | The amount of gas to allocate for the verification step |
|
||||
@@ -59,7 +59,7 @@ To avoid Ethereum consensus changes, we do not attempt to create new transaction
|
||||
| `maxFeePerGas` | `uint256` | Maximum fee per gas (similar to EIP 1559 `max_fee_per_gas`) |
|
||||
| `maxPriorityFeePerGas` | `uint256` | Maximum priority fee per gas (similar to EIP 1559 `max_priority_fee_per_gas`) |
|
||||
| `paymasterAndData` | `bytes` | Address of paymaster sponsoring the transaction, followed by extra data to send to the paymaster (empty for self-sponsored transaction) |
|
||||
| `signature` | `bytes` | Data passed into the wallet along with the nonce during the verification step |
|
||||
| `signature` | `bytes` | Data passed into the account along with the nonce during the verification step |
|
||||
|
||||
Users send `UserOperation` objects to a dedicated user operation mempool. A specialized class of actors called **bundlers** (either miners running special-purpose code, or users that can relay transactions to miners eg. through a bundle marketplace such as Flashbots that can guarantee next-block-or-never inclusion) listen in on the user operation mempool, and create **bundle transactions**. A bundle transaction packages up multiple `UserOperation` objects into a single `handleOps` call to a pre-published global **entry point contract**.
|
||||
|
||||
@@ -99,40 +99,35 @@ struct AggregationInfo {
|
||||
}
|
||||
```
|
||||
|
||||
The core interface required for a wallet to have is:
|
||||
The core interface required for an account to have is:
|
||||
|
||||
```solidity
|
||||
interface IWallet {
|
||||
interface IAccount {
|
||||
function validateUserOp
|
||||
(UserOperation calldata userOp, bytes32 userOpHash, address aggregator, uint256 missingWalletFunds)
|
||||
(UserOperation calldata userOp, bytes32 userOpHash, address aggregator, uint256 missingAccountFunds)
|
||||
external returns (uint256 deadline);
|
||||
}
|
||||
```
|
||||
The wallet
|
||||
The account
|
||||
* MUST validate the caller is a trusted EntryPoint
|
||||
* The userOpHash is a hash over the userOp (except signature), entryPoint and chainId
|
||||
* If the wallet does not support signature aggregation, it MUST validate the signature is a valid signature of the `userOpHash`
|
||||
In order to support eth_callUserOperation for unsigned UserOperation, the wallet SHOULD NOT revert on failed signature check if `tx.origin` is the zero address.
|
||||
e.g (assuming an ECDSA signature):
|
||||
```javascript
|
||||
require(owner == hash.recover(userOp.signature) || tx.origin == address(0), "wallet: wrong signature");
|
||||
```
|
||||
* MUST pay the entryPoint (caller) at least the "missingWalletFunds" (which might be zero, in case current wallet's deposit is high enough)
|
||||
* The wallet MAY pay more than this minimum, to cover future transactions (it can always issue `withdrawTo` to retrieve it)
|
||||
* The `aggregator` SHOULD be ignored for wallets that don't use an aggregator
|
||||
* If the account does not support signature aggregation, it MUST validate the signature is a valid signature of the `requestId`
|
||||
* MUST pay the entryPoint (caller) at least the "missingAccountFunds" (which might be zero, in case current account's deposit is high enough)
|
||||
* The account MAY pay more than this minimum, to cover future transactions (it can always issue `withdrawTo` to retrieve it)
|
||||
* The `aggregator` SHOULD be ignored for accounts that don't use an aggregator
|
||||
* The return value `deadline` is either zero (meaning "indefinitely"), or the last timestamp this request is deemed valid.
|
||||
|
||||
A Wallet that works with aggregated signature should have the interface:
|
||||
An account that works with aggregated signature should have the interface:
|
||||
```solidity
|
||||
interface IAggregatedWallet is IWallet {
|
||||
interface IAggregatedAccount is IAccount {
|
||||
|
||||
function getAggregator() view returns (address);
|
||||
}
|
||||
```
|
||||
* **getAggregator()** returns the aggregator this wallet supports.
|
||||
* **validateUserOp()** (inherited from IWallet interface) MUST verify the `aggregator` parameter is valid and the same as `getAggregator`
|
||||
* The wallet should also support aggregator-specific getter (e.g. `getAggregationInfo()`).
|
||||
This method should export the wallet's public-key to the aggregator, and possibly more info
|
||||
* **getAggregator()** returns the aggregator this account supports.
|
||||
* **validateUserOp()** (inherited from IAccount interface) MUST verify the `aggregator` parameter is valid and the same as `getAggregator`
|
||||
* The account should also support aggregator-specific getter (e.g. `getAggregationInfo()`).
|
||||
This method should export the account's public-key to the aggregator, and possibly more info
|
||||
(note that it is not called directly by the entryPoint)
|
||||
* validateUserOp MAY ignore the signature field
|
||||
|
||||
@@ -149,7 +144,7 @@ interface IAggregator {
|
||||
}
|
||||
```
|
||||
|
||||
* If a wallet uses an aggregator (returns it with getAggregator()), then its address is returned by `simulateValidation()` reverting with `SimulationResultWithAggregator` instead of `SimulationResult`
|
||||
* If an account uses an aggregator (returns it with getAggregator()), then its address is returned by `simulateValidation()` reverting with `SimulationResultWithAggregator` instead of `SimulationResult`
|
||||
* To accept the UserOp, the bundler must call **validateUserOpSignature()** to validate the userOp's signature.
|
||||
* **aggregateSignatures()** must aggregate all UserOp signature into a single value.
|
||||
* Note that the above methods are helper method for the bundler. The bundler MAY use a native library to perform the same validation and aggregation logic.
|
||||
@@ -158,7 +153,7 @@ interface IAggregator {
|
||||
|
||||
#### Using signature aggregators
|
||||
|
||||
A wallet signify it uses signature aggregation by exposing the aggregator's address in the `getAggregator()` method.
|
||||
An account signify it uses signature aggregation by exposing the aggregator's address in the `getAggregator()` method.
|
||||
During `simulateValidation`, this aggregator is returned (in the `SimulationResultWithAggregator`)
|
||||
|
||||
The bundler should first accept the validator (validate its stake info and that it is not throttled/banned)
|
||||
@@ -170,18 +165,18 @@ resources (or revert) when the above methods are called in view mode, or if the
|
||||
### Required entry point contract functionality
|
||||
|
||||
There are 2 separate entry point methods: `handleOps` and `handleAggregatedOps`
|
||||
* `handleOps` handle userOps of wallets that don't require any signature aggregator.
|
||||
* `handleOps` handle userOps of accounts that don't require any signature aggregator.
|
||||
* `handleAggregatedOps` can handle a batch that contains userOps of multiple aggregators (and also requests without any aggregator)
|
||||
* `handleAggregatedOps` performs the same logic below as `handleOps`, but it must transfer the correct aggregator to each userOp, and also must call `validateSignatures` on each aggregator after doing all the per-wallet validation.
|
||||
* `handleAggregatedOps` performs the same logic below as `handleOps`, but it must transfer the correct aggregator to each userOp, and also must call `validateSignatures` on each aggregator after doing all the per-account validation.
|
||||
The entry point's `handleOps` function must perform the following steps (we first describe the simpler non-paymaster case). It must make two loops, the **verification loop** and the **execution loop**. In the verification loop, the `handleOps` call must perform the following steps for each `UserOperation`:
|
||||
|
||||
* **Create the wallet if it does not yet exist**, using the initcode provided in the `UserOperation`. If the wallet does not exist, _and_ the initcode is empty, or does not deploy a contract at the "sender" address, the call must fail.
|
||||
* **Call `validateUserOp` on the wallet**, passing in the `UserOperation`, the required fee and aggregator (if there is one). The wallet should verify the operation's signature, and pay the fee if the wallet considers the operation valid. If any `validateUserOp` call fails, `handleOps` must skip execution of at least that operation, and may revert entirely.
|
||||
* Validate the wallet's deposit in the entryPoint is high enough to cover the max possible cost (cover the already-done verification and max execution gas)
|
||||
* **Create the account if it does not yet exist**, using the initcode provided in the `UserOperation`. If the account does not exist, _and_ the initcode is empty, or does not deploy a contract at the "sender" address, the call must fail.
|
||||
* **Call `validateUserOp` on the account**, passing in the `UserOperation`, the required fee and aggregator (if there is one). The account should verify the operation's signature, and pay the fee if the account considers the operation valid. If any `validateUserOp` call fails, `handleOps` must skip execution of at least that operation, and may revert entirely.
|
||||
* Validate the account's deposit in the entryPoint is high enough to cover the max possible cost (cover the already-done verification and max execution gas)
|
||||
|
||||
In the execution loop, the `handleOps` call must perform the following steps for each `UserOperation`:
|
||||
|
||||
* **Call the wallet with the `UserOperation`'s calldata**. It's up to the wallet to choose how to parse the calldata; an expected workflow is for the wallet to have an `execute` function that parses the remaining calldata as a series of one or more calls that the wallet should make.
|
||||
* **Call the account with the `UserOperation`'s calldata**. It's up to the account to choose how to parse the calldata; an expected workflow is for the account to have an `execute` function that parses the remaining calldata as a series of one or more calls that the account should make.
|
||||
|
||||

|
||||
|
||||
@@ -196,7 +191,7 @@ We extend the entry point logic to support **paymasters** that can sponsor trans
|
||||
|
||||
First, a paymaster must issue `addStake()` to lock some eth for a period of time. Note that the amount staked (and unstake delay time) are not defined on-chain, and MUST be
|
||||
validated by nodes during the UserOperation validation phase. The staked value should be above PAYMASTER_STAKE_VALUE and delay above PAYMASTER_MIN_UNSTAKE_DELAY seconds.
|
||||
During the verification loop, in addition to calling `validateUserOp`, the `handleOps` execution also must check that the paymaster is staked, and also has enough ETH deposited with the entry point to pay for the operation, and then call `validatePaymasterUserOp` on the paymaster to verify that the paymaster is willing to pay for the operation. Note that in this case, the `validateUserOp` is called with a `missingWalletFunds` of 0 to reflect that the wallet's deposit is not used for payment for this userOp.
|
||||
During the verification loop, in addition to calling `validateUserOp`, the `handleOps` execution also must check that the paymaster is staked, and also has enough ETH deposited with the entry point to pay for the operation, and then call `validatePaymasterUserOp` on the paymaster to verify that the paymaster is willing to pay for the operation. Note that in this case, the `validateUserOp` is called with a `missingAccountFunds` of 0 to reflect that the account's deposit is not used for payment for this userOp.
|
||||
|
||||
During the execution loop, the `handleOps` execution must call `postOp` on the paymaster after making the main execution call. It must guarantee the execution of `postOp`, by making the main execution inside an inner call context, and if the inner call context reverts attempting to call `postOp` again in an outer call context.
|
||||
|
||||
@@ -232,7 +227,7 @@ function unlockStake() external
|
||||
function withdrawStake(address payable withdrawAddress) external
|
||||
```
|
||||
|
||||
The paymaster must also have a deposit, which the entry point will charge UserOperation costs from. The entry point must implement the following interface to allow paymasters (and optionally wallets) manage their deposit:
|
||||
The paymaster must also have a deposit, which the entry point will charge UserOperation costs from. The entry point must implement the following interface to allow paymasters (and optionally accounts) manage their deposit:
|
||||
|
||||
```c++
|
||||
// return the deposit of an account
|
||||
@@ -276,7 +271,7 @@ If the call reverts with other error, the client rejects this `userOp`.
|
||||
|
||||
The simulated call performs the full validation, by
|
||||
calling:
|
||||
1. `wallet.validateUserOp`.
|
||||
1. `account.validateUserOp`.
|
||||
2. if specified a paymaster: `paymaster.validatePaymasterUserOp`.
|
||||
|
||||
Either `validateUserOp` or `validatePaymasterUserOp` may return a "deadline", which is the latest timestamp that this UserOperation is valid on-chain.
|
||||
@@ -289,12 +284,12 @@ While simulating `userOp` validation, the client should make sure that:
|
||||
|
||||
1. Neither call's execution trace invokes any **forbidden opcodes**
|
||||
2. Must not use GAS opcode (unless followed immediately by one of { `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` }.)
|
||||
3. The first (validateUserOp) call is allowed to access storage slot of the wallet itself (see Storage access by Slots, below),
|
||||
but only on contracts that are not part of the current bundle (that is, not in a paymaster or any other wallet)
|
||||
Note that the wallet is allowed (and expected) to deposit to its own balance in the EntryPoint. That is allowed by this rule.
|
||||
4. The second (validatePaymasterUserOp), the paymaster may access storage slots of itself AND of the wallet, above.
|
||||
3. The first (validateUserOp) call is allowed to access storage slot of the account itself (see Storage access by Slots, below),
|
||||
but only on contracts that are not part of the current bundle (that is, not in a paymaster or any other account)
|
||||
Note that the account is allowed (and expected) to deposit to its own balance in the EntryPoint. That is allowed by this rule.
|
||||
4. The second (validatePaymasterUserOp), the paymaster may access storage slots of itself AND of the account, above.
|
||||
5. Limitation on "CALL" opcodes (`CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL`):
|
||||
1. must not use value (except from wallet to the entrypoint)
|
||||
1. must not use value (except from account to the entrypoint)
|
||||
2. must not revert with out-of-gas
|
||||
3. destination address must have code (EXTCODESIZE>0)
|
||||
6. `EXTCODEHASH` of every address accessed (by any opcode) does not change between first and second simulations of the op.
|
||||
@@ -333,7 +328,7 @@ After creating the batch, before including the transaction in a block, the clien
|
||||
- Run `eth_estimateGas` with maximum possible gas, to verify the entire `handleOps` batch transaction, and use the estimated gas for the actual transaction execution.
|
||||
- If the call reverted, check the `FailedOp` event. A `FailedOp` during `handleOps` simulation is an unexpected event since it was supposed to be caught by the single-UserOperation simulation. Remove the failed op that caused the revert from the batch and drop from the mempool. Other ops from the same paymaster should be removed from the current batch, but kept in the mempool. Repeat until `eth_estimateGas` succeeds.
|
||||
|
||||
In practice, restrictions (2) and (3) basically mean that the only external accesses that the wallet and the paymaster can make are reading code of other contracts if their code is guaranteed to be immutable (eg. this is useful for calling or delegatecalling to libraries).
|
||||
In practice, restrictions (2) and (3) basically mean that the only external accesses that the account and the paymaster can make are reading code of other contracts if their code is guaranteed to be immutable (eg. this is useful for calling or delegatecalling to libraries).
|
||||
|
||||
If any of the three conditions is violated, the client should reject the `op`. If both calls succeed (or, if `op.paymaster == ZERO_ADDRESS` and the first call succeeds)without violating the three conditions, the client should accept the op. On a bundler node, the storage keys accessed by both calls must be saved as the `accessList` of the `UserOperation`
|
||||
|
||||
@@ -341,7 +336,7 @@ When a bundler includes a bundle in a block it must ensure that earlier transact
|
||||
|
||||
#### Forbidden opcodes
|
||||
|
||||
The forbidden opcodes are to be forbidden when `depth > 2` (i.e. when it is the wallet, paymaster, or other contracts called by them that are being executed). They are: `GASPRICE`, `GASLIMIT`, `DIFFICULTY`, `TIMESTAMP`, `BASEFEE`, `BLOCKHASH`, `NUMBER`, `SELFBALANCE`, `BALANCE`, `ORIGIN`, `GAS`, `CREATE`, `COINBASE`. They should only be forbidden during verification, not execution. These opcodes are forbidden because their outputs may differ between simulation and execution, so simulation of calls using these opcodes does not reliably tell what would happen if these calls are later done on-chain.
|
||||
The forbidden opcodes are to be forbidden when `depth > 2` (i.e. when it is the account, paymaster, or other contracts called by them that are being executed). They are: `GASPRICE`, `GASLIMIT`, `DIFFICULTY`, `TIMESTAMP`, `BASEFEE`, `BLOCKHASH`, `NUMBER`, `SELFBALANCE`, `BALANCE`, `ORIGIN`, `GAS`, `CREATE`, `COINBASE`. They should only be forbidden during verification, not execution. These opcodes are forbidden because their outputs may differ between simulation and execution, so simulation of calls using these opcodes does not reliably tell what would happen if these calls are later done on-chain.
|
||||
|
||||
Exceptions to the forbidden opcodes:
|
||||
1. A single `CREATE2` is allowed if `op.initcode.length != 0` and must result in the deployment of a previously-undeployed `UserOperation.sender`.
|
||||
@@ -474,9 +469,9 @@ eth_supportedEntryPoints returns an array of the entryPoint addresses supported
|
||||
|
||||
The main challenge with a purely smart contract wallet based account abstraction system is DoS safety: how can a miner including an operation make sure that it will actually pay fees, without having to first execute the entire operation? Requiring the miner to execute the entire operation opens a DoS attack vector, as an attacker could easily send many operations that pretend to pay a fee but then revert at the last moment after a long execution. Similarly, to prevent attackers from cheaply clogging the mempool, nodes in the P2P network need to check if an operation will pay a fee before they are willing to forward it.
|
||||
|
||||
In this proposal, we expect wallets to have a `validateUserOp` method that takes as input a `UserOperation`, and verify the signature and pay the fee. This method is required to be almost-pure: it is only allowed to access the storage of the wallet itself, cannot use environment opcodes (eg. `TIMESTAMP`), and can only edit the storage of the wallet, and can also send out ETH (needed to pay the entry point). The method is gas-limited by the `verificationGasLimit` of the `UserOperation`; nodes can choose to reject operations whose `verificationGasLimit` is too high. These restrictions allow miners and network nodes to simulate the verification step locally, and be confident that the result will match the result when the operation actually gets included into a block.
|
||||
In this proposal, we expect accounts to have a `validateUserOp` method that takes as input a `UserOperation`, and verify the signature and pay the fee. This method is required to be almost-pure: it is only allowed to access the storage of the account itself, cannot use environment opcodes (eg. `TIMESTAMP`), and can only edit the storage of the account, and can also send out ETH (needed to pay the entry point). The method is gas-limited by the `verificationGasLimit` of the `UserOperation`; nodes can choose to reject operations whose `verificationGasLimit` is too high. These restrictions allow miners and network nodes to simulate the verification step locally, and be confident that the result will match the result when the operation actually gets included into a block.
|
||||
|
||||
The entry point-based approach allows for a clean separation between verification and execution, and keeps wallets' logic simple. The alternative would be to require wallets to follow a template where they first self-call to verify and then self-call to execute (so that the execution is sandboxed and cannot cause the fee payment to revert); template-based approaches were rejected due to being harder to implement, as existing code compilation and verification tooling is not designed around template verification.
|
||||
The entry point-based approach allows for a clean separation between verification and execution, and keeps accounts' logic simple. The alternative would be to require accounts to follow a template where they first self-call to verify and then self-call to execute (so that the execution is sandboxed and cannot cause the fee payment to revert); template-based approaches were rejected due to being harder to implement, as existing code compilation and verification tooling is not designed around template verification.
|
||||
|
||||
### Paymasters
|
||||
|
||||
@@ -487,32 +482,32 @@ Paymasters facilitate transaction sponsorship, allowing third-party-designed mec
|
||||
|
||||
The paymaster scheme allows a contract to passively pay on users' behalf under arbitrary conditions. It even allows ERC-20 token paymasters to secure a guarantee that they would only need to pay if the user pays them: the paymaster contract can check that there is sufficient approved ERC-20 balance in the `validatePaymasterUserOp` method, and then extract it with `transferFrom` in the `postOp` call; if the op itself transfers out or de-approves too much of the ERC-20s, the inner `postOp` will fail and revert the execution and the outer `postOp` can extract payment (note that because of storage access restrictions the ERC-20 would need to be a wrapper defined within the paymaster itself).
|
||||
|
||||
### First-time wallet creation
|
||||
### First-time account creation
|
||||
|
||||
It is an important design goal of this proposal to replicate the key property of EOAs that users do not need to perform some custom action or rely on an existing user to create their wallet; they can simply generate an address locally and immediately start accepting funds.
|
||||
It is an important design goal of this proposal to replicate the key property of EOAs that users do not need to perform some custom action or rely on an existing user to create their account; they can simply generate an address locally and immediately start accepting funds.
|
||||
|
||||
The wallet creation itself is done by a "factory" contract, with wallet-specific data.
|
||||
The factory is expected to use CREATE2 (not CREATE) to create the wallet, so that the order of creation of wallets doesn't interfere with the generated addresses.
|
||||
The account creation itself is done by a "factory" contract, with account-specific data.
|
||||
The factory is expected to use CREATE2 (not CREATE) to create the account, so that the order of creation of accounts doesn't interfere with the generated addresses.
|
||||
The `initCode` field (if non-zero length) is parsed as a 20-byte address, followed by "calldata" to pass to this address.
|
||||
This method call is expected to create a wallet and return its address.
|
||||
If the factory does use CREATE2 or some other deterministic method to create the wallet, it's expected to return the wallet address even if the wallet has already been created. This is to make it easier for clients to query the address without knowing if the wallet has already been deployed, by simulating a call to `entryPoint.getSenderAddress()`, which calls the factory under the hood.
|
||||
This method call is expected to create an account and return its address.
|
||||
If the factory does use CREATE2 or some other deterministic method to create the account, it's expected to return the account address even if the account has already been created. This is to make it easier for clients to query the address without knowing if the account has already been deployed, by simulating a call to `entryPoint.getSenderAddress()`, which calls the factory under the hood.
|
||||
When `initCode` is specified, if either the `sender` address points to an existing contract, or (after calling the initCode) the `sender` address still does not exist,
|
||||
then the operation is aborted.
|
||||
The `initCode` MUST NOT be called directly from the entryPoint, but from another address.
|
||||
The contract created by this factory method should accept a call to `validateUserOp` to validate the UserOp's signature.
|
||||
For security reasons, it is important that the generated contract address will depend on the initial signature.
|
||||
This way, even if someone can create a wallet at that address, he can't set different credentials to control it.
|
||||
This way, even if someone can create an account at that address, he can't set different credentials to control it.
|
||||
|
||||
NOTE: In order for the wallet to determine the "counterfactual" address of the wallet (prior its creation),
|
||||
NOTE: In order for the wallet to determine the "counterfactual" address of the account (prior its creation),
|
||||
it should make a static call to the `entryPoint.getSenderAddress()`
|
||||
|
||||
### Entry point upgrading
|
||||
|
||||
Wallets are encouraged to be DELEGATECALL forwarding contracts for gas efficiency and to allow wallet upgradability. The wallet code is expected to hard-code the entry point into their code for gas efficiency. If a new entry point is introduced, whether to add new functionality, improve gas efficiency, or fix a critical security bug, users can self-call to replace their wallet's code address with a new code address containing code that points to a new entry point. During an upgrade process, it's expected that two mempools will run in parallel.
|
||||
Accounts are encouraged to be DELEGATECALL forwarding contracts for gas efficiency and to allow account upgradability. The account code is expected to hard-code the entry point into their code for gas efficiency. If a new entry point is introduced, whether to add new functionality, improve gas efficiency, or fix a critical security bug, users can self-call to replace their account's code address with a new code address containing code that points to a new entry point. During an upgrade process, it's expected that two mempools will run in parallel.
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
This ERC does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. Unfortunately it is not easily compatible with pre-ERC-4337 wallets, because those wallets do not have a `validateUserOp` function. If the wallet has a function for authorizing a trusted op submitter, then this could be fixed by creating an ERC-4337-compatible wallet that re-implements the verification logic as a wrapper and setting it to be the original wallet's trusted op submitter.
|
||||
This ERC does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. Unfortunately it is not easily compatible with pre-ERC-4337 accounts, because those accounts do not have a `validateUserOp` function. If the account has a function for authorizing a trusted op submitter, then this could be fixed by creating an ERC-4337-compatible account that re-implements the verification logic as a wrapper and setting it to be the original account's trusted op submitter.
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
@@ -520,11 +515,11 @@ See [https://github.com/eth-infinitism/account-abstraction/tree/main/contracts](
|
||||
|
||||
## Security considerations
|
||||
|
||||
The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ ERC 4337 wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ have to do becomes much smaller (they need only verify the `validateUserOp` function and its "check signature, increment nonce and pay fees" logic) and check that other functions are `msg.sender == ENTRY_POINT` gated (perhaps also allowing `msg.sender == self`), but it is nevertheless the case that this is done precisely by concentrating security risk in the entry point contract that needs to be verified to be very robust.
|
||||
The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ ERC 4337 accounts. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _accounts_ have to do becomes much smaller (they need only verify the `validateUserOp` function and its "check signature, increment nonce and pay fees" logic) and check that other functions are `msg.sender == ENTRY_POINT` gated (perhaps also allowing `msg.sender == self`), but it is nevertheless the case that this is done precisely by concentrating security risk in the entry point contract that needs to be verified to be very robust.
|
||||
|
||||
Verification would need to cover two primary claims (not including claims needed to protect paymasters, and claims needed to establish p2p-level DoS resistance):
|
||||
|
||||
* **Safety against arbitrary hijacking**: The entry point only calls a wallet generically if `validateUserOp` to that specific wallet has passed (and with `op.calldata` equal to the generic call's calldata)
|
||||
* **Safety against arbitrary hijacking**: The entry point only calls an account generically if `validateUserOp` to that specific account has passed (and with `op.calldata` equal to the generic call's calldata)
|
||||
* **Safety against fee draining**: If the entry point calls `validateUserOp` and passes, it also must make the generic call with calldata equal to `op.calldata`
|
||||
|
||||
## Copyright
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GasChecker } from './GasChecker'
|
||||
|
||||
context('simple wallet', function () {
|
||||
context('simple account', function () {
|
||||
this.timeout(60000)
|
||||
const g = new GasChecker()
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
AddressZero,
|
||||
checkForGeth,
|
||||
createAddress,
|
||||
createWalletOwner,
|
||||
createAccountOwner,
|
||||
deployEntryPoint
|
||||
} from '../test/testutils'
|
||||
import { EntryPoint, EntryPoint__factory, SimpleWallet__factory } from '../typechain'
|
||||
import { EntryPoint, EntryPoint__factory, SimpleAccount__factory } from '../typechain'
|
||||
import { BigNumberish, Wallet } from 'ethers'
|
||||
import hre from 'hardhat'
|
||||
import { fillAndSign } from '../test/UserOp'
|
||||
@@ -17,7 +17,7 @@ import { table, TableUserConfig } from 'table'
|
||||
import { Create2Factory } from '../src/Create2Factory'
|
||||
import { hexValue } from '@ethersproject/bytes'
|
||||
import * as fs from 'fs'
|
||||
import { SimpleWalletInterface } from '../typechain/contracts/samples/SimpleWallet'
|
||||
import { SimpleAccountInterface } from '../typechain/contracts/samples/SimpleAccount'
|
||||
|
||||
const gasCheckerLogFile = './reports/gas-checker.txt'
|
||||
|
||||
@@ -39,7 +39,7 @@ interface GasTestInfo {
|
||||
diffLastGas: boolean
|
||||
paymaster: string
|
||||
count: number
|
||||
// address, or 'random' or 'self' (for wallet itself)
|
||||
// address, or 'random' or 'self' (for account itself)
|
||||
dest: string
|
||||
destValue: BigNumberish
|
||||
destCallData: string
|
||||
@@ -48,7 +48,7 @@ interface GasTestInfo {
|
||||
}
|
||||
|
||||
export const DefaultGasTestInfo: Partial<GasTestInfo> = {
|
||||
dest: 'self', // destination is the wallet itself.
|
||||
dest: 'self', // destination is the account itself.
|
||||
destValue: parseEther('0'),
|
||||
destCallData: '0xaffed0e0', // nonce()
|
||||
gasPrice: 10e9
|
||||
@@ -58,7 +58,7 @@ interface GasTestResult {
|
||||
title: string
|
||||
count: number
|
||||
gasUsed: number // actual gas used
|
||||
walletEst: number // estimateGas of the inner transaction (from EP to wallet)
|
||||
accountEst: number // estimateGas of the inner transaction (from EP to account)
|
||||
gasDiff?: number // different from last test's gas used
|
||||
receipt?: TransactionReceipt
|
||||
}
|
||||
@@ -74,58 +74,58 @@ interface GasTestResult {
|
||||
// we assume a given call signature has the same gas usage
|
||||
// (TODO: the estimate also depends on contract code. for test purposes, assume each contract implementation has different method signature)
|
||||
// at the end of the checks, we report the gas usage of all those method calls
|
||||
const gasEstimatePerExec: { [key: string]: { title: string, walletEst: number } } = {}
|
||||
const gasEstimatePerExec: { [key: string]: { title: string, accountEst: number } } = {}
|
||||
|
||||
/**
|
||||
* helper contract to generate gas test.
|
||||
* see runTest() method for "test template" info
|
||||
* override for different wallet implementation:
|
||||
* - walletInitCode() - the constructor code
|
||||
* - walletExec() the wallet execution method.
|
||||
* override for different account implementation:
|
||||
* - accountInitCode() - the constructor code
|
||||
* - accountExec() the account execution method.
|
||||
*/
|
||||
export class GasChecker {
|
||||
wallets: { [wallet: string]: Wallet } = {}
|
||||
accounts: { [account: string]: Wallet } = {}
|
||||
|
||||
walletOwner: Wallet
|
||||
accountOwner: Wallet
|
||||
|
||||
walletInterface: SimpleWalletInterface
|
||||
accountInterface: SimpleAccountInterface
|
||||
|
||||
constructor () {
|
||||
this.walletOwner = createWalletOwner()
|
||||
this.walletInterface = SimpleWallet__factory.createInterface()
|
||||
this.accountOwner = createAccountOwner()
|
||||
this.accountInterface = SimpleAccount__factory.createInterface()
|
||||
void GasCheckCollector.init()
|
||||
}
|
||||
|
||||
// generate the "exec" calldata for this wallet
|
||||
walletExec (dest: string, value: BigNumberish, data: string): string {
|
||||
return this.walletInterface.encodeFunctionData('execFromEntryPoint', [dest, value, data])
|
||||
// generate the "exec" calldata for this account
|
||||
accountExec (dest: string, value: BigNumberish, data: string): string {
|
||||
return this.accountInterface.encodeFunctionData('execFromEntryPoint', [dest, value, data])
|
||||
}
|
||||
|
||||
// generate the wallet "creation code"
|
||||
walletInitCode (): string {
|
||||
return hexValue(new SimpleWallet__factory(ethersSigner).getDeployTransaction(GasCheckCollector.inst.entryPoint.address, this.walletOwner.address).data!)
|
||||
// generate the account "creation code"
|
||||
accountInitCode (): string {
|
||||
return hexValue(new SimpleAccount__factory(ethersSigner).getDeployTransaction(GasCheckCollector.inst.entryPoint.address, this.accountOwner.address).data!)
|
||||
}
|
||||
|
||||
/**
|
||||
* create wallets up to this counter.
|
||||
* create accounts up to this counter.
|
||||
* make sure they all have balance.
|
||||
* do nothing for wallet already created
|
||||
* do nothing for account already created
|
||||
* @param count
|
||||
*/
|
||||
async createWallets1 (count: number): Promise<void> {
|
||||
async createAccounts1 (count: number): Promise<void> {
|
||||
const fact = new Create2Factory(provider)
|
||||
// create wallets
|
||||
// create accounts
|
||||
for (const n of range(count)) {
|
||||
const salt = n
|
||||
const initCode = this.walletInitCode()
|
||||
const initCode = this.accountInitCode()
|
||||
|
||||
const addr = fact.getDeployedAddress(initCode, salt)
|
||||
this.wallets[addr] = this.walletOwner
|
||||
this.accounts[addr] = this.accountOwner
|
||||
|
||||
// deploy if not already deployed.
|
||||
await fact.deploy(initCode, salt, 2e6)
|
||||
const walletBalance = await GasCheckCollector.inst.entryPoint.balanceOf(addr)
|
||||
if (walletBalance.lte(minDepositOrBalance)) {
|
||||
const accountBalance = await GasCheckCollector.inst.entryPoint.balanceOf(addr)
|
||||
if (accountBalance.lte(minDepositOrBalance)) {
|
||||
await GasCheckCollector.inst.entryPoint.depositTo(addr, { value: minDepositOrBalance.mul(5) })
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ export class GasChecker {
|
||||
/**
|
||||
* helper: run a test scenario, and add a table row
|
||||
* @param params - test parameters. missing values filled in from DefaultGasTestInfo
|
||||
* note that 2 important params are methods: walletExec() and walletInitCode()
|
||||
* note that 2 important params are methods: accountExec() and accountInitCode()
|
||||
*/
|
||||
async addTestRow (params: Partial<GasTestInfo>): Promise<void> {
|
||||
await GasCheckCollector.init()
|
||||
@@ -144,7 +144,7 @@ export class GasChecker {
|
||||
/**
|
||||
* run a single test scenario
|
||||
* @param params - test parameters. missing values filled in from DefaultGasTestInfo
|
||||
* note that 2 important params are methods: walletExec() and walletInitCode()
|
||||
* note that 2 important params are methods: accountExec() and accountInitCode()
|
||||
*/
|
||||
async runTest (params: Partial<GasTestInfo>): Promise<GasTestResult> {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
@@ -152,18 +152,18 @@ export class GasChecker {
|
||||
|
||||
console.debug('== running test count=', info.count)
|
||||
|
||||
// fill wallets up to this code.
|
||||
await this.createWallets1(info.count)
|
||||
// fill accounts up to this code.
|
||||
await this.createAccounts1(info.count)
|
||||
|
||||
let walletEst: number = 0
|
||||
let accountEst: number = 0
|
||||
const userOps = await Promise.all(range(info.count)
|
||||
.map(index => Object.entries(this.wallets)[index])
|
||||
.map(async ([wallet, walletOwner]) => {
|
||||
.map(index => Object.entries(this.accounts)[index])
|
||||
.map(async ([account, accountOwner]) => {
|
||||
const paymaster = info.paymaster
|
||||
|
||||
let { dest, destValue, destCallData } = info
|
||||
if (dest === 'self') {
|
||||
dest = wallet
|
||||
dest = account
|
||||
} else if (dest === 'random') {
|
||||
dest = createAddress()
|
||||
const destBalance = await getBalance(dest)
|
||||
@@ -172,34 +172,34 @@ export class GasChecker {
|
||||
await ethersSigner.sendTransaction({ to: dest, value: 1 })
|
||||
}
|
||||
}
|
||||
const walletExecFromEntryPoint = this.walletExec(dest, destValue, destCallData)
|
||||
const accountExecFromEntryPoint = this.accountExec(dest, destValue, destCallData)
|
||||
|
||||
// remove the "dest" from the key to the saved estimations
|
||||
// so we have a single estimation per method.
|
||||
const estimateGasKey = this.walletExec(AddressZero, destValue, destCallData)
|
||||
const estimateGasKey = this.accountExec(AddressZero, destValue, destCallData)
|
||||
|
||||
let est = gasEstimatePerExec[estimateGasKey]
|
||||
// technically, each UserOp needs estimate - but we know they are all the same for each test.
|
||||
if (est == null) {
|
||||
const walletEst = (await ethers.provider.estimateGas({
|
||||
const accountEst = (await ethers.provider.estimateGas({
|
||||
from: GasCheckCollector.inst.entryPoint.address,
|
||||
to: wallet,
|
||||
data: walletExecFromEntryPoint
|
||||
to: account,
|
||||
data: accountExecFromEntryPoint
|
||||
})).toNumber()
|
||||
est = gasEstimatePerExec[estimateGasKey] = { walletEst, title: info.title }
|
||||
est = gasEstimatePerExec[estimateGasKey] = { accountEst, title: info.title }
|
||||
}
|
||||
// console.debug('== wallet est=', walletEst.toString())
|
||||
walletEst = est.walletEst
|
||||
// console.debug('== account est=', accountEst.toString())
|
||||
accountEst = est.accountEst
|
||||
const op = await fillAndSign({
|
||||
sender: wallet,
|
||||
callData: walletExecFromEntryPoint,
|
||||
sender: account,
|
||||
callData: accountExecFromEntryPoint,
|
||||
maxPriorityFeePerGas: info.gasPrice,
|
||||
maxFeePerGas: info.gasPrice,
|
||||
callGasLimit: walletEst,
|
||||
callGasLimit: accountEst,
|
||||
verificationGasLimit: 1000000,
|
||||
paymasterAndData: paymaster,
|
||||
preVerificationGas: 1
|
||||
}, walletOwner, GasCheckCollector.inst.entryPoint)
|
||||
}, accountOwner, GasCheckCollector.inst.entryPoint)
|
||||
// const packed = packUserOp(op, false)
|
||||
// console.log('== packed cost=', callDataCost(packed), packed)
|
||||
return op
|
||||
@@ -223,7 +223,7 @@ export class GasChecker {
|
||||
const ret1: GasTestResult = {
|
||||
count: info.count,
|
||||
gasUsed,
|
||||
walletEst,
|
||||
accountEst,
|
||||
title: info.title
|
||||
// receipt: rcpt
|
||||
}
|
||||
@@ -284,8 +284,8 @@ export class GasCheckCollector {
|
||||
'count',
|
||||
'total gasUsed',
|
||||
'per UserOp gas\n(delta for\none UserOp)',
|
||||
// 'wallet.exec()\nestimateGas',
|
||||
'per UserOp overhead\n(compared to\nwallet.exec())'
|
||||
// 'account.exec()\nestimateGas',
|
||||
'per UserOp overhead\n(compared to\naccount.exec())'
|
||||
]
|
||||
|
||||
this.initTable(tableHeaders)
|
||||
@@ -327,11 +327,11 @@ export class GasCheckCollector {
|
||||
fs.appendFileSync(gasCheckerLogFile, s + '\n')
|
||||
}
|
||||
|
||||
write('== gas estimate of direct calling the wallet\'s "execFromEntryPoint" method')
|
||||
write(' the destination is "wallet.nonce()", which is known to be "hot" address used by this wallet')
|
||||
write(' it little higher than EOA call: its an exec from entrypoint (or wallet owner) into wallet contract, verifying msg.sender and exec to target)')
|
||||
Object.values(gasEstimatePerExec).forEach(({ title, walletEst }) => {
|
||||
write(`- gas estimate "${title}" - ${walletEst}`)
|
||||
write('== gas estimate of direct calling the account\'s "execFromEntryPoint" method')
|
||||
write(' the destination is "account.nonce()", which is known to be "hot" address used by this account')
|
||||
write(' it little higher than EOA call: its an exec from entrypoint (or account owner) into account contract, verifying msg.sender and exec to target)')
|
||||
Object.values(gasEstimatePerExec).forEach(({ title, accountEst }) => {
|
||||
write(`- gas estimate "${title}" - ${accountEst}`)
|
||||
})
|
||||
|
||||
const tableOutput = table(this.tabRows, this.tableConfig)
|
||||
@@ -340,14 +340,14 @@ export class GasCheckCollector {
|
||||
|
||||
addRow (res: GasTestResult): void {
|
||||
const gasUsed = res.gasDiff != null ? '' : res.gasUsed // hide "total gasUsed" if there is a diff
|
||||
const perOp = res.gasDiff != null ? res.gasDiff - res.walletEst : ''
|
||||
const perOp = res.gasDiff != null ? res.gasDiff - res.accountEst : ''
|
||||
|
||||
this.tabRows.push([
|
||||
res.title,
|
||||
res.count,
|
||||
gasUsed,
|
||||
res.gasDiff ?? '',
|
||||
// res.walletEst,
|
||||
// res.accountEst,
|
||||
perOp])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ const config: HardhatUserConfig = {
|
||||
}],
|
||||
overrides: {
|
||||
'contracts/core/EntryPoint.sol': optimizedComilerSettings,
|
||||
'contracts/samples/SimpleWallet.sol': optimizedComilerSettings
|
||||
'contracts/samples/SimpleAccount.sol': optimizedComilerSettings
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
== gas estimate of direct calling the wallet's "execFromEntryPoint" method
|
||||
the destination is "wallet.nonce()", which is known to be "hot" address used by this wallet
|
||||
it little higher than EOA call: its an exec from entrypoint (or wallet owner) into wallet contract, verifying msg.sender and exec to target)
|
||||
== gas estimate of direct calling the account's "execFromEntryPoint" method
|
||||
the destination is "account.nonce()", which is known to be "hot" address used by this account
|
||||
it little higher than EOA call: its an exec from entrypoint (or account owner) into account contract, verifying msg.sender and exec to target)
|
||||
- gas estimate "simple" - 27860
|
||||
- gas estimate "big tx 5k" - 110612
|
||||
╔════════════════════════════════╤═══════╤═══════════════╤════════════════╤═════════════════════╗
|
||||
║ handleOps description │ count │ total gasUsed │ per UserOp gas │ per UserOp overhead ║
|
||||
║ │ │ │ (delta for │ (compared to ║
|
||||
║ │ │ │ one UserOp) │ wallet.exec()) ║
|
||||
║ │ │ │ one UserOp) │ account.exec()) ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple │ 1 │ 71769 │ │ ║
|
||||
║ simple │ 1 │ 71731 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple - diff from previous │ 2 │ │ 39716 │ 11856 ║
|
||||
║ simple - diff from previous │ 2 │ │ 39754 │ 11894 ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple │ 10 │ 429396 │ │ ║
|
||||
║ simple │ 10 │ 429490 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple - diff from previous │ 11 │ │ 39944 │ 12084 ║
|
||||
║ simple - diff from previous │ 11 │ │ 39812 │ 11952 ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple paymaster │ 1 │ 71743 │ │ ║
|
||||
║ simple paymaster │ 1 │ 71769 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple paymaster with diff │ 2 │ │ 39730 │ 11870 ║
|
||||
║ simple paymaster with diff │ 2 │ │ 39690 │ 11830 ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple paymaster │ 10 │ 429462 │ │ ║
|
||||
║ simple paymaster │ 10 │ 429528 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ simple paymaster with diff │ 11 │ │ 39918 │ 12058 ║
|
||||
║ simple paymaster with diff │ 11 │ │ 39848 │ 11988 ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ big tx 5k │ 1 │ 159164 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
@@ -30,6 +30,6 @@
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ big tx 5k │ 10 │ 1305534 │ │ ║
|
||||
╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢
|
||||
║ big tx - diff from previous │ 11 │ │ 128238 │ 17626 ║
|
||||
║ big tx - diff from previous │ 11 │ │ 128298 │ 17686 ║
|
||||
╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BigNumber, Bytes, ethers, Signer, Event } from 'ethers'
|
||||
import { BaseProvider, Provider, TransactionRequest } from '@ethersproject/providers'
|
||||
import { Deferrable, resolveProperties } from '@ethersproject/properties'
|
||||
import { SimpleWallet, SimpleWallet__factory, EntryPoint, EntryPoint__factory } from '../typechain'
|
||||
import { SimpleAccount, SimpleAccount__factory, EntryPoint, EntryPoint__factory } from '../typechain'
|
||||
import { BytesLike, hexValue } from '@ethersproject/bytes'
|
||||
import { TransactionResponse } from '@ethersproject/abstract-provider'
|
||||
import { fillAndSign, getUserOpHash } from '../test/UserOp'
|
||||
@@ -156,7 +156,7 @@ async function sendQueuedUserOps (queueSender: QueueSendUserOp, entryPoint: Entr
|
||||
* for testing: instead of connecting through RPC to a remote host, directly send the transaction
|
||||
* @param entryPointAddress the entryPoint address to use.
|
||||
* @param signer ethers provider to send the request (must have eth balance to send)
|
||||
* @param beneficiary the account to receive the payment (from wallet/paymaster). defaults to the signer's address
|
||||
* @param beneficiary the account to receive the payment (from account/paymaster). defaults to the signer's address
|
||||
*/
|
||||
export function localUserOpSender (entryPointAddress: string, signer: Signer, beneficiary?: string): SendUserOp {
|
||||
const entryPoint = EntryPoint__factory.connect(entryPointAddress, signer)
|
||||
@@ -191,7 +191,7 @@ export class AAProvider extends BaseProvider {
|
||||
* a signer that wraps account-abstraction.
|
||||
*/
|
||||
export class AASigner extends Signer {
|
||||
_wallet?: SimpleWallet
|
||||
_account?: SimpleAccount
|
||||
|
||||
private _isPhantom = true
|
||||
public entryPoint: EntryPoint
|
||||
@@ -203,7 +203,7 @@ export class AASigner extends Signer {
|
||||
* @param signer - the underlying signer. has no funds (=can't send TXs)
|
||||
* @param entryPoint the entryPoint contract. used for read-only operations
|
||||
* @param sendUserOp function to actually send the UserOp to the entryPoint.
|
||||
* @param index - index of this wallet for this signer.
|
||||
* @param index - index of this account for this signer.
|
||||
*/
|
||||
constructor (readonly signer: Signer, readonly entryPointAddress: string, readonly sendUserOp: SendUserOp, readonly index = 0, readonly provider = signer.provider) {
|
||||
super()
|
||||
@@ -211,15 +211,15 @@ export class AASigner extends Signer {
|
||||
}
|
||||
|
||||
// connect to a specific pre-deployed address
|
||||
// (note: in order to send transactions, the underlying signer address must be valid signer for this wallet (its owner)
|
||||
async connectWalletAddress (address: string): Promise<void> {
|
||||
if (this._wallet != null) {
|
||||
throw Error('already connected to wallet')
|
||||
// (note: in order to send transactions, the underlying signer address must be valid signer for this account (its owner)
|
||||
async connectAccountAddress (address: string): Promise<void> {
|
||||
if (this._account != null) {
|
||||
throw Error('already connected to account')
|
||||
}
|
||||
if (await this.provider!.getCode(address).then(code => code.length) <= 2) {
|
||||
throw new Error('cannot connect to non-existing contract')
|
||||
}
|
||||
this._wallet = SimpleWallet__factory.connect(address, this.signer)
|
||||
this._account = SimpleAccount__factory.connect(address, this.signer)
|
||||
this._isPhantom = false
|
||||
}
|
||||
|
||||
@@ -233,13 +233,13 @@ export class AASigner extends Signer {
|
||||
|
||||
async _deploymentTransaction (): Promise<BytesLike> {
|
||||
const ownerAddress = await this.signer.getAddress()
|
||||
return new SimpleWallet__factory(this.signer)
|
||||
return new SimpleAccount__factory(this.signer)
|
||||
.getDeployTransaction(this.entryPoint.address, ownerAddress).data!
|
||||
}
|
||||
|
||||
async getAddress (): Promise<string> {
|
||||
await this.syncAccount()
|
||||
return this._wallet!.address
|
||||
return this._account!.address
|
||||
}
|
||||
|
||||
async signMessage (message: Bytes | string): Promise<string> {
|
||||
@@ -250,9 +250,9 @@ export class AASigner extends Signer {
|
||||
throw new Error('signMessage: unsupported by AA')
|
||||
}
|
||||
|
||||
async getWallet (): Promise<SimpleWallet> {
|
||||
async getAccount (): Promise<SimpleAccount> {
|
||||
await this.syncAccount()
|
||||
return this._wallet!
|
||||
return this._account!
|
||||
}
|
||||
|
||||
// fabricate a response in a format usable by ethers users...
|
||||
@@ -343,23 +343,23 @@ export class AASigner extends Signer {
|
||||
}
|
||||
|
||||
async syncAccount (): Promise<void> {
|
||||
if (this._wallet == null) {
|
||||
if (this._account == null) {
|
||||
const address = await this._deploymentAddress()
|
||||
this._wallet = SimpleWallet__factory.connect(address, this.signer)
|
||||
this._account = SimpleAccount__factory.connect(address, this.signer)
|
||||
}
|
||||
|
||||
this._chainId = this.provider?.getNetwork().then(net => net.chainId)
|
||||
// once an account is deployed, it can no longer be a phantom.
|
||||
// but until then, we need to re-check
|
||||
if (this._isPhantom) {
|
||||
const size = await this.signer.provider?.getCode(this._wallet.address).then(x => x.length)
|
||||
// console.log(`== __isPhantom. addr=${this._wallet.address} re-checking code size. result = `, size)
|
||||
const size = await this.signer.provider?.getCode(this._account.address).then(x => x.length)
|
||||
// console.log(`== __isPhantom. addr=${this._account.address} re-checking code size. result = `, size)
|
||||
this._isPhantom = size === 2
|
||||
// !await this.entryPoint.isContractDeployed(await this.getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
// return true if wallet not yet created.
|
||||
// return true if account not yet created.
|
||||
async isPhantom (): Promise<boolean> {
|
||||
await this.syncAccount()
|
||||
return this._isPhantom
|
||||
@@ -378,7 +378,7 @@ export class AASigner extends Signer {
|
||||
initCallData
|
||||
])
|
||||
}
|
||||
const execFromEntryPoint = await this._wallet!.populateTransaction.execFromEntryPoint(tx.to!, tx.value ?? 0, tx.data!)
|
||||
const execFromEntryPoint = await this._account!.populateTransaction.execFromEntryPoint(tx.to!, tx.value ?? 0, tx.data!)
|
||||
|
||||
let { gasPrice, maxPriorityFeePerGas, maxFeePerGas } = tx
|
||||
// gasPrice is legacy, and overrides eip1559 values:
|
||||
@@ -388,7 +388,7 @@ export class AASigner extends Signer {
|
||||
maxFeePerGas = gasPrice
|
||||
}
|
||||
const userOp = await fillAndSign({
|
||||
sender: this._wallet!.address,
|
||||
sender: this._account!.address,
|
||||
initCode,
|
||||
nonce: initCode == null ? tx.nonce : this.index,
|
||||
callData: execFromEntryPoint.data!,
|
||||
|
||||
14
src/runop.ts
14
src/runop.ts
@@ -48,28 +48,28 @@ import { TransactionReceipt } from '@ethersproject/abstract-provider/src.ts/inde
|
||||
}
|
||||
} else { sendUserOp = localUserOpSender(entryPointAddress, ethersSigner) }
|
||||
|
||||
// index is unique for a wallet (so same owner can have multiple wallets, with different index
|
||||
// index is unique for an account (so same owner can have multiple accounts, with different index
|
||||
const index = parseInt(process.env.AA_INDEX ?? '0')
|
||||
console.log('using account index (AA_INDEX)', index)
|
||||
const aasigner = new AASigner(ethersSigner, entryPointAddress, sendUserOp, index)
|
||||
// connect to pre-deployed wallet
|
||||
// await aasigner.connectWalletAddress(walletAddress)
|
||||
// connect to pre-deployed account
|
||||
// await aasigner.connectAccountAddress(accountAddress)
|
||||
const myAddress = await aasigner.getAddress()
|
||||
if (await provider.getBalance(myAddress) < parseEther('0.01')) {
|
||||
console.log('prefund wallet')
|
||||
console.log('prefund account')
|
||||
await ethersSigner.sendTransaction({ to: myAddress, value: parseEther('0.01') })
|
||||
}
|
||||
|
||||
// usually, a wallet will deposit for itself (that is, get created using eth, run "addDeposit" for itself
|
||||
// usually, an account will deposit for itself (that is, get created using eth, run "addDeposit" for itself
|
||||
// and from there on will use deposit
|
||||
// for testing,
|
||||
const entryPoint = EntryPoint__factory.connect(entryPointAddress, ethersSigner)
|
||||
console.log('wallet address=', myAddress)
|
||||
console.log('account address=', myAddress)
|
||||
let preDeposit = await entryPoint.balanceOf(myAddress)
|
||||
console.log('current deposit=', preDeposit, 'current balance', await provider.getBalance(myAddress))
|
||||
|
||||
if (preDeposit.lte(parseEther('0.005'))) {
|
||||
console.log('depositing for wallet')
|
||||
console.log('depositing for account')
|
||||
await entryPoint.depositTo(myAddress, { value: parseEther('0.01') })
|
||||
preDeposit = await entryPoint.balanceOf(myAddress)
|
||||
}
|
||||
|
||||
@@ -156,16 +156,16 @@ export function fillUserOpDefaults (op: Partial<UserOperation>, defaults = Defau
|
||||
}
|
||||
|
||||
// helper to fill structure:
|
||||
// - default callGasLimit to estimate call from entryPoint to wallet (TODO: add overhead)
|
||||
// - default callGasLimit to estimate call from entryPoint to account (TODO: add overhead)
|
||||
// if there is initCode:
|
||||
// - calculate sender by eth_call the deployment code
|
||||
// - default verificationGasLimit estimateGas of deployment code plus default 100000
|
||||
// no initCode:
|
||||
// - update nonce from wallet.nonce()
|
||||
// - update nonce from account.nonce()
|
||||
// entryPoint param is only required to fill in "sender address when specifying "initCode"
|
||||
// nonce: assume contract as "nonce()" function, and fill in.
|
||||
// sender - only in case of construction: fill sender from initCode.
|
||||
// callGasLimit: VERY crude estimation (by estimating call to wallet, and add rough entryPoint overhead
|
||||
// callGasLimit: VERY crude estimation (by estimating call to account, and add rough entryPoint overhead
|
||||
// verificationGasLimit: hard-code default at 100k. should add "create2" cost
|
||||
export async function fillUserOp (op: Partial<UserOperation>, entryPoint?: EntryPoint): Promise<UserOperation> {
|
||||
const op1 = { ...op }
|
||||
|
||||
@@ -2,8 +2,8 @@ import './aa.init'
|
||||
import { ethers } from 'hardhat'
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
SimpleWallet,
|
||||
SimpleWallet__factory,
|
||||
SimpleAccount,
|
||||
SimpleAccount__factory,
|
||||
EntryPoint,
|
||||
DepositPaymaster,
|
||||
DepositPaymaster__factory,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '../typechain'
|
||||
import {
|
||||
AddressZero, createAddress,
|
||||
createWalletOwner,
|
||||
createAccountOwner,
|
||||
deployEntryPoint, FIVE_ETH, ONE_ETH, simulationResultCatch, userOpsWithoutAgg
|
||||
} from './testutils'
|
||||
import { fillAndSign } from './UserOp'
|
||||
@@ -42,20 +42,20 @@ describe('DepositPaymaster', () => {
|
||||
})
|
||||
|
||||
describe('deposit', () => {
|
||||
let wallet: SimpleWallet
|
||||
let account: SimpleAccount
|
||||
|
||||
before(async () => {
|
||||
wallet = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, await ethersSigner.getAddress())
|
||||
account = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, await ethersSigner.getAddress())
|
||||
})
|
||||
it('should deposit and read balance', async () => {
|
||||
await paymaster.addDepositFor(token.address, wallet.address, 100)
|
||||
expect(await paymaster.depositInfo(token.address, wallet.address)).to.eql({ amount: 100 })
|
||||
await paymaster.addDepositFor(token.address, account.address, 100)
|
||||
expect(await paymaster.depositInfo(token.address, account.address)).to.eql({ amount: 100 })
|
||||
})
|
||||
it('should fail to withdraw without unlock', async () => {
|
||||
const paymasterWithdraw = await paymaster.populateTransaction.withdrawTokensTo(token.address, AddressZero, 1).then(tx => tx.data!)
|
||||
|
||||
await expect(
|
||||
wallet.exec(paymaster.address, 0, paymasterWithdraw)
|
||||
account.exec(paymaster.address, 0, paymasterWithdraw)
|
||||
).to.revertedWith('DepositPaymaster: must unlockTokenDeposit')
|
||||
})
|
||||
it('should fail to withdraw within the same block ', async () => {
|
||||
@@ -63,32 +63,32 @@ describe('DepositPaymaster', () => {
|
||||
const paymasterWithdraw = await paymaster.populateTransaction.withdrawTokensTo(token.address, AddressZero, 1).then(tx => tx.data!)
|
||||
|
||||
await expect(
|
||||
wallet.execBatch([paymaster.address, paymaster.address], [paymasterUnlock, paymasterWithdraw])
|
||||
account.execBatch([paymaster.address, paymaster.address], [paymasterUnlock, paymasterWithdraw])
|
||||
).to.be.revertedWith('DepositPaymaster: must unlockTokenDeposit')
|
||||
})
|
||||
it('should succeed to withdraw after unlock', async () => {
|
||||
const paymasterUnlock = await paymaster.populateTransaction.unlockTokenDeposit().then(tx => tx.data!)
|
||||
const target = createAddress()
|
||||
const paymasterWithdraw = await paymaster.populateTransaction.withdrawTokensTo(token.address, target, 1).then(tx => tx.data!)
|
||||
await wallet.exec(paymaster.address, 0, paymasterUnlock)
|
||||
await wallet.exec(paymaster.address, 0, paymasterWithdraw)
|
||||
await account.exec(paymaster.address, 0, paymasterUnlock)
|
||||
await account.exec(paymaster.address, 0, paymasterWithdraw)
|
||||
expect(await token.balanceOf(target)).to.eq(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#validatePaymasterUserOp', () => {
|
||||
let wallet: SimpleWallet
|
||||
let account: SimpleAccount
|
||||
const gasPrice = 1e9
|
||||
let walletOwner: string
|
||||
let accountOwner: string
|
||||
|
||||
before(async () => {
|
||||
walletOwner = await ethersSigner.getAddress()
|
||||
wallet = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, walletOwner)
|
||||
accountOwner = await ethersSigner.getAddress()
|
||||
account = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, accountOwner)
|
||||
})
|
||||
|
||||
it('should fail if no token', async () => {
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: paymaster.address
|
||||
}, ethersSigner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(userOp)).to.be.revertedWith('paymasterAndData must specify token')
|
||||
@@ -96,7 +96,7 @@ describe('DepositPaymaster', () => {
|
||||
|
||||
it('should fail with wrong token', async () => {
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, hexZeroPad('0x1234', 20)])
|
||||
}, ethersSigner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(userOp, { gasPrice })).to.be.revertedWith('DepositPaymaster: unsupported token')
|
||||
@@ -104,20 +104,20 @@ describe('DepositPaymaster', () => {
|
||||
|
||||
it('should reject if no deposit', async () => {
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)])
|
||||
}, ethersSigner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(userOp, { gasPrice })).to.be.revertedWith('DepositPaymaster: deposit too low')
|
||||
})
|
||||
|
||||
it('should reject if deposit is not locked', async () => {
|
||||
await paymaster.addDepositFor(token.address, wallet.address, ONE_ETH)
|
||||
await paymaster.addDepositFor(token.address, account.address, ONE_ETH)
|
||||
|
||||
const paymasterUnlock = await paymaster.populateTransaction.unlockTokenDeposit().then(tx => tx.data!)
|
||||
await wallet.exec(paymaster.address, 0, paymasterUnlock)
|
||||
await account.exec(paymaster.address, 0, paymasterUnlock)
|
||||
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)])
|
||||
}, ethersSigner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(userOp, { gasPrice })).to.be.revertedWith('not locked')
|
||||
@@ -126,35 +126,35 @@ describe('DepositPaymaster', () => {
|
||||
it('succeed with valid deposit', async () => {
|
||||
// needed only if previous test did unlock.
|
||||
const paymasterLockTokenDeposit = await paymaster.populateTransaction.lockTokenDeposit().then(tx => tx.data!)
|
||||
await wallet.exec(paymaster.address, 0, paymasterLockTokenDeposit)
|
||||
await account.exec(paymaster.address, 0, paymasterLockTokenDeposit)
|
||||
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)])
|
||||
}, ethersSigner, entryPoint)
|
||||
await entryPoint.callStatic.simulateValidation(userOp).catch(simulationResultCatch)
|
||||
})
|
||||
})
|
||||
describe('#handleOps', () => {
|
||||
let wallet: SimpleWallet
|
||||
const walletOwner = createWalletOwner()
|
||||
let account: SimpleAccount
|
||||
const accountOwner = createAccountOwner()
|
||||
let counter: TestCounter
|
||||
let callData: string
|
||||
before(async () => {
|
||||
wallet = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, walletOwner.address)
|
||||
account = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, accountOwner.address)
|
||||
counter = await new TestCounter__factory(ethersSigner).deploy()
|
||||
const counterJustEmit = await counter.populateTransaction.justemit().then(tx => tx.data!)
|
||||
callData = await wallet.populateTransaction.execFromEntryPoint(counter.address, 0, counterJustEmit).then(tx => tx.data!)
|
||||
callData = await account.populateTransaction.execFromEntryPoint(counter.address, 0, counterJustEmit).then(tx => tx.data!)
|
||||
|
||||
await paymaster.addDepositFor(token.address, wallet.address, ONE_ETH)
|
||||
await paymaster.addDepositFor(token.address, account.address, ONE_ETH)
|
||||
})
|
||||
it('should pay with deposit (and revert user\'s call) if user can\'t pay with tokens', async () => {
|
||||
const beneficiary = createAddress()
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)]),
|
||||
callData
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
await entryPoint.handleAggregatedOps(userOpsWithoutAgg([userOp]), beneficiary)
|
||||
|
||||
@@ -168,23 +168,23 @@ describe('DepositPaymaster', () => {
|
||||
const beneficiary = createAddress()
|
||||
const beneficiary1 = createAddress()
|
||||
const initialTokens = parseEther('1')
|
||||
await token.mint(wallet.address, initialTokens)
|
||||
await token.mint(account.address, initialTokens)
|
||||
|
||||
// need to "approve" the paymaster to use the tokens. we issue a UserOp for that (which uses the deposit to execute)
|
||||
const tokenApprovePaymaster = await token.populateTransaction.approve(paymaster.address, ethers.constants.MaxUint256).then(tx => tx.data!)
|
||||
const execApprove = await wallet.populateTransaction.execFromEntryPoint(token.address, 0, tokenApprovePaymaster).then(tx => tx.data!)
|
||||
const execApprove = await account.populateTransaction.execFromEntryPoint(token.address, 0, tokenApprovePaymaster).then(tx => tx.data!)
|
||||
const userOp1 = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)]),
|
||||
callData: execApprove
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
await entryPoint.handleAggregatedOps(userOpsWithoutAgg([userOp1]), beneficiary1)
|
||||
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, hexZeroPad(token.address, 20)]),
|
||||
callData
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
await entryPoint.handleAggregatedOps(userOpsWithoutAgg([userOp]), beneficiary)
|
||||
|
||||
const [log] = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(), await ethers.provider.getBlockNumber())
|
||||
|
||||
@@ -3,25 +3,25 @@ import { BigNumber, Wallet } from 'ethers'
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
EntryPoint,
|
||||
SimpleWallet,
|
||||
SimpleWallet__factory,
|
||||
SimpleAccount,
|
||||
SimpleAccount__factory,
|
||||
TestCounter,
|
||||
TestCounter__factory,
|
||||
TestExpirePaymaster,
|
||||
TestExpirePaymaster__factory,
|
||||
TestExpiryWallet,
|
||||
TestExpiryWallet__factory,
|
||||
TestExpiryAccount,
|
||||
TestExpiryAccount__factory,
|
||||
TestPaymasterAcceptAll,
|
||||
TestPaymasterAcceptAll__factory
|
||||
} from '../typechain'
|
||||
import {
|
||||
AddressZero,
|
||||
createWalletOwner,
|
||||
createAccountOwner,
|
||||
fund,
|
||||
checkForGeth,
|
||||
rethrow,
|
||||
tostr,
|
||||
getWalletDeployer,
|
||||
getAccountDeployer,
|
||||
calcGasUsage,
|
||||
checkForBannedOps,
|
||||
ONE_ETH,
|
||||
@@ -29,9 +29,9 @@ import {
|
||||
deployEntryPoint,
|
||||
getBalance,
|
||||
createAddress,
|
||||
getWalletAddress,
|
||||
getAccountAddress,
|
||||
HashZero,
|
||||
getAggregatedWalletDeployer,
|
||||
getAggregatedAccountDeployer,
|
||||
simulationResultCatch,
|
||||
simulationResultWithAggregationCatch
|
||||
} from './testutils'
|
||||
@@ -43,18 +43,18 @@ import { defaultAbiCoder, hexConcat, hexZeroPad, parseEther } from 'ethers/lib/u
|
||||
import { debugTransaction } from './debugTx'
|
||||
import { BytesLike } from '@ethersproject/bytes'
|
||||
import { TestSignatureAggregator } from '../typechain/contracts/samples/TestSignatureAggregator'
|
||||
import { TestAggregatedWallet } from '../typechain/contracts/samples/TestAggregatedWallet'
|
||||
import { TestAggregatedAccount } from '../typechain/contracts/samples/TestAggregatedAccount'
|
||||
import {
|
||||
TestSignatureAggregator__factory
|
||||
} from '../typechain/factories/contracts/samples/TestSignatureAggregator__factory'
|
||||
import { TestAggregatedWallet__factory } from '../typechain/factories/contracts/samples/TestAggregatedWallet__factory'
|
||||
import { TestAggregatedAccount__factory } from '../typechain/factories/contracts/samples/TestAggregatedAccount__factory'
|
||||
|
||||
describe('EntryPoint', function () {
|
||||
let entryPoint: EntryPoint
|
||||
|
||||
let walletOwner: Wallet
|
||||
let accountOwner: Wallet
|
||||
const ethersSigner = ethers.provider.getSigner()
|
||||
let wallet: SimpleWallet
|
||||
let account: SimpleAccount
|
||||
|
||||
const globalUnstakeDelaySec = 2
|
||||
const paymasterStake = ethers.utils.parseEther('2')
|
||||
@@ -66,12 +66,12 @@ describe('EntryPoint', function () {
|
||||
|
||||
entryPoint = await deployEntryPoint()
|
||||
|
||||
walletOwner = createWalletOwner()
|
||||
wallet = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, await walletOwner.getAddress())
|
||||
await fund(wallet)
|
||||
accountOwner = createAccountOwner()
|
||||
account = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, await accountOwner.getAddress())
|
||||
await fund(account)
|
||||
|
||||
// sanity: validate helper functions
|
||||
const sampleOp = await fillAndSign({ sender: wallet.address }, walletOwner, entryPoint)
|
||||
const sampleOp = await fillAndSign({ sender: account.address }, accountOwner, entryPoint)
|
||||
expect(getUserOpHash(sampleOp, entryPoint.address, chainId)).to.eql(await entryPoint.getUserOpHash(sampleOp))
|
||||
})
|
||||
|
||||
@@ -197,49 +197,49 @@ describe('EntryPoint', function () {
|
||||
})
|
||||
describe('with deposit', () => {
|
||||
let owner: string
|
||||
let wallet: SimpleWallet
|
||||
let account: SimpleAccount
|
||||
before(async () => {
|
||||
owner = await ethersSigner.getAddress()
|
||||
wallet = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, owner)
|
||||
await wallet.addDeposit({ value: ONE_ETH })
|
||||
expect(await getBalance(wallet.address)).to.equal(0)
|
||||
expect(await wallet.getDeposit()).to.eql(ONE_ETH)
|
||||
account = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, owner)
|
||||
await account.addDeposit({ value: ONE_ETH })
|
||||
expect(await getBalance(account.address)).to.equal(0)
|
||||
expect(await account.getDeposit()).to.eql(ONE_ETH)
|
||||
})
|
||||
it('should be able to withdraw', async () => {
|
||||
const depositBefore = await wallet.getDeposit()
|
||||
await wallet.withdrawDepositTo(wallet.address, ONE_ETH)
|
||||
expect(await getBalance(wallet.address)).to.equal(1e18)
|
||||
expect(await wallet.getDeposit()).to.equal(depositBefore.sub(ONE_ETH))
|
||||
const depositBefore = await account.getDeposit()
|
||||
await account.withdrawDepositTo(account.address, ONE_ETH)
|
||||
expect(await getBalance(account.address)).to.equal(1e18)
|
||||
expect(await account.getDeposit()).to.equal(depositBefore.sub(ONE_ETH))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#simulateValidation', () => {
|
||||
const walletOwner1 = createWalletOwner()
|
||||
let wallet1: SimpleWallet
|
||||
const accountOwner1 = createAccountOwner()
|
||||
let account1: SimpleAccount
|
||||
|
||||
before(async () => {
|
||||
wallet1 = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, await walletOwner1.getAddress())
|
||||
account1 = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, await accountOwner1.getAddress())
|
||||
})
|
||||
|
||||
it('should fail if validateUserOp fails', async () => {
|
||||
// using wrong owner for wallet1
|
||||
const op = await fillAndSign({ sender: wallet1.address }, walletOwner, entryPoint)
|
||||
// using wrong owner for account1
|
||||
const op = await fillAndSign({ sender: account1.address }, accountOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(op).catch(rethrow())).to
|
||||
.revertedWith('wrong signature')
|
||||
})
|
||||
|
||||
it('should succeed if validateUserOp succeeds', async () => {
|
||||
const op = await fillAndSign({ sender: wallet1.address }, walletOwner1, entryPoint)
|
||||
await fund(wallet1)
|
||||
const op = await fillAndSign({ sender: account1.address }, accountOwner1, entryPoint)
|
||||
await fund(account1)
|
||||
await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch)
|
||||
})
|
||||
|
||||
it('should prevent overflows: fail if any numeric value is more than 120 bits', async () => {
|
||||
const op = await fillAndSign({
|
||||
preVerificationGas: BigNumber.from(2).pow(130),
|
||||
sender: wallet1.address
|
||||
}, walletOwner1, entryPoint)
|
||||
sender: account1.address
|
||||
}, accountOwner1, entryPoint)
|
||||
await expect(
|
||||
entryPoint.callStatic.simulateValidation(op)
|
||||
).to.revertedWith('gas values overflow')
|
||||
@@ -247,45 +247,45 @@ describe('EntryPoint', function () {
|
||||
|
||||
it('should fail creation for wrong sender', async () => {
|
||||
const op1 = await fillAndSign({
|
||||
initCode: getWalletDeployer(entryPoint.address, walletOwner1.address),
|
||||
initCode: getAccountDeployer(entryPoint.address, accountOwner1.address),
|
||||
sender: '0x'.padEnd(42, '1'),
|
||||
verificationGasLimit: 1e6
|
||||
}, walletOwner1, entryPoint)
|
||||
}, accountOwner1, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(op1).catch(rethrow()))
|
||||
.to.revertedWith('sender doesn\'t match initCode address')
|
||||
})
|
||||
|
||||
it('should succeed for creating a wallet', async () => {
|
||||
const sender = getWalletAddress(entryPoint.address, walletOwner1.address)
|
||||
it('should succeed for creating an account', async () => {
|
||||
const sender = getAccountAddress(entryPoint.address, accountOwner1.address)
|
||||
const op1 = await fillAndSign({
|
||||
sender,
|
||||
initCode: getWalletDeployer(entryPoint.address, walletOwner1.address)
|
||||
}, walletOwner1, entryPoint)
|
||||
initCode: getAccountDeployer(entryPoint.address, accountOwner1.address)
|
||||
}, accountOwner1, entryPoint)
|
||||
await fund(op1.sender)
|
||||
|
||||
await entryPoint.callStatic.simulateValidation(op1).catch(simulationResultCatch)
|
||||
})
|
||||
|
||||
it('should not call initCode from entrypoint', async () => {
|
||||
// a possible attack: call a wallet's execFromEntryPoint through initCode. This might lead to stolen funds.
|
||||
const wallet = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, walletOwner.address)
|
||||
// a possible attack: call an account's execFromEntryPoint through initCode. This might lead to stolen funds.
|
||||
const account = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, accountOwner.address)
|
||||
const sender = createAddress()
|
||||
const op1 = await fillAndSign({
|
||||
initCode: hexConcat([
|
||||
wallet.address,
|
||||
wallet.interface.encodeFunctionData('execFromEntryPoint', [sender, 0, '0x'])
|
||||
account.address,
|
||||
account.interface.encodeFunctionData('execFromEntryPoint', [sender, 0, '0x'])
|
||||
]),
|
||||
sender
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
const error = await entryPoint.callStatic.simulateValidation(op1).catch(e => e)
|
||||
expect(error.message).to.match(/initCode failed/, error)
|
||||
})
|
||||
|
||||
it('should not use banned ops during simulateValidation', async () => {
|
||||
const op1 = await fillAndSign({
|
||||
initCode: getWalletDeployer(entryPoint.address, walletOwner1.address),
|
||||
sender: getWalletAddress(entryPoint.address, walletOwner1.address)
|
||||
}, walletOwner1, entryPoint)
|
||||
initCode: getAccountDeployer(entryPoint.address, accountOwner1.address),
|
||||
sender: getAccountAddress(entryPoint.address, accountOwner1.address)
|
||||
}, accountOwner1, entryPoint)
|
||||
await fund(op1.sender)
|
||||
await entryPoint.simulateValidation(op1, { gasLimit: 10e6 }).catch(e => e)
|
||||
const block = await ethers.provider.getBlock('latest')
|
||||
@@ -297,22 +297,22 @@ describe('EntryPoint', function () {
|
||||
describe('without paymaster (account pays in eth)', () => {
|
||||
describe('#handleOps', () => {
|
||||
let counter: TestCounter
|
||||
let walletExecFromEntryPoint: PopulatedTransaction
|
||||
let accountExecFromEntryPoint: PopulatedTransaction
|
||||
before(async () => {
|
||||
counter = await new TestCounter__factory(ethersSigner).deploy()
|
||||
const count = await counter.populateTransaction.count()
|
||||
walletExecFromEntryPoint = await wallet.populateTransaction.execFromEntryPoint(counter.address, 0, count.data!)
|
||||
accountExecFromEntryPoint = await account.populateTransaction.execFromEntryPoint(counter.address, 0, count.data!)
|
||||
})
|
||||
|
||||
it('wallet should pay for tx', async function () {
|
||||
it('account should pay for tx', async function () {
|
||||
const op = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
callData: walletExecFromEntryPoint.data,
|
||||
sender: account.address,
|
||||
callData: accountExecFromEntryPoint.data,
|
||||
verificationGasLimit: 1e6,
|
||||
callGasLimit: 1e6
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
const beneficiaryAddress = createAddress()
|
||||
const countBefore = await counter.counters(wallet.address)
|
||||
const countBefore = await counter.counters(account.address)
|
||||
// for estimateGas, must specify maxFeePerGas, otherwise our gas check fails
|
||||
console.log(' == est gas=', await entryPoint.estimateGas.handleOps([op], beneficiaryAddress, { maxFeePerGas: 1e9 }).then(tostr))
|
||||
|
||||
@@ -323,7 +323,7 @@ describe('EntryPoint', function () {
|
||||
gasLimit: 1e7
|
||||
}).then(async t => await t.wait())
|
||||
|
||||
const countAfter = await counter.counters(wallet.address)
|
||||
const countAfter = await counter.counters(account.address)
|
||||
expect(countAfter.toNumber()).to.equal(countBefore.toNumber() + 1)
|
||||
console.log('rcpt.gasUsed=', rcpt.gasUsed.toString(), rcpt.transactionHash)
|
||||
|
||||
@@ -332,13 +332,13 @@ describe('EntryPoint', function () {
|
||||
|
||||
it('legacy mode (maxPriorityFee==maxFeePerGas) should not use "basefee" opcode', async function () {
|
||||
const op = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
callData: walletExecFromEntryPoint.data,
|
||||
sender: account.address,
|
||||
callData: accountExecFromEntryPoint.data,
|
||||
maxPriorityFeePerGas: 10e9,
|
||||
maxFeePerGas: 10e9,
|
||||
verificationGasLimit: 1e6,
|
||||
callGasLimit: 1e6
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
const beneficiaryAddress = createAddress()
|
||||
|
||||
// (gasLimit, to prevent estimateGas to fail on missing maxFeePerGas, see above..)
|
||||
@@ -352,22 +352,22 @@ describe('EntryPoint', function () {
|
||||
expect(ops).to.not.include('BASEFEE')
|
||||
})
|
||||
|
||||
it('if wallet has a deposit, it should use it to pay', async function () {
|
||||
await wallet.addDeposit({ value: ONE_ETH })
|
||||
it('if account has a deposit, it should use it to pay', async function () {
|
||||
await account.addDeposit({ value: ONE_ETH })
|
||||
const op = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
callData: walletExecFromEntryPoint.data,
|
||||
sender: account.address,
|
||||
callData: accountExecFromEntryPoint.data,
|
||||
verificationGasLimit: 1e6,
|
||||
callGasLimit: 1e6
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
const beneficiaryAddress = createAddress()
|
||||
|
||||
const countBefore = await counter.counters(wallet.address)
|
||||
const countBefore = await counter.counters(account.address)
|
||||
// for estimateGas, must specify maxFeePerGas, otherwise our gas check fails
|
||||
console.log(' == est gas=', await entryPoint.estimateGas.handleOps([op], beneficiaryAddress, { maxFeePerGas: 1e9 }).then(tostr))
|
||||
|
||||
const balBefore = await getBalance(wallet.address)
|
||||
const depositBefore = await entryPoint.balanceOf(wallet.address)
|
||||
const balBefore = await getBalance(account.address)
|
||||
const depositBefore = await entryPoint.balanceOf(account.address)
|
||||
// must specify at least one of maxFeePerGas, gasLimit
|
||||
// (gasLimit, to prevent estimateGas to fail on missing maxFeePerGas, see above..)
|
||||
const rcpt = await entryPoint.handleOps([op], beneficiaryAddress, {
|
||||
@@ -375,12 +375,12 @@ describe('EntryPoint', function () {
|
||||
gasLimit: 1e7
|
||||
}).then(async t => await t.wait())
|
||||
|
||||
const countAfter = await counter.counters(wallet.address)
|
||||
const countAfter = await counter.counters(account.address)
|
||||
expect(countAfter.toNumber()).to.equal(countBefore.toNumber() + 1)
|
||||
console.log('rcpt.gasUsed=', rcpt.gasUsed.toString(), rcpt.transactionHash)
|
||||
|
||||
const balAfter = await getBalance(wallet.address)
|
||||
const depositAfter = await entryPoint.balanceOf(wallet.address)
|
||||
const balAfter = await getBalance(account.address)
|
||||
const depositAfter = await entryPoint.balanceOf(account.address)
|
||||
expect(balAfter).to.equal(balBefore, 'should pay from stake, not balance')
|
||||
const depositUsed = depositBefore.sub(depositAfter)
|
||||
expect(await ethers.provider.getBalance(beneficiaryAddress)).to.equal(depositUsed)
|
||||
@@ -390,11 +390,11 @@ describe('EntryPoint', function () {
|
||||
|
||||
it('should pay for reverted tx', async () => {
|
||||
const op = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
callData: '0xdeadface',
|
||||
verificationGasLimit: 1e6,
|
||||
callGasLimit: 1e6
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
const beneficiaryAddress = createAddress()
|
||||
|
||||
const rcpt = await entryPoint.handleOps([op], beneficiaryAddress, {
|
||||
@@ -411,15 +411,15 @@ describe('EntryPoint', function () {
|
||||
const beneficiaryAddress = createAddress()
|
||||
|
||||
const op = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
callData: walletExecFromEntryPoint.data
|
||||
}, walletOwner, entryPoint)
|
||||
sender: account.address,
|
||||
callData: accountExecFromEntryPoint.data
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
const countBefore = await counter.counters(wallet.address)
|
||||
const countBefore = await counter.counters(account.address)
|
||||
const rcpt = await entryPoint.handleOps([op], beneficiaryAddress, {
|
||||
gasLimit: 1e7
|
||||
}).then(async t => await t.wait())
|
||||
const countAfter = await counter.counters(wallet.address)
|
||||
const countAfter = await counter.counters(account.address)
|
||||
expect(countAfter.toNumber()).to.equal(countBefore.toNumber() + 1)
|
||||
|
||||
console.log('rcpt.gasUsed=', rcpt.gasUsed.toString(), rcpt.transactionHash)
|
||||
@@ -436,10 +436,10 @@ describe('EntryPoint', function () {
|
||||
|
||||
it('should reject create if sender address is wrong', async () => {
|
||||
const op = await fillAndSign({
|
||||
initCode: getWalletDeployer(entryPoint.address, walletOwner.address),
|
||||
initCode: getAccountDeployer(entryPoint.address, accountOwner.address),
|
||||
verificationGasLimit: 2e6,
|
||||
sender: '0x'.padEnd(42, '1')
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
await expect(entryPoint.callStatic.handleOps([op], beneficiaryAddress, {
|
||||
gasLimit: 1e7
|
||||
@@ -448,9 +448,9 @@ describe('EntryPoint', function () {
|
||||
|
||||
it('should reject create if account not funded', async () => {
|
||||
const op = await fillAndSign({
|
||||
initCode: getWalletDeployer(entryPoint.address, walletOwner.address),
|
||||
initCode: getAccountDeployer(entryPoint.address, accountOwner.address),
|
||||
verificationGasLimit: 2e6
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
expect(await ethers.provider.getBalance(op.sender)).to.eq(0)
|
||||
|
||||
@@ -459,20 +459,20 @@ describe('EntryPoint', function () {
|
||||
gasPrice: await ethers.provider.getGasPrice()
|
||||
})).to.revertedWith('didn\'t pay prefund')
|
||||
|
||||
// await expect(await ethers.provider.getCode(op.sender).then(x => x.length)).to.equal(2, "wallet exists before creation")
|
||||
// await expect(await ethers.provider.getCode(op.sender).then(x => x.length)).to.equal(2, "account exists before creation")
|
||||
})
|
||||
|
||||
it('should succeed to create account after prefund', async () => {
|
||||
const preAddr = getWalletAddress(entryPoint.address, walletOwner.address)
|
||||
const preAddr = getAccountAddress(entryPoint.address, accountOwner.address)
|
||||
await fund(preAddr)
|
||||
createOp = await fillAndSign({
|
||||
initCode: getWalletDeployer(entryPoint.address, walletOwner.address),
|
||||
initCode: getAccountDeployer(entryPoint.address, accountOwner.address),
|
||||
callGasLimit: 1e7,
|
||||
verificationGasLimit: 2e6
|
||||
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
await expect(await ethers.provider.getCode(preAddr).then(x => x.length)).to.equal(2, 'wallet exists before creation')
|
||||
await expect(await ethers.provider.getCode(preAddr).then(x => x.length)).to.equal(2, 'account exists before creation')
|
||||
const rcpt = await entryPoint.handleOps([createOp], beneficiaryAddress, {
|
||||
gasLimit: 1e7
|
||||
}).then(async tx => await tx.wait()).catch(rethrow())
|
||||
@@ -480,7 +480,7 @@ describe('EntryPoint', function () {
|
||||
})
|
||||
|
||||
it('should reject if account already created', async function () {
|
||||
const preAddr = getWalletAddress(entryPoint.address, walletOwner.address)
|
||||
const preAddr = getAccountAddress(entryPoint.address, accountOwner.address)
|
||||
if (await ethers.provider.getCode(preAddr).then(x => x.length) === 2) {
|
||||
this.skip()
|
||||
}
|
||||
@@ -497,55 +497,55 @@ describe('EntryPoint', function () {
|
||||
}
|
||||
/**
|
||||
* attempt a batch:
|
||||
* 1. create wallet1 + "initialize" (by calling counter.count())
|
||||
* 2. wallet2.exec(counter.count()
|
||||
* (wallet created in advance)
|
||||
* 1. create account1 + "initialize" (by calling counter.count())
|
||||
* 2. account2.exec(counter.count()
|
||||
* (account created in advance)
|
||||
*/
|
||||
let counter: TestCounter
|
||||
let walletExecCounterFromEntryPoint: PopulatedTransaction
|
||||
let accountExecCounterFromEntryPoint: PopulatedTransaction
|
||||
const beneficiaryAddress = createAddress()
|
||||
const walletOwner1 = createWalletOwner()
|
||||
let wallet1: string
|
||||
const walletOwner2 = createWalletOwner()
|
||||
let wallet2: SimpleWallet
|
||||
const accountOwner1 = createAccountOwner()
|
||||
let account1: string
|
||||
const accountOwner2 = createAccountOwner()
|
||||
let account2: SimpleAccount
|
||||
|
||||
before('before', async () => {
|
||||
counter = await new TestCounter__factory(ethersSigner).deploy()
|
||||
const count = await counter.populateTransaction.count()
|
||||
walletExecCounterFromEntryPoint = await wallet.populateTransaction.execFromEntryPoint(counter.address, 0, count.data!)
|
||||
wallet1 = getWalletAddress(entryPoint.address, walletOwner1.address)
|
||||
wallet2 = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, walletOwner2.address)
|
||||
await fund(wallet1)
|
||||
await fund(wallet2.address)
|
||||
accountExecCounterFromEntryPoint = await account.populateTransaction.execFromEntryPoint(counter.address, 0, count.data!)
|
||||
account1 = getAccountAddress(entryPoint.address, accountOwner1.address)
|
||||
account2 = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, accountOwner2.address)
|
||||
await fund(account1)
|
||||
await fund(account2.address)
|
||||
// execute and increment counter
|
||||
const op1 = await fillAndSign({
|
||||
initCode: getWalletDeployer(entryPoint.address, walletOwner1.address),
|
||||
callData: walletExecCounterFromEntryPoint.data,
|
||||
initCode: getAccountDeployer(entryPoint.address, accountOwner1.address),
|
||||
callData: accountExecCounterFromEntryPoint.data,
|
||||
callGasLimit: 2e6,
|
||||
verificationGasLimit: 2e6
|
||||
}, walletOwner1, entryPoint)
|
||||
}, accountOwner1, entryPoint)
|
||||
|
||||
const op2 = await fillAndSign({
|
||||
callData: walletExecCounterFromEntryPoint.data,
|
||||
sender: wallet2.address,
|
||||
callData: accountExecCounterFromEntryPoint.data,
|
||||
sender: account2.address,
|
||||
callGasLimit: 2e6,
|
||||
verificationGasLimit: 76000
|
||||
}, walletOwner2, entryPoint)
|
||||
}, accountOwner2, entryPoint)
|
||||
|
||||
await entryPoint.callStatic.simulateValidation(op2, { gasPrice: 1e9 }).catch(simulationResultCatch)
|
||||
|
||||
await fund(op1.sender)
|
||||
await fund(wallet2.address)
|
||||
await fund(account2.address)
|
||||
await entryPoint.handleOps([op1!, op2], beneficiaryAddress).catch((rethrow())).then(async r => r!.wait())
|
||||
// console.log(ret.events!.map(e=>({ev:e.event, ...objdump(e.args!)})))
|
||||
})
|
||||
it('should execute', async () => {
|
||||
expect(await counter.counters(wallet1)).equal(1)
|
||||
expect(await counter.counters(wallet2.address)).equal(1)
|
||||
expect(await counter.counters(account1)).equal(1)
|
||||
expect(await counter.counters(account2.address)).equal(1)
|
||||
})
|
||||
it('should pay for tx', async () => {
|
||||
// const cost1 = prebalance1.sub(await ethers.provider.getBalance(wallet1))
|
||||
// const cost2 = prebalance2.sub(await ethers.provider.getBalance(wallet2.address))
|
||||
// const cost1 = prebalance1.sub(await ethers.provider.getBalance(account1))
|
||||
// const cost2 = prebalance2.sub(await ethers.provider.getBalance(account2.address))
|
||||
// console.log('cost1=', cost1)
|
||||
// console.log('cost2=', cost2)
|
||||
})
|
||||
@@ -554,28 +554,28 @@ describe('EntryPoint', function () {
|
||||
describe('aggregation tests', () => {
|
||||
const beneficiaryAddress = createAddress()
|
||||
let aggregator: TestSignatureAggregator
|
||||
let aggWallet: TestAggregatedWallet
|
||||
let aggWallet2: TestAggregatedWallet
|
||||
let aggAccount: TestAggregatedAccount
|
||||
let aggAccount2: TestAggregatedAccount
|
||||
|
||||
before(async () => {
|
||||
aggregator = await new TestSignatureAggregator__factory(ethersSigner).deploy()
|
||||
aggWallet = await new TestAggregatedWallet__factory(ethersSigner).deploy(entryPoint.address, aggregator.address)
|
||||
aggWallet2 = await new TestAggregatedWallet__factory(ethersSigner).deploy(entryPoint.address, aggregator.address)
|
||||
await ethersSigner.sendTransaction({ to: aggWallet.address, value: parseEther('0.1') })
|
||||
await ethersSigner.sendTransaction({ to: aggWallet2.address, value: parseEther('0.1') })
|
||||
aggAccount = await new TestAggregatedAccount__factory(ethersSigner).deploy(entryPoint.address, aggregator.address)
|
||||
aggAccount2 = await new TestAggregatedAccount__factory(ethersSigner).deploy(entryPoint.address, aggregator.address)
|
||||
await ethersSigner.sendTransaction({ to: aggAccount.address, value: parseEther('0.1') })
|
||||
await ethersSigner.sendTransaction({ to: aggAccount2.address, value: parseEther('0.1') })
|
||||
})
|
||||
it('should fail to execute aggregated wallet without an aggregator', async () => {
|
||||
it('should fail to execute aggregated account without an aggregator', async () => {
|
||||
const userOp = await fillAndSign({
|
||||
sender: aggWallet.address
|
||||
}, walletOwner, entryPoint)
|
||||
sender: aggAccount.address
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
// no aggregator is kind of "wrong aggregator"
|
||||
await expect(entryPoint.handleOps([userOp], beneficiaryAddress)).to.revertedWith('wrong aggregator')
|
||||
})
|
||||
it('should fail to execute aggregated wallet with wrong aggregator', async () => {
|
||||
it('should fail to execute aggregated account with wrong aggregator', async () => {
|
||||
const userOp = await fillAndSign({
|
||||
sender: aggWallet.address
|
||||
}, walletOwner, entryPoint)
|
||||
sender: aggAccount.address
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
const wrongAggregator = await new TestSignatureAggregator__factory(ethersSigner).deploy()
|
||||
const sig = HashZero
|
||||
@@ -587,10 +587,10 @@ describe('EntryPoint', function () {
|
||||
}], beneficiaryAddress)).to.revertedWith('wrong aggregator')
|
||||
})
|
||||
|
||||
it('should fail to execute aggregated wallet with wrong agg. signature', async () => {
|
||||
it('should fail to execute aggregated account with wrong agg. signature', async () => {
|
||||
const userOp = await fillAndSign({
|
||||
sender: aggWallet.address
|
||||
}, walletOwner, entryPoint)
|
||||
sender: aggAccount.address
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
const wrongSig = hexZeroPad('0x123456', 32)
|
||||
const aggAddress: string = aggregator.address
|
||||
@@ -602,23 +602,23 @@ describe('EntryPoint', function () {
|
||||
}], beneficiaryAddress)).to.revertedWith(`SignatureValidationFailed("${aggAddress}")`)
|
||||
})
|
||||
|
||||
it('should run with multiple aggregators (and non-aggregated-wallets)', async () => {
|
||||
it('should run with multiple aggregators (and non-aggregated-accounts)', async () => {
|
||||
const aggregator3 = await new TestSignatureAggregator__factory(ethersSigner).deploy()
|
||||
const aggWallet3 = await new TestAggregatedWallet__factory(ethersSigner).deploy(entryPoint.address, aggregator3.address)
|
||||
await ethersSigner.sendTransaction({ to: aggWallet3.address, value: parseEther('0.1') })
|
||||
const aggAccount3 = await new TestAggregatedAccount__factory(ethersSigner).deploy(entryPoint.address, aggregator3.address)
|
||||
await ethersSigner.sendTransaction({ to: aggAccount3.address, value: parseEther('0.1') })
|
||||
|
||||
const userOp1 = await fillAndSign({
|
||||
sender: aggWallet.address
|
||||
}, walletOwner, entryPoint)
|
||||
sender: aggAccount.address
|
||||
}, accountOwner, entryPoint)
|
||||
const userOp2 = await fillAndSign({
|
||||
sender: aggWallet2.address
|
||||
}, walletOwner, entryPoint)
|
||||
sender: aggAccount2.address
|
||||
}, accountOwner, entryPoint)
|
||||
const userOp_agg3 = await fillAndSign({
|
||||
sender: aggWallet3.address
|
||||
}, walletOwner, entryPoint)
|
||||
sender: aggAccount3.address
|
||||
}, accountOwner, entryPoint)
|
||||
const userOp_noAgg = await fillAndSign({
|
||||
sender: wallet.address
|
||||
}, walletOwner, entryPoint)
|
||||
sender: account.address
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
// extract signature from userOps, and create aggregated signature
|
||||
// (not really required with the test aggregator, but should work with any aggregator
|
||||
@@ -649,27 +649,27 @@ describe('EntryPoint', function () {
|
||||
let userOp2: UserOperation
|
||||
before(async () => {
|
||||
userOp1 = await fillAndSign({
|
||||
sender: aggWallet.address
|
||||
}, walletOwner, entryPoint)
|
||||
sender: aggAccount.address
|
||||
}, accountOwner, entryPoint)
|
||||
userOp2 = await fillAndSign({
|
||||
sender: aggWallet2.address
|
||||
}, walletOwner, entryPoint)
|
||||
sender: aggAccount2.address
|
||||
}, accountOwner, entryPoint)
|
||||
userOp1.signature = '0x'
|
||||
userOp2.signature = '0x'
|
||||
})
|
||||
|
||||
context('create wallet', () => {
|
||||
context('create account', () => {
|
||||
let initCode: BytesLike
|
||||
let addr: string
|
||||
let userOp: UserOperation
|
||||
before(async () => {
|
||||
initCode = await getAggregatedWalletDeployer(entryPoint.address, aggregator.address)
|
||||
initCode = await getAggregatedAccountDeployer(entryPoint.address, aggregator.address)
|
||||
addr = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender)
|
||||
await ethersSigner.sendTransaction({ to: addr, value: parseEther('0.1') })
|
||||
userOp = await fillAndSign({
|
||||
initCode,
|
||||
nonce: 10
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
})
|
||||
it('simulateValidation should return aggregator and its stake', async () => {
|
||||
await aggregator.addStake(entryPoint.address, 3, { value: TWO_ETH })
|
||||
@@ -678,7 +678,7 @@ describe('EntryPoint', function () {
|
||||
expect(aggregationInfo.aggregatorStake).to.equal(TWO_ETH)
|
||||
expect(aggregationInfo.aggregatorUnstakeDelay).to.equal(3)
|
||||
})
|
||||
it('should create wallet in handleOps', async () => {
|
||||
it('should create account in handleOps', async () => {
|
||||
await aggregator.validateUserOpSignature(userOp)
|
||||
const sig = await aggregator.aggregateSignatures([userOp])
|
||||
await entryPoint.handleAggregatedOps([{
|
||||
@@ -694,26 +694,26 @@ describe('EntryPoint', function () {
|
||||
describe('with paymaster (account with no eth)', () => {
|
||||
let paymaster: TestPaymasterAcceptAll
|
||||
let counter: TestCounter
|
||||
let walletExecFromEntryPoint: PopulatedTransaction
|
||||
const wallet2Owner = createWalletOwner()
|
||||
let accountExecFromEntryPoint: PopulatedTransaction
|
||||
const account2Owner = createAccountOwner()
|
||||
|
||||
before(async () => {
|
||||
paymaster = await new TestPaymasterAcceptAll__factory(ethersSigner).deploy(entryPoint.address)
|
||||
await paymaster.addStake(globalUnstakeDelaySec, { value: paymasterStake })
|
||||
counter = await new TestCounter__factory(ethersSigner).deploy()
|
||||
const count = await counter.populateTransaction.count()
|
||||
walletExecFromEntryPoint = await wallet.populateTransaction.execFromEntryPoint(counter.address, 0, count.data!)
|
||||
accountExecFromEntryPoint = await account.populateTransaction.execFromEntryPoint(counter.address, 0, count.data!)
|
||||
})
|
||||
|
||||
it('should fail if paymaster has no deposit', async function () {
|
||||
const op = await fillAndSign({
|
||||
paymasterAndData: paymaster.address,
|
||||
callData: walletExecFromEntryPoint.data,
|
||||
initCode: getWalletDeployer(entryPoint.address, wallet2Owner.address),
|
||||
callData: accountExecFromEntryPoint.data,
|
||||
initCode: getAccountDeployer(entryPoint.address, account2Owner.address),
|
||||
|
||||
verificationGasLimit: 1e6,
|
||||
callGasLimit: 1e6
|
||||
}, wallet2Owner, entryPoint)
|
||||
}, account2Owner, entryPoint)
|
||||
const beneficiaryAddress = createAddress()
|
||||
await expect(entryPoint.handleOps([op], beneficiaryAddress)).to.revertedWith('"paymaster deposit too low"')
|
||||
})
|
||||
@@ -722,9 +722,9 @@ describe('EntryPoint', function () {
|
||||
await paymaster.deposit({ value: ONE_ETH })
|
||||
const op = await fillAndSign({
|
||||
paymasterAndData: paymaster.address,
|
||||
callData: walletExecFromEntryPoint.data,
|
||||
initCode: getWalletDeployer(entryPoint.address, wallet2Owner.address)
|
||||
}, wallet2Owner, entryPoint)
|
||||
callData: accountExecFromEntryPoint.data,
|
||||
initCode: getAccountDeployer(entryPoint.address, account2Owner.address)
|
||||
}, account2Owner, entryPoint)
|
||||
const beneficiaryAddress = createAddress()
|
||||
|
||||
const rcpt = await entryPoint.handleOps([op], beneficiaryAddress).then(async t => t.wait())
|
||||
@@ -735,12 +735,12 @@ describe('EntryPoint', function () {
|
||||
})
|
||||
it('simulate should return paymaster stake and delay', async () => {
|
||||
await paymaster.deposit({ value: ONE_ETH })
|
||||
const anOwner = createWalletOwner()
|
||||
const anOwner = createAccountOwner()
|
||||
|
||||
const op = await fillAndSign({
|
||||
paymasterAndData: paymaster.address,
|
||||
callData: walletExecFromEntryPoint.data,
|
||||
initCode: getWalletDeployer(entryPoint.address, anOwner.address)
|
||||
callData: accountExecFromEntryPoint.data,
|
||||
initCode: getAccountDeployer(entryPoint.address, anOwner.address)
|
||||
}, anOwner, entryPoint)
|
||||
|
||||
const { paymasterInfo } = await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch)
|
||||
@@ -756,42 +756,42 @@ describe('EntryPoint', function () {
|
||||
|
||||
describe('Validation deadline', () => {
|
||||
describe('validateUserOp deadline', function () {
|
||||
let wallet: TestExpiryWallet
|
||||
let account: TestExpiryAccount
|
||||
let now: number
|
||||
before('init wallet with session key', async () => {
|
||||
// create a test wallet. The primary owner is the global ethersSigner, so that we can easily add a temporaryOwner, below
|
||||
wallet = await new TestExpiryWallet__factory(ethersSigner).deploy(entryPoint.address, await ethersSigner.getAddress())
|
||||
await ethersSigner.sendTransaction({ to: wallet.address, value: parseEther('0.1') })
|
||||
before('init account with session key', async () => {
|
||||
// create a test account. The primary owner is the global ethersSigner, so that we can easily add a temporaryOwner, below
|
||||
account = await new TestExpiryAccount__factory(ethersSigner).deploy(entryPoint.address, await ethersSigner.getAddress())
|
||||
await ethersSigner.sendTransaction({ to: account.address, value: parseEther('0.1') })
|
||||
now = await ethers.provider.getBlock('latest').then(block => block.timestamp)
|
||||
})
|
||||
|
||||
it('should accept non-expired owner', async () => {
|
||||
const sessionOwner = createWalletOwner()
|
||||
await wallet.addTemporaryOwner(sessionOwner.address, now + 60)
|
||||
const sessionOwner = createAccountOwner()
|
||||
await account.addTemporaryOwner(sessionOwner.address, now + 60)
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address
|
||||
sender: account.address
|
||||
}, sessionOwner, entryPoint)
|
||||
const { deadline } = await entryPoint.callStatic.simulateValidation(userOp).catch(simulationResultCatch)
|
||||
expect(deadline).to.eql(now + 60)
|
||||
})
|
||||
|
||||
it('should reject expired owner', async () => {
|
||||
const sessionOwner = createWalletOwner()
|
||||
await wallet.addTemporaryOwner(sessionOwner.address, now - 60)
|
||||
const sessionOwner = createAccountOwner()
|
||||
await account.addTemporaryOwner(sessionOwner.address, now - 60)
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address
|
||||
sender: account.address
|
||||
}, sessionOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(userOp)).to.revertedWith('expired')
|
||||
})
|
||||
})
|
||||
|
||||
describe('validatePaymasterUserOp with deadline', function () {
|
||||
let wallet: TestExpiryWallet
|
||||
let account: TestExpiryAccount
|
||||
let paymaster: TestExpirePaymaster
|
||||
let now: number
|
||||
before('init wallet with session key', async () => {
|
||||
// wallet without eth - must be paid by paymaster.
|
||||
wallet = await new TestExpiryWallet__factory(ethersSigner).deploy(entryPoint.address, await ethersSigner.getAddress())
|
||||
before('init account with session key', async () => {
|
||||
// account without eth - must be paid by paymaster.
|
||||
account = await new TestExpiryAccount__factory(ethersSigner).deploy(entryPoint.address, await ethersSigner.getAddress())
|
||||
paymaster = await new TestExpirePaymaster__factory(ethersSigner).deploy(entryPoint.address)
|
||||
await paymaster.addStake(1, { value: paymasterStake })
|
||||
await paymaster.deposit({ value: parseEther('0.1') })
|
||||
@@ -801,7 +801,7 @@ describe('EntryPoint', function () {
|
||||
it('should accept non-expired paymaster request', async () => {
|
||||
const expireTime = defaultAbiCoder.encode(['uint256'], [now + 60])
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, expireTime])
|
||||
}, ethersSigner, entryPoint)
|
||||
const { deadline } = await entryPoint.callStatic.simulateValidation(userOp).catch(simulationResultCatch)
|
||||
@@ -811,7 +811,7 @@ describe('EntryPoint', function () {
|
||||
it('should reject expired paymaster request', async () => {
|
||||
const expireTime = defaultAbiCoder.encode(['uint256'], [now - 60])
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, expireTime])
|
||||
}, ethersSigner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(userOp)).to.revertedWith('expired')
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import {
|
||||
AddressZero,
|
||||
createAddress,
|
||||
createWalletOwner,
|
||||
createAccountOwner,
|
||||
deployEntryPoint,
|
||||
getBalance,
|
||||
HashZero,
|
||||
@@ -52,7 +52,7 @@ describe('Gnosis Proxy', function () {
|
||||
safeSingleton = await new GnosisSafe__factory(ethersSigner).deploy()
|
||||
entryPoint = await deployEntryPoint()
|
||||
manager = await new EIP4337Manager__factory(ethersSigner).deploy(entryPoint.address)
|
||||
owner = createWalletOwner()
|
||||
owner = createAccountOwner()
|
||||
ownerAddress = await owner.getAddress()
|
||||
counter = await new TestCounter__factory(ethersSigner).deploy()
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('Gnosis Proxy', function () {
|
||||
|
||||
const anotherEntryPoint = await new EntryPoint__factory(ethersSigner).deploy()
|
||||
|
||||
await expect(anotherEntryPoint.handleOps([op], beneficiary)).to.revertedWith('wallet: not from entrypoint')
|
||||
await expect(anotherEntryPoint.handleOps([op], beneficiary)).to.revertedWith('account: not from entrypoint')
|
||||
})
|
||||
|
||||
it('should fail on invalid userop', async function () {
|
||||
@@ -91,10 +91,10 @@ describe('Gnosis Proxy', function () {
|
||||
callGasLimit: 1e6,
|
||||
callData: safe_execTxCallData
|
||||
}, owner, entryPoint)
|
||||
await expect(entryPoint.handleOps([op], beneficiary)).to.revertedWith('wallet: invalid nonce')
|
||||
await expect(entryPoint.handleOps([op], beneficiary)).to.revertedWith('account: invalid nonce')
|
||||
|
||||
op.callGasLimit = 1
|
||||
await expect(entryPoint.handleOps([op], beneficiary)).to.revertedWith('wallet: wrong signature')
|
||||
await expect(entryPoint.handleOps([op], beneficiary)).to.revertedWith('account: wrong signature')
|
||||
})
|
||||
|
||||
it('should exec', async function () {
|
||||
@@ -112,7 +112,7 @@ describe('Gnosis Proxy', function () {
|
||||
})
|
||||
|
||||
let counterfactualAddress: string
|
||||
it('should create wallet', async function () {
|
||||
it('should create account', async function () {
|
||||
const ctrCode = hexValue(await new SafeProxy4337__factory(ethersSigner).getDeployTransaction(safeSingleton.address, manager.address, ownerAddress).data!)
|
||||
const initCode = hexConcat([
|
||||
Create2Factory.contractAddress,
|
||||
|
||||
@@ -2,18 +2,18 @@ import { Wallet } from 'ethers'
|
||||
import { ethers } from 'hardhat'
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
SimpleWallet,
|
||||
SimpleWallet__factory,
|
||||
SimpleAccount,
|
||||
SimpleAccount__factory,
|
||||
EntryPoint,
|
||||
TokenPaymaster,
|
||||
TokenPaymaster__factory,
|
||||
TestCounter__factory,
|
||||
SimpleWalletDeployer,
|
||||
SimpleWalletDeployer__factory
|
||||
SimpleAccountDeployer,
|
||||
SimpleAccountDeployer__factory
|
||||
} from '../typechain'
|
||||
import {
|
||||
AddressZero,
|
||||
createWalletOwner,
|
||||
createAccountOwner,
|
||||
fund,
|
||||
getBalance,
|
||||
getTokenBalance,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
checkForBannedOps,
|
||||
createAddress,
|
||||
ONE_ETH,
|
||||
getWalletAddress
|
||||
getAccountAddress
|
||||
} from './testutils'
|
||||
import { fillAndSign } from './UserOp'
|
||||
import { hexConcat, parseEther } from 'ethers/lib/utils'
|
||||
@@ -33,16 +33,16 @@ import { hexValue } from '@ethersproject/bytes'
|
||||
|
||||
describe('EntryPoint with paymaster', function () {
|
||||
let entryPoint: EntryPoint
|
||||
let walletOwner: Wallet
|
||||
let accountOwner: Wallet
|
||||
const ethersSigner = ethers.provider.getSigner()
|
||||
let wallet: SimpleWallet
|
||||
let account: SimpleAccount
|
||||
const beneficiaryAddress = '0x'.padEnd(42, '1')
|
||||
let deployer: SimpleWalletDeployer
|
||||
let deployer: SimpleAccountDeployer
|
||||
|
||||
function getWalletDeployer (entryPoint: string, walletOwner: string): string {
|
||||
function getAccountDeployer (entryPoint: string, accountOwner: string): string {
|
||||
return hexConcat([
|
||||
deployer.address,
|
||||
hexValue(deployer.interface.encodeFunctionData('deployWallet', [entryPoint, walletOwner, 0])!)
|
||||
hexValue(deployer.interface.encodeFunctionData('deployAccount', [entryPoint, accountOwner, 0])!)
|
||||
])
|
||||
}
|
||||
|
||||
@@ -50,11 +50,11 @@ describe('EntryPoint with paymaster', function () {
|
||||
await checkForGeth()
|
||||
|
||||
entryPoint = await deployEntryPoint()
|
||||
deployer = await new SimpleWalletDeployer__factory(ethersSigner).deploy()
|
||||
deployer = await new SimpleAccountDeployer__factory(ethersSigner).deploy()
|
||||
|
||||
walletOwner = createWalletOwner()
|
||||
wallet = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, await walletOwner.getAddress())
|
||||
await fund(wallet)
|
||||
accountOwner = createAccountOwner()
|
||||
account = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, await accountOwner.getAddress())
|
||||
await fund(account)
|
||||
})
|
||||
|
||||
describe('#TokenPaymaster', () => {
|
||||
@@ -92,15 +92,15 @@ describe('EntryPoint with paymaster', function () {
|
||||
describe('#handleOps', () => {
|
||||
let calldata: string
|
||||
before(async () => {
|
||||
const updateEntryPoint = await wallet.populateTransaction.updateEntryPoint(AddressZero).then(tx => tx.data!)
|
||||
calldata = await wallet.populateTransaction.execFromEntryPoint(wallet.address, 0, updateEntryPoint).then(tx => tx.data!)
|
||||
const updateEntryPoint = await account.populateTransaction.updateEntryPoint(AddressZero).then(tx => tx.data!)
|
||||
calldata = await account.populateTransaction.execFromEntryPoint(account.address, 0, updateEntryPoint).then(tx => tx.data!)
|
||||
})
|
||||
it('paymaster should reject if wallet doesn\'t have tokens', async () => {
|
||||
it('paymaster should reject if account doesn\'t have tokens', async () => {
|
||||
const op = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: paymaster.address,
|
||||
callData: calldata
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.handleOps([op], beneficiaryAddress, {
|
||||
gasLimit: 1e7
|
||||
}).catch(rethrow())).to.revertedWith('TokenPaymaster: no balance')
|
||||
@@ -117,10 +117,10 @@ describe('EntryPoint with paymaster', function () {
|
||||
|
||||
it('should reject if account not funded', async () => {
|
||||
const op = await fillAndSign({
|
||||
initCode: getWalletDeployer(entryPoint.address, walletOwner.address),
|
||||
initCode: getAccountDeployer(entryPoint.address, accountOwner.address),
|
||||
verificationGasLimit: 1e7,
|
||||
paymasterAndData: paymaster.address
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.handleOps([op], beneficiaryAddress, {
|
||||
gasLimit: 1e7
|
||||
}).catch(rethrow())).to.revertedWith('TokenPaymaster: no balance')
|
||||
@@ -128,11 +128,11 @@ describe('EntryPoint with paymaster', function () {
|
||||
|
||||
it('should succeed to create account with tokens', async () => {
|
||||
createOp = await fillAndSign({
|
||||
initCode: getWalletDeployer(entryPoint.address, walletOwner.address),
|
||||
initCode: getAccountDeployer(entryPoint.address, accountOwner.address),
|
||||
verificationGasLimit: 1e7,
|
||||
paymasterAndData: paymaster.address,
|
||||
nonce: 0
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
const preAddr = createOp.sender
|
||||
await paymaster.mintTokens(preAddr, parseEther('1'))
|
||||
@@ -156,8 +156,8 @@ describe('EntryPoint with paymaster', function () {
|
||||
const ethRedeemed = await getBalance(beneficiaryAddress)
|
||||
expect(ethRedeemed).to.above(100000)
|
||||
|
||||
const walletAddr = getWalletAddress(entryPoint.address, walletOwner.address)
|
||||
const postBalance = await getTokenBalance(paymaster, walletAddr)
|
||||
const accountAddr = getAccountAddress(entryPoint.address, accountOwner.address)
|
||||
const postBalance = await getTokenBalance(paymaster, accountAddr)
|
||||
expect(1e18 - postBalance).to.above(10000)
|
||||
})
|
||||
|
||||
@@ -175,29 +175,29 @@ describe('EntryPoint with paymaster', function () {
|
||||
const beneficiaryAddress = createAddress()
|
||||
const testCounter = await new TestCounter__factory(ethersSigner).deploy()
|
||||
const justEmit = testCounter.interface.encodeFunctionData('justemit')
|
||||
const execFromSingleton = wallet.interface.encodeFunctionData('execFromEntryPoint', [testCounter.address, 0, justEmit])
|
||||
const execFromSingleton = account.interface.encodeFunctionData('execFromEntryPoint', [testCounter.address, 0, justEmit])
|
||||
|
||||
const ops: UserOperation[] = []
|
||||
const wallets: SimpleWallet[] = []
|
||||
const accounts: SimpleAccount[] = []
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const aWallet = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, await walletOwner.getAddress())
|
||||
await paymaster.mintTokens(aWallet.address, parseEther('1'))
|
||||
const aAccount = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, await accountOwner.getAddress())
|
||||
await paymaster.mintTokens(aAccount.address, parseEther('1'))
|
||||
const op = await fillAndSign({
|
||||
sender: aWallet.address,
|
||||
sender: aAccount.address,
|
||||
callData: execFromSingleton,
|
||||
paymasterAndData: paymaster.address
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
wallets.push(aWallet)
|
||||
accounts.push(aAccount)
|
||||
ops.push(op)
|
||||
}
|
||||
|
||||
const pmBalanceBefore = await paymaster.balanceOf(paymaster.address).then(b => b.toNumber())
|
||||
await entryPoint.handleOps(ops, beneficiaryAddress).then(async tx => tx.wait())
|
||||
const totalPaid = await paymaster.balanceOf(paymaster.address).then(b => b.toNumber()) - pmBalanceBefore
|
||||
for (let i = 0; i < wallets.length; i++) {
|
||||
const bal = await getTokenBalance(paymaster, wallets[i].address)
|
||||
for (let i = 0; i < accounts.length; i++) {
|
||||
const bal = await getTokenBalance(paymaster, accounts[i].address)
|
||||
const paid = parseEther('1').sub(bal.toString()).toNumber()
|
||||
|
||||
// roughly each account should pay 1/4th of total price, within 15%
|
||||
@@ -206,50 +206,50 @@ describe('EntryPoint with paymaster', function () {
|
||||
}
|
||||
})
|
||||
|
||||
// wallets attempt to grief paymaster: both wallets pass validatePaymasterUserOp (since they have enough balance)
|
||||
// but the execution of wallet1 drains wallet2.
|
||||
// accounts attempt to grief paymaster: both accounts pass validatePaymasterUserOp (since they have enough balance)
|
||||
// but the execution of account1 drains account2.
|
||||
// as a result, the postOp of the paymaster reverts, and cause entire handleOp to revert.
|
||||
describe('grief attempt', () => {
|
||||
let wallet2: SimpleWallet
|
||||
let account2: SimpleAccount
|
||||
let approveCallData: string
|
||||
before(async () => {
|
||||
wallet2 = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, await walletOwner.getAddress())
|
||||
await paymaster.mintTokens(wallet2.address, parseEther('1'))
|
||||
await paymaster.mintTokens(wallet.address, parseEther('1'))
|
||||
approveCallData = paymaster.interface.encodeFunctionData('approve', [wallet.address, ethers.constants.MaxUint256])
|
||||
// need to call approve from wallet2. use paymaster for that
|
||||
account2 = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, await accountOwner.getAddress())
|
||||
await paymaster.mintTokens(account2.address, parseEther('1'))
|
||||
await paymaster.mintTokens(account.address, parseEther('1'))
|
||||
approveCallData = paymaster.interface.encodeFunctionData('approve', [account.address, ethers.constants.MaxUint256])
|
||||
// need to call approve from account2. use paymaster for that
|
||||
const approveOp = await fillAndSign({
|
||||
sender: wallet2.address,
|
||||
callData: wallet2.interface.encodeFunctionData('execFromEntryPoint', [paymaster.address, 0, approveCallData]),
|
||||
sender: account2.address,
|
||||
callData: account2.interface.encodeFunctionData('execFromEntryPoint', [paymaster.address, 0, approveCallData]),
|
||||
paymasterAndData: paymaster.address
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
await entryPoint.handleOps([approveOp], beneficiaryAddress)
|
||||
expect(await paymaster.allowance(wallet2.address, wallet.address)).to.eq(ethers.constants.MaxUint256)
|
||||
expect(await paymaster.allowance(account2.address, account.address)).to.eq(ethers.constants.MaxUint256)
|
||||
})
|
||||
|
||||
it('griefing attempt should cause handleOp to revert', async () => {
|
||||
// wallet1 is approved to withdraw going to withdraw wallet2's balance
|
||||
// account1 is approved to withdraw going to withdraw account2's balance
|
||||
|
||||
const wallet2Balance = await paymaster.balanceOf(wallet2.address)
|
||||
const transferCost = parseEther('1').sub(wallet2Balance)
|
||||
const withdrawAmount = wallet2Balance.sub(transferCost.mul(0))
|
||||
const withdrawTokens = paymaster.interface.encodeFunctionData('transferFrom', [wallet2.address, wallet.address, withdrawAmount])
|
||||
// const withdrawTokens = paymaster.interface.encodeFunctionData('transfer', [wallet.address, parseEther('0.1')])
|
||||
const execFromEntryPoint = wallet.interface.encodeFunctionData('execFromEntryPoint', [paymaster.address, 0, withdrawTokens])
|
||||
const account2Balance = await paymaster.balanceOf(account2.address)
|
||||
const transferCost = parseEther('1').sub(account2Balance)
|
||||
const withdrawAmount = account2Balance.sub(transferCost.mul(0))
|
||||
const withdrawTokens = paymaster.interface.encodeFunctionData('transferFrom', [account2.address, account.address, withdrawAmount])
|
||||
// const withdrawTokens = paymaster.interface.encodeFunctionData('transfer', [account.address, parseEther('0.1')])
|
||||
const execFromEntryPoint = account.interface.encodeFunctionData('execFromEntryPoint', [paymaster.address, 0, withdrawTokens])
|
||||
|
||||
const userOp1 = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
callData: execFromEntryPoint,
|
||||
paymasterAndData: paymaster.address
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
// wallet2's operation is unimportant, as it is going to be reverted - but the paymaster will have to pay for it..
|
||||
// account2's operation is unimportant, as it is going to be reverted - but the paymaster will have to pay for it..
|
||||
const userOp2 = await fillAndSign({
|
||||
sender: wallet2.address,
|
||||
sender: account2.address,
|
||||
callData: execFromEntryPoint,
|
||||
paymasterAndData: paymaster.address,
|
||||
callGasLimit: 1e6
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
|
||||
await expect(
|
||||
entryPoint.handleOps([
|
||||
|
||||
@@ -2,22 +2,22 @@ import { Wallet } from 'ethers'
|
||||
import { ethers } from 'hardhat'
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
SimpleWallet,
|
||||
SimpleWalletDeployer__factory,
|
||||
SimpleWallet__factory,
|
||||
SimpleAccount,
|
||||
SimpleAccountDeployer__factory,
|
||||
SimpleAccount__factory,
|
||||
TestUtil,
|
||||
TestUtil__factory
|
||||
} from '../typechain'
|
||||
import { AddressZero, createAddress, createWalletOwner, getBalance, isDeployed, ONE_ETH } from './testutils'
|
||||
import { AddressZero, createAddress, createAccountOwner, getBalance, isDeployed, ONE_ETH } from './testutils'
|
||||
import { fillUserOpDefaults, getUserOpHash, packUserOp, signUserOp } from './UserOp'
|
||||
import { parseEther } from 'ethers/lib/utils'
|
||||
import { UserOperation } from './UserOperation'
|
||||
|
||||
describe('SimpleWallet', function () {
|
||||
describe('SimpleAccount', function () {
|
||||
const entryPoint = '0x'.padEnd(42, '2')
|
||||
let accounts: string[]
|
||||
let testUtil: TestUtil
|
||||
let walletOwner: Wallet
|
||||
let accountOwner: Wallet
|
||||
const ethersSigner = ethers.provider.getSigner()
|
||||
|
||||
before(async function () {
|
||||
@@ -25,17 +25,17 @@ describe('SimpleWallet', function () {
|
||||
// ignore in geth.. this is just a sanity test. should be refactored to use a single-account mode..
|
||||
if (accounts.length < 2) this.skip()
|
||||
testUtil = await new TestUtil__factory(ethersSigner).deploy()
|
||||
walletOwner = createWalletOwner()
|
||||
accountOwner = createAccountOwner()
|
||||
})
|
||||
|
||||
it('owner should be able to call transfer', async () => {
|
||||
const wallet = await new SimpleWallet__factory(ethers.provider.getSigner()).deploy(entryPoint, accounts[0])
|
||||
await ethersSigner.sendTransaction({ from: accounts[0], to: wallet.address, value: parseEther('2') })
|
||||
await wallet.transfer(accounts[2], ONE_ETH)
|
||||
const account = await new SimpleAccount__factory(ethers.provider.getSigner()).deploy(entryPoint, accounts[0])
|
||||
await ethersSigner.sendTransaction({ from: accounts[0], to: account.address, value: parseEther('2') })
|
||||
await account.transfer(accounts[2], ONE_ETH)
|
||||
})
|
||||
it('other account should not be able to call transfer', async () => {
|
||||
const wallet = await new SimpleWallet__factory(ethers.provider.getSigner()).deploy(entryPoint, accounts[0])
|
||||
await expect(wallet.connect(ethers.provider.getSigner(1)).transfer(accounts[2], ONE_ETH))
|
||||
const account = await new SimpleAccount__factory(ethers.provider.getSigner()).deploy(entryPoint, accounts[0])
|
||||
await expect(account.connect(ethers.provider.getSigner(1)).transfer(accounts[2], ONE_ETH))
|
||||
.to.be.revertedWith('only owner')
|
||||
})
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('SimpleWallet', function () {
|
||||
})
|
||||
|
||||
describe('#validateUserOp', () => {
|
||||
let wallet: SimpleWallet
|
||||
let account: SimpleAccount
|
||||
let userOp: UserOperation
|
||||
let userOpHash: string
|
||||
let preBalance: number
|
||||
@@ -57,54 +57,54 @@ describe('SimpleWallet', function () {
|
||||
before(async () => {
|
||||
// that's the account of ethersSigner
|
||||
const entryPoint = accounts[2]
|
||||
wallet = await new SimpleWallet__factory(await ethers.getSigner(entryPoint)).deploy(entryPoint, walletOwner.address)
|
||||
await ethersSigner.sendTransaction({ from: accounts[0], to: wallet.address, value: parseEther('0.2') })
|
||||
account = await new SimpleAccount__factory(await ethers.getSigner(entryPoint)).deploy(entryPoint, accountOwner.address)
|
||||
await ethersSigner.sendTransaction({ from: accounts[0], to: account.address, value: parseEther('0.2') })
|
||||
const callGasLimit = 200000
|
||||
const verificationGasLimit = 100000
|
||||
const maxFeePerGas = 3e9
|
||||
const chainId = await ethers.provider.getNetwork().then(net => net.chainId)
|
||||
|
||||
userOp = signUserOp(fillUserOpDefaults({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
callGasLimit,
|
||||
verificationGasLimit,
|
||||
maxFeePerGas
|
||||
}), walletOwner, entryPoint, chainId)
|
||||
}), accountOwner, entryPoint, chainId)
|
||||
|
||||
userOpHash = await getUserOpHash(userOp, entryPoint, chainId)
|
||||
|
||||
expectedPay = actualGasPrice * (callGasLimit + verificationGasLimit)
|
||||
|
||||
preBalance = await getBalance(wallet.address)
|
||||
const ret = await wallet.validateUserOp(userOp, userOpHash, AddressZero, expectedPay, { gasPrice: actualGasPrice })
|
||||
preBalance = await getBalance(account.address)
|
||||
const ret = await account.validateUserOp(userOp, userOpHash, AddressZero, expectedPay, { gasPrice: actualGasPrice })
|
||||
await ret.wait()
|
||||
})
|
||||
|
||||
it('should pay', async () => {
|
||||
const postBalance = await getBalance(wallet.address)
|
||||
const postBalance = await getBalance(account.address)
|
||||
expect(preBalance - postBalance).to.eql(expectedPay)
|
||||
})
|
||||
|
||||
it('should increment nonce', async () => {
|
||||
expect(await wallet.nonce()).to.equal(1)
|
||||
expect(await account.nonce()).to.equal(1)
|
||||
})
|
||||
it('should reject same TX on nonce error', async () => {
|
||||
await expect(wallet.validateUserOp(userOp, userOpHash, AddressZero, 0)).to.revertedWith('invalid nonce')
|
||||
await expect(account.validateUserOp(userOp, userOpHash, AddressZero, 0)).to.revertedWith('invalid nonce')
|
||||
})
|
||||
it('should reject tx with wrong signature', async () => {
|
||||
// validateUserOp doesn't check the actual UserOp for the signature, but relies on the userOpHash given by
|
||||
// the entrypoint
|
||||
const wrongUserOpHash = ethers.constants.HashZero
|
||||
await expect(wallet.validateUserOp(userOp, wrongUserOpHash, AddressZero, 0)).to.revertedWith('wallet: wrong signature')
|
||||
await expect(account.validateUserOp(userOp, wrongUserOpHash, AddressZero, 0)).to.revertedWith('account: wrong signature')
|
||||
})
|
||||
})
|
||||
context('SimpleWalletDeployer', () => {
|
||||
context('SimpleAccountDeployer', () => {
|
||||
it('sanity: check deployer', async () => {
|
||||
const ownerAddr = createAddress()
|
||||
const deployer = await new SimpleWalletDeployer__factory(ethersSigner).deploy()
|
||||
const target = await deployer.callStatic.deployWallet(entryPoint, ownerAddr, 1234)
|
||||
const deployer = await new SimpleAccountDeployer__factory(ethersSigner).deploy()
|
||||
const target = await deployer.callStatic.deployAccount(entryPoint, ownerAddr, 1234)
|
||||
expect(await isDeployed(target)).to.eq(false)
|
||||
await deployer.deployWallet(entryPoint, ownerAddr, 1234)
|
||||
await deployer.deployAccount(entryPoint, ownerAddr, 1234)
|
||||
expect(await isDeployed(target)).to.eq(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ethers } from 'hardhat'
|
||||
import { arrayify, getCreate2Address, hexConcat, keccak256, parseEther } from 'ethers/lib/utils'
|
||||
import { BigNumber, BigNumberish, Contract, ContractReceipt, Wallet } from 'ethers'
|
||||
import { EntryPoint, EntryPoint__factory, IEntryPoint, IERC20, SimpleWallet__factory, TestAggregatedWallet__factory } from '../typechain'
|
||||
import { EntryPoint, EntryPoint__factory, IEntryPoint, IERC20, SimpleAccount__factory, TestAggregatedAccount__factory } from '../typechain'
|
||||
import { BytesLike, hexValue } from '@ethersproject/bytes'
|
||||
import { expect } from 'chai'
|
||||
import { Create2Factory } from '../src/Create2Factory'
|
||||
@@ -49,14 +49,14 @@ export async function getTokenBalance (token: IERC20, address: string): Promise<
|
||||
let counter = 0
|
||||
|
||||
// create non-random account, so gas calculations are deterministic
|
||||
export function createWalletOwner (): Wallet {
|
||||
export function createAccountOwner (): Wallet {
|
||||
const privateKey = keccak256(Buffer.from(arrayify(BigNumber.from(++counter))))
|
||||
return new ethers.Wallet(privateKey, ethers.provider)
|
||||
// return new ethers.Wallet('0x'.padEnd(66, privkeyBase), ethers.provider);
|
||||
}
|
||||
|
||||
export function createAddress (): string {
|
||||
return createWalletOwner().address
|
||||
return createAccountOwner().address
|
||||
}
|
||||
|
||||
export function callDataCost (data: string): number {
|
||||
@@ -80,34 +80,34 @@ export async function calcGasUsage (rcpt: ContractReceipt, entryPoint: EntryPoin
|
||||
return { actualGasCost }
|
||||
}
|
||||
|
||||
// helper function to create a deployer (initCode) call to our wallet. relies on the global "create2Deployer"
|
||||
// helper function to create a deployer (initCode) call to our account. relies on the global "create2Deployer"
|
||||
// note that this is a very naive deployer: merely calls "create2", which means entire constructor code is passed
|
||||
// with each deployment. a better deployer will only receive the constructor parameters.
|
||||
export function getWalletDeployer (entryPoint: string, owner: string): BytesLike {
|
||||
const walletCtr = new SimpleWallet__factory(ethers.provider.getSigner()).getDeployTransaction(entryPoint, owner).data!
|
||||
export function getAccountDeployer (entryPoint: string, owner: string): BytesLike {
|
||||
const accountCtr = new SimpleAccount__factory(ethers.provider.getSigner()).getDeployTransaction(entryPoint, owner).data!
|
||||
const factory = new Create2Factory(ethers.provider)
|
||||
const initCallData = factory.getDeployTransactionCallData(hexValue(walletCtr), 0)
|
||||
const initCallData = factory.getDeployTransactionCallData(hexValue(accountCtr), 0)
|
||||
return hexConcat([
|
||||
Create2Factory.contractAddress,
|
||||
initCallData
|
||||
])
|
||||
}
|
||||
|
||||
export async function getAggregatedWalletDeployer (entryPoint: string, aggregator: string): Promise<BytesLike> {
|
||||
const walletCtr = await new TestAggregatedWallet__factory(ethers.provider.getSigner()).getDeployTransaction(entryPoint, aggregator).data!
|
||||
export async function getAggregatedAccountDeployer (entryPoint: string, aggregator: string): Promise<BytesLike> {
|
||||
const accountCtr = await new TestAggregatedAccount__factory(ethers.provider.getSigner()).getDeployTransaction(entryPoint, aggregator).data!
|
||||
|
||||
const factory = new Create2Factory(ethers.provider)
|
||||
const initCallData = factory.getDeployTransactionCallData(hexValue(walletCtr), 0)
|
||||
const initCallData = factory.getDeployTransactionCallData(hexValue(accountCtr), 0)
|
||||
return hexConcat([
|
||||
Create2Factory.contractAddress,
|
||||
initCallData
|
||||
])
|
||||
}
|
||||
|
||||
// given the parameters as WalletDeployer, return the resulting "counterfactual address" that it would create.
|
||||
export function getWalletAddress (entryPoint: string, owner: string): string {
|
||||
const walletCtr = new SimpleWallet__factory(ethers.provider.getSigner()).getDeployTransaction(entryPoint, owner).data!
|
||||
return getCreate2Address(Create2Factory.contractAddress, HashZero, keccak256(hexValue(walletCtr)))
|
||||
// given the parameters as AccountDeployer, return the resulting "counterfactual address" that it would create.
|
||||
export function getAccountAddress (entryPoint: string, owner: string): string {
|
||||
const accountCtr = new SimpleAccount__factory(ethers.provider.getSigner()).getDeployTransaction(entryPoint, owner).data!
|
||||
return getCreate2Address(Create2Factory.contractAddress, HashZero, keccak256(hexValue(accountCtr)))
|
||||
}
|
||||
|
||||
const panicCodes: { [key: number]: string } = {
|
||||
@@ -214,9 +214,9 @@ export async function checkForBannedOps (txHash: string, checkPaymaster: boolean
|
||||
const logs = tx.structLogs
|
||||
const blockHash = logs.map((op, index) => ({ op: op.op, index })).filter(op => op.op === 'NUMBER')
|
||||
expect(blockHash.length).to.equal(1, 'expected exactly 1 call to NUMBER (Just before validatePaymasterUserOp)')
|
||||
const validateWalletOps = logs.slice(0, blockHash[0].index - 1)
|
||||
const validateAccountOps = logs.slice(0, blockHash[0].index - 1)
|
||||
const validatePaymasterOps = logs.slice(blockHash[0].index + 1)
|
||||
const ops = validateWalletOps.filter(log => log.depth > 1).map(log => log.op)
|
||||
const ops = validateAccountOps.filter(log => log.depth > 1).map(log => log.op)
|
||||
const paymasterOps = validatePaymasterOps.filter(log => log.depth > 1).map(log => log.op)
|
||||
|
||||
expect(ops).to.include('POP', 'not a valid ops list: ' + JSON.stringify(ops)) // sanity
|
||||
|
||||
@@ -2,14 +2,14 @@ import { Wallet } from 'ethers'
|
||||
import { ethers } from 'hardhat'
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
SimpleWallet,
|
||||
SimpleWallet__factory,
|
||||
SimpleAccount,
|
||||
SimpleAccount__factory,
|
||||
EntryPoint,
|
||||
VerifyingPaymaster,
|
||||
VerifyingPaymaster__factory
|
||||
} from '../typechain'
|
||||
import {
|
||||
createWalletOwner,
|
||||
createAccountOwner,
|
||||
deployEntryPoint, simulationResultCatch
|
||||
} from './testutils'
|
||||
import { fillAndSign } from './UserOp'
|
||||
@@ -17,51 +17,51 @@ import { arrayify, hexConcat, parseEther } from 'ethers/lib/utils'
|
||||
|
||||
describe('EntryPoint with VerifyingPaymaster', function () {
|
||||
let entryPoint: EntryPoint
|
||||
let walletOwner: Wallet
|
||||
let accountOwner: Wallet
|
||||
const ethersSigner = ethers.provider.getSigner()
|
||||
let wallet: SimpleWallet
|
||||
let account: SimpleAccount
|
||||
let offchainSigner: Wallet
|
||||
|
||||
let paymaster: VerifyingPaymaster
|
||||
before(async function () {
|
||||
entryPoint = await deployEntryPoint()
|
||||
|
||||
offchainSigner = createWalletOwner()
|
||||
walletOwner = createWalletOwner()
|
||||
offchainSigner = createAccountOwner()
|
||||
accountOwner = createAccountOwner()
|
||||
|
||||
paymaster = await new VerifyingPaymaster__factory(ethersSigner).deploy(entryPoint.address, offchainSigner.address)
|
||||
await paymaster.addStake(1, { value: parseEther('2') })
|
||||
await entryPoint.depositTo(paymaster.address, { value: parseEther('1') })
|
||||
wallet = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, walletOwner.address)
|
||||
account = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, accountOwner.address)
|
||||
})
|
||||
|
||||
describe('#validatePaymasterUserOp', () => {
|
||||
it('should reject on no signature', async () => {
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, '0x1234'])
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(userOp)).to.be.revertedWith('invalid signature length in paymasterAndData')
|
||||
})
|
||||
|
||||
it('should reject on invalid signature', async () => {
|
||||
const userOp = await fillAndSign({
|
||||
sender: wallet.address,
|
||||
sender: account.address,
|
||||
paymasterAndData: hexConcat([paymaster.address, '0x' + '1c'.repeat(65)])
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
await expect(entryPoint.callStatic.simulateValidation(userOp)).to.be.revertedWith('ECDSA: invalid signature')
|
||||
})
|
||||
|
||||
it('succeed with valid signature', async () => {
|
||||
const userOp1 = await fillAndSign({
|
||||
sender: wallet.address
|
||||
}, walletOwner, entryPoint)
|
||||
sender: account.address
|
||||
}, accountOwner, entryPoint)
|
||||
const hash = await paymaster.getHash(userOp1)
|
||||
const sig = await offchainSigner.signMessage(arrayify(hash))
|
||||
const userOp = await fillAndSign({
|
||||
...userOp1,
|
||||
paymasterAndData: hexConcat([paymaster.address, sig])
|
||||
}, walletOwner, entryPoint)
|
||||
}, accountOwner, entryPoint)
|
||||
await entryPoint.callStatic.simulateValidation(userOp).catch(simulationResultCatch)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
BLSOpen__factory,
|
||||
BLSSignatureAggregator,
|
||||
BLSSignatureAggregator__factory,
|
||||
BLSWallet,
|
||||
BLSWallet__factory,
|
||||
BLSAccount,
|
||||
BLSAccount__factory,
|
||||
EntryPoint
|
||||
} from '../typechain'
|
||||
import { ethers } from 'hardhat'
|
||||
@@ -16,10 +16,10 @@ import { keccak256 } from 'ethereumjs-util'
|
||||
import { hashToPoint } from '@thehubbleproject/bls/dist/mcl'
|
||||
import { BigNumber } from 'ethers'
|
||||
import { BytesLike, hexValue } from '@ethersproject/bytes'
|
||||
import { BLSWalletDeployer } from '../typechain/contracts/bls/BLSWallet.sol'
|
||||
import { BLSWalletDeployer__factory } from '../typechain/factories/contracts/bls/BLSWallet.sol'
|
||||
import { BLSAccountDeployer } from '../typechain/contracts/bls/BLSAccount.sol'
|
||||
import { BLSAccountDeployer__factory } from '../typechain/factories/contracts/bls/BLSAccount.sol'
|
||||
|
||||
describe('bls wallet', function () {
|
||||
describe('bls account', function () {
|
||||
this.timeout(20000)
|
||||
const BLS_DOMAIN = arrayify(keccak256(Buffer.from('eip4337.bls.domain')))
|
||||
const etherSigner = ethers.provider.getSigner()
|
||||
@@ -28,9 +28,9 @@ describe('bls wallet', function () {
|
||||
let signer2: any
|
||||
let blsAgg: BLSSignatureAggregator
|
||||
let entrypoint: EntryPoint
|
||||
let wallet1: BLSWallet
|
||||
let wallet2: BLSWallet
|
||||
let walletDeployer: BLSWalletDeployer
|
||||
let account1: BLSAccount
|
||||
let account2: BLSAccount
|
||||
let accountDeployer: BLSAccountDeployer
|
||||
before(async () => {
|
||||
entrypoint = await deployEntryPoint()
|
||||
const BLSOpenLib = await new BLSOpen__factory(ethers.provider.getSigner()).deploy()
|
||||
@@ -43,10 +43,10 @@ describe('bls wallet', function () {
|
||||
signer1 = fact.getSigner(arrayify(BLS_DOMAIN), '0x01')
|
||||
signer2 = fact.getSigner(arrayify(BLS_DOMAIN), '0x02')
|
||||
|
||||
walletDeployer = await new BLSWalletDeployer__factory(etherSigner).deploy()
|
||||
accountDeployer = await new BLSAccountDeployer__factory(etherSigner).deploy()
|
||||
|
||||
wallet1 = await new BLSWallet__factory(etherSigner).deploy(entrypoint.address, blsAgg.address, signer1.pubkey)
|
||||
wallet2 = await new BLSWallet__factory(etherSigner).deploy(entrypoint.address, blsAgg.address, signer2.pubkey)
|
||||
account1 = await new BLSAccount__factory(etherSigner).deploy(entrypoint.address, blsAgg.address, signer1.pubkey)
|
||||
account2 = await new BLSAccount__factory(etherSigner).deploy(entrypoint.address, blsAgg.address, signer2.pubkey)
|
||||
})
|
||||
|
||||
it('#getTrailingPublicKey', async () => {
|
||||
@@ -66,7 +66,7 @@ describe('bls wallet', function () {
|
||||
|
||||
it('#userOpToMessage', async () => {
|
||||
const userOp1 = await fillUserOp({
|
||||
sender: wallet1.address
|
||||
sender: account1.address
|
||||
}, entrypoint)
|
||||
const requestHash = await blsAgg.getUserOpHash(userOp1)
|
||||
const solPoint: BigNumber[] = await blsAgg.userOpToMessage(userOp1)
|
||||
@@ -76,7 +76,7 @@ describe('bls wallet', function () {
|
||||
|
||||
it('#validateUserOpSignature', async () => {
|
||||
const userOp1 = await fillUserOp({
|
||||
sender: wallet1.address
|
||||
sender: account1.address
|
||||
}, entrypoint)
|
||||
const requestHash = await blsAgg.getUserOpHash(userOp1)
|
||||
|
||||
@@ -95,14 +95,14 @@ describe('bls wallet', function () {
|
||||
// yes, it does take long on hardhat, but quick on geth.
|
||||
this.timeout(30000)
|
||||
const userOp1 = await fillUserOp({
|
||||
sender: wallet1.address
|
||||
sender: account1.address
|
||||
}, entrypoint)
|
||||
const requestHash = await blsAgg.getUserOpHash(userOp1)
|
||||
const sig1 = signer1.sign(requestHash)
|
||||
userOp1.signature = hexConcat(sig1)
|
||||
|
||||
const userOp2 = await fillUserOp({
|
||||
sender: wallet2.address
|
||||
sender: account2.address
|
||||
}, entrypoint)
|
||||
const requestHash2 = await blsAgg.getUserOpHash(userOp2)
|
||||
const sig2 = signer2.sign(requestHash2)
|
||||
@@ -132,8 +132,8 @@ describe('bls wallet', function () {
|
||||
before(async () => {
|
||||
signer3 = fact.getSigner(arrayify(BLS_DOMAIN), '0x03')
|
||||
initCode = hexConcat([
|
||||
walletDeployer.address,
|
||||
walletDeployer.interface.encodeFunctionData('deployWallet', [entrypoint.address, blsAgg.address, 0, signer3.pubkey])
|
||||
accountDeployer.address,
|
||||
accountDeployer.interface.encodeFunctionData('deployAccount', [entrypoint.address, blsAgg.address, 0, signer3.pubkey])
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -4,21 +4,21 @@ import { describe } from 'mocha'
|
||||
import { BigNumber, Wallet } from 'ethers'
|
||||
import { expect } from 'chai'
|
||||
import {
|
||||
SimpleWallet,
|
||||
SimpleWallet__factory,
|
||||
SimpleAccount,
|
||||
SimpleAccount__factory,
|
||||
EntryPoint,
|
||||
TestCounter,
|
||||
TestCounter__factory
|
||||
} from '../typechain'
|
||||
import {
|
||||
createWalletOwner,
|
||||
createAccountOwner,
|
||||
fund,
|
||||
checkForGeth,
|
||||
rethrow,
|
||||
getWalletDeployer,
|
||||
getAccountDeployer,
|
||||
tonumber,
|
||||
deployEntryPoint,
|
||||
callDataCost, createAddress, getWalletAddress, simulationResultCatch
|
||||
callDataCost, createAddress, getAccountAddress, simulationResultCatch
|
||||
} from './testutils'
|
||||
import { fillAndSign } from './UserOp'
|
||||
import { UserOperation } from './UserOperation'
|
||||
@@ -37,8 +37,8 @@ describe('Batch gas testing', function () {
|
||||
const ethersSigner = ethers.provider.getSigner()
|
||||
let entryPoint: EntryPoint
|
||||
|
||||
let walletOwner: Wallet
|
||||
let wallet: SimpleWallet
|
||||
let accountOwner: Wallet
|
||||
let account: SimpleAccount
|
||||
|
||||
const results: Array<() => void> = []
|
||||
before(async function () {
|
||||
@@ -47,9 +47,9 @@ describe('Batch gas testing', function () {
|
||||
await checkForGeth()
|
||||
entryPoint = await deployEntryPoint()
|
||||
// static call must come from address zero, to validate it can only be called off-chain.
|
||||
walletOwner = createWalletOwner()
|
||||
wallet = await new SimpleWallet__factory(ethersSigner).deploy(entryPoint.address, await walletOwner.getAddress())
|
||||
await fund(wallet)
|
||||
accountOwner = createAccountOwner()
|
||||
account = await new SimpleAccount__factory(ethersSigner).deploy(entryPoint.address, await accountOwner.getAddress())
|
||||
await fund(account)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
@@ -71,18 +71,18 @@ describe('Batch gas testing', function () {
|
||||
* attempt big batch.
|
||||
*/
|
||||
let counter: TestCounter
|
||||
let walletExecCounterFromEntryPoint: PopulatedTransaction
|
||||
let accountExecCounterFromEntryPoint: PopulatedTransaction
|
||||
let execCounterCount: PopulatedTransaction
|
||||
const beneficiaryAddress = createAddress()
|
||||
|
||||
before(async () => {
|
||||
counter = await new TestCounter__factory(ethersSigner).deploy()
|
||||
const count = await counter.populateTransaction.count()
|
||||
execCounterCount = await wallet.populateTransaction.exec(counter.address, 0, count.data!)
|
||||
walletExecCounterFromEntryPoint = await wallet.populateTransaction.execFromEntryPoint(counter.address, 0, count.data!)
|
||||
execCounterCount = await account.populateTransaction.exec(counter.address, 0, count.data!)
|
||||
accountExecCounterFromEntryPoint = await account.populateTransaction.execFromEntryPoint(counter.address, 0, count.data!)
|
||||
})
|
||||
|
||||
const wallets: Array<{ w: string, owner: Wallet }> = []
|
||||
const accounts: Array<{ w: string, owner: Wallet }> = []
|
||||
|
||||
it('batch of create', async () => {
|
||||
const ops: UserOperation[] = []
|
||||
@@ -91,15 +91,15 @@ describe('Batch gas testing', function () {
|
||||
let opsGasCollected = 0
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
while (++count) {
|
||||
const walletOwner1 = createWalletOwner()
|
||||
const wallet1 = getWalletAddress(entryPoint.address, walletOwner1.address)
|
||||
await fund(wallet1, '0.5')
|
||||
const accountOwner1 = createAccountOwner()
|
||||
const account1 = getAccountAddress(entryPoint.address, accountOwner1.address)
|
||||
await fund(account1, '0.5')
|
||||
const op1 = await fillAndSign({
|
||||
initCode: getWalletDeployer(entryPoint.address, walletOwner1.address),
|
||||
initCode: getAccountDeployer(entryPoint.address, accountOwner1.address),
|
||||
nonce: 0,
|
||||
// callData: walletExecCounterFromEntryPoint.data,
|
||||
// callData: accountExecCounterFromEntryPoint.data,
|
||||
maxPriorityFeePerGas: 1e9
|
||||
}, walletOwner1, entryPoint)
|
||||
}, accountOwner1, entryPoint)
|
||||
// requests are the same, so estimate is the same too.
|
||||
const { preOpGas } = await entryPoint.callStatic.simulateValidation(op1, { gasPrice: 1e9 }).catch(simulationResultCatch)
|
||||
const txgas = BigNumber.from(preOpGas).add(op1.callGasLimit).toNumber()
|
||||
@@ -110,8 +110,8 @@ describe('Batch gas testing', function () {
|
||||
}
|
||||
opsGasCollected += txgas
|
||||
ops.push(op1)
|
||||
wallets.push({ owner: walletOwner1, w: wallet1 })
|
||||
if (wallets.length >= maxCount) break
|
||||
accounts.push({ owner: accountOwner1, w: account1 })
|
||||
if (accounts.length >= maxCount) break
|
||||
}
|
||||
|
||||
await call_handleOps_and_stats('Create', ops, count)
|
||||
@@ -119,15 +119,15 @@ describe('Batch gas testing', function () {
|
||||
|
||||
it('batch of tx', async function () {
|
||||
this.timeout(30000)
|
||||
if (wallets.length === 0) {
|
||||
if (accounts.length === 0) {
|
||||
this.skip()
|
||||
}
|
||||
|
||||
const ops: UserOperation[] = []
|
||||
for (const { w, owner } of wallets) {
|
||||
for (const { w, owner } of accounts) {
|
||||
const op1 = await fillAndSign({
|
||||
sender: w,
|
||||
callData: walletExecCounterFromEntryPoint.data,
|
||||
callData: accountExecCounterFromEntryPoint.data,
|
||||
maxPriorityFeePerGas: 1e9,
|
||||
verificationGasLimit: 1.3e6
|
||||
}, owner, entryPoint)
|
||||
@@ -136,9 +136,9 @@ describe('Batch gas testing', function () {
|
||||
if (once) {
|
||||
once = false
|
||||
console.log('direct call:', await counter.estimateGas.count())
|
||||
console.log('through wallet:', await ethers.provider.estimateGas({
|
||||
from: walletOwner.address,
|
||||
to: wallet.address,
|
||||
console.log('through account:', await ethers.provider.estimateGas({
|
||||
from: accountOwner.address,
|
||||
to: account.address,
|
||||
data: execCounterCount.data!
|
||||
}), 'datacost=', callDataCost(execCounterCount.data!))
|
||||
console.log('through handleOps:', await entryPoint.estimateGas.handleOps([op1], beneficiaryAddress))
|
||||
@@ -150,19 +150,19 @@ describe('Batch gas testing', function () {
|
||||
|
||||
it('batch of expensive ops', async function () {
|
||||
this.timeout(30000)
|
||||
if (wallets.length === 0) {
|
||||
if (accounts.length === 0) {
|
||||
this.skip()
|
||||
}
|
||||
|
||||
const waster = await counter.populateTransaction.gasWaster(40, '')
|
||||
const walletExecFromEntryPoint_waster: PopulatedTransaction =
|
||||
await wallet.populateTransaction.execFromEntryPoint(counter.address, 0, waster.data!)
|
||||
const accountExecFromEntryPoint_waster: PopulatedTransaction =
|
||||
await account.populateTransaction.execFromEntryPoint(counter.address, 0, waster.data!)
|
||||
|
||||
const ops: UserOperation[] = []
|
||||
for (const { w, owner } of wallets) {
|
||||
for (const { w, owner } of accounts) {
|
||||
const op1 = await fillAndSign({
|
||||
sender: w,
|
||||
callData: walletExecFromEntryPoint_waster.data,
|
||||
callData: accountExecFromEntryPoint_waster.data,
|
||||
maxPriorityFeePerGas: 1e9,
|
||||
verificationGasLimit: 1.3e6
|
||||
}, owner, entryPoint)
|
||||
@@ -174,19 +174,19 @@ describe('Batch gas testing', function () {
|
||||
|
||||
it('batch of large ops', async function () {
|
||||
this.timeout(30000)
|
||||
if (wallets.length === 0) {
|
||||
if (accounts.length === 0) {
|
||||
this.skip()
|
||||
}
|
||||
|
||||
const waster = await counter.populateTransaction.gasWaster(0, '1'.repeat(16384))
|
||||
const walletExecFromEntryPoint_waster: PopulatedTransaction =
|
||||
await wallet.populateTransaction.execFromEntryPoint(counter.address, 0, waster.data!)
|
||||
const accountExecFromEntryPoint_waster: PopulatedTransaction =
|
||||
await account.populateTransaction.execFromEntryPoint(counter.address, 0, waster.data!)
|
||||
|
||||
const ops: UserOperation[] = []
|
||||
for (const { w, owner } of wallets) {
|
||||
for (const { w, owner } of accounts) {
|
||||
const op1 = await fillAndSign({
|
||||
sender: w,
|
||||
callData: walletExecFromEntryPoint_waster.data,
|
||||
callData: accountExecFromEntryPoint_waster.data,
|
||||
maxPriorityFeePerGas: 1e9,
|
||||
verificationGasLimit: 1.3e6
|
||||
}, owner, entryPoint)
|
||||
|
||||
Reference in New Issue
Block a user