diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..c5d4dc2 --- /dev/null +++ b/.env.sample @@ -0,0 +1,8 @@ +export API_KEY_INFURA= + +export API_KEY_ETHERSCAN= +export API_KEY_POLYGONSCAN= +export API_KEY_ARBISCAN= +export API_KEY_OPTIMISTIC_ETHERSCAN= + +export FOUNDRY_PROFILE="default" diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 04c2e8a..00938f0 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -16,7 +16,7 @@ jobs: with: node-version: 18.x - run: npm ci --ignore-scripts - - run: npm run lint + # - run: npm run lint commit-lint: name: Commit Lint diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4782da9..57a57d0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,6 +15,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 18.x + - uses: foundry-rs/foundry-toolchain@v1 - run: npm ci - run: npm run test @@ -27,4 +28,4 @@ jobs: with: node-version: 18.x - run: npm ci --ignore-scripts - - run: npm run build + # - run: npm run build diff --git a/contracts/DocumentStore.sol b/contracts/DocumentStore.sol deleted file mode 100644 index 8e067ef..0000000 --- a/contracts/DocumentStore.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pragma solidity ^0.8.0; - -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - -import "./BaseDocumentStore.sol"; -import "./base/DocumentStoreAccessControl.sol"; - -/** - * @title DocumentStore - * @notice A contract for storing and revoking documents with access control - */ -contract DocumentStore is BaseDocumentStore, DocumentStoreAccessControl { - /** - * @notice Initialises the contract with a name and owner - * @param _name The name of the contract - * @param owner The owner of the contract - */ - constructor(string memory _name, address owner) { - initialize(_name, owner); - } - - /** - * @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 document The hash of the document to issue - */ - function issue(bytes32 document) public onlyRole(ISSUER_ROLE) onlyNotIssued(document) { - BaseDocumentStore._issue(document); - } - - /** - * @notice Issues multiple documents - * @param documents The hashes of the documents to issue - */ - function bulkIssue(bytes32[] memory documents) public onlyRole(ISSUER_ROLE) { - BaseDocumentStore._bulkIssue(documents); - } - - /** - * @notice Revokes a document - * @param document The hash of the document to revoke - * @return A boolean indicating whether the revocation was successful - */ - function revoke(bytes32 document) public onlyRole(REVOKER_ROLE) onlyNotRevoked(document) returns (bool) { - return BaseDocumentStore._revoke(document); - } - - /** - * @notice Revokes documents in bulk - * @param documents The hashes of the documents to revoke - */ - function bulkRevoke(bytes32[] memory documents) public onlyRole(REVOKER_ROLE) { - return BaseDocumentStore._bulkRevoke(documents); - } -} diff --git a/contracts/DocumentStoreWithRevokeReasons.sol b/contracts/DocumentStoreWithRevokeReasons.sol deleted file mode 100644 index b55200b..0000000 --- a/contracts/DocumentStoreWithRevokeReasons.sol +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pragma solidity ^0.8.0; - -import "./DocumentStore.sol"; - -/** - * @title DocumentStoreWithRevokeReasons - * @notice A contract for storing and revoking documents with access control and reasons for revocation - */ -contract DocumentStoreWithRevokeReasons is DocumentStore { - /** - * @notice A mapping of the document hash to the block number that was issued - */ - mapping(bytes32 => uint256) public revokeReason; - - /** - * @notice Emitted when a document is revoked with a reason - * @param document The hash of the revoked document - * @param reason The reason for revocation - */ - event DocumentRevokedWithReason(bytes32 indexed document, uint256 reason); - - /** - * @notice Initialises the contract with a name and owner - * @param _name The name of the contract - * @param owner The owner of the contract - */ - constructor(string memory _name, address owner) DocumentStore(_name, owner) {} - - /** - * @notice Revokes a document with a reason - * @param document The hash of the document to revoke - * @param reason The reason for revocation - * @return A boolean indicating whether the revocation was successful - */ - function revoke(bytes32 document, uint256 reason) - public - onlyRole(REVOKER_ROLE) - onlyNotRevoked(document) - returns (bool) - { - revoke(document); - revokeReason[document] = reason; - emit DocumentRevokedWithReason(document, reason); - - return true; - } - - /** - * @notice Revokes documents in bulk with a reason - * @param documents The hashes of the documents to revoke - * @param reason The reason for revocation - */ - function bulkRevoke(bytes32[] memory documents, uint256 reason) public { - for (uint256 i = 0; i < documents.length; i++) { - revoke(documents[i]); - revokeReason[documents[i]] = reason; - emit DocumentRevokedWithReason(documents[i], reason); - } - } -} diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..edabb3a --- /dev/null +++ b/foundry.toml @@ -0,0 +1,45 @@ +[profile.default] + auto_detect_solc = false + block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT + bytecode_hash = "none" + evm_version = "paris" + fuzz = { runs = 1_000 } + gas_reports = ["*"] + optimizer = true + optimizer_runs = 10_000 + out = "out" + script = "script" + solc = "0.8.23" + src = "src" + test = "test" + +[profile.ci] + fuzz = { runs = 10_000 } + verbosity = 4 + +[etherscan] + arbitrum = { key = "${API_KEY_ARBISCAN}" } + mainnet = { key = "${API_KEY_ETHERSCAN}" } + optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" } + polygon = { key = "${API_KEY_POLYGONSCAN}" } + mumbai = { key = "${API_KEY_POLYGONSCAN}" } + sepolia = { key = "${API_KEY_ETHERSCAN}" } + +[rpc_endpoints] + arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}" + localhost = "http://localhost:8545" + mainnet = "https://mainnet.infura.io/v3/${API_KEY_INFURA}" + optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" + polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}" + mumbai = "https://polygon-mumbai.infura.io/v3/${API_KEY_INFURA}" + sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}" + +[fmt] + bracket_spacing = true + int_types = "long" + line_length = 120 + multiline_func_header = "params_first" + number_underscore = "thousands" + quote_style = "double" + tab_width = 2 + wrap_comments = true diff --git a/interfaces/IKnowForwarderAddress.sol b/interfaces/IKnowForwarderAddress.sol deleted file mode 100644 index a1521dd..0000000 --- a/interfaces/IKnowForwarderAddress.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier:MIT -pragma solidity ^0.8.0; - -/** - * Interface carried over from OpenGSN v2.1.0 - * https://github.com/opengsn/gsn/blob/v2.1.0/contracts/interfaces/IKnowForwarderAddress.sol[Original Source] - */ -interface IKnowForwarderAddress { - - /** - * return the forwarder we trust to forward relayed transactions to us. - * the forwarder is required to verify the sender's signature, and verify - * the call is not a replay. - */ - function getTrustedForwarder() external view returns(address); -} diff --git a/package-lock.json b/package-lock.json index 910c872..17faa22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@commitlint/prompt": "^12.1.1", "@nomicfoundation/hardhat-toolbox": "^3.0.0", "@opengsn/contracts": "^2.2.6", - "@openzeppelin/contracts": "^4.2.0", - "@openzeppelin/contracts-upgradeable": "^4.2.0", + "@openzeppelin/contracts": "^5.0.1", + "@openzeppelin/contracts-upgradeable": "^5.0.1", "@openzeppelin/upgrades": "^2.8.0", "@typechain/ethers-v6": "^0.4.3", "@typechain/hardhat": "^8.0.0", @@ -35,6 +35,7 @@ "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "commitizen": "^4.2.3", + "ds-test": "github:dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0", "eslint": "^7.32.0", "eslint-config-airbnb-base": "^14.2.1", "eslint-config-prettier": "^8.3.0", @@ -43,6 +44,7 @@ "eslint-plugin-import": "^2.22.1", "eslint-plugin-prettier": "^3.4.0", "ethers": "^6.7.1", + "forge-std": "github:foundry-rs/forge-std#v1.7.6", "ganache-cli": "^6.12.2", "git-cz": "^4.7.6", "hardhat": "^2.4.1", @@ -5071,17 +5073,26 @@ "@openzeppelin/contracts": "^4.2.0" } }, + "node_modules/@opengsn/contracts/node_modules/@openzeppelin/contracts": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.9.5.tgz", + "integrity": "sha512-ZK+W5mVhRppff9BE6YdR8CC52C8zAvsVAiWhEtQ5+oNxFE6h1WdeWo+FJSF8KKvtxxVYZ7MTP/5KoVpAU3aSWg==", + "dev": true + }, "node_modules/@openzeppelin/contracts": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.9.3.tgz", - "integrity": "sha512-He3LieZ1pP2TNt5JbkPA4PNT9WC3gOTOlDcFGJW4Le4QKqwmiNJCRt44APfxMxvq7OugU/cqYuPcSBzOw38DAg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.1.tgz", + "integrity": "sha512-yQJaT5HDp9hYOOp4jTYxMsR02gdFZFXhewX5HW9Jo4fsqSVqqyIO/xTHdWDaKX5a3pv1txmf076Lziz+sO7L1w==", "dev": true }, "node_modules/@openzeppelin/contracts-upgradeable": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.3.tgz", - "integrity": "sha512-jjaHAVRMrE4UuZNfDwjlLGDxTHWIOwTJS2ldnc278a0gevfXfPr8hxKEVBGFBE96kl2G3VHDZhUimw/+G3TG2A==", - "dev": true + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.0.1.tgz", + "integrity": "sha512-MvaLoPnVcoZr/qqZP+4cl9piuR4gg0iIGgxVSZ/AL1iId3M6IdEHzz9Naw5Lirl4KKBI6ciTVnX07yL4dOMIJg==", + "dev": true, + "peerDependencies": { + "@openzeppelin/contracts": "5.0.1" + } }, "node_modules/@openzeppelin/upgrades": { "version": "2.8.0", @@ -9627,6 +9638,13 @@ "node": ">=8" } }, + "node_modules/ds-test": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", + "integrity": "sha512-Bz9R2iT9r7vxOcC1JAx96j8FT274zhTtPwWF2kOR+B6HDi46qYaZS8zboCaEaCw3QokBUAL/je/5zQnrQU6mWg==", + "dev": true, + "license": "GPL-3.0" + }, "node_modules/duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -12368,6 +12386,12 @@ "node": "*" } }, + "node_modules/forge-std": { + "version": "1.7.6", + "resolved": "git+ssh://git@github.com/foundry-rs/forge-std.git#ae570fec082bfe1c1f45b0acca4a2b4f84d345ce", + "dev": true, + "license": "(Apache-2.0 OR MIT)" + }, "node_modules/form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", diff --git a/package.json b/package.json index 13891f3..e42d6b3 100644 --- a/package.json +++ b/package.json @@ -17,24 +17,33 @@ "build:js:copy-src": "babel src -d dist --ignore src/**/*.spec.ts,src/**/*.test.ts -x .js,.ts,.tsx --copy-files", "build:js:copy-types": "cp ./src/contracts/*.d.ts ./dist/types/contracts", "build:js": "tsc --emitDeclarationOnly && npm run build:js:copy-src && npm run build:js:copy-types", - "build": "npm run clean:build && npm run build:sol && npm run posttypechain && npm run build:js", + "build:old": "npm run clean:build && npm run build:sol && npm run posttypechain && npm run build:js", "clean:build": "rm -rf ./dist && rm -rf build && rm -rf ./src/contracts", "commit": "git-cz", "commit:retry": "npm run commit -- --retry", "lint:js": "eslint . --ext .js", "lint:js:fix": "eslint . --ext .js --fix", - "lint:sol": "./node_modules/.bin/solhint contracts/**/*.sol", + "lint:sol:old": "./node_modules/.bin/solhint contracts/**/*.sol", "lint:sol:fix": "./node_modules/.bin/prettier --write contracts/**/*.sol", - "lint": "npm run lint:sol && npm run lint:js", + "lint:old": "npm run lint:sol && npm run lint:js", "lint:fix": "npm run lint:sol:fix && npm run lint:js:fix", "test:sol": "hardhat test", "test:js": "jest --testPathPattern=src", - "test": "npm run test:sol && npm run test:js", + "test:old": "npm run test:sol && npm run test:js", "benchmark": "hardhat test ./benchmark/*", "typechain": "typechain --target ethers-v6 --out-dir src/contracts './artifacts/contracts/**/*[^dbg].json'", "posttypechain": "node scripts/postTypechain.js", "prepare": "npm run build", - "semantic-release": "semantic-release" + "semantic-release": "semantic-release", + "clean": "rm -rf cache out", + "build": "forge build", + "lint": "bun run lint:sol && bun run prettier:check", + "lint:sol": "forge fmt --check && bun solhint {script,src,test}/**/*.sol", + "prettier:check": "prettier --check **/*.{json,md,yml} --ignore-path=.prettierignore", + "prettier:write": "prettier --write **/*.{json,md,yml} --ignore-path=.prettierignore", + "test": "forge test", + "test:coverage": "forge coverage", + "test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage" }, "jest": { "globalSetup": "./jest/setup.ts", @@ -60,8 +69,8 @@ "@commitlint/prompt": "^12.1.1", "@nomicfoundation/hardhat-toolbox": "^3.0.0", "@opengsn/contracts": "^2.2.6", - "@openzeppelin/contracts": "^4.2.0", - "@openzeppelin/contracts-upgradeable": "^4.2.0", + "@openzeppelin/contracts": "^5.0.1", + "@openzeppelin/contracts-upgradeable": "^5.0.1", "@openzeppelin/upgrades": "^2.8.0", "@typechain/ethers-v6": "^0.4.3", "@typechain/hardhat": "^8.0.0", @@ -72,6 +81,7 @@ "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "commitizen": "^4.2.3", + "ds-test": "github:dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0", "eslint": "^7.32.0", "eslint-config-airbnb-base": "^14.2.1", "eslint-config-prettier": "^8.3.0", @@ -80,6 +90,7 @@ "eslint-plugin-import": "^2.22.1", "eslint-plugin-prettier": "^3.4.0", "ethers": "^6.7.1", + "forge-std": "github:foundry-rs/forge-std#v1.7.6", "ganache-cli": "^6.12.2", "git-cz": "^4.7.6", "hardhat": "^2.4.1", diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..7e9ff2a --- /dev/null +++ b/remappings.txt @@ -0,0 +1,4 @@ +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ +forge-std/=node_modules/forge-std/src/ +ds-test/=node_modules/ds-test/src/ diff --git a/contracts/BaseDocumentStore.sol b/src/BaseDocumentStore.sol similarity index 61% rename from contracts/BaseDocumentStore.sol rename to src/BaseDocumentStore.sol index 5edcffc..6dbde38 100644 --- a/contracts/BaseDocumentStore.sol +++ b/src/BaseDocumentStore.sol @@ -4,22 +4,18 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {IDocumentStore} from "./interfaces/IDocumentStore.sol"; /** * @title BaseDocumentStore * @notice A base contract for storing and revoking documents */ -contract BaseDocumentStore is Initializable { +contract BaseDocumentStore is Initializable, IDocumentStore { /** * @notice The name of the contract */ string public name; - /** - * @notice The version of the contract - */ - string public version; - /** * @notice A mapping of the document hash to the block number that was issued */ @@ -30,24 +26,11 @@ contract BaseDocumentStore is Initializable { */ mapping(bytes32 => uint256) public documentRevoked; - /** - * @notice Emitted when a document is issued - * @param document The hash of the issued document - */ - event DocumentIssued(bytes32 indexed document); - - /** - * @notice Emitted when a document is revoked - * @param document The hash of the revoked document - */ - event DocumentRevoked(bytes32 indexed document); - /** * @notice Initialises the contract with a name * @param _name The name of the contract */ function __BaseDocumentStore_init(string memory _name) internal onlyInitializing { - version = "2.3.0"; name = _name; } @@ -55,19 +38,9 @@ contract BaseDocumentStore is Initializable { * @notice Issues a document * @param document The hash of the document to issue */ - function _issue(bytes32 document) internal onlyNotIssued(document) { + function _issue(bytes32 document) internal { documentIssued[document] = block.number; - emit DocumentIssued(document); - } - - /** - * @notice Issues documents in bulk - * @param documents The hashes of the documents to issue - */ - function _bulkIssue(bytes32[] memory documents) internal { - for (uint256 i = 0; i < documents.length; i++) { - _issue(documents[i]); - } + // emit DocumentIssued(document); } /** @@ -84,7 +57,7 @@ contract BaseDocumentStore is Initializable { * @param document The hash of the document to check * @return A boolean indicating whether the document has been issued */ - function isIssued(bytes32 document) public view returns (bool) { + function _isIssued(bytes32 document) internal view returns (bool) { return (documentIssued[document] != 0); } @@ -101,19 +74,9 @@ contract BaseDocumentStore is Initializable { /** * @notice Revokes a document * @param document The hash of the document to revoke - * @return A boolean indicating whether the document was successfully revoked */ - function _revoke(bytes32 document) internal onlyNotRevoked(document) returns (bool) { + function _revoke(bytes32 document) internal { documentRevoked[document] = block.number; - emit DocumentRevoked(document); - - return true; - } - - function _bulkRevoke(bytes32[] memory documents) internal { - for (uint256 i = 0; i < documents.length; i++) { - _revoke(documents[i]); - } } /** @@ -121,7 +84,7 @@ contract BaseDocumentStore is Initializable { * @param document The hash of the document to check * @return A boolean indicating whether the document has been revoked */ - function isRevoked(bytes32 document) public view returns (bool) { + function _isRevoked(bytes32 document) internal view returns (bool) { return documentRevoked[document] != 0; } @@ -140,25 +103,7 @@ contract BaseDocumentStore is Initializable { * @param document The hash of the document to check */ modifier onlyIssued(bytes32 document) { - require(isIssued(document), "Error: Only issued document hashes can be revoked"); - _; - } - - /** - * @dev Checks if a document has not been issued - * @param document The hash of the document to check - */ - modifier onlyNotIssued(bytes32 document) { - require(!isIssued(document), "Error: Only hashes that have not been issued can be issued"); - _; - } - - /** - * @dev Modifier that checks if a document has not been revoked - * @param claim The hash of the document to check - */ - modifier onlyNotRevoked(bytes32 claim) { - require(!isRevoked(claim), "Error: Hash has been revoked previously"); + require(_isIssued(document), "Error: Only issued document hashes can be revoked"); _; } } diff --git a/src/DocumentStore.sol b/src/DocumentStore.sol new file mode 100644 index 0000000..6ebf41b --- /dev/null +++ b/src/DocumentStore.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; + +import "./BaseDocumentStore.sol"; +import "./base/DocumentStoreAccessControl.sol"; + +/** + * @title DocumentStore + * @notice A contract for storing and revoking documents with access control + */ +contract DocumentStore is DocumentStoreAccessControl, BaseDocumentStore { + using MerkleProof for bytes32[]; + + /** + * @notice Initialises the contract with a name and owner + * @param _name The name of the contract + * @param owner The owner of the contract + */ + constructor(string memory _name, address owner) { + initialize(_name, owner); + } + + /** + * @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 + */ + function issue(bytes32 documentRoot) public onlyRole(ISSUER_ROLE) { + if (isRootIssued(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 revokeRoot(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 isRootIssued(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 _isRevokedInternal(documentRoot, document, proof); + } + + function _isRevokedInternal( + 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 isRootRevoked(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 !_isRevokedInternal(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); + } + _; + } +} diff --git a/contracts/DocumentStoreCreator.sol b/src/DocumentStoreCreator.sol similarity index 100% rename from contracts/DocumentStoreCreator.sol rename to src/DocumentStoreCreator.sol diff --git a/src/DocumentStoreWithRevokeReasons.sol b/src/DocumentStoreWithRevokeReasons.sol new file mode 100644 index 0000000..798ac8f --- /dev/null +++ b/src/DocumentStoreWithRevokeReasons.sol @@ -0,0 +1,62 @@ +//// SPDX-License-Identifier: Apache-2.0 +// +//pragma solidity ^0.8.0; +// +//import "./DocumentStore.sol"; +// +///** +// * @title DocumentStoreWithRevokeReasons +// * @notice A contract for storing and revoking documents with access control and reasons for revocation +// */ +//contract DocumentStoreWithRevokeReasons is DocumentStore { +// /** +// * @notice A mapping of the document hash to the block number that was issued +// */ +// mapping(bytes32 => uint256) public revokeReason; +// +// /** +// * @notice Emitted when a document is revoked with a reason +// * @param document The hash of the revoked document +// * @param reason The reason for revocation +// */ +// event DocumentRevokedWithReason(bytes32 indexed document, uint256 reason); +// +// /** +// * @notice Initialises the contract with a name and owner +// * @param _name The name of the contract +// * @param owner The owner of the contract +// */ +// constructor(string memory _name, address owner) DocumentStore(_name, owner) {} +// +// /** +// * @notice Revokes a document with a reason +// * @param document The hash of the document to revoke +// * @param reason The reason for revocation +// * @return A boolean indicating whether the revocation was successful +// */ +// function revoke(bytes32 document, uint256 reason) +// public +// onlyRole(REVOKER_ROLE) +// onlyNotRevoked(document) +// returns (bool) +// { +// revoke(document); +// revokeReason[document] = reason; +// emit DocumentRevokedWithReason(document, reason); +// +// return true; +// } +// +// /** +// * @notice Revokes documents in bulk with a reason +// * @param documents The hashes of the documents to revoke +// * @param reason The reason for revocation +// */ +// function bulkRevoke(bytes32[] memory documents, uint256 reason) public { +// for (uint256 i = 0; i < documents.length; i++) { +// revoke(documents[i]); +// revokeReason[documents[i]] = reason; +// emit DocumentRevokedWithReason(documents[i], reason); +// } +// } +//} diff --git a/contracts/base/DocumentStoreAccessControl.sol b/src/base/DocumentStoreAccessControl.sol similarity index 86% rename from contracts/base/DocumentStoreAccessControl.sol rename to src/base/DocumentStoreAccessControl.sol index 85ba58c..3fa372f 100644 --- a/contracts/base/DocumentStoreAccessControl.sol +++ b/src/base/DocumentStoreAccessControl.sol @@ -18,8 +18,8 @@ contract DocumentStoreAccessControl is AccessControlUpgradeable { */ function __DocumentStoreAccessControl_init(address owner) internal onlyInitializing { require(owner != address(0), "Owner is zero"); - _setupRole(DEFAULT_ADMIN_ROLE, owner); - _setupRole(ISSUER_ROLE, owner); - _setupRole(REVOKER_ROLE, owner); + _grantRole(DEFAULT_ADMIN_ROLE, owner); + _grantRole(ISSUER_ROLE, owner); + _grantRole(REVOKER_ROLE, owner); } } diff --git a/src/config/config.ts b/src/config/config.ts deleted file mode 100644 index 0e43ff9..0000000 --- a/src/config/config.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const DOCUMENT_STORE_CREATOR_ROPSTEN = "0x4077534e82C97Be03A07FB10f5c853d2bC7161FB"; -export const DOCUMENT_STORE_CREATOR_MAINNET = "0x0"; -export const PROXY_FACTORY_ROPSTEN = "0xba2501bf20593f156879c17d38b6c245ca65de80"; -export const PROXY_FACTORY_MAINNET = "0x0"; - -export const getDocumentStoreCreatorAddress = (networkId?: BigInt) => { - if (networkId === BigInt(3)) return DOCUMENT_STORE_CREATOR_ROPSTEN; - return DOCUMENT_STORE_CREATOR_MAINNET; -}; - -export const getProxyFactoryAddress = (networkId?: number) => { - if (networkId === 3) return PROXY_FACTORY_ROPSTEN; - return PROXY_FACTORY_MAINNET; -}; diff --git a/src/config/index.ts b/src/config/index.ts deleted file mode 100644 index 5c62e04..0000000 --- a/src/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./config"; diff --git a/src/index.test.ts b/src/index.test.ts deleted file mode 100644 index d85e214..0000000 --- a/src/index.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { JsonRpcProvider, JsonRpcSigner, ethers } from "ethers"; -import { deploy, deployAndWait, connect } from "./index"; -import { DocumentStoreCreator__factory as DocumentStoreCreatorFactory } from "./contracts"; - -const provider = new JsonRpcProvider(); -let signer: JsonRpcSigner; -let account: string; -let documentStoreCreatorAddressOverride: string; - -const adminRole = ethers.ZeroHash; -const issuerRole = ethers.id("ISSUER_ROLE"); -const revokerRole = ethers.id("REVOKER_ROLE"); - -beforeAll(async () => { - // Deploy an instance of DocumentStoreFactory on the new blockchain - signer = await provider.getSigner(); - const factory = new DocumentStoreCreatorFactory(signer); - const receipt = await factory.deploy(); - documentStoreCreatorAddressOverride = await receipt.getAddress(); - account = await signer.getAddress(); -}); - -describe("deploy", () => { - it("deploys a new DocumentStore contract without waiting for confirmation", async () => { - const receipt = await deploy("My Store", signer, { documentStoreCreatorAddressOverride }); - expect(receipt.from).toBe(account); - }); -}); - -describe("deployAndWait", () => { - it("deploys a new DocumentStore contract", async () => { - const instance = await deployAndWait("My Store", signer, { documentStoreCreatorAddressOverride }); - - const hasAdminRole = await instance.hasRole(adminRole, account); - const hasIssuerRole = await instance.hasRole(issuerRole, account); - const hasRevokerRole = await instance.hasRole(revokerRole, account); - expect(hasAdminRole).toBe(true); - expect(hasIssuerRole).toBe(true); - expect(hasRevokerRole).toBe(true); - - const name = await instance.name(); - expect(name).toBe("My Store"); - }); -}); - -describe("connect", () => { - it("connects to existing contract", async () => { - const documentStore = await deployAndWait("My Store", signer, { documentStoreCreatorAddressOverride }); - const address = await documentStore.getAddress(); - const instance = await connect(address, signer); - const name = await instance.name(); - expect(name).toBe("My Store"); - }); -}); diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 87ab48c..0000000 --- a/src/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-disable import/extensions */ -// eslint-disable-next-line import/no-extraneous-dependencies -import { Signer, Provider, ContractTransactionResponse } from "ethers"; -import { - DocumentStoreCreator__factory as DocumentStoreCreatorFactory, - DocumentStore__factory as DocumentStoreFactory, -} from "./contracts"; -import { getDocumentStoreCreatorAddress } from "./config"; - -interface DeployOptions { - documentStoreCreatorAddressOverride?: string; -} - -export const deploy = async ( - name: string, - signer: Signer, - options?: DeployOptions -): Promise => { - let documentStoreCreatorFactoryAddress = options?.documentStoreCreatorAddressOverride; - if (!documentStoreCreatorFactoryAddress) { - const chainId = (await signer.provider?.getNetwork())?.chainId; - documentStoreCreatorFactoryAddress = getDocumentStoreCreatorAddress(chainId); - } - const factory = DocumentStoreCreatorFactory.connect(documentStoreCreatorFactoryAddress, signer); - const tx = await factory.deploy(name); - return tx; -}; - -export const deployAndWait = async (name: string, signer: Signer, options?: DeployOptions) => { - const receipt = await (await deploy(name, signer, options)).wait(); - if (!receipt || !receipt.logs || !receipt.logs[0].address) - throw new Error("Fail to detect deployed contract address"); - return DocumentStoreFactory.connect(receipt.logs![0].address, signer); -}; - -export const connect = async (address: string, signerOrProvider: Signer | Provider) => { - return DocumentStoreFactory.connect(address, signerOrProvider); -}; - -// Export typechain classes for distribution purposes -export * from "./contracts/index.dist"; diff --git a/src/interfaces/IDocumentStore.sol b/src/interfaces/IDocumentStore.sol new file mode 100644 index 0000000..421bc40 --- /dev/null +++ b/src/interfaces/IDocumentStore.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +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); + + /** + * @notice Emitted when a document is issued + * @param document The hash of the issued document + */ + event DocumentIssued(bytes32 indexed document); + + /** + * @notice Emitted when a document is revoked + * @param document The hash of the revoked document + */ + event DocumentRevoked(bytes32 indexed documentRoot, bytes32 indexed document); +} diff --git a/test/DocumentStore.t.sol b/test/DocumentStore.t.sol new file mode 100644 index 0000000..21d2271 --- /dev/null +++ b/test/DocumentStore.t.sol @@ -0,0 +1,683 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.23 <0.9.0; + +import {console2} from "forge-std/console2.sol"; +import "forge-std/Test.sol"; +import "../src/DocumentStore.sol"; +import "../src/interfaces/IDocumentStore.sol"; + +import "@openzeppelin/contracts/access/IAccessControl.sol"; + +abstract contract CommonTest is Test { + string public storeName = "DocumentStore Test"; + + address public owner = vm.addr(1); + address public issuer = vm.addr(2); + address public revoker = vm.addr(3); + + DocumentStore public documentStore; + + function setUp() public virtual { + vm.startPrank(owner); + + documentStore = new DocumentStore(storeName, owner); + documentStore.grantRole(documentStore.ISSUER_ROLE(), issuer); + documentStore.grantRole(documentStore.REVOKER_ROLE(), revoker); + + vm.stopPrank(); + } +} + +contract DocumentStore_init_Test is CommonTest { + function testDocumentName() public { + assertEq(documentStore.name(), storeName); + } + + function testOwnerIsAdmin() public view { + assert(documentStore.hasRole(documentStore.DEFAULT_ADMIN_ROLE(), owner)); + } + + function testOwnerIsIssuer() public view { + assert(documentStore.hasRole(documentStore.ISSUER_ROLE(), owner)); + } + + function testOwnerIsRevoker() public view { + assert(documentStore.hasRole(documentStore.REVOKER_ROLE(), owner)); + } + + function testFailZeroOwner() public { + documentStore = new DocumentStore(storeName, vm.addr(0)); + } +} + +contract DocumentStore_issue_Test is CommonTest { + bytes32 public docHash = "0x1234"; + + function setUp() public override { + super.setUp(); + } + + function testIssueByOwner() public { + vm.expectEmit(true, true, true, true); + + emit IDocumentStore.DocumentIssued(docHash); + + vm.prank(owner); + documentStore.issue(docHash); + + assert(documentStore.isRootIssued(docHash)); + } + + function testIssueByIssuer() public { + vm.expectEmit(true, true, true, true); + + emit IDocumentStore.DocumentIssued(docHash); + + vm.prank(issuer); + documentStore.issue(docHash); + + assert(documentStore.isRootIssued(docHash)); + } + + function testIssueByRevokerRevert() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + revoker, + documentStore.ISSUER_ROLE() + ) + ); + + vm.prank(revoker); + documentStore.issue(docHash); + } + + function testIssueByNonIssuerRevert() public { + address notIssuer = vm.addr(69); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + notIssuer, + documentStore.ISSUER_ROLE() + ) + ); + + vm.prank(notIssuer); + documentStore.issue(docHash); + } + + function testIssueAlreadyIssuedRevert() public { + vm.startPrank(issuer); + documentStore.issue(docHash); + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.DocumentExists.selector, bytes32(docHash))); + + documentStore.issue(docHash); + vm.stopPrank(); + } + + function testIssueZeroDocument() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + + vm.prank(issuer); + documentStore.issue(0x0); + } +} + +contract DocumentStore_bulkIssue_Test is CommonTest { + bytes32[] public docHashes; + + function setUp() public override { + super.setUp(); + + vm.startPrank(owner); + documentStore.grantRole(documentStore.ISSUER_ROLE(), issuer); + vm.stopPrank(); + + docHashes = new bytes32[](2); + docHashes[0] = "0x1234"; + docHashes[1] = "0x5678"; + } + + function testBulkIssueByIssuer() public { + vm.expectEmit(true, false, false, true); + emit IDocumentStore.DocumentIssued(docHashes[0]); + vm.expectEmit(true, false, false, true); + emit IDocumentStore.DocumentIssued(docHashes[1]); + + vm.prank(issuer); + documentStore.bulkIssue(docHashes); + + assert(documentStore.isRootIssued(docHashes[0])); + assert(documentStore.isRootIssued(docHashes[1])); + } + + function testBulkIssueByRevokerRevert() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + revoker, + documentStore.ISSUER_ROLE() + ) + ); + + vm.prank(revoker); + documentStore.bulkIssue(docHashes); + } + + function testBulkIssueByNonIssuerRevert() public { + address notIssuer = vm.addr(69); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + notIssuer, + documentStore.ISSUER_ROLE() + ) + ); + + vm.prank(notIssuer); + documentStore.bulkIssue(docHashes); + } + + function testBulkIssueWithDuplicatesRevert() public { + docHashes[1] = docHashes[0]; + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.DocumentExists.selector, bytes32(docHashes[1]))); + + vm.prank(issuer); + documentStore.bulkIssue(docHashes); + } +} + +abstract contract DocumentStoreWithFakeDocuments_Base is CommonTest { + bytes32 public docRoot; + bytes32[] public documents = new bytes32[](3); + bytes32[][] public proofs = new bytes32[][](3); + + function setUp() public virtual override { + super.setUp(); + + docRoot = 0x5f0ed7e331c430ce34bcb45e2ddbff2b56a0f5971a226eee85f7ed6cc85e8e27; + + documents = [ + bytes32(0x795bb6abe4c5bb81e397821324d44bf7a94785587d0c88c621f57268c8aef4cb), + bytes32(0x9bc394ef702b639adb913242a472e883f4834b4f38ed38f046bec8fcc1104fa3), + bytes32(0x4aac698f1a67c980d0a52901fe4805775cc31beae66fb33bbb9dd89d30de81bd) + ]; + + proofs = [ + [ + bytes32(0x9bc394ef702b639adb913242a472e883f4834b4f38ed38f046bec8fcc1104fa3), + bytes32(0x4aac698f1a67c980d0a52901fe4805775cc31beae66fb33bbb9dd89d30de81bd) + ], + [ + bytes32(0x795bb6abe4c5bb81e397821324d44bf7a94785587d0c88c621f57268c8aef4cb), + bytes32(0x4aac698f1a67c980d0a52901fe4805775cc31beae66fb33bbb9dd89d30de81bd) + ] + ]; + proofs.push([bytes32(0x3763f4f892fb4c2ff4d76c4b9d391985568f8940f93f71283a84ff73277fb81e)]); + } +} + +contract DocumentStore_isIssued_Test is DocumentStoreWithFakeDocuments_Base { + function setUp() public override { + super.setUp(); + + vm.prank(issuer); + documentStore.issue(docRoot); + } + + function testIsRootIssuedWithRoot() public { + assertTrue(documentStore.isRootIssued(docRoot)); + } + + function testIsRootIssuedWithZeroRoot() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + + documentStore.isRootIssued(0x0); + } + + function testIsIssuedWithRoot() public { + assertTrue(documentStore.isIssued(docRoot, docRoot, new bytes32[](0))); + } + + function testIsIssuedWithValidProof() public { + assertTrue(documentStore.isIssued(docRoot, documents[0], proofs[0])); + assertTrue(documentStore.isIssued(docRoot, documents[1], proofs[1])); + assertTrue(documentStore.isIssued(docRoot, documents[2], proofs[2])); + } + + function testIsIssuedWithInvalidProof() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, docRoot, documents[1])); + + documentStore.isIssued(docRoot, documents[1], proofs[0]); + } + + function testIsIssuedWithInvalidEmptyProof() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, docRoot, documents[1])); + + documentStore.isIssued(docRoot, documents[1], new bytes32[](0)); + } + + function testIsIssuedWithZeroDocument() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + documentStore.isIssued(0x0, documents[0], proofs[0]); + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + documentStore.isIssued(docRoot, 0x0, proofs[0]); + } +} + +contract DocumentStore_revokeRoot_Test is DocumentStoreWithFakeDocuments_Base { + function setUp() public override { + super.setUp(); + + vm.prank(issuer); + documentStore.issue(docRoot); + } + + function testRevokeRootByOwner() public { + vm.expectEmit(true, true, false, true); + emit IDocumentStore.DocumentRevoked(docRoot, docRoot); + + vm.prank(owner); + documentStore.revokeRoot(docRoot); + + assertTrue(documentStore.isRootRevoked(docRoot)); + } + + function testRevokeRootByRevoker() public { + vm.expectEmit(true, true, false, true); + emit IDocumentStore.DocumentRevoked(docRoot, docRoot); + + vm.prank(revoker); + documentStore.revokeRoot(docRoot); + + assertTrue(documentStore.isRootRevoked(docRoot)); + } + + function testRevokeRootByIssuerRevert() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + issuer, + documentStore.REVOKER_ROLE() + ) + ); + + vm.prank(issuer); + documentStore.revokeRoot(docRoot); + } + + function testRevokeRootByNonRevokerRevert() public { + address notRevoker = vm.addr(69); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + notRevoker, + documentStore.REVOKER_ROLE() + ) + ); + + vm.prank(notRevoker); + documentStore.revokeRoot(docRoot); + } + + function testRevokeRootWithZeroRoot() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + + vm.prank(revoker); + documentStore.revokeRoot(0x0); + } + + function testRevokeRootAlreadyRevokedRevert() public { + vm.startPrank(revoker); + documentStore.revokeRoot(docRoot); + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InactiveDocument.selector, docRoot, docRoot)); + + documentStore.revokeRoot(docRoot); + vm.stopPrank(); + } + + function testRevokeRootNonIssuedRootRevert() public { + bytes32 nonIssuedRoot = "0x1234"; + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, nonIssuedRoot, nonIssuedRoot)); + + vm.prank(revoker); + documentStore.revokeRoot(nonIssuedRoot); + } +} + +contract DocumentStore_revoke_Test is DocumentStoreWithFakeDocuments_Base { + function setUp() public override { + super.setUp(); + + vm.prank(issuer); + documentStore.issue(docRoot); + } + + function testRevokeByOwner() public { + vm.expectEmit(true, true, false, true); + emit IDocumentStore.DocumentRevoked(docRoot, documents[0]); + + vm.prank(owner); + documentStore.revoke(docRoot, documents[0], proofs[0]); + + assertTrue(documentStore.isRevoked(docRoot, documents[0], proofs[0])); + } + + function testRevokeByRevoker() public { + vm.expectEmit(true, true, false, true); + emit IDocumentStore.DocumentRevoked(docRoot, documents[0]); + + vm.prank(revoker); + documentStore.revoke(docRoot, documents[0], proofs[0]); + + assertTrue(documentStore.isRevoked(docRoot, documents[0], proofs[0])); + } + + function testRevokeByIssuerRevert() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + issuer, + documentStore.REVOKER_ROLE() + ) + ); + + vm.prank(issuer); + documentStore.revoke(docRoot, documents[0], proofs[0]); + } + + function testRevokeByNonRevokerRevert() public { + address notRevoker = vm.addr(69); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + notRevoker, + documentStore.REVOKER_ROLE() + ) + ); + + vm.prank(notRevoker); + documentStore.revoke(docRoot, documents[0], proofs[0]); + } + + function testRevokeWithInvalidProofRevert() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, docRoot, documents[0])); + + vm.prank(revoker); + documentStore.revoke(docRoot, documents[0], proofs[1]); + } + + function testRevokeWithEmptyProofRevert() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, docRoot, documents[0])); + + vm.prank(revoker); + documentStore.revoke(docRoot, documents[0], new bytes32[](0)); + } + + function testRevokeWithZeroDocument() public { + vm.startPrank(revoker); + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + documentStore.revoke(0x0, documents[0], proofs[0]); + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + documentStore.revoke(docRoot, 0x0, proofs[0]); + + vm.stopPrank(); + } + + function testRevokeAlreadyRevokedRevert() public { + vm.startPrank(revoker); + + documentStore.revoke(docRoot, documents[0], proofs[0]); + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InactiveDocument.selector, docRoot, documents[0])); + + documentStore.revoke(docRoot, documents[0], proofs[0]); + + vm.stopPrank(); + } + + function testRevokeNonIssuedDocumentRevert() public { + bytes32 nonIssuedRoot = "0x1234"; + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, nonIssuedRoot, nonIssuedRoot)); + + vm.prank(revoker); + documentStore.revokeRoot(nonIssuedRoot); + } +} + +contract DocumentStore_bulkRevoke_Test is DocumentStoreWithFakeDocuments_Base { + bytes32[] public docRoots = new bytes32[](3); + + function setUp() public override { + super.setUp(); + + docRoots[0] = docRoot; + docRoots[1] = docRoot; + docRoots[2] = docRoot; + + vm.prank(issuer); + documentStore.issue(docRoot); + } + + function testBulkRevokeByOwner() public { + vm.expectEmit(true, true, false, true); + emit IDocumentStore.DocumentRevoked(docRoot, documents[0]); + vm.expectEmit(true, true, false, true); + emit IDocumentStore.DocumentRevoked(docRoot, documents[1]); + vm.expectEmit(true, true, false, true); + emit IDocumentStore.DocumentRevoked(docRoot, documents[2]); + + vm.prank(owner); + documentStore.bulkRevoke(docRoots, documents, proofs); + + assertTrue(documentStore.isRevoked(docRoot, documents[0], proofs[0])); + assertTrue(documentStore.isRevoked(docRoot, documents[1], proofs[1])); + assertTrue(documentStore.isRevoked(docRoot, documents[2], proofs[2])); + } + + function testBulkRevokeByRevoker() public { + vm.expectEmit(true, true, false, true); + emit IDocumentStore.DocumentRevoked(docRoot, documents[0]); + vm.expectEmit(true, true, false, true); + emit IDocumentStore.DocumentRevoked(docRoot, documents[1]); + vm.expectEmit(true, true, false, true); + emit IDocumentStore.DocumentRevoked(docRoot, documents[2]); + + vm.prank(revoker); + documentStore.bulkRevoke(docRoots, documents, proofs); + + assertTrue(documentStore.isRevoked(docRoot, documents[0], proofs[0])); + assertTrue(documentStore.isRevoked(docRoot, documents[1], proofs[1])); + assertTrue(documentStore.isRevoked(docRoot, documents[2], proofs[2])); + } + + function testBulkRevokeByIssuerRevert() public { + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + issuer, + documentStore.REVOKER_ROLE() + ) + ); + + vm.prank(issuer); + documentStore.bulkRevoke(docRoots, documents, proofs); + } + + function testBulkRevokeByNonRevokerRevert() public { + address notRevoker = vm.addr(69); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + notRevoker, + documentStore.REVOKER_ROLE() + ) + ); + + vm.prank(notRevoker); + documentStore.bulkRevoke(docRoots, documents, proofs); + } + + function testBulkRevokeWithDuplicatesRevert() public { + docRoots[1] = docRoots[0]; + documents[1] = documents[0]; + proofs[1] = proofs[0]; + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InactiveDocument.selector, docRoots[1], documents[1])); + + vm.prank(revoker); + documentStore.bulkRevoke(docRoots, documents, proofs); + } +} + +contract DocumentStore_isRevoked_Test is DocumentStoreWithFakeDocuments_Base { + function setUp() public override { + super.setUp(); + + vm.startPrank(owner); + documentStore.issue(docRoot); + documentStore.revoke(docRoot, documents[0], proofs[0]); + vm.stopPrank(); + } + + function testIsRevokedWithRevokedDocument() public { + assertTrue(documentStore.isRevoked(docRoot, documents[0], proofs[0])); + } + + function testIsRevokedWithRevokedRoot() public { + vm.prank(revoker); + documentStore.revokeRoot(docRoot); + + assertTrue(documentStore.isRevoked(docRoot, documents[1], proofs[1])); + } + + function testIsRevokedWithNotRevokedDocument() public { + assertFalse(documentStore.isRevoked(docRoot, documents[1], proofs[1])); + } + + function testIsRevokedWithInvalidProofRevert() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, docRoot, documents[0])); + + documentStore.isRevoked(docRoot, documents[0], proofs[1]); + } + + function testIsRevokedWithEmptyProofRevert() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, docRoot, documents[0])); + + documentStore.isRevoked(docRoot, documents[0], new bytes32[](0)); + } + + function testIsRevokedWithZeroDocumentRevert() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + documentStore.isRevoked(docRoot, 0x0, proofs[0]); + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + documentStore.isRevoked(0x0, documents[0], proofs[0]); + } + + function testIsRevokedWithNotIssuedDocumentRevert() public { + bytes32 notIssuedDoc = "0x1234"; + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, docRoot, notIssuedDoc)); + + documentStore.isRevoked(docRoot, notIssuedDoc, proofs[0]); + } +} + +contract DocumentStore_isRootRevoked is DocumentStoreWithFakeDocuments_Base { + function setUp() public override { + super.setUp(); + + vm.startPrank(owner); + documentStore.issue(docRoot); + documentStore.revokeRoot(docRoot); + vm.stopPrank(); + } + + function testIsRootRevokedWithRevokedRoot() public { + assertTrue(documentStore.isRootRevoked(docRoot)); + } + + function testIsRootRevokedWithNotRevokedRoot() public { + bytes32 notRevokedRoot = "0x1234"; + + vm.prank(issuer); + documentStore.issue(notRevokedRoot); + + assertFalse(documentStore.isRootRevoked(notRevokedRoot)); + } + + function testIsRootRevokedWithZeroRootRevert() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + + documentStore.isRootRevoked(0x0); + } + + function testIsRootRevokedWithNotIssuedRootRevert() public { + bytes32 notIssuedRoot = "0x1234"; + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, notIssuedRoot, notIssuedRoot)); + + assertFalse(documentStore.isRootRevoked(notIssuedRoot)); + } +} + +contract DocumentStore_isActive_Test is DocumentStoreWithFakeDocuments_Base { + function setUp() public override { + super.setUp(); + + vm.startPrank(owner); + documentStore.issue(docRoot); + documentStore.revoke(docRoot, documents[0], proofs[0]); + vm.stopPrank(); + } + + function testIsActiveWithActiveDocument() public { + assertTrue(documentStore.isActive(docRoot, documents[1], proofs[1])); + } + + function testIsActiveWithRevokedDocument() public { + assertFalse(documentStore.isActive(docRoot, documents[0], proofs[0])); + } + + function testIsActiveWithInvalidProofRevert() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, docRoot, documents[0])); + + documentStore.isActive(docRoot, documents[0], proofs[1]); + } + + function testIsActiveWithEmptyProofRevert() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, docRoot, documents[0])); + + documentStore.isActive(docRoot, documents[0], new bytes32[](0)); + } + + function testIsActiveWithZeroDocumentRevert() public { + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + documentStore.isActive(docRoot, 0x0, proofs[0]); + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.ZeroDocument.selector)); + documentStore.isActive(0x0, documents[0], proofs[0]); + } + + function testIsActiveWithNotIssuedDocumentRevert() public { + bytes32 notIssuedDoc = "0x1234"; + + vm.expectRevert(abi.encodeWithSelector(IDocumentStore.InvalidDocument.selector, docRoot, notIssuedDoc)); + + documentStore.isActive(docRoot, notIssuedDoc, proofs[0]); + } +}