From 5c238e7eb0dab84b2220d5109a39541c52dc3265 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 17 Mar 2025 20:51:42 +0100 Subject: [PATCH 1/9] EIP7702Utils --- contracts/account/utils/EIP7702Utils.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 contracts/account/utils/EIP7702Utils.sol diff --git a/contracts/account/utils/EIP7702Utils.sol b/contracts/account/utils/EIP7702Utils.sol new file mode 100644 index 00000000000..ee3fa12afc5 --- /dev/null +++ b/contracts/account/utils/EIP7702Utils.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev Library with common EIP-7702 utility functions. + * + * See https://eips.ethereum.org/EIPS/eip-7702[ERC-7702]. + */ +library EIP7702Utils { + bytes3 internal constant EIP7702_PREFIX = 0xef0100; + + function fetchDelegate(address account) internal view returns (bool isEIP7702, address delegate) { + bytes23 delegation = bytes23(account.code); + + isEIP7702 = bytes3(delegation) == EIP7702_PREFIX; + delegate = isEIP7702 ? address(bytes20(delegation << 24)) : address(0); + } +} From dbd74f9200721ed224f8a63e9104923eb4ad7706 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 26 Mar 2025 12:05:32 +0100 Subject: [PATCH 2/9] Update EIP7702Utils.sol --- contracts/account/utils/EIP7702Utils.sol | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/contracts/account/utils/EIP7702Utils.sol b/contracts/account/utils/EIP7702Utils.sol index ee3fa12afc5..a63682f39b1 100644 --- a/contracts/account/utils/EIP7702Utils.sol +++ b/contracts/account/utils/EIP7702Utils.sol @@ -10,10 +10,13 @@ pragma solidity ^0.8.20; library EIP7702Utils { bytes3 internal constant EIP7702_PREFIX = 0xef0100; - function fetchDelegate(address account) internal view returns (bool isEIP7702, address delegate) { + /** + * @dev Returns the address of the delegate if `account` as an EIP-7702 delegation setup, or address(0) otherwise. + */ + function fetchDelegate(address account) internal view returns (address) { bytes23 delegation = bytes23(account.code); - - isEIP7702 = bytes3(delegation) == EIP7702_PREFIX; - delegate = isEIP7702 ? address(bytes20(delegation << 24)) : address(0); + return bytes3(delegation) == EIP7702_PREFIX + ? address(bytes20(delegation << 24)) + : address(0); } } From 18f6252fed390fec2a59877cc364039e2465c085 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 May 2025 13:56:47 +0200 Subject: [PATCH 3/9] add tests --- contracts/account/utils/EIP7702Utils.sol | 4 +- test/account/utils/EIP7702Utils.test.js | 53 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 test/account/utils/EIP7702Utils.test.js diff --git a/contracts/account/utils/EIP7702Utils.sol b/contracts/account/utils/EIP7702Utils.sol index a63682f39b1..ca3eeb6f133 100644 --- a/contracts/account/utils/EIP7702Utils.sol +++ b/contracts/account/utils/EIP7702Utils.sol @@ -15,8 +15,6 @@ library EIP7702Utils { */ function fetchDelegate(address account) internal view returns (address) { bytes23 delegation = bytes23(account.code); - return bytes3(delegation) == EIP7702_PREFIX - ? address(bytes20(delegation << 24)) - : address(0); + return bytes3(delegation) == EIP7702_PREFIX ? address(bytes20(delegation << 24)) : address(0); } } diff --git a/test/account/utils/EIP7702Utils.test.js b/test/account/utils/EIP7702Utils.test.js new file mode 100644 index 00000000000..5a1c61c2ac2 --- /dev/null +++ b/test/account/utils/EIP7702Utils.test.js @@ -0,0 +1,53 @@ +const { ethers, config } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +// [NOTE] +// +// ethers.getSigners() returns object than cannot currently send type-4 transaction, or sign authorization. Therefore, +// we have to instantiate the eoa AND the relayer manually using ethers 6.14.0 wallets. This can be improved when +// @nomicfoundation/hardhat-ethers starts instantiating signers with 7702 support. +const relayAuthorization = authorization => + ethers.Wallet.fromPhrase(config.networks.hardhat.accounts.mnemonic, ethers.provider).sendTransaction({ + to: ethers.ZeroAddress, + authorizationList: [authorization], + gasLimit: 46_000n, + }); + +const fixture = async () => { + const eoa = ethers.Wallet.createRandom().connect(ethers.provider); + const mock = await ethers.deployContract('$EIP7702Utils'); + return { eoa, mock }; +}; + +describe('EIP7702Utils', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('fetchDelegate', function () { + it('EOA without delegation', async function () { + await expect(this.mock.$fetchDelegate(this.eoa)).to.eventually.equal(ethers.ZeroAddress); + }); + + it('EOA with delegation', async function () { + // set delegation + await this.eoa.authorize({ address: this.mock.target }).then(relayAuthorization); + + await expect(this.mock.$fetchDelegate(this.eoa)).to.eventually.equal(this.mock); + }); + + it('EOA with revoked delegation', async function () { + // set delegation + await this.eoa.authorize({ address: this.mock.target }).then(relayAuthorization); + // reset delegation + await this.eoa.authorize({ address: ethers.ZeroAddress }).then(relayAuthorization); + + await expect(this.mock.$fetchDelegate(this.eoa)).to.eventually.equal(ethers.ZeroAddress); + }); + + it('other smart contract', async function () { + await expect(this.mock.$fetchDelegate(this.mock)).to.eventually.equal(ethers.ZeroAddress); + }); + }); +}); From 02a9c84060c3d604a10a71d25c5e91134197c8d2 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 May 2025 14:10:49 +0200 Subject: [PATCH 4/9] simplify --- test/account/utils/EIP7702Utils.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/account/utils/EIP7702Utils.test.js b/test/account/utils/EIP7702Utils.test.js index 5a1c61c2ac2..a19a4f8358b 100644 --- a/test/account/utils/EIP7702Utils.test.js +++ b/test/account/utils/EIP7702Utils.test.js @@ -15,7 +15,7 @@ const relayAuthorization = authorization => }); const fixture = async () => { - const eoa = ethers.Wallet.createRandom().connect(ethers.provider); + const eoa = ethers.Wallet.createRandom(ethers.provider); const mock = await ethers.deployContract('$EIP7702Utils'); return { eoa, mock }; }; From 79a8056d5191de52f905a57d9ecc182f7c2ab8f1 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 May 2025 16:24:03 +0200 Subject: [PATCH 5/9] add EIP7702Utils to Stateless.sol --- contracts/mocks/Stateless.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 5c323299c2b..6bbf2ac4ba4 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -11,22 +11,23 @@ import {Base64} from "../utils/Base64.sol"; import {BitMaps} from "../utils/structs/BitMaps.sol"; import {Blockhash} from "../utils/Blockhash.sol"; import {Bytes} from "../utils/Bytes.sol"; -import {CAIP2} from "../utils/CAIP2.sol"; import {CAIP10} from "../utils/CAIP10.sol"; +import {CAIP2} from "../utils/CAIP2.sol"; import {Checkpoints} from "../utils/structs/Checkpoints.sol"; import {CircularBuffer} from "../utils/structs/CircularBuffer.sol"; import {Clones} from "../proxy/Clones.sol"; import {Create2} from "../utils/Create2.sol"; import {DoubleEndedQueue} from "../utils/structs/DoubleEndedQueue.sol"; import {ECDSA} from "../utils/cryptography/ECDSA.sol"; +import {EIP7702Utils} from "../account/utils/EIP7702Utils.sol"; import {EnumerableMap} from "../utils/structs/EnumerableMap.sol"; import {EnumerableSet} from "../utils/structs/EnumerableSet.sol"; import {ERC1155Holder} from "../token/ERC1155/utils/ERC1155Holder.sol"; import {ERC165} from "../utils/introspection/ERC165.sol"; import {ERC165Checker} from "../utils/introspection/ERC165Checker.sol"; import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol"; -import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; import {ERC4337Utils} from "../account/utils/draft-ERC4337Utils.sol"; +import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; import {ERC7579Utils} from "../account/utils/draft-ERC7579Utils.sol"; import {Heap} from "../utils/structs/Heap.sol"; import {Math} from "../utils/math/Math.sol"; @@ -35,8 +36,8 @@ import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol"; import {Nonces} from "../utils/Nonces.sol"; import {NoncesKeyed} from "../utils/NoncesKeyed.sol"; import {P256} from "../utils/cryptography/P256.sol"; -import {Panic} from "../utils/Panic.sol"; import {Packing} from "../utils/Packing.sol"; +import {Panic} from "../utils/Panic.sol"; import {RSA} from "../utils/cryptography/RSA.sol"; import {SafeCast} from "../utils/math/SafeCast.sol"; import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol"; From 9e87b08a0a4e09ae004e36da795f8dab18df2948 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 May 2025 16:24:57 +0200 Subject: [PATCH 6/9] reorder --- contracts/mocks/Stateless.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 6bbf2ac4ba4..5cd19aad692 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -11,8 +11,8 @@ import {Base64} from "../utils/Base64.sol"; import {BitMaps} from "../utils/structs/BitMaps.sol"; import {Blockhash} from "../utils/Blockhash.sol"; import {Bytes} from "../utils/Bytes.sol"; -import {CAIP10} from "../utils/CAIP10.sol"; import {CAIP2} from "../utils/CAIP2.sol"; +import {CAIP10} from "../utils/CAIP10.sol"; import {Checkpoints} from "../utils/structs/Checkpoints.sol"; import {CircularBuffer} from "../utils/structs/CircularBuffer.sol"; import {Clones} from "../proxy/Clones.sol"; @@ -25,9 +25,9 @@ import {EnumerableSet} from "../utils/structs/EnumerableSet.sol"; import {ERC1155Holder} from "../token/ERC1155/utils/ERC1155Holder.sol"; import {ERC165} from "../utils/introspection/ERC165.sol"; import {ERC165Checker} from "../utils/introspection/ERC165Checker.sol"; +import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol"; import {ERC4337Utils} from "../account/utils/draft-ERC4337Utils.sol"; -import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; import {ERC7579Utils} from "../account/utils/draft-ERC7579Utils.sol"; import {Heap} from "../utils/structs/Heap.sol"; import {Math} from "../utils/math/Math.sol"; From 15b6bfca90b919bd4c082bb2b93a01109837e19f Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 May 2025 16:27:16 +0200 Subject: [PATCH 7/9] simplify --- test/account/utils/EIP7702Utils.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/account/utils/EIP7702Utils.test.js b/test/account/utils/EIP7702Utils.test.js index a19a4f8358b..fb045f75325 100644 --- a/test/account/utils/EIP7702Utils.test.js +++ b/test/account/utils/EIP7702Utils.test.js @@ -32,14 +32,14 @@ describe('EIP7702Utils', function () { it('EOA with delegation', async function () { // set delegation - await this.eoa.authorize({ address: this.mock.target }).then(relayAuthorization); + await this.eoa.authorize({ address: this.mock }).then(relayAuthorization); await expect(this.mock.$fetchDelegate(this.eoa)).to.eventually.equal(this.mock); }); it('EOA with revoked delegation', async function () { // set delegation - await this.eoa.authorize({ address: this.mock.target }).then(relayAuthorization); + await this.eoa.authorize({ address: this.mock }).then(relayAuthorization); // reset delegation await this.eoa.authorize({ address: ethers.ZeroAddress }).then(relayAuthorization); From f1570b84299dc0869a6e62ee7df3e4a9eb43cdc5 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 May 2025 17:08:06 +0200 Subject: [PATCH 8/9] add changeset --- .changeset/full-ways-help.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/full-ways-help.md diff --git a/.changeset/full-ways-help.md b/.changeset/full-ways-help.md new file mode 100644 index 00000000000..90a27e9e856 --- /dev/null +++ b/.changeset/full-ways-help.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`EIP7702Utils`: Add a library for checking if an address has an EIP-7702 delegation in place. From 98ab7d321b4f65f8de1b6b9cd17d94a4925b1f15 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 27 May 2025 17:08:55 +0200 Subject: [PATCH 9/9] Update Stateless.sol --- contracts/mocks/Stateless.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 5cd19aad692..baa2905cd85 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -22,10 +22,10 @@ import {ECDSA} from "../utils/cryptography/ECDSA.sol"; import {EIP7702Utils} from "../account/utils/EIP7702Utils.sol"; import {EnumerableMap} from "../utils/structs/EnumerableMap.sol"; import {EnumerableSet} from "../utils/structs/EnumerableSet.sol"; -import {ERC1155Holder} from "../token/ERC1155/utils/ERC1155Holder.sol"; import {ERC165} from "../utils/introspection/ERC165.sol"; import {ERC165Checker} from "../utils/introspection/ERC165Checker.sol"; import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; +import {ERC1155Holder} from "../token/ERC1155/utils/ERC1155Holder.sol"; import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol"; import {ERC4337Utils} from "../account/utils/draft-ERC4337Utils.sol"; import {ERC7579Utils} from "../account/utils/draft-ERC7579Utils.sol";