Sfoglia il codice sorgente

Cherrypick 0a77e54c307d9becf0473b73541c4f9d3b2743a3 onto release-v5.3 (#5605)

Signed-off-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: ernestognw <ernestognw@gmail.com>
Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com>
Hadrien Croubois 6 mesi fa
parent
commit
bc3b02ff52

+ 18 - 25
contracts/account/utils/draft-ERC4337Utils.sol

@@ -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.

+ 46 - 29
hardhat/common-contracts.js

@@ -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 - 0
scripts/fetch-common-contracts.js

@@ -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', () => {}),
+        ),
+    ]),
+  ),
+);

+ 12 - 16
test/account/utils/draft-ERC4337Utils.test.js

@@ -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 () {

+ 1 - 1
test/account/utils/draft-ERC7579Utils.t.sol

@@ -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 {

File diff suppressed because it is too large
+ 0 - 0
test/bin/EntryPoint080.abi


BIN
test/bin/EntryPoint080.bytecode


+ 1 - 0
test/bin/SenderCreator080.abi

@@ -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


+ 2 - 20
test/helpers/erc4337.js

@@ -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);
   }
 }
 

Some files were not shown because too many files changed in this diff