Use Entrypoint's provided hashing function to support v0.8.0 change of hash (#5586)
Co-authored-by: ernestognw <ernestognw@gmail.com> Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com>
This commit is contained in:
@ -8,6 +8,11 @@ import {Math} from "../../utils/math/Math.sol";
|
||||
import {Calldata} from "../../utils/Calldata.sol";
|
||||
import {Packing} from "../../utils/Packing.sol";
|
||||
|
||||
/// @dev This is available on all entrypoint since v0.4.0, but is not formally part of the ERC.
|
||||
interface IEntryPointExtra {
|
||||
function getUserOpHash(PackedUserOperation calldata userOp) external view returns (bytes32);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Library with common ERC-4337 utility functions.
|
||||
*
|
||||
@ -19,6 +24,9 @@ library ERC4337Utils {
|
||||
/// @dev Address of the entrypoint v0.7.0
|
||||
IEntryPoint internal constant ENTRYPOINT_V07 = IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032);
|
||||
|
||||
/// @dev Address of the entrypoint v0.8.0
|
||||
IEntryPoint internal constant ENTRYPOINT_V08 = IEntryPoint(0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108);
|
||||
|
||||
/// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) return this value on success.
|
||||
uint256 internal constant SIG_VALIDATION_SUCCESS = 0;
|
||||
|
||||
@ -77,31 +85,16 @@ library ERC4337Utils {
|
||||
return (aggregator_, block.timestamp < validAfter || validUntil < block.timestamp);
|
||||
}
|
||||
|
||||
/// @dev Computes the hash of a user operation for a given entrypoint and chainid.
|
||||
function hash(
|
||||
PackedUserOperation calldata self,
|
||||
address entrypoint,
|
||||
uint256 chainid
|
||||
) internal pure returns (bytes32) {
|
||||
bytes32 result = keccak256(
|
||||
abi.encode(
|
||||
keccak256(
|
||||
abi.encode(
|
||||
self.sender,
|
||||
self.nonce,
|
||||
keccak256(self.initCode),
|
||||
keccak256(self.callData),
|
||||
self.accountGasLimits,
|
||||
self.preVerificationGas,
|
||||
self.gasFees,
|
||||
keccak256(self.paymasterAndData)
|
||||
)
|
||||
),
|
||||
entrypoint,
|
||||
chainid
|
||||
)
|
||||
);
|
||||
return result;
|
||||
/// @dev Get the hash of a user operation for a given entrypoint
|
||||
function hash(PackedUserOperation calldata self, address entrypoint) internal view returns (bytes32) {
|
||||
// NOTE: getUserOpHash is available since v0.4.0
|
||||
//
|
||||
// Prior to v0.8.0, this was easy to replicate for any entrypoint and chainId. Since v0.8.0 of the
|
||||
// entrypoint, this depends on the Entrypoint's domain separator, which cannot be hardcoded and is complex
|
||||
// to recompute. Domain separator could be fetch using the `getDomainSeparatorV4` getter, or recomputed from
|
||||
// the ERC-5267 getter, but both operation would require doing a view call to the entrypoint. Overall it feels
|
||||
// simpler and less error prone to get that functionality from the entrypoint directly.
|
||||
return IEntryPointExtra(entrypoint).getUserOpHash(self);
|
||||
}
|
||||
|
||||
/// @dev Returns `factory` from the {PackedUserOperation}, or address(0) if the initCode is empty or not properly formatted.
|
||||
|
||||
@ -6,41 +6,58 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const INSTANCES = {
|
||||
// Entrypoint v0.7.0
|
||||
// ERC-4337 Entrypoints
|
||||
entrypoint: {
|
||||
address: '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
|
||||
abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../test/bin/EntryPoint070.abi'), 'utf-8')),
|
||||
bytecode: fs.readFileSync(path.resolve(__dirname, '../test/bin/EntryPoint070.bytecode'), 'hex'),
|
||||
v07: {
|
||||
address: '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
|
||||
abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../test/bin/EntryPoint070.abi'), 'utf-8')),
|
||||
bytecode: fs.readFileSync(path.resolve(__dirname, '../test/bin/EntryPoint070.bytecode'), 'hex'),
|
||||
},
|
||||
v08: {
|
||||
address: '0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108',
|
||||
abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../test/bin/EntryPoint080.abi'), 'utf-8')),
|
||||
bytecode: fs.readFileSync(path.resolve(__dirname, '../test/bin/EntryPoint080.bytecode'), 'hex'),
|
||||
},
|
||||
},
|
||||
senderCreator: {
|
||||
address: '0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C',
|
||||
abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../test/bin/SenderCreator070.abi'), 'utf-8')),
|
||||
bytecode: fs.readFileSync(path.resolve(__dirname, '../test/bin/SenderCreator070.bytecode'), 'hex'),
|
||||
v07: {
|
||||
address: '0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C',
|
||||
abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../test/bin/SenderCreator070.abi'), 'utf-8')),
|
||||
bytecode: fs.readFileSync(path.resolve(__dirname, '../test/bin/SenderCreator070.bytecode'), 'hex'),
|
||||
},
|
||||
v08: {
|
||||
address: '0x449ED7C3e6Fee6a97311d4b55475DF59C44AdD33',
|
||||
abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../test/bin/SenderCreator080.abi'), 'utf-8')),
|
||||
bytecode: fs.readFileSync(path.resolve(__dirname, '../test/bin/SenderCreator080.bytecode'), 'hex'),
|
||||
},
|
||||
},
|
||||
// Arachnid's deterministic deployment proxy
|
||||
// See: https://github.com/Arachnid/deterministic-deployment-proxy/tree/master
|
||||
arachnidDeployer: {
|
||||
address: '0x4e59b44847b379578588920cA78FbF26c0B4956C',
|
||||
abi: [],
|
||||
bytecode:
|
||||
'0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3',
|
||||
},
|
||||
// Micah's deployer
|
||||
micahDeployer: {
|
||||
address: '0x7A0D94F55792C434d74a40883C6ed8545E406D12',
|
||||
abi: [],
|
||||
bytecode: '0x60003681823780368234f58015156014578182fd5b80825250506014600cf3',
|
||||
deployer: {
|
||||
// Arachnid's deterministic deployment proxy
|
||||
// See: https://github.com/Arachnid/deterministic-deployment-proxy/tree/master
|
||||
arachnid: {
|
||||
address: '0x4e59b44847b379578588920cA78FbF26c0B4956C',
|
||||
abi: [],
|
||||
bytecode:
|
||||
'0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3',
|
||||
},
|
||||
// Micah's deployer
|
||||
micah: {
|
||||
address: '0x7A0D94F55792C434d74a40883C6ed8545E406D12',
|
||||
abi: [],
|
||||
bytecode: '0x60003681823780368234f58015156014578182fd5b80825250506014600cf3',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const setup = (input, ethers) =>
|
||||
input.address && input.abi && input.bytecode
|
||||
? setCode(input.address, '0x' + input.bytecode.replace(/0x/, '')).then(() =>
|
||||
ethers.getContractAt(input.abi, input.address),
|
||||
)
|
||||
: Promise.all(
|
||||
Object.entries(input).map(([name, entry]) => setup(entry, ethers).then(result => [name, result])),
|
||||
).then(Object.fromEntries);
|
||||
|
||||
task(TASK_TEST_SETUP_TEST_ENVIRONMENT).setAction((_, env, runSuper) =>
|
||||
runSuper().then(() =>
|
||||
Promise.all(
|
||||
Object.entries(INSTANCES).map(([name, { address, abi, bytecode }]) =>
|
||||
setCode(address, '0x' + bytecode.replace(/0x/, '')).then(() =>
|
||||
env.ethers.getContractAt(abi, address).then(instance => (env[name] = instance)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
runSuper().then(() => setup(INSTANCES, env.ethers).then(result => Object.assign(env, result))),
|
||||
);
|
||||
|
||||
50
scripts/fetch-common-contracts.js
Executable file
50
scripts/fetch-common-contracts.js
Executable file
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// This script snapshots the bytecode and ABI for the `hardhat/common-contracts.js` script.
|
||||
// - Bytecode is fetched directly from the blockchain by querying the provided client endpoint. If no endpoint is
|
||||
// provided, ethers default provider is used instead.
|
||||
// - ABI is fetched from etherscan's API using the provided etherscan API key. If no API key is provided, ABI will not
|
||||
// be fetched and saved.
|
||||
//
|
||||
// The produced artifacts are stored in the `output` folder ('test/bin' by default). For each contract, two files are
|
||||
// produced:
|
||||
// - `<name>.bytecode` containing the contract bytecode (in binary encoding)
|
||||
// - `<name>.abi` containing the ABI (in utf-8 encoding)
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { ethers } = require('ethers');
|
||||
const { request } = require('undici');
|
||||
const { hideBin } = require('yargs/helpers');
|
||||
const { argv } = require('yargs/yargs')(hideBin(process.argv))
|
||||
.env('')
|
||||
.options({
|
||||
output: { type: 'string', default: 'test/bin/' },
|
||||
client: { type: 'string' },
|
||||
etherscan: { type: 'string' },
|
||||
});
|
||||
|
||||
// List of contract names and addresses to fetch
|
||||
const config = {
|
||||
EntryPoint070: '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
|
||||
SenderCreator070: '0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C',
|
||||
EntryPoint080: '0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108',
|
||||
SenderCreator080: '0x449ED7C3e6Fee6a97311d4b55475DF59C44AdD33',
|
||||
};
|
||||
|
||||
Promise.all(
|
||||
Object.entries(config).flatMap(([name, addr]) =>
|
||||
Promise.all([
|
||||
argv.etherscan &&
|
||||
request(`https://api.etherscan.io/api?module=contract&action=getabi&address=${addr}&apikey=${argv.etherscan}`)
|
||||
.then(({ body }) => body.json())
|
||||
.then(({ result: abi }) => fs.writeFile(path.join(argv.output, `${name}.abi`), abi, 'utf-8', () => {})),
|
||||
ethers
|
||||
.getDefaultProvider(argv.client)
|
||||
.getCode(addr)
|
||||
.then(bytecode =>
|
||||
fs.writeFile(path.join(argv.output, `${name}.bytecode`), ethers.getBytes(bytecode), 'binary', () => {}),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
@ -22,7 +22,11 @@ describe('ERC4337Utils', function () {
|
||||
|
||||
describe('entrypoint', function () {
|
||||
it('v0.7.0', async function () {
|
||||
await expect(this.utils.$ENTRYPOINT_V07()).to.eventually.equal(entrypoint);
|
||||
await expect(this.utils.$ENTRYPOINT_V07()).to.eventually.equal(entrypoint.v07);
|
||||
});
|
||||
|
||||
it('v0.8.0', async function () {
|
||||
await expect(this.utils.$ENTRYPOINT_V08()).to.eventually.equal(entrypoint.v08);
|
||||
});
|
||||
});
|
||||
|
||||
@ -172,22 +176,14 @@ describe('ERC4337Utils', function () {
|
||||
});
|
||||
|
||||
describe('hash', function () {
|
||||
it('returns the operation hash with specified entrypoint and chainId', async function () {
|
||||
const userOp = new UserOperation({ sender: this.sender, nonce: 1 });
|
||||
const chainId = await ethers.provider.getNetwork().then(({ chainId }) => chainId);
|
||||
const otherChainId = 0xdeadbeef;
|
||||
for (const [version, instance] of Object.entries(entrypoint)) {
|
||||
it(`returns the operation hash for entrypoint ${version}`, async function () {
|
||||
const userOp = new UserOperation({ sender: this.sender, nonce: 1 });
|
||||
const expected = await userOp.hash(instance);
|
||||
|
||||
// check that helper matches entrypoint logic
|
||||
await expect(entrypoint.getUserOpHash(userOp.packed)).to.eventually.equal(userOp.hash(entrypoint, chainId));
|
||||
|
||||
// check library against helper
|
||||
await expect(this.utils.$hash(userOp.packed, entrypoint, chainId)).to.eventually.equal(
|
||||
userOp.hash(entrypoint, chainId),
|
||||
);
|
||||
await expect(this.utils.$hash(userOp.packed, entrypoint, otherChainId)).to.eventually.equal(
|
||||
userOp.hash(entrypoint, otherChainId),
|
||||
);
|
||||
});
|
||||
await expect(this.utils.$hash(userOp.packed, instance)).to.eventually.equal(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('userOp values', function () {
|
||||
|
||||
@ -375,7 +375,7 @@ contract ERC7579UtilsTest is Test {
|
||||
}
|
||||
|
||||
function hashUserOperation(PackedUserOperation calldata useroperation) public view returns (bytes32) {
|
||||
return useroperation.hash(address(ERC4337Utils.ENTRYPOINT_V07), block.chainid);
|
||||
return useroperation.hash(address(ERC4337Utils.ENTRYPOINT_V07));
|
||||
}
|
||||
|
||||
function _collectAndPrintLogs(bool includeTotalValue) internal {
|
||||
|
||||
1
test/bin/EntryPoint080.abi
Normal file
1
test/bin/EntryPoint080.abi
Normal file
File diff suppressed because one or more lines are too long
BIN
test/bin/EntryPoint080.bytecode
Normal file
BIN
test/bin/EntryPoint080.bytecode
Normal file
Binary file not shown.
1
test/bin/SenderCreator080.abi
Normal file
1
test/bin/SenderCreator080.abi
Normal file
@ -0,0 +1 @@
|
||||
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"opIndex","type":"uint256"},{"internalType":"string","name":"reason","type":"string"},{"internalType":"bytes","name":"inner","type":"bytes"}],"name":"FailedOpWithRevert","type":"error"},{"inputs":[{"internalType":"bytes","name":"initCode","type":"bytes"}],"name":"createSender","outputs":[{"internalType":"address","name":"sender","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"entryPoint","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"bytes","name":"initCallData","type":"bytes"}],"name":"initEip7702Sender","outputs":[],"stateMutability":"nonpayable","type":"function"}]
|
||||
BIN
test/bin/SenderCreator080.bytecode
Normal file
BIN
test/bin/SenderCreator080.bytecode
Normal file
Binary file not shown.
@ -78,26 +78,8 @@ class UserOperation {
|
||||
};
|
||||
}
|
||||
|
||||
hash(entrypoint, chainId) {
|
||||
const p = this.packed;
|
||||
const h = ethers.keccak256(
|
||||
ethers.AbiCoder.defaultAbiCoder().encode(
|
||||
['address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256', 'uint256', 'uint256'],
|
||||
[
|
||||
p.sender,
|
||||
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, getAddress(entrypoint), chainId]),
|
||||
);
|
||||
hash(entrypoint) {
|
||||
return entrypoint.getUserOpHash(this.packed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user