Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions contracts/interfaces/IERC7913.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
* @dev Signature verifier interface.
*/
interface IERC7913SignatureVerifier {
/**
* @dev Verifies `signature` as a valid signature of `hash` by `key`.
*
* MUST return the bytes4 magic value IERC7913SignatureVerifier.verify.selector if the signature is valid.
* SHOULD return 0xffffffff or revert if the signature is not valid.
* SHOULD return 0xffffffff or revert if the key is empty
*/
function verify(bytes calldata key, bytes32 hash, bytes calldata signature) external view returns (bytes4);
}
28 changes: 28 additions & 0 deletions contracts/mocks/ERC7913VerifierMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {IERC7913SignatureVerifier} from "../../contracts/interfaces/IERC7913.sol";

contract ERC7913VerifierMock is IERC7913SignatureVerifier {
// Store valid keys and their corresponding signatures
mapping(bytes32 => bool) private _validKeys;
mapping(bytes32 => mapping(bytes32 => bool)) private _validSignatures;

constructor() {
// For testing purposes, we'll consider a specific key as valid
bytes32 validKeyHash = keccak256(abi.encodePacked("valid_key"));
_validKeys[validKeyHash] = true;
}

function verify(bytes calldata key, bytes32 /* hash */, bytes calldata signature) external pure returns (bytes4) {
// For testing purposes, we'll only accept a specific key and signature combination
if (
keccak256(key) == keccak256(abi.encodePacked("valid_key")) &&
keccak256(signature) == keccak256(abi.encodePacked("valid_signature"))
) {
return IERC7913SignatureVerifier.verify.selector;
}
return 0xffffffff;
}
}
1 change: 1 addition & 0 deletions contracts/mocks/import.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
pragma solidity ^0.8.20;

import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {ERC1271WalletMock} from "@openzeppelin/contracts/mocks/ERC1271WalletMock.sol";
49 changes: 49 additions & 0 deletions contracts/utils/cryptography/ERC7913Utils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol";

/**
* @dev Library that provides common ERC-7913 utility functions.
*
* This library extends the functionality of
* https://docs.openzeppelin.com/contracts/5.x/api/utils#SignatureChecker[SignatureChecker]
* to support signature verification for keys that do not have an Ethereum address of their own
* as with ERC-1271.
*
* See https://eips.ethereum.org/EIPS/eip-7913[ERC-7913].
*/
library ERC7913Utils {
/**
* @dev Verifies a signature for a given signer and hash.
*
* The signer is a `bytes` object that is the concatenation of an address and optionally a key:
* `verifier || key`. A signer must be at least 20 bytes long.
*
* Verification is done as follows:
* - If `signer.length < 20`: verification fails
* - If `signer.length == 20`: verification is done using {SignatureChecker}
* - Otherwise: verification is done using {IERC7913SignatureVerifier}
*/
function isValidSignatureNow(
bytes calldata signer,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's cool that signer is in calldata but I suspect most of the times it will end up in memory as arguments are used to construct the external call. Perhaps it makes sense to turn this into memory eventually. Not a blocker

bytes32 hash,
bytes memory signature
) internal view returns (bool) {
if (signer.length < 20) {
return false;
} else if (signer.length == 20) {
return SignatureChecker.isValidSignatureNow(address(bytes20(signer)), hash, signature);
} else {
try IERC7913SignatureVerifier(address(bytes20(signer[0:20]))).verify(signer[20:], hash, signature) returns (
bytes4 magic
) {
return magic == IERC7913SignatureVerifier.verify.selector;
} catch {
return false;
}
}
}
}
127 changes: 127 additions & 0 deletions test/utils/cryptography/ERC7913Utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
const { expect } = require('chai');
const { ethers } = require('hardhat');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');

const TEST_MESSAGE = ethers.id('OpenZeppelin');
const TEST_MESSAGE_HASH = ethers.hashMessage(TEST_MESSAGE);

const WRONG_MESSAGE = ethers.id('Nope');
const WRONG_MESSAGE_HASH = ethers.hashMessage(WRONG_MESSAGE);

async function fixture() {
const [, signer, other] = await ethers.getSigners();
const mock = await ethers.deployContract('$ERC7913Utils');

// Deploy a mock ERC-1271 wallet
const wallet = await ethers.deployContract('ERC1271WalletMock', [signer]);

// Deploy a mock ERC-7913 verifier
const verifier = await ethers.deployContract('ERC7913VerifierMock');

// Create test keys
const validKey = ethers.toUtf8Bytes('valid_key');
const invalidKey = ethers.randomBytes(32);

// Create signer bytes (verifier address + key)
const validSignerBytes = ethers.concat([verifier.target, validKey]);
const invalidKeySignerBytes = ethers.concat([verifier.target, invalidKey]);

// Create test signatures
const validSignature = ethers.toUtf8Bytes('valid_signature');
const invalidSignature = ethers.randomBytes(65);

// Get EOA signature from the signer
const eoaSignature = await signer.signMessage(TEST_MESSAGE);

return {
signer,
other,
mock,
wallet,
verifier,
validKey,
invalidKey,
validSignerBytes,
invalidKeySignerBytes,
validSignature,
invalidSignature,
eoaSignature,
};
}

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

describe('isValidSignatureNow', function () {
describe('with EOA signer', function () {
it('with matching signer and signature', async function () {
const eoaSigner = ethers.zeroPadValue(this.signer.address, 20);
await expect(this.mock.$isValidSignatureNow(eoaSigner, TEST_MESSAGE_HASH, this.eoaSignature)).to.eventually.be
.true;
});

it('with invalid signer', async function () {
const eoaSigner = ethers.zeroPadValue(this.other.address, 20);
await expect(this.mock.$isValidSignatureNow(eoaSigner, TEST_MESSAGE_HASH, this.eoaSignature)).to.eventually.be
.false;
});

it('with invalid signature', async function () {
const eoaSigner = ethers.zeroPadValue(this.signer.address, 20);
await expect(this.mock.$isValidSignatureNow(eoaSigner, WRONG_MESSAGE_HASH, this.eoaSignature)).to.eventually.be
.false;
});
});

describe('with ERC-1271 wallet', function () {
it('with matching signer and signature', async function () {
const walletSigner = ethers.zeroPadValue(this.wallet.target, 20);
await expect(this.mock.$isValidSignatureNow(walletSigner, TEST_MESSAGE_HASH, this.eoaSignature)).to.eventually
.be.true;
});

it('with invalid signer', async function () {
const walletSigner = ethers.zeroPadValue(this.mock.target, 20);
await expect(this.mock.$isValidSignatureNow(walletSigner, TEST_MESSAGE_HASH, this.eoaSignature)).to.eventually
.be.false;
});

it('with invalid signature', async function () {
const walletSigner = ethers.zeroPadValue(this.wallet.target, 20);
await expect(this.mock.$isValidSignatureNow(walletSigner, WRONG_MESSAGE_HASH, this.eoaSignature)).to.eventually
.be.false;
});
});

describe('with ERC-7913 verifier', function () {
it('with matching signer and signature', async function () {
await expect(this.mock.$isValidSignatureNow(this.validSignerBytes, TEST_MESSAGE_HASH, this.validSignature)).to
.eventually.be.true;
});

it('with invalid verifier', async function () {
const invalidVerifierSigner = ethers.concat([this.mock.target, this.validKey]);
await expect(this.mock.$isValidSignatureNow(invalidVerifierSigner, TEST_MESSAGE_HASH, this.validSignature)).to
.eventually.be.false;
});

it('with invalid key', async function () {
await expect(this.mock.$isValidSignatureNow(this.invalidKeySignerBytes, TEST_MESSAGE_HASH, this.validSignature))
.to.eventually.be.false;
});

it('with invalid signature', async function () {
await expect(this.mock.$isValidSignatureNow(this.validSignerBytes, TEST_MESSAGE_HASH, this.invalidSignature)).to
.eventually.be.false;
});

it('with signer too short', async function () {
const shortSigner = ethers.randomBytes(19);
await expect(this.mock.$isValidSignatureNow(shortSigner, TEST_MESSAGE_HASH, this.validSignature)).to.eventually
.be.false;
});
});
});
});
Loading