Skip to content

Commit 91759bc

Browse files
authored
Add ERC-7913 interface and utils (#109)
1 parent 2f34da2 commit 91759bc

File tree

5 files changed

+222
-0
lines changed

5 files changed

+222
-0
lines changed

contracts/interfaces/IERC7913.sol

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.0;
4+
5+
/**
6+
* @dev Signature verifier interface.
7+
*/
8+
interface IERC7913SignatureVerifier {
9+
/**
10+
* @dev Verifies `signature` as a valid signature of `hash` by `key`.
11+
*
12+
* MUST return the bytes4 magic value IERC7913SignatureVerifier.verify.selector if the signature is valid.
13+
* SHOULD return 0xffffffff or revert if the signature is not valid.
14+
* SHOULD return 0xffffffff or revert if the key is empty
15+
*/
16+
function verify(bytes calldata key, bytes32 hash, bytes calldata signature) external view returns (bytes4);
17+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {IERC7913SignatureVerifier} from "../../contracts/interfaces/IERC7913.sol";
6+
7+
contract ERC7913VerifierMock is IERC7913SignatureVerifier {
8+
// Store valid keys and their corresponding signatures
9+
mapping(bytes32 => bool) private _validKeys;
10+
mapping(bytes32 => mapping(bytes32 => bool)) private _validSignatures;
11+
12+
constructor() {
13+
// For testing purposes, we'll consider a specific key as valid
14+
bytes32 validKeyHash = keccak256(abi.encodePacked("valid_key"));
15+
_validKeys[validKeyHash] = true;
16+
}
17+
18+
function verify(bytes calldata key, bytes32 /* hash */, bytes calldata signature) external pure returns (bytes4) {
19+
// For testing purposes, we'll only accept a specific key and signature combination
20+
if (
21+
keccak256(key) == keccak256(abi.encodePacked("valid_key")) &&
22+
keccak256(signature) == keccak256(abi.encodePacked("valid_signature"))
23+
) {
24+
return IERC7913SignatureVerifier.verify.selector;
25+
}
26+
return 0xffffffff;
27+
}
28+
}

contracts/mocks/import.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
pragma solidity ^0.8.20;
44

55
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
6+
import {ERC1271WalletMock} from "@openzeppelin/contracts/mocks/ERC1271WalletMock.sol";
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
6+
import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol";
7+
8+
/**
9+
* @dev Library that provides common ERC-7913 utility functions.
10+
*
11+
* This library extends the functionality of
12+
* https://docs.openzeppelin.com/contracts/5.x/api/utils#SignatureChecker[SignatureChecker]
13+
* to support signature verification for keys that do not have an Ethereum address of their own
14+
* as with ERC-1271.
15+
*
16+
* See https://eips.ethereum.org/EIPS/eip-7913[ERC-7913].
17+
*/
18+
library ERC7913Utils {
19+
/**
20+
* @dev Verifies a signature for a given signer and hash.
21+
*
22+
* The signer is a `bytes` object that is the concatenation of an address and optionally a key:
23+
* `verifier || key`. A signer must be at least 20 bytes long.
24+
*
25+
* Verification is done as follows:
26+
* - If `signer.length < 20`: verification fails
27+
* - If `signer.length == 20`: verification is done using {SignatureChecker}
28+
* - Otherwise: verification is done using {IERC7913SignatureVerifier}
29+
*/
30+
function isValidSignatureNow(
31+
bytes calldata signer,
32+
bytes32 hash,
33+
bytes memory signature
34+
) internal view returns (bool) {
35+
if (signer.length < 20) {
36+
return false;
37+
} else if (signer.length == 20) {
38+
return SignatureChecker.isValidSignatureNow(address(bytes20(signer)), hash, signature);
39+
} else {
40+
try IERC7913SignatureVerifier(address(bytes20(signer[0:20]))).verify(signer[20:], hash, signature) returns (
41+
bytes4 magic
42+
) {
43+
return magic == IERC7913SignatureVerifier.verify.selector;
44+
} catch {
45+
return false;
46+
}
47+
}
48+
}
49+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
const { expect } = require('chai');
2+
const { ethers } = require('hardhat');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
5+
const TEST_MESSAGE = ethers.id('OpenZeppelin');
6+
const TEST_MESSAGE_HASH = ethers.hashMessage(TEST_MESSAGE);
7+
8+
const WRONG_MESSAGE = ethers.id('Nope');
9+
const WRONG_MESSAGE_HASH = ethers.hashMessage(WRONG_MESSAGE);
10+
11+
async function fixture() {
12+
const [, signer, other] = await ethers.getSigners();
13+
const mock = await ethers.deployContract('$ERC7913Utils');
14+
15+
// Deploy a mock ERC-1271 wallet
16+
const wallet = await ethers.deployContract('ERC1271WalletMock', [signer]);
17+
18+
// Deploy a mock ERC-7913 verifier
19+
const verifier = await ethers.deployContract('ERC7913VerifierMock');
20+
21+
// Create test keys
22+
const validKey = ethers.toUtf8Bytes('valid_key');
23+
const invalidKey = ethers.randomBytes(32);
24+
25+
// Create signer bytes (verifier address + key)
26+
const validSignerBytes = ethers.concat([verifier.target, validKey]);
27+
const invalidKeySignerBytes = ethers.concat([verifier.target, invalidKey]);
28+
29+
// Create test signatures
30+
const validSignature = ethers.toUtf8Bytes('valid_signature');
31+
const invalidSignature = ethers.randomBytes(65);
32+
33+
// Get EOA signature from the signer
34+
const eoaSignature = await signer.signMessage(TEST_MESSAGE);
35+
36+
return {
37+
signer,
38+
other,
39+
mock,
40+
wallet,
41+
verifier,
42+
validKey,
43+
invalidKey,
44+
validSignerBytes,
45+
invalidKeySignerBytes,
46+
validSignature,
47+
invalidSignature,
48+
eoaSignature,
49+
};
50+
}
51+
52+
describe('ERC7913Utils', function () {
53+
beforeEach(async function () {
54+
Object.assign(this, await loadFixture(fixture));
55+
});
56+
57+
describe('isValidSignatureNow', function () {
58+
describe('with EOA signer', function () {
59+
it('with matching signer and signature', async function () {
60+
const eoaSigner = ethers.zeroPadValue(this.signer.address, 20);
61+
await expect(this.mock.$isValidSignatureNow(eoaSigner, TEST_MESSAGE_HASH, this.eoaSignature)).to.eventually.be
62+
.true;
63+
});
64+
65+
it('with invalid signer', async function () {
66+
const eoaSigner = ethers.zeroPadValue(this.other.address, 20);
67+
await expect(this.mock.$isValidSignatureNow(eoaSigner, TEST_MESSAGE_HASH, this.eoaSignature)).to.eventually.be
68+
.false;
69+
});
70+
71+
it('with invalid signature', async function () {
72+
const eoaSigner = ethers.zeroPadValue(this.signer.address, 20);
73+
await expect(this.mock.$isValidSignatureNow(eoaSigner, WRONG_MESSAGE_HASH, this.eoaSignature)).to.eventually.be
74+
.false;
75+
});
76+
});
77+
78+
describe('with ERC-1271 wallet', function () {
79+
it('with matching signer and signature', async function () {
80+
const walletSigner = ethers.zeroPadValue(this.wallet.target, 20);
81+
await expect(this.mock.$isValidSignatureNow(walletSigner, TEST_MESSAGE_HASH, this.eoaSignature)).to.eventually
82+
.be.true;
83+
});
84+
85+
it('with invalid signer', async function () {
86+
const walletSigner = ethers.zeroPadValue(this.mock.target, 20);
87+
await expect(this.mock.$isValidSignatureNow(walletSigner, TEST_MESSAGE_HASH, this.eoaSignature)).to.eventually
88+
.be.false;
89+
});
90+
91+
it('with invalid signature', async function () {
92+
const walletSigner = ethers.zeroPadValue(this.wallet.target, 20);
93+
await expect(this.mock.$isValidSignatureNow(walletSigner, WRONG_MESSAGE_HASH, this.eoaSignature)).to.eventually
94+
.be.false;
95+
});
96+
});
97+
98+
describe('with ERC-7913 verifier', function () {
99+
it('with matching signer and signature', async function () {
100+
await expect(this.mock.$isValidSignatureNow(this.validSignerBytes, TEST_MESSAGE_HASH, this.validSignature)).to
101+
.eventually.be.true;
102+
});
103+
104+
it('with invalid verifier', async function () {
105+
const invalidVerifierSigner = ethers.concat([this.mock.target, this.validKey]);
106+
await expect(this.mock.$isValidSignatureNow(invalidVerifierSigner, TEST_MESSAGE_HASH, this.validSignature)).to
107+
.eventually.be.false;
108+
});
109+
110+
it('with invalid key', async function () {
111+
await expect(this.mock.$isValidSignatureNow(this.invalidKeySignerBytes, TEST_MESSAGE_HASH, this.validSignature))
112+
.to.eventually.be.false;
113+
});
114+
115+
it('with invalid signature', async function () {
116+
await expect(this.mock.$isValidSignatureNow(this.validSignerBytes, TEST_MESSAGE_HASH, this.invalidSignature)).to
117+
.eventually.be.false;
118+
});
119+
120+
it('with signer too short', async function () {
121+
const shortSigner = ethers.randomBytes(19);
122+
await expect(this.mock.$isValidSignatureNow(shortSigner, TEST_MESSAGE_HASH, this.validSignature)).to.eventually
123+
.be.false;
124+
});
125+
});
126+
});
127+
});

0 commit comments

Comments
 (0)