Skip to content

Commit

Permalink
erc4337 js helper
Browse files Browse the repository at this point in the history
  • Loading branch information
Amxx committed Apr 26, 2024
1 parent a13ad48 commit 342256c
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 39 deletions.
3 changes: 3 additions & 0 deletions contracts/abstraction/utils/ERC4337Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity ^0.8.20;
import {IEntryPoint, PackedUserOperation} from "../../interfaces/IERC4337.sol";
import {Math} from "../../utils/math/Math.sol";
import {Call} from "../../utils/Call.sol";
import {Memory} from "../../utils/Memory.sol";
import {Packing} from "../../utils/Packing.sol";

library ERC4337Utils {
Expand Down Expand Up @@ -198,6 +199,7 @@ library ERC4337Utils {
}

function load(UserOpInfo memory self, PackedUserOperation calldata source) internal view {
Memory.FreePtr ptr = Memory.save();
self.sender = source.sender;
self.nonce = source.nonce;
(self.verificationGasLimit, self.callGasLimit) = source.accountGasLimits.asUint128x2().split();
Expand All @@ -218,6 +220,7 @@ library ERC4337Utils {
self.prefund = 0;
self.preOpGas = 0;
self.context = "";
Memory.load(ptr);
}

function requiredPrefund(UserOpInfo memory self) internal pure returns (uint256) {
Expand Down
57 changes: 18 additions & 39 deletions test/abstraction/entrypoint.test.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs');

const { ERC4337Context } = require('../helpers/erc4337');

async function fixture() {
const accounts = await ethers.getSigners();
const entrypoint = await ethers.deployContract('EntryPoint');
const factory = await ethers.deployContract('$Create2');

const makeAA = (user, salt = ethers.randomBytes(32)) =>
ethers.getContractFactory('SimpleAccount').then(accountFactory =>
accountFactory
.getDeployTransaction(entrypoint, user)
.then(tx => factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data]))
.then(deployCode => ethers.concat([factory.target, deployCode]))
.then(initCode =>
entrypoint.getSenderAddress
.staticCall(initCode)
.then(sender => Object.assign(accountFactory.attach(sender), { initCode, salt })),
),
);
const context = new ERC4337Context();
await context.wait();

return {
accounts,
entrypoint,
factory,
makeAA,
context,
entrypoint: context.entrypoint,
factory: context.factory,
};
}

Expand All @@ -36,37 +26,26 @@ describe('EntryPoint', function () {
it('', async function () {
const user = this.accounts[0];
const beneficiary = this.accounts[1];
const sender = await this.makeAA(user);
const sender = await this.context.newAccount(user);

expect(await ethers.provider.getCode(sender)).to.equal('0x');

await user.sendTransaction({ to: sender, value: ethers.parseEther('1') });
await expect(
this.entrypoint.handleOps(
[
{
sender,
nonce: 0n,
initCode: sender.initCode,
callData: '0x',
accountGasLimits: ethers.toBeHex((2000000n << 128n) | 100000n, 32), // concatenation of verificationGas (16 bytes) and callGas (16 bytes)
preVerificationGas: 100000n,
gasFees: ethers.toBeHex((100000n << 128n) | 100000n, 32), // concatenation of maxPriorityFee (16 bytes) and maxFeePerGas (16 bytes)
paymasterAndData: '0x', // concatenation of paymaster fields (or empty)
signature: '0x',
},
],
beneficiary,
),
)

const operation = sender.createOp({}, true);
await expect(this.entrypoint.handleOps([operation.packed], beneficiary))
.to.emit(sender, 'OwnershipTransferred')
.withArgs(ethers.ZeroAddress, user)
.to.emit(this.factory, 'return$deploy')
.withArgs(sender)
.to.emit(this.entrypoint, 'AccountDeployed')
.to.emit(this.entrypoint, 'Transfer') // Deposit
.withArgs(operation.hash, sender, this.context.factory, ethers.ZeroAddress)
.to.emit(this.entrypoint, 'Transfer')
.withArgs(ethers.ZeroAddress, sender, anyValue)
.to.emit(this.entrypoint, 'BeforeExecution')
.to.emit(this.entrypoint, 'UserOperationEvent');
// BeforeExecution has no args
.to.emit(this.entrypoint, 'UserOperationEvent')
.withArgs(operation.hash, sender, ethers.ZeroAddress, operation.nonce, true, anyValue, anyValue);

expect(await ethers.provider.getCode(sender)).to.not.equal('0x');
});
Expand Down
112 changes: 112 additions & 0 deletions test/helpers/erc4337.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const { ethers } = require('hardhat');

function pack(left, right) {
return ethers.toBeHex((left << 128n) | right, 32);
}

class ERC4337Context {
constructor() {
this.entrypointAsPromise = ethers.deployContract('EntryPoint');
this.factoryAsPromise = ethers.deployContract('$Create2');
this.accountAsPromise = ethers.getContractFactory('SimpleAccount');
this.chainIdAsPromise = ethers.provider.getNetwork().then(({ chainId }) => chainId);
}

async wait() {
this.entrypoint = await this.entrypointAsPromise;
this.factory = await this.factoryAsPromise;
this.account = await this.accountAsPromise;
this.chainId = await this.chainIdAsPromise;
return this;
}

async newAccount(user, salt = ethers.randomBytes(32)) {
await this.wait();
const initCode = await this.account
.getDeployTransaction(this.entrypoint, user)
.then(tx => this.factory.interface.encodeFunctionData('$deploy', [0, salt, tx.data]))
.then(deployCode => ethers.concat([this.factory.target, deployCode]));
const instance = await this.entrypoint.getSenderAddress
.staticCall(initCode)
.then(address => this.account.attach(address));
return new AbstractAccount(instance, initCode, this);
}
}

class AbstractAccount extends ethers.BaseContract {
constructor(instance, initCode, context) {
super(instance.target, instance.interface, instance.runner, instance.deployTx);
this.address = instance.target;
this.initCode = initCode;
this.context = context;
}

createOp(params = {}, withInit = false) {
return new UserOperation({
...params,
sender: this,
initCode: withInit ? this.initCode : '0x',
});
}
}

class UserOperation {
constructor(params) {
this.sender = params.sender;
this.nonce = params.nonce ?? 0n;
this.initCode = params.initCode ?? '0x';
this.callData = params.callData ?? '0x';
this.verificationGas = params.verificationGas ?? 2_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.paymasterAndData = params.paymasterAndData ?? '0x';
this.signature = params.signature ?? '0x';
}

get packed() {
return {
sender: this.sender,
nonce: this.nonce,
initCode: this.initCode,
callData: this.callData,
accountGasLimits: pack(this.verificationGas, this.callGas),
preVerificationGas: this.preVerificationGas,
gasFees: pack(this.maxPriorityFee, this.maxFeePerGas),
paymasterAndData: this.paymasterAndData,
signature: this.signature,
};
}

get hash() {
const p = this.packed;
const h = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256', 'uint256', 'uint256'],
[
p.sender.target,
p.nonce,
ethers.keccak256(p.initCode),
ethers.keccak256(p.callData),
p.accountGasLimits,
p.preVerificationGas,
p.gasFees,
ethers.keccak256(p.paymasterAndData),
],
),
);
return ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
['bytes32', 'address', 'uint256'],
[h, this.sender.context.entrypoint.target, this.sender.context.chainId],
),
);
}
}

module.exports = {
ERC4337Context,
AbstractAccount,
UserOperation,
};

0 comments on commit 342256c

Please sign in to comment.