Skip to content

Commit

Permalink
update 4337 helper
Browse files Browse the repository at this point in the history
  • Loading branch information
Amxx committed Apr 29, 2024
1 parent 342256c commit 30c2d12
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 127 deletions.
114 changes: 28 additions & 86 deletions contracts/abstraction/utils/ERC4337Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,93 +56,37 @@ library ERC4337Utils {
}
}

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

// Packed user operation
function hash(PackedUserOperation calldata self) internal pure returns (bytes32) {
return keccak256(encode(self));
function hash(PackedUserOperation calldata self) internal view returns (bytes32) {
return hash(self, address(this), block.chainid);
}

function encode(PackedUserOperation calldata self) internal pure returns (bytes memory ret) {
return
function hash(
PackedUserOperation calldata self,
address entrypoint,
uint256 chainid
) internal pure returns (bytes32) {
Memory.FreePtr ptr = Memory.save();
bytes32 result = keccak256(
abi.encode(
self.sender,
self.nonce,
keccak256(self.initCode),
keccak256(self.callData),
self.accountGasLimits,
self.preVerificationGas,
self.gasFees,
keccak256(self.paymasterAndData)
);
keccak256(
abi.encode(
self.sender,
self.nonce,
keccak256(self.initCode),
keccak256(self.callData),
self.accountGasLimits,
self.preVerificationGas,
self.gasFees,
keccak256(self.paymasterAndData)
)
),
entrypoint,
chainid
)
);
Memory.load(ptr);
return result;
}

function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) {
Expand Down Expand Up @@ -199,7 +143,6 @@ 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 @@ -216,11 +159,10 @@ library ERC4337Utils {
self.paymasterVerificationGasLimit = 0;
self.paymasterPostOpGasLimit = 0;
}
self.userOpHash = keccak256(abi.encode(hash(source), address(this), block.chainid));
self.userOpHash = hash(source);
self.prefund = 0;
self.preOpGas = 0;
self.context = "";
Memory.load(ptr);
}

function requiredPrefund(UserOpInfo memory self) internal pure returns (uint256) {
Expand Down
112 changes: 81 additions & 31 deletions test/abstraction/entrypoint.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,100 @@ const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs');

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

async function fixture() {
const accounts = await ethers.getSigners();
const context = new ERC4337Context();
await context.wait();
const helper = new ERC4337Helper();
await helper.wait();

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

describe('EntryPoint', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));

this.user = this.accounts.shift();
this.beneficiary = this.accounts.shift();
this.sender = await this.helper.newAccount(this.user);
});

it('', async function () {
const user = this.accounts[0];
const beneficiary = this.accounts[1];
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') });

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')
.withArgs(operation.hash, sender, this.context.factory, ethers.ZeroAddress)
.to.emit(this.entrypoint, 'Transfer')
.withArgs(ethers.ZeroAddress, sender, anyValue)
.to.emit(this.entrypoint, 'BeforeExecution')
// 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');
describe('deploy wallet contract', function () {
it('success: counterfactual funding', async function () {
await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') });

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

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

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

it.skip('[TODO] success: paymaster funding', async function () {
// TODO: deploy paymaster
// TODO: fund paymaster's account in entrypoint

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

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

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

it("error: AA21 didn't pay prefund", async function () {
expect(await ethers.provider.getCode(this.sender)).to.equal('0x');

const operation = await this.sender.createOp({}, true);
await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary))
.to.be.revertedWithCustomError(this.entrypoint, 'FailedOp')
.withArgs(0, "AA21 didn't pay prefund");

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

it('error: AA25 invalid account nonce', async function () {
await this.user.sendTransaction({ to: this.sender, value: ethers.parseEther('1') });

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

const operation = await this.sender.createOp({ nonce: 1n }, true);
await expect(this.entrypoint.handleOps([operation.packed], this.beneficiary))
.to.be.revertedWithCustomError(this.entrypoint, 'FailedOp')
.withArgs(0, 'AA25 invalid account nonce');

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

function pack(left, right) {
return ethers.toBeHex((left << 128n) | right, 32);
return ethers.solidityPacked(['uint128', 'uint128'], [left, right]);
}

class ERC4337Context {
class ERC4337Helper {
constructor() {
this.entrypointAsPromise = ethers.deployContract('EntryPoint');
this.factoryAsPromise = ethers.deployContract('$Create2');
Expand Down Expand Up @@ -41,19 +41,30 @@ class AbstractAccount extends ethers.BaseContract {
this.context = context;
}

createOp(params = {}, withInit = false) {
return new UserOperation({
...params,
sender: this,
initCode: withInit ? this.initCode : '0x',
});
async createOp(args = {}, withInit = false) {
const params = Object.assign({ sender: this, initCode: withInit ? this.initCode : '0x' }, args);
// fetch nonce
if (!params.nonce) {
params.nonce = await this.context.entrypointAsPromise.then(entrypoint => entrypoint.getNonce(this, 0));
}
// prepare paymaster and data
if (ethers.isAddressable(params.paymaster)) {
params.paymaster = await ethers.resolveAddress(params.paymaster);
params.paymasterVerificationGasLimit ??= 100_000n;
params.paymasterPostOpGasLimit ??= 100_000n;
params.paymasterAndData = ethers.solidityPacked(
['address', 'uint128', 'uint128'],
[params.paymaster, params.paymasterVerificationGasLimit, params.paymasterPostOpGasLimit],
);
}
return new UserOperation(params);
}
}

class UserOperation {
constructor(params) {
this.sender = params.sender;
this.nonce = params.nonce ?? 0n;
this.nonce = params.nonce;
this.initCode = params.initCode ?? '0x';
this.callData = params.callData ?? '0x';
this.verificationGas = params.verificationGas ?? 2_000_000n;
Expand Down Expand Up @@ -106,7 +117,7 @@ class UserOperation {
}

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

0 comments on commit 30c2d12

Please sign in to comment.