Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## XX-XX-XXXX

- `ERC7913Utils`: Utilities library for verifying signatures by ERC-7913 formatted signers.
- `SignerERC7913`: Abstract signer that verifies signatures using the ERC-7913 workflow.
- `ERC7913SignatureVerifierP256` and `ERC7913SignatureVerifierRSA`: Ready to use ERC-7913 verifiers that implement key verification for P256 (secp256r1) and RSA keys.

## 03-04-2025

- `PaymasterNFT`: Extension of `PaymasterCore` that approves sponsoring of user operation based on ownership of an ERC-721 NFT.
Expand Down
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);
}
25 changes: 25 additions & 0 deletions contracts/mocks/account/AccountERC7913Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {Account} from "../../account/Account.sol";
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import {ERC7739} from "../../utils/cryptography/ERC7739.sol";
import {ERC7821} from "../../account/extensions/ERC7821.sol";
import {SignerERC7913} from "../../utils/cryptography/SignerERC7913.sol";

abstract contract AccountERC7913Mock is Account, SignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
constructor(bytes memory _signer) {
_setSigner(_signer);
}

/// @inheritdoc ERC7821
function _erc7821AuthorizedExecutor(
address caller,
bytes32 mode,
bytes calldata executionData
) internal view virtual override returns (bool) {
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
}
}
20 changes: 17 additions & 3 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,37 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
* {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
* {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
* {ERC7913Utils}: utilities library that implements ERC-7913 signature verification with fallback to ERC-1271 and ECDSA.
* {SignerECDSA}, {SignerERC7913}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
* {ERC7913SignatureVerifierP256}, {ERC7913SignatureVerifierRSA}: Ready to use ERC-7913 signature verifiers for P256 and RSA keys
* {Masks}: Library to handle `bytes32` masks.

== Cryptography

{{AbstractSigner}}

{{ERC7739}}

{{ERC7739Utils}}

=== Abstract signers

{{AbstractSigner}}

{{SignerECDSA}}

{{SignerERC7913}}

{{SignerP256}}

{{SignerRSA}}

=== ERC-7913

{{ERC7913Utils}}

{{ERC7913SignatureVerifierP256}}

{{ERC7913SignatureVerifierRSA}}

== Libraries

{{Masks}}
26 changes: 26 additions & 0 deletions contracts/utils/cryptography/ERC7913SignatureVerifierP256.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

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

/**
* @dev ERC-7913 signature verifier that support P256 (secp256r1) keys.
*/
contract ERC7913SignatureVerifierP256 is IERC7913SignatureVerifier {
/// @inheritdoc IERC7913SignatureVerifier
function verify(bytes calldata key, bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) {
// Signature length may be 0x40 or 0x41.
if (key.length == 0x40 && signature.length >= 0x40) {
bytes32 qx = bytes32(key[0x00:0x20]);
bytes32 qy = bytes32(key[0x20:0x40]);
bytes32 r = bytes32(signature[0x00:0x20]);
bytes32 s = bytes32(signature[0x20:0x40]);
if (P256.verify(hash, r, s, qx, qy)) {
return IERC7913SignatureVerifier.verify.selector;
}
}
return 0xFFFFFFFF;
}
}
20 changes: 20 additions & 0 deletions contracts/utils/cryptography/ERC7913SignatureVerifierRSA.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

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

/**
* @dev ERC-7913 signature verifier that support RSA keys.
*/
contract ERC7913SignatureVerifierRSA is IERC7913SignatureVerifier {
/// @inheritdoc IERC7913SignatureVerifier
function verify(bytes calldata key, bytes32 hash, bytes calldata signature) public view virtual returns (bytes4) {
(bytes memory e, bytes memory n) = abi.decode(key, (bytes, bytes));
return
RSA.pkcs1Sha256(abi.encodePacked(hash), signature, e, n)
? IERC7913SignatureVerifier.verify.selector
: bytes4(0xFFFFFFFF);
}
}
74 changes: 74 additions & 0 deletions contracts/utils/cryptography/ERC7913Utils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

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

/**
* @dev Helper library to verify key signatures following the ERC-7913 standard, with fallback to ECDSA and ERC-1271
* when the signer's key is empty (as specified in ERC-7913)
*/
library ERC7913Utils {
Copy link
Member

Choose a reason for hiding this comment

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

I think I would prefer having just one implementation of the isValidSignatureNow function. The rationale is that signature always ends up in memory for any valid verification, and a portion of signer will be copied too. So I doubt there's actual benefits in having two versions. I'll simplify for now following #109

Copy link
Collaborator Author

@Amxx Amxx Apr 12, 2025

Choose a reason for hiding this comment

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

The memory version uses a slice, that does a memory copy.

Overall, calldata versions are often cheaper because they don't copy to memory until the very last point (when the Abi call is encoded) contrary to the memory version that copy to memory at least once more (in this case 2 times)

Overall not a big deal. Having a single "memory" version is probably good enough. Just explaining the initial idea

Copy link
Member

@ernestognw ernestognw Apr 12, 2025

Choose a reason for hiding this comment

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

Yeah I saw the memory version is required, so I'd agree with just one "memory" version for now unless there are significant savings measured.

Thanks for answering though, now please you go 👨‍🍼

using Bytes for bytes;
/**
* @dev Checks if a signature is valid for a given signer and data hash. The signer is interpreted following
* ERC-7913:
* * If the signer's key is not empty the signature is verified using the signer's verifier ERC-7913 interface.
* * Otherwise, the signature is verified using the `SignatureChecker` library, which supports both ECDSA and
* ERC-1271 signature verification
*
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
* change through time. It could return true at block N and false at block N+1 (or the opposite).
*/
function isValidSignatureNow(
bytes memory signer,
bytes32 hash,
bytes calldata 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))).verify(signer.slice(20), hash, signature) returns (
bytes4 magic
) {
return magic == IERC7913SignatureVerifier.verify.selector;
} catch {
return false;
}
}
}

/**
* @dev Checks if a signature is valid for a given signer and data hash. The signer is interpreted following
* ERC-7913:
* * If the signer's key is not empty the signature is verified using the signer's verifier ERC-7913 interface.
* * Otherwise, the signature is verified using the `SignatureChecker` library, which supports both ECDSA and
* ERC-1271 signature verification
*
* NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus
* change through time. It could return true at block N and false at block N+1 (or the opposite).
*/
function isValidSignatureNowCalldata(
bytes calldata signer,
bytes32 hash,
bytes calldata 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;
}
}
}
}
48 changes: 48 additions & 0 deletions contracts/utils/cryptography/SignerERC7913.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {AbstractSigner} from "./AbstractSigner.sol";
import {ERC7913Utils} from "./ERC7913Utils.sol";

/**
* @dev Implementation of {AbstractSigner} that supports ERC-7913 signers.
*
* For {Account} usage, an {_setSigner} function is provided to set the ERC-7913 formatted {signer}.
* Doing so it's easier for a factory, whose likely to use initializable clones of this contract.
*
* Example of usage:
*
* ```solidity
* contract MyAccountERC7913 is Account, SignerERC7913, Initializable {
* constructor() EIP712("MyAccountERC7913", "1") {}
*
* function initialize(bytes memory signer) public initializer {
* _setSigner(signer);
* }
* }
* ```
*
* IMPORTANT: Avoiding to call {_setSigner} either during construction (if used standalone)
* or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
*/

abstract contract SignerERC7913 is AbstractSigner {
bytes private _signer;

function _setSigner(bytes memory signer_) internal {
_signer = signer_;
}

function signer() public view virtual returns (bytes memory) {
return _signer;
}

/// @inheritdoc AbstractSigner
function _rawSignatureValidation(
bytes32 hash,
bytes calldata signature
) internal view virtual override returns (bool) {
return ERC7913Utils.isValidSignatureNow(signer(), hash, signature);
}
}
106 changes: 106 additions & 0 deletions test/account/AccountERC7913.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const { ethers, entrypoint } = require('hardhat');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');

const { getDomain } = require('@openzeppelin/contracts/test/helpers/eip712');
const { ERC4337Helper } = require('../helpers/erc4337');
const { NonNativeSigner, P256SigningKey, RSASHA256SigningKey } = require('../helpers/signers');
const { PackedUserOperation } = require('../helpers/eip712-types');

const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');

// Prepare signer in advance (RSA are long to initialize)
const signerECDSA = ethers.Wallet.createRandom();
const signerP256 = new NonNativeSigner(P256SigningKey.random());
const signerRSA = new NonNativeSigner(RSASHA256SigningKey.random());

// Minimal fixture common to the different signer verifiers
async function fixture() {
// EOAs and environment
const [beneficiary, other] = await ethers.getSigners();
const target = await ethers.deployContract('CallReceiverMockExtended');

// ERC-7913 verifiers
const verifierP256 = await ethers.deployContract('ERC7913SignatureVerifierP256');
const verifierRSA = await ethers.deployContract('ERC7913SignatureVerifierRSA');

// ERC-4337 env
const helper = new ERC4337Helper();
await helper.wait();
const entrypointDomain = await getDomain(entrypoint.v08);
const domain = { name: 'AccountERC7913', version: '1', chainId: entrypointDomain.chainId }; // Missing verifyingContract,

const makeMock = signer =>
helper.newAccount('$AccountERC7913Mock', ['AccountERC7913', '1', signer]).then(mock => {
domain.verifyingContract = mock.address;
return mock;
});

const signUserOp = function (userOp) {
return this.signer
.signTypedData(entrypointDomain, { PackedUserOperation }, userOp.packed)
.then(signature => Object.assign(userOp, { signature }));
};

return { helper, verifierP256, verifierRSA, domain, target, beneficiary, other, makeMock, signUserOp };
}

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

// Using ECDSA key as verifier
describe('ECDSA key', function () {
beforeEach(async function () {
this.signer = signerECDSA;
this.mock = await this.makeMock(this.signer.address);
});

shouldBehaveLikeAccountCore();
shouldBehaveLikeAccountHolder();
shouldBehaveLikeERC1271({ erc7739: true });
shouldBehaveLikeERC7821();
});

// Using P256 key with an ERC-7913 verifier
describe('P256 key', function () {
beforeEach(async function () {
this.signer = signerP256;
this.mock = await this.makeMock(
ethers.concat([
this.verifierP256.target,
this.signer.signingKey.publicKey.qx,
this.signer.signingKey.publicKey.qy,
]),
);
});

shouldBehaveLikeAccountCore();
shouldBehaveLikeAccountHolder();
shouldBehaveLikeERC1271({ erc7739: true });
shouldBehaveLikeERC7821();
});

// Using RSA key with an ERC-7913 verifier
describe('RSA key', function () {
beforeEach(async function () {
this.signer = signerRSA;
this.mock = await this.makeMock(
ethers.concat([
this.verifierRSA.target,
ethers.AbiCoder.defaultAbiCoder().encode(
['bytes', 'bytes'],
[this.signer.signingKey.publicKey.e, this.signer.signingKey.publicKey.n],
),
]),
);
});

shouldBehaveLikeAccountCore();
shouldBehaveLikeAccountHolder();
shouldBehaveLikeERC1271({ erc7739: true });
shouldBehaveLikeERC7821();
});
});
Loading