Files
openzeppelin-contracts/test/helpers/erc4337.js
2025-06-02 08:22:57 -06:00

218 lines
7.0 KiB
JavaScript

const { ethers, config, entrypoint, senderCreator } = require('hardhat');
const SIG_VALIDATION_SUCCESS = '0x0000000000000000000000000000000000000000';
const SIG_VALIDATION_FAILURE = '0x0000000000000000000000000000000000000001';
function getAddress(account) {
return account.target ?? account.address ?? account;
}
function pack(left, right) {
return ethers.solidityPacked(['uint128', 'uint128'], [left, right]);
}
function packValidationData(validAfter, validUntil, authorizer) {
return ethers.solidityPacked(
['uint48', 'uint48', 'address'],
[
validAfter,
validUntil,
typeof authorizer == 'boolean'
? authorizer
? SIG_VALIDATION_SUCCESS
: SIG_VALIDATION_FAILURE
: getAddress(authorizer),
],
);
}
function packInitCode(factory, factoryData) {
return ethers.solidityPacked(['address', 'bytes'], [getAddress(factory), factoryData]);
}
function packPaymasterAndData(paymaster, paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData) {
return ethers.solidityPacked(
['address', 'uint128', 'uint128', 'bytes'],
[getAddress(paymaster), paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData],
);
}
/// Represent one user operation
class UserOperation {
constructor(params) {
this.sender = getAddress(params.sender);
this.nonce = params.nonce;
this.factory = params.factory ?? undefined;
this.factoryData = params.factoryData ?? '0x';
this.callData = params.callData ?? '0x';
this.verificationGas = params.verificationGas ?? 10_000_000n;
this.callGas = params.callGas ?? 100_000n;
this.preVerificationGas = params.preVerificationGas ?? 100_000n;
this.maxPriorityFee = params.maxPriorityFee ?? 100_000n;
this.maxFeePerGas = params.maxFeePerGas ?? 100_000n;
this.paymaster = params.paymaster ?? undefined;
this.paymasterVerificationGasLimit = params.paymasterVerificationGasLimit ?? 0n;
this.paymasterPostOpGasLimit = params.paymasterPostOpGasLimit ?? 0n;
this.paymasterData = params.paymasterData ?? '0x';
this.signature = params.signature ?? '0x';
}
get packed() {
return {
sender: this.sender,
nonce: this.nonce,
initCode: this.factory ? packInitCode(this.factory, this.factoryData) : '0x',
callData: this.callData,
accountGasLimits: pack(this.verificationGas, this.callGas),
preVerificationGas: this.preVerificationGas,
gasFees: pack(this.maxPriorityFee, this.maxFeePerGas),
paymasterAndData: this.paymaster
? packPaymasterAndData(
this.paymaster,
this.paymasterVerificationGasLimit,
this.paymasterPostOpGasLimit,
this.paymasterData,
)
: '0x',
signature: this.signature,
};
}
hash(entrypoint) {
return entrypoint.getUserOpHash(this.packed);
}
}
const parseInitCode = initCode => ({
factory: '0x' + initCode.replace(/0x/, '').slice(0, 40),
factoryData: '0x' + initCode.replace(/0x/, '').slice(40),
});
/// Global ERC-4337 environment helper.
class ERC4337Helper {
constructor() {
this.factoryAsPromise = ethers.deployContract('$Create2');
}
async wait() {
this.factory = await this.factoryAsPromise;
return this;
}
async newAccount(name, extraArgs = [], params = {}) {
const env = {
entrypoint: params.entrypoint ?? entrypoint.v08,
senderCreator: params.senderCreator ?? senderCreator.v08,
};
const { factory } = await this.wait();
const accountFactory = await ethers.getContractFactory(name);
if (params.erc7702signer) {
const delegate = await accountFactory.deploy(...extraArgs);
const instance = await params.erc7702signer.getAddress().then(address => accountFactory.attach(address));
const authorization = await params.erc7702signer.authorize({ address: delegate.target });
return new ERC7702SmartAccount(instance, authorization, env);
} else {
const initCode = await accountFactory
.getDeployTransaction(...extraArgs)
.then(tx =>
factory.interface.encodeFunctionData('$deploy', [0, params.salt ?? ethers.randomBytes(32), tx.data]),
)
.then(deployCode => ethers.concat([factory.target, deployCode]));
const instance = await ethers.provider
.call({
from: env.entrypoint,
to: env.senderCreator,
data: env.senderCreator.interface.encodeFunctionData('createSender', [initCode]),
})
.then(result => ethers.getAddress(ethers.hexlify(ethers.getBytes(result).slice(-20))))
.then(address => accountFactory.attach(address));
return new SmartAccount(instance, initCode, env);
}
}
}
/// Represent one ERC-4337 account contract.
class SmartAccount extends ethers.BaseContract {
constructor(instance, initCode, env) {
super(instance.target, instance.interface, instance.runner, instance.deployTx);
this.address = instance.target;
this.initCode = initCode;
this._env = env;
}
async deploy(account = this.runner) {
const { factory: to, factoryData: data } = parseInitCode(this.initCode);
this.deployTx = await account.sendTransaction({ to, data });
return this;
}
async createUserOp(userOp = {}) {
userOp.sender ??= this;
userOp.nonce ??= await this._env.entrypoint.getNonce(userOp.sender, 0);
if (ethers.isAddressable(userOp.paymaster)) {
userOp.paymaster = await ethers.resolveAddress(userOp.paymaster);
userOp.paymasterVerificationGasLimit ??= 100_000n;
userOp.paymasterPostOpGasLimit ??= 100_000n;
}
return new UserOperationWithContext(userOp, this._env);
}
}
class ERC7702SmartAccount extends SmartAccount {
constructor(instance, authorization, env) {
super(instance, undefined, env);
this.authorization = authorization;
}
async deploy() {
// hardhat signers from @nomicfoundation/hardhat-ethers do not support type 4 txs.
// so we rebuild it using "native" ethers
await ethers.Wallet.fromPhrase(config.networks.hardhat.accounts.mnemonic, ethers.provider).sendTransaction({
to: ethers.ZeroAddress,
authorizationList: [this.authorization],
gasLimit: 46_000n, // 21,000 base + PER_EMPTY_ACCOUNT_COST
});
return this;
}
}
class UserOperationWithContext extends UserOperation {
constructor(userOp, env) {
super(userOp);
this._sender = userOp.sender;
this._env = env;
}
addInitCode() {
if (this._sender?.initCode) {
return Object.assign(this, parseInitCode(this._sender.initCode));
} else throw new Error('No init code available for the sender of this user operation');
}
getAuthorization() {
if (this._sender?.authorization) {
return this._sender.authorization;
} else throw new Error('No EIP-7702 authorization available for the sender of this user operation');
}
hash() {
return super.hash(this._env.entrypoint);
}
}
module.exports = {
SIG_VALIDATION_SUCCESS,
SIG_VALIDATION_FAILURE,
packValidationData,
packInitCode,
packPaymasterAndData,
UserOperation,
ERC4337Helper,
};