diff --git a/src/BaseDocumentStore.sol b/src/BaseDocumentStore.sol index 8541e42..aaa644d 100644 --- a/src/BaseDocumentStore.sol +++ b/src/BaseDocumentStore.sol @@ -3,14 +3,24 @@ pragma solidity >=0.8.23 <0.9.0; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; -import {IDocumentStore} from "./interfaces/IDocumentStore.sol"; +import "./interfaces/IDocumentStore.sol"; +import "./interfaces/IDocumentStoreBatchable.sol"; +import "./base/DocumentStoreAccessControl.sol"; /** * @title BaseDocumentStore * @notice A base contract for storing and revoking documents */ -abstract contract BaseDocumentStore is Initializable, IDocumentStore { +abstract contract BaseDocumentStore is + Initializable, + IDocumentStoreBatchable, + IDocumentStore, + DocumentStoreAccessControl +{ + using MerkleProof for bytes32[]; + /** * @notice The name of the contract */ @@ -19,52 +29,160 @@ abstract contract BaseDocumentStore is Initializable, IDocumentStore { /** * @notice A mapping of the document hash to the block number that was issued */ - mapping(bytes32 => uint256) public documentIssued; + mapping(bytes32 => uint256) internal documentIssued; /** * @notice A mapping of the hash of the claim being revoked to the revocation block number */ - mapping(bytes32 => uint256) public documentRevoked; + mapping(bytes32 => uint256) internal documentRevoked; /** * @notice Initialises the contract with a name * @param _name The name of the contract */ - function __BaseDocumentStore_init(string memory _name) internal onlyInitializing { + function __BaseDocumentStore_init(string memory _name, address initAdmin) internal onlyInitializing { + __DocumentStoreAccessControl_init(initAdmin); name = _name; } /** * @notice Issues a document - * @param document The hash of the document to issue + * @param documentRoot The hash of the document to issue */ - function _issue(bytes32 document) internal { - documentIssued[document] = block.number; + function issue(bytes32 documentRoot) external onlyRole(ISSUER_ROLE) { + _issue(documentRoot); } /** - * @notice Checks if a document has been issued - * @param document The hash of the document to check - * @return A boolean indicating whether the document has been issued + * @notice Issues multiple documents + * @param documentRoots The hashes of the documents to issue */ - function _isIssued(bytes32 document) internal view returns (bool) { - return (documentIssued[document] != 0); + function bulkIssue(bytes32[] memory documentRoots) external onlyRole(ISSUER_ROLE) { + for (uint256 i = 0; i < documentRoots.length; i++) { + _issue(documentRoots[i]); + } + } + + function revoke(bytes32 documentRoot, bytes32 document, bytes32[] memory proof) external onlyRole(REVOKER_ROLE) { + _revoke(documentRoot, document, proof); } /** * @notice Revokes a document - * @param document The hash of the document to revoke + * @param documentRoot The hash of the document to revoke */ - function _revoke(bytes32 document) internal { - documentRevoked[document] = block.number; + function revoke(bytes32 documentRoot) external onlyRole(REVOKER_ROLE) { + _revoke(documentRoot, documentRoot, new bytes32[](0)); + } + + /** + * @notice Revokes documents in bulk + * @param documentRoots The hashes of the documents to revoke + */ + function bulkRevoke( + bytes32[] memory documentRoots, + bytes32[] memory documents, + bytes32[][] memory proofs + ) external onlyRole(REVOKER_ROLE) { + for (uint256 i = 0; i < documentRoots.length; i++) { + _revoke(documentRoots[i], documents[i], proofs[i]); + } + } + + function isIssued( + bytes32 documentRoot, + bytes32 document, + bytes32[] memory proof + ) public view onlyValidDocument(documentRoot, document, proof) returns (bool) { + if (documentRoot == document && proof.length == 0) { + return documentIssued[document] != 0; + } + return documentIssued[documentRoot] != 0; + } + + function isIssued(bytes32 documentRoot) public view returns (bool) { + return isIssued(documentRoot, documentRoot, new bytes32[](0)); + } + + function isRevoked( + bytes32 documentRoot, + bytes32 document, + bytes32[] memory proof + ) public view onlyValidDocument(documentRoot, document, proof) returns (bool) { + if (!isIssued(documentRoot, document, proof)) { + revert DocumentNotIssued(documentRoot, document); + } + return _isRevoked(documentRoot, document, proof); } /** * @notice Checks if a document has been revoked - * @param document The hash of the document to check + * @param documentRoot The hash of the document to check * @return A boolean indicating whether the document has been revoked */ - function _isRevoked(bytes32 document) internal view returns (bool) { - return documentRevoked[document] != 0; + function isRevoked(bytes32 documentRoot) public view returns (bool) { + return isRevoked(documentRoot, documentRoot, new bytes32[](0)); + } + + function isActive(bytes32 documentRoot, bytes32 document, bytes32[] memory proof) public view returns (bool) { + if (!isIssued(documentRoot, document, proof)) { + revert DocumentNotIssued(documentRoot, document); + } + return !_isRevoked(documentRoot, document, proof); + } + + function isActive(bytes32 documentRoot) public view returns (bool) { + return isActive(documentRoot, documentRoot, new bytes32[](0)); + } + + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return + interfaceId == type(IDocumentStore).interfaceId || + interfaceId == type(IDocumentStoreBatchable).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @notice Issues a document + * @param documentRoot The hash of the document to issue + */ + function _issue(bytes32 documentRoot) internal { + if (isIssued(documentRoot)) { + revert DocumentExists(documentRoot); + } + + documentIssued[documentRoot] = block.number; + + emit DocumentIssued(documentRoot); + } + + function _revoke(bytes32 documentRoot, bytes32 document, bytes32[] memory proof) internal { + bool active = isActive(documentRoot, document, proof); + if (!active) { + revert InactiveDocument(documentRoot, document); + } + documentRevoked[document] = block.number; + emit DocumentRevoked(documentRoot, document); + } + + function _isRevoked(bytes32 documentRoot, bytes32 document, bytes32[] memory proof) internal view returns (bool) { + if (documentRoot == document && proof.length == 0) { + return documentRevoked[document] != 0; + } + return documentRevoked[documentRoot] != 0 || documentRevoked[document] != 0; + } + + modifier onlyValidDocument( + bytes32 documentRoot, + bytes32 document, + bytes32[] memory proof + ) { + if (document == 0x0 || documentRoot == 0x0) { + revert ZeroDocument(); + } + if (!proof.verify(documentRoot, document)) { + revert InvalidDocument(documentRoot, document); + } + _; } } diff --git a/src/DocumentStore.sol b/src/DocumentStore.sol index 7f3fb75..449ba41 100644 --- a/src/DocumentStore.sol +++ b/src/DocumentStore.sol @@ -6,148 +6,28 @@ import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import "./BaseDocumentStore.sol"; import "./base/DocumentStoreAccessControl.sol"; +import "./interfaces/IDocumentStoreBatchable.sol"; /** * @title DocumentStore * @notice A contract for storing and revoking documents with access control */ -contract DocumentStore is DocumentStoreAccessControl, BaseDocumentStore { - using MerkleProof for bytes32[]; - +contract DocumentStore is BaseDocumentStore { /** - * @notice Initialises the contract with a name and owner + * @notice Initialises the contract with a name and initial admin * @param _name The name of the contract - * @param owner The owner of the contract + * @param initAdmin The initial admin of the contract */ - constructor(string memory _name, address owner) { - initialize(_name, owner); + constructor(string memory _name, address initAdmin) { + initialize(_name, initAdmin); } /** * @notice Internally initialises the contract with a name and owner * @param _name The name of the contract - * @param owner The owner of the contract - */ - function initialize(string memory _name, address owner) internal initializer { - __DocumentStoreAccessControl_init(owner); - __BaseDocumentStore_init(_name); - } - - /** - * @notice Issues a document - * @param documentRoot The hash of the document to issue + * @param initAdmin The owner of the contract */ - function issue(bytes32 documentRoot) public onlyRole(ISSUER_ROLE) { - if (isIssued(documentRoot)) { - revert DocumentExists(documentRoot); - } - - _issue(documentRoot); - - emit DocumentIssued(documentRoot); - } - - /** - * @notice Issues multiple documents - * @param documentRoots The hashes of the documents to issue - */ - function bulkIssue(bytes32[] memory documentRoots) public { - for (uint256 i = 0; i < documentRoots.length; i++) { - issue(documentRoots[i]); - } - } - - /** - * @notice Revokes a document - * @param documentRoot The hash of the document to revoke - */ - function revoke(bytes32 documentRoot) public onlyRole(REVOKER_ROLE) { - revoke(documentRoot, documentRoot, new bytes32[](0)); - } - - function revoke(bytes32 documentRoot, bytes32 document, bytes32[] memory proof) public onlyRole(REVOKER_ROLE) { - bool active = isActive(documentRoot, document, proof); - if (!active) { - revert InactiveDocument(documentRoot, document); - } - _revoke(document); - emit DocumentRevoked(documentRoot, document); - } - - /** - * @notice Revokes documents in bulk - * @param documentRoots The hashes of the documents to revoke - */ - function bulkRevoke( - bytes32[] memory documentRoots, - bytes32[] memory documents, - bytes32[][] memory proofs - ) public onlyRole(REVOKER_ROLE) { - for (uint256 i = 0; i < documentRoots.length; i++) { - revoke(documentRoots[i], documents[i], proofs[i]); - } - } - - function isIssued( - bytes32 documentRoot, - bytes32 document, - bytes32[] memory proof - ) public view onlyValidDocument(documentRoot, document, proof) returns (bool) { - if (documentRoot == document && proof.length == 0) { - return _isIssued(document); - } - return _isIssued(documentRoot); - } - - function isIssued(bytes32 documentRoot) public view returns (bool) { - return isIssued(documentRoot, documentRoot, new bytes32[](0)); - } - - function isRevoked( - bytes32 documentRoot, - bytes32 document, - bytes32[] memory proof - ) public view onlyValidDocument(documentRoot, document, proof) returns (bool) { - if (!isIssued(documentRoot, document, proof)) { - revert InvalidDocument(documentRoot, document); - } - return _isRevoked(documentRoot, document, proof); - } - - function _isRevoked(bytes32 documentRoot, bytes32 document, bytes32[] memory proof) internal view returns (bool) { - if (documentRoot == document && proof.length == 0) { - return _isRevoked(document); - } - return (_isRevoked(documentRoot) || _isRevoked(document)); - } - - /** - * @notice Checks if a document has been revoked - * @param documentRoot The hash of the document to check - * @return A boolean indicating whether the document has been revoked - */ - function isRevoked(bytes32 documentRoot) public view returns (bool) { - return isRevoked(documentRoot, documentRoot, new bytes32[](0)); - } - - function isActive(bytes32 documentRoot, bytes32 document, bytes32[] memory proof) public view returns (bool) { - if (!isIssued(documentRoot, document, proof)) { - revert InvalidDocument(documentRoot, document); - } - return !_isRevoked(documentRoot, document, proof); - } - - modifier onlyValidDocument( - bytes32 documentRoot, - bytes32 document, - bytes32[] memory proof - ) { - if (document == 0x0 || documentRoot == 0x0) { - revert ZeroDocument(); - } - if (!proof.verify(documentRoot, document)) { - revert InvalidDocument(documentRoot, document); - } - _; + function initialize(string memory _name, address initAdmin) internal initializer { + __BaseDocumentStore_init(_name, initAdmin); } } diff --git a/src/interfaces/IDocumentStore.sol b/src/interfaces/IDocumentStore.sol index 421bc40..d2a0e69 100644 --- a/src/interfaces/IDocumentStore.sol +++ b/src/interfaces/IDocumentStore.sol @@ -3,10 +3,15 @@ pragma solidity >=0.8.23 <0.9.0; interface IDocumentStore { error InactiveDocument(bytes32 documentRoot, bytes32 document); + error DocumentExists(bytes32 document); + error ZeroDocument(); + error InvalidDocument(bytes32 documentRoot, bytes32 document); + error DocumentNotIssued(bytes32 documentRoot, bytes32 document); + /** * @notice Emitted when a document is issued * @param document The hash of the issued document @@ -18,4 +23,16 @@ interface IDocumentStore { * @param document The hash of the revoked document */ event DocumentRevoked(bytes32 indexed documentRoot, bytes32 indexed document); + + function name() external view returns (string memory); + + function issue(bytes32 documentRoot) external; + + function revoke(bytes32 documentRoot) external; + + function isIssued(bytes32 documentRoot) external view returns (bool); + + function isRevoked(bytes32 documentRoot) external view returns (bool); + + function isActive(bytes32 documentRoot) external view returns (bool); } diff --git a/src/interfaces/IDocumentStoreBatchable.sol b/src/interfaces/IDocumentStoreBatchable.sol new file mode 100644 index 0000000..9dacfd8 --- /dev/null +++ b/src/interfaces/IDocumentStoreBatchable.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.23 <0.9.0; + +interface IDocumentStoreBatchable { + function revoke(bytes32 documentRoot, bytes32 document, bytes32[] memory proof) external; + + function isIssued(bytes32 documentRoot, bytes32 document, bytes32[] memory proof) external view returns (bool); + + function isRevoked(bytes32 documentRoot, bytes32 document, bytes32[] memory proof) external view returns (bool); + + function isActive(bytes32 documentRoot, bytes32 document, bytes32[] memory proof) external view returns (bool); +} diff --git a/test/DocumentStore.t.sol b/test/DocumentStore.t.sol index a1fdaf4..059f8fd 100644 --- a/test/DocumentStore.t.sol +++ b/test/DocumentStore.t.sol @@ -300,7 +300,7 @@ contract DocumentStore_revokeRoot_Test is DocumentStoreWithFakeDocuments_Base { function testRevokeRootNonIssuedRootRevert(bytes32 nonIssuedRoot) public { vm.assume(nonIssuedRoot != docRoot && nonIssuedRoot != bytes32(0)); - vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, nonIssuedRoot, nonIssuedRoot)); + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.DocumentNotIssued.selector, nonIssuedRoot, nonIssuedRoot)); vm.prank(revoker); documentStore.revoke(nonIssuedRoot); @@ -404,7 +404,7 @@ contract DocumentStore_revoke_Test is DocumentStoreWithFakeDocuments_Base { function testRevokeNonIssuedDocumentRevert(bytes32 nonIssuedRoot) public { vm.assume(nonIssuedRoot != docRoot && nonIssuedRoot != bytes32(0)); - vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, nonIssuedRoot, nonIssuedRoot)); + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.DocumentNotIssued.selector, nonIssuedRoot, nonIssuedRoot)); vm.prank(revoker); documentStore.revoke(nonIssuedRoot); @@ -583,7 +583,7 @@ contract DocumentStore_isRootRevoked is DocumentStoreWithFakeDocuments_Base { function testIsRootRevokedWithNotIssuedRootRevert(bytes32 notIssuedRoot) public { vm.assume(notIssuedRoot != docRoot && notIssuedRoot != bytes32(0)); - vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, notIssuedRoot, notIssuedRoot)); + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.DocumentNotIssued.selector, notIssuedRoot, notIssuedRoot)); assertFalse(documentStore.isRevoked(notIssuedRoot)); } @@ -630,8 +630,20 @@ contract DocumentStore_isActive_Test is DocumentStoreWithFakeDocuments_Base { function testIsActiveWithNotIssuedDocumentRevert(bytes32 notIssuedDoc) public { vm.assume(notIssuedDoc != docRoot && notIssuedDoc != bytes32(0)); - vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, docRoot, notIssuedDoc)); + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.DocumentNotIssued.selector, notIssuedDoc, notIssuedDoc)); + + documentStore.isActive(notIssuedDoc, notIssuedDoc, new bytes32[](0)); + } + + function testIsActiveWithNotIssuedRootRevert() public { + bytes32 notIssuedRoot = 0xb841229d504c5c9bcb8132078db8c4a483825ad811078144c6f9aec84213d798; + bytes32 notIssuedDoc = 0xd56c26db0fde817dcd82269d0f9a3f50ea256ee0c870e43c3ec2ebdd655e3f37; + + bytes32[] memory proofs = new bytes32[](1); + proofs[0] = 0x9800b3feae3c44fe4263f6cbb2d8dd529c26c3a1c3ca7208a30cfa5efbc362e7; + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.DocumentNotIssued.selector, notIssuedRoot, notIssuedDoc)); - documentStore.isActive(docRoot, notIssuedDoc, proofs[0]); + documentStore.isActive(notIssuedRoot, notIssuedDoc, proofs); } }