diff --git a/CHANGELOG.md b/CHANGELOG.md index 345d277e47e..0051deb3e56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * `ERC777`: make reception acquirement optional in `_mint`. ([#2552](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2552)) * `ERC20Permit`: add a `_useNonce` to enable further usage of ERC712 signatures. ([#2565](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2565)) * `ERC20FlashMint`: add an implementation of the ERC3156 extension for flash-minting ERC20 tokens. ([#2543](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2543)) + * `SignatureChecker`: add a signature verification library that supports both EOA and ERC1271 compliant contracts as signers. ([#2532](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2532)) ## 4.0.0 (2021-03-23) diff --git a/contracts/interfaces/IERC1271.sol b/contracts/interfaces/IERC1271.sol new file mode 100644 index 00000000000..ca5de924cc5 --- /dev/null +++ b/contracts/interfaces/IERC1271.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC1271 standard signature validation method for + * contracts as defined in https://eips.ethereum.org/EIPS/eip-1271[ERC-1271]. + */ +interface IERC1271 { + /** + * @dev Should return whether the signature provided is valid for the provided data + * @param hash Hash of the data to be signed + * @param signature Signature byte array associated with _data + */ + function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue); +} diff --git a/contracts/mocks/ERC1271WalletMock.sol b/contracts/mocks/ERC1271WalletMock.sol new file mode 100644 index 00000000000..c92acdba63e --- /dev/null +++ b/contracts/mocks/ERC1271WalletMock.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../access/Ownable.sol"; +import "../interfaces/IERC1271.sol"; +import "../utils/cryptography/ECDSA.sol"; + +contract ERC1271WalletMock is Ownable, IERC1271 { + constructor(address originalOwner) { + transferOwnership(originalOwner); + } + + function isValidSignature(bytes32 hash, bytes memory signature) public view override returns (bytes4 magicValue) { + return ECDSA.recover(hash, signature) == owner() ? this.isValidSignature.selector : bytes4(0); + } +} diff --git a/contracts/mocks/SignatureCheckerMock.sol b/contracts/mocks/SignatureCheckerMock.sol new file mode 100644 index 00000000000..5671540ec4a --- /dev/null +++ b/contracts/mocks/SignatureCheckerMock.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/cryptography/SignatureChecker.sol"; + +contract SignatureCheckerMock { + using SignatureChecker for address; + + function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) public view returns (bool) { + return signer.isValidSignatureNow(hash, signature); + } +} diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index e5cdd3947cf..cdc429ebd91 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -36,6 +36,8 @@ Finally, {Create2} contains all necessary utilities to safely use the https://bl {{ECDSA}} +{{SignatureChecker}} + {{MerkleProof}} {{EIP712}} diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol new file mode 100644 index 00000000000..881ae579a86 --- /dev/null +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./ECDSA.sol"; +import "../Address.sol"; +import "../../interfaces/IERC1271.sol"; + +/** + * @dev Signature verification helper: Provide a single mechanism to verify both private-key (EOA) ECDSA signature and + * ERC1271 contract sigantures. Using this instead of ECDSA.recover in your contract will make them compatible with + * smart contract wallets such as Argent and Gnosis. + * + * Note: unlike ECDSA signatures, contract signature's 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). + */ +library SignatureChecker { + function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { + if (Address.isContract(signer)) { + try IERC1271(signer).isValidSignature(hash, signature) returns (bytes4 magicValue) { + return magicValue == IERC1271(signer).isValidSignature.selector; + } catch { + return false; + } + } else { + return ECDSA.recover(hash, signature) == signer; + } + } +} diff --git a/test/utils/cryptography/SignatureChecker.test.js b/test/utils/cryptography/SignatureChecker.test.js new file mode 100644 index 00000000000..4364d150e11 --- /dev/null +++ b/test/utils/cryptography/SignatureChecker.test.js @@ -0,0 +1,71 @@ +const { toEthSignedMessageHash, fixSignature } = require('../../helpers/sign'); + +const { expect } = require('chai'); + +const SignatureCheckerMock = artifacts.require('SignatureCheckerMock'); +const ERC1271WalletMock = artifacts.require('ERC1271WalletMock'); + +const TEST_MESSAGE = web3.utils.sha3('OpenZeppelin'); +const WRONG_MESSAGE = web3.utils.sha3('Nope'); + +contract('SignatureChecker (ERC1271)', function (accounts) { + const [signer, other] = accounts; + + before('deploying', async function () { + this.signaturechecker = await SignatureCheckerMock.new(); + this.wallet = await ERC1271WalletMock.new(signer); + this.signature = fixSignature(await web3.eth.sign(TEST_MESSAGE, signer)); + }); + + context('EOA account', function () { + it('with matching signer and signature', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + signer, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + )).to.equal(true); + }); + + it('with invalid signer', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + other, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + )).to.equal(false); + }); + + it('with invalid signature', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + signer, + toEthSignedMessageHash(WRONG_MESSAGE), + this.signature, + )).to.equal(false); + }); + }); + + context('ERC1271 wallet', function () { + it('with matching signer and signature', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + this.wallet.address, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + )).to.equal(true); + }); + + it('with invalid signer', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + this.signaturechecker.address, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + )).to.equal(false); + }); + + it('with invalid signature', async function () { + expect(await this.signaturechecker.isValidSignatureNow( + this.wallet.address, + toEthSignedMessageHash(WRONG_MESSAGE), + this.signature, + )).to.equal(false); + }); + }); +});