├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ ├── publish-prerelease.yml │ ├── publish-release.yml │ └── push_checking.yml ├── .gitignore ├── .solcover.js ├── .solhint.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── contracts ├── ClaimIssuer.sol ├── Identity.sol ├── Test.sol ├── _testContracts │ └── VerifierUser.sol ├── factory │ ├── IIdFactory.sol │ └── IdFactory.sol ├── gateway │ └── Gateway.sol ├── interface │ ├── IClaimIssuer.sol │ ├── IERC734.sol │ ├── IERC735.sol │ ├── IIdentity.sol │ └── IImplementationAuthority.sol ├── proxy │ ├── IdentityProxy.sol │ └── ImplementationAuthority.sol ├── storage │ ├── Storage.sol │ └── Structs.sol ├── verifiers │ └── Verifier.sol └── version │ └── Version.sol ├── hardhat.config.ts ├── index.d.ts ├── index.js ├── onchainid_logo_final.png ├── package-lock.json ├── package.json ├── scripts ├── deploy-claim-issuer.ts ├── deploy-factory.ts └── deploy-identity.ts ├── tasks ├── add-claim.task.ts ├── add-key.task.ts ├── deploy-identity.task.ts ├── deploy-proxy.task.ts ├── remove-claim.task.ts ├── remove-key.task.ts └── revoke.task.ts ├── test ├── claim-issuers │ └── claim-issuer.test.ts ├── factory │ ├── factory.test.ts │ └── token-oid.test.ts ├── fixtures.ts ├── gateway │ └── gateway.test.ts ├── identities │ ├── claims.test.ts │ ├── executions.test.ts │ ├── init.test.ts │ └── keys.test.ts ├── proxy.test.ts └── verifiers │ ├── verifier-user.test.ts │ └── verifier.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 2 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.sol] 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [], 3 | parserOptions: { ecmaVersion: 2018 }, 4 | root: true, 5 | rules: { 6 | 'prettier/prettier': ['error', { singleQuote: true }], 7 | 'sort-imports': [ 8 | 'error', 9 | { 10 | ignoreCase: true, 11 | ignoreDeclarationSort: true, 12 | ignoreMemberSort: false, 13 | }, 14 | ], 15 | 'import/no-unresolved': 'off', 16 | 'import/order': [ 17 | 'error', 18 | { 19 | groups: ['builtin', 'external', 'internal'], 20 | 'newlines-between': 'always', 21 | }, 22 | ], 23 | 'no-plusplus': 'off', 24 | 'no-undef': 'off', 25 | 'func-names': 'off', 26 | 'no-param-reassign': 'off', 27 | 'no-console': 'off', 28 | 'no-multi-str': 'off', 29 | 'no-unused-expressions': 'off', 30 | 'no-restricted-syntax': 'off', 31 | }, 32 | overrides: [ 33 | { 34 | files: ['test/**/*.spec.js'], 35 | env: { 36 | mocha: true, 37 | }, 38 | globals: { 39 | artifacts: 'readonly', 40 | contract: 'readonly', 41 | }, 42 | rules: { 43 | 'no-await-in-loop': 'off', 44 | }, 45 | }, 46 | ], 47 | }; 48 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | -------------------------------------------------------------------------------- /.github/workflows/publish-prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Publish Beta Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | if: "github.event.release.prerelease" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | - run: npm ci 17 | - run: npm run build 18 | 19 | publish-gpr: 20 | needs: build 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-node@v3 25 | with: 26 | node-version: 16 27 | registry-url: https://npm.pkg.github.com/ 28 | scope: '@onchain-id' 29 | - run: npm ci 30 | - run: npm run build 31 | - run: npm publish --tag beta 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 34 | 35 | publish-npm: 36 | needs: build 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v3 41 | with: 42 | node-version: 16 43 | registry-url: https://registry.npmjs.org/ 44 | scope: '@onchain-id' 45 | - run: npm ci 46 | - run: npm run build 47 | - run: npm publish --tag beta 48 | env: 49 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 50 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | if: "!github.event.release.prerelease" 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | - run: npm ci 17 | - run: npm run build 18 | 19 | publish-gpr: 20 | needs: build 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-node@v3 25 | with: 26 | node-version: 16 27 | registry-url: https://npm.pkg.github.com/ 28 | scope: '@onchain-id' 29 | - run: npm ci 30 | - run: npm run build 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 34 | 35 | publish-npm: 36 | needs: build 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v3 41 | with: 42 | node-version: 16 43 | registry-url: https://registry.npmjs.org/ 44 | scope: '@onchain-id' 45 | - run: npm ci 46 | - run: npm run build 47 | - run: npm publish 48 | env: 49 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 50 | -------------------------------------------------------------------------------- /.github/workflows/push_checking.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests workflow 2 | on: push 3 | 4 | jobs: 5 | 6 | lint: 7 | name: "Lint" 8 | runs-on: ubuntu-latest 9 | container: node:16 10 | 11 | strategy: 12 | matrix: 13 | node-version: [16.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | registry-url: https://npm.pkg.github.com/ 22 | scope: '@tokenyico' 23 | - name: Install dependencies 24 | run: npm ci 25 | env: 26 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN_FOR_GPR}} 27 | - name: Lint sources 28 | run: 29 | npm run lint 30 | 31 | 32 | test: 33 | name: "Build and Test" 34 | runs-on: ubuntu-latest 35 | container: node:16 36 | 37 | strategy: 38 | matrix: 39 | node-version: [16.x] 40 | 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Use Node.js ${{ matrix.node-version }} 44 | uses: actions/setup-node@v1 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | registry-url: https://npm.pkg.github.com/ 48 | scope: '@tokenyico' 49 | - name: Install dependencies 50 | run: npm ci 51 | env: 52 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN_FOR_GPR}} 53 | - name: Build application 54 | run: npm run build 55 | - name: Run tests 56 | run: npm run coverage 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Coverage 6 | coverage 7 | 8 | # Dependencies 9 | node_modules 10 | 11 | # Build 12 | build/ 13 | 14 | # macOS 15 | .DS_Store 16 | 17 | # IDE 18 | .idea 19 | .vscode 20 | 21 | # Artifacts 22 | artifacts 23 | coverage.json 24 | cache 25 | typechain-types 26 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | skipFiles: [ 3 | "_testContracts", 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "^0.8.17"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }], 6 | "reentrancy": "error", 7 | "state-visibility": "error", 8 | "quotes": ["error", "double"], 9 | "const-name-snakecase": "error", 10 | "contract-name-camelcase": "error", 11 | "event-name-camelcase": "error", 12 | "func-name-mixedcase": "error", 13 | "func-param-name-mixedcase": "error", 14 | "modifier-name-mixedcase": "error", 15 | "private-vars-leading-underscore": ["error", { "strict": false }], 16 | "use-forbidden-name": "error", 17 | "var-name-mixedcase": "error", 18 | "imports-on-top": "error", 19 | "ordering": "error", 20 | "visibility-modifier-order": "error", 21 | "code-complexity": ["error", 11], 22 | "function-max-lines": ["error", 50], 23 | "max-line-length": ["error", 130], 24 | "max-states-count": ["error", 15], 25 | "no-empty-blocks": "error", 26 | "no-unused-vars": "error", 27 | "payable-fallback": "error", 28 | "constructor-syntax": "error", 29 | "not-rely-on-time": "off", 30 | "reason-string": "off" 31 | }, 32 | "plugins": ["prettier"] 33 | } 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [2.2.1] 8 | 9 | ### Changed 10 | - Replaced the storage slot used for ImplementationAuthority on the proxies, to avoid conflict with ERC-1822 on 11 | block explorers. By using the same storage slot, the explorers were identifying this proxy as an ERC-1822, while 12 | it's a different implementation here, the storage slot is not used to store the address of the implementation but 13 | the address to ImplementationAuthority contract that references the implementation 14 | 15 | ## [2.2.0] 16 | 17 | ### Added 18 | - Identities are now required to implement the standardized `function isClaimValid(IIdentity _identity, uint256 19 | claimTopic, bytes calldata sig, bytes calldata data) external view returns (bool)`, used for self-attested claims 20 | (`_identity` is the address of the Identity contract). 21 | - Implemented the `isClaimValid` function in the `Identity` contract. 22 | - IdFactory now implements the `implementationAuthority()` getter. 23 | 24 | ## [2.1.0] 25 | 26 | ### Added 27 | - Implemented a new contract `Gateway` to interact with the `IdFactory`. The `Gateway` contract allows individual 28 | accounts (being EOA or contracts) to deploy identities for their own address as a salt. To deploy using 29 | a custom salt, a signature from an approved signer is required. 30 | - Implemented a new base contract `Verifier` to be extended by contract requiring identity verification based on claims 31 | and trusted issuers. 32 | 33 | ## [2.0.1] 34 | 35 | ### Added 36 | - added method createIdentityWithManagementKeys() that allows the factory to issue identities with multiple 37 | management keys. 38 | - tests for the createIdentityWithManagementKeys() method 39 | 40 | ## [2.0.0] 41 | 42 | Version 2.0.0 Audited by Hacken, more details [here](https://tokeny.com/wp-content/uploads/2023/04/Tokeny_ONCHAINID_SC-Audit_Report.pdf) 43 | 44 | ### Breaking changes 45 | 46 | ## Deprecation Notice 47 | - ClaimIssuer `revokeClaim` is now deprecated, usage of `revokeClaimBySignature(bytes signature)` is preferred. 48 | 49 | ### Added 50 | - Add typechain-types (targeting ethers v5). 51 | - Add tests cases for `execute` and `approve` methods. 52 | - Add method `revokeClaimBySignature(bytes signature)` in ClaimIssuer, prefer using this method instead of the now 53 | deprecated `revokeClaim` method. 54 | - Add checks on ClaimIssuer to prevent revoking an already revoked claim. 55 | - Added Factory for ONCHAINIDs 56 | 57 | ### Updated 58 | - Switch development tooling to hardhat. 59 | - Implemented tests for hardhat (using fixture for faster testing time). 60 | - Prevent calling `approve` method with a non-request execute nonce (added a require on `executionNone`). 61 | - Update NatSpec of `execute` and `approve` methods. 62 | 63 | ## [1.4.0] - 2021-01-26 64 | ### Updated 65 | - Remove constructor's visibility 66 | 67 | ## [1.3.0] - 2021-01-21 68 | ### Added 69 | - Ownable 0.8.0 70 | - Context 0.8.0 71 | ### Updated 72 | - Update version to 1.3.0 73 | - Update contracts to SOL =0.8.0 74 | - Update test to work with truffle 75 | - Update truffle-config.js 76 | - Update solhint config 77 | 78 | ## [1.2.0] - 2020-11-27 79 | ### Added 80 | - Custom Upgradable Proxy contract that behaves similarly to the [EIP-1822](https://eips.ethereum.org/EIPS/eip-1822): Universal Upgradeable Proxy Standard (UUPS), except that it points to an Authority contract which in itself points to an implementation (which can be updated). 81 | - New ImplementationAuthority contract that acts as an authority for proxy contracts 82 | - Library Lock contract to ensure no one can manipulate the Logic Contract once it is deployed 83 | - Version contract that gives the versioning information of the implementation contract 84 | ### Moved 85 | - variables in a separate contract (Storage.sol) 86 | - structs in a separate contract (Structs.sol) 87 | ### Updated 88 | - Update contracts to SOL =0.6.9 89 | 90 | ## [1.1.2] - 2020-09-30 91 | ### Fixed 92 | - Add Constructor on ClaimIssuer Contract 93 | 94 | ## [1.1.1] - 2020-09-22 95 | ### Fixed 96 | - Fix CI 97 | 98 | ## [1.1.0] - 2020-09-16 99 | ### Added 100 | - ONCHAINID contract uses Proxy based on [EIP-1167](https://eips.ethereum.org/EIPS/eip-1167). 101 | - New contracts,CloneFactory and IdentityFactory 102 | - Github workflows actions 103 | - Build script 104 | - Lint rules for both Solidity and JS 105 | - Ganache-Cli 106 | - Rules for eslint (eslintrc) 107 | - Rules for solhint 108 | - new Tests for Proxy behavior 109 | 110 | ### Changed 111 | - Replaced Constructor by "Set" function on ERC734 112 | - "Set" function is callable only once on ERC734 113 | - Replaced Yarn by Npm 114 | - Replaced coverage script by coverage plugin 115 | - old Tests for compatibility with new proxy 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![OnchainID Smart Contracts](./onchainid_logo_final.png) 2 | --- 3 | 4 | ![GitHub](https://img.shields.io/github/license/onchain-id/solidity?color=green) 5 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/onchain-id/solidity) 6 | ![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/onchain-id/solidity/publish-release.yml) 7 | ![GitHub repo size](https://img.shields.io/github/repo-size/onchain-id/solidity) 8 | ![GitHub Release Date](https://img.shields.io/github/release-date/onchain-id/solidity) 9 | 10 | --- 11 | # OnchainID Smart Contracts 12 | 13 | Smart Contracts for secure Blockchain Identities, implementation of the ERC734 and ERC735 proposal standards. 14 | 15 | Learn more about OnchainID and Blockchain Identities on the official OnchainID website: [https://onchainid.com](https://onchainid.com). 16 | 17 | ## Usage 18 | 19 | - Install contracts package to use in your repository `yarn add @onchain-id/solidity` 20 | - Require desired contracts in-code (should you need to deploy them): 21 | ```javascript 22 | const { contracts: { ERC734, Identity } } = require('@onchain-id/solidity'); 23 | ``` 24 | - Require desired interfaces in-code (should you need to interact with deployed contracts): 25 | ```javascript 26 | const { interfaces: { IERC734, IERC735 } } = require('@onchain-id/solidity'); 27 | ``` 28 | - Access contract ABI `ERC734.abi` and ByteCode `ERC734.bytecode`. 29 | 30 | ## Development 31 | 32 | - Install dev dependencies `npm ci` 33 | - Update interfaces and contracts code. 34 | - Run lint `npm run lint` 35 | - Compile code `npm run compile` 36 | 37 | ### Testing 38 | 39 | - Run `npm ci` 40 | - Run `npm test` 41 | - Test will be executed against a local Hardhat network. 42 | 43 | --- 44 | 45 |
46 | 47 | Proofed by Hacken - Smart contract audit 48 | 49 |
50 | -------------------------------------------------------------------------------- /contracts/ClaimIssuer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | 4 | import "./interface/IClaimIssuer.sol"; 5 | import "./Identity.sol"; 6 | 7 | contract ClaimIssuer is IClaimIssuer, Identity { 8 | mapping (bytes => bool) public revokedClaims; 9 | 10 | // solhint-disable-next-line no-empty-blocks 11 | constructor(address initialManagementKey) Identity(initialManagementKey, false) {} 12 | 13 | /** 14 | * @dev See {IClaimIssuer-revokeClaimBySignature}. 15 | */ 16 | function revokeClaimBySignature(bytes calldata signature) external override delegatedOnly onlyManager { 17 | require(!revokedClaims[signature], "Conflict: Claim already revoked"); 18 | 19 | revokedClaims[signature] = true; 20 | 21 | emit ClaimRevoked(signature); 22 | } 23 | 24 | /** 25 | * @dev See {IClaimIssuer-revokeClaim}. 26 | */ 27 | function revokeClaim(bytes32 _claimId, address _identity) external override delegatedOnly onlyManager returns(bool) { 28 | uint256 foundClaimTopic; 29 | uint256 scheme; 30 | address issuer; 31 | bytes memory sig; 32 | bytes memory data; 33 | 34 | ( foundClaimTopic, scheme, issuer, sig, data, ) = Identity(_identity).getClaim(_claimId); 35 | 36 | require(!revokedClaims[sig], "Conflict: Claim already revoked"); 37 | 38 | revokedClaims[sig] = true; 39 | emit ClaimRevoked(sig); 40 | return true; 41 | } 42 | 43 | /** 44 | * @dev See {IClaimIssuer-isClaimValid}. 45 | */ 46 | function isClaimValid( 47 | IIdentity _identity, 48 | uint256 claimTopic, 49 | bytes memory sig, 50 | bytes memory data) 51 | public override(Identity, IClaimIssuer) view returns (bool claimValid) 52 | { 53 | bytes32 dataHash = keccak256(abi.encode(_identity, claimTopic, data)); 54 | // Use abi.encodePacked to concatenate the message prefix and the message to sign. 55 | bytes32 prefixedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)); 56 | 57 | // Recover address of data signer 58 | address recovered = getRecoveredAddress(sig, prefixedHash); 59 | 60 | // Take hash of recovered address 61 | bytes32 hashedAddr = keccak256(abi.encode(recovered)); 62 | 63 | // Does the trusted identifier have they key which signed the user's claim? 64 | // && (isClaimRevoked(_claimId) == false) 65 | if (keyHasPurpose(hashedAddr, 3) && (isClaimRevoked(sig) == false)) { 66 | return true; 67 | } 68 | 69 | return false; 70 | } 71 | 72 | /** 73 | * @dev See {IClaimIssuer-isClaimRevoked}. 74 | */ 75 | function isClaimRevoked(bytes memory _sig) public override view returns (bool) { 76 | if (revokedClaims[_sig]) { 77 | return true; 78 | } 79 | 80 | return false; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /contracts/Identity.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | 4 | import "./interface/IIdentity.sol"; 5 | import "./interface/IClaimIssuer.sol"; 6 | import "./version/Version.sol"; 7 | import "./storage/Storage.sol"; 8 | 9 | /** 10 | * @dev Implementation of the `IERC734` "KeyHolder" and the `IERC735` "ClaimHolder" interfaces 11 | * into a common Identity Contract. 12 | * This implementation has a separate contract were it declares all storage, 13 | * allowing for it to be used as an upgradable logic contract. 14 | */ 15 | contract Identity is Storage, IIdentity, Version { 16 | 17 | /** 18 | * @notice Prevent any direct calls to the implementation contract (marked by _canInteract = false). 19 | */ 20 | modifier delegatedOnly() { 21 | require(_canInteract == true, "Interacting with the library contract is forbidden."); 22 | _; 23 | } 24 | 25 | /** 26 | * @notice requires management key to call this function, or internal call 27 | */ 28 | modifier onlyManager() { 29 | require(msg.sender == address(this) || keyHasPurpose(keccak256(abi.encode(msg.sender)), 1) 30 | , "Permissions: Sender does not have management key"); 31 | _; 32 | } 33 | 34 | /** 35 | * @notice requires claim key to call this function, or internal call 36 | */ 37 | modifier onlyClaimKey() { 38 | require(msg.sender == address(this) || keyHasPurpose(keccak256(abi.encode(msg.sender)), 3) 39 | , "Permissions: Sender does not have claim signer key"); 40 | _; 41 | } 42 | 43 | /** 44 | * @notice constructor of the Identity contract 45 | * @param initialManagementKey the address of the management key at deployment 46 | * @param _isLibrary boolean value stating if the contract is library or not 47 | * calls __Identity_init if contract is not library 48 | */ 49 | constructor(address initialManagementKey, bool _isLibrary) { 50 | require(initialManagementKey != address(0), "invalid argument - zero address"); 51 | 52 | if (!_isLibrary) { 53 | __Identity_init(initialManagementKey); 54 | } else { 55 | _initialized = true; 56 | } 57 | } 58 | 59 | /** 60 | * @notice When using this contract as an implementation for a proxy, call this initializer with a delegatecall. 61 | * 62 | * @param initialManagementKey The ethereum address to be set as the management key of the ONCHAINID. 63 | */ 64 | function initialize(address initialManagementKey) external { 65 | require(initialManagementKey != address(0), "invalid argument - zero address"); 66 | __Identity_init(initialManagementKey); 67 | } 68 | 69 | /** 70 | * @dev See {IERC734-execute}. 71 | * @notice Passes an execution instruction to the keymanager. 72 | * If the sender is an ACTION key and the destination address is not the identity contract itself, then the 73 | * execution is immediately approved and performed. 74 | * If the destination address is the identity itself, then the execution would be performed immediately only if 75 | * the sender is a MANAGEMENT key. 76 | * Otherwise the execution request must be approved via the `approve` method. 77 | * @return executionId to use in the approve function, to approve or reject this execution. 78 | */ 79 | function execute(address _to, uint256 _value, bytes memory _data) 80 | external 81 | delegatedOnly 82 | override 83 | payable 84 | returns (uint256 executionId) 85 | { 86 | uint256 _executionId = _executionNonce; 87 | _executions[_executionId].to = _to; 88 | _executions[_executionId].value = _value; 89 | _executions[_executionId].data = _data; 90 | _executionNonce++; 91 | 92 | emit ExecutionRequested(_executionId, _to, _value, _data); 93 | 94 | if (keyHasPurpose(keccak256(abi.encode(msg.sender)), 1)) { 95 | approve(_executionId, true); 96 | } 97 | else if (_to != address(this) && keyHasPurpose(keccak256(abi.encode(msg.sender)), 2)){ 98 | approve(_executionId, true); 99 | } 100 | 101 | return _executionId; 102 | } 103 | 104 | /** 105 | * @dev See {IERC734-getKey}. 106 | * @notice Implementation of the getKey function from the ERC-734 standard 107 | * @param _key The public key. for non-hex and long keys, its the Keccak256 hash of the key 108 | * @return purposes Returns the full key data, if present in the identity. 109 | * @return keyType Returns the full key data, if present in the identity. 110 | * @return key Returns the full key data, if present in the identity. 111 | */ 112 | function getKey(bytes32 _key) 113 | external 114 | override 115 | view 116 | returns(uint256[] memory purposes, uint256 keyType, bytes32 key) 117 | { 118 | return (_keys[_key].purposes, _keys[_key].keyType, _keys[_key].key); 119 | } 120 | 121 | /** 122 | * @dev See {IERC734-getKeyPurposes}. 123 | * @notice gets the purposes of a key 124 | * @param _key The public key. for non-hex and long keys, its the Keccak256 hash of the key 125 | * @return _purposes Returns the purposes of the specified key 126 | */ 127 | function getKeyPurposes(bytes32 _key) 128 | external 129 | override 130 | view 131 | returns(uint256[] memory _purposes) 132 | { 133 | return (_keys[_key].purposes); 134 | } 135 | 136 | /** 137 | * @dev See {IERC734-getKeysByPurpose}. 138 | * @notice gets all the keys with a specific purpose from an identity 139 | * @param _purpose a uint256[] Array of the key types, like 1 = MANAGEMENT, 2 = ACTION, 3 = CLAIM, 4 = ENCRYPTION 140 | * @return keys Returns an array of public key bytes32 hold by this identity and having the specified purpose 141 | */ 142 | function getKeysByPurpose(uint256 _purpose) 143 | external 144 | override 145 | view 146 | returns(bytes32[] memory keys) 147 | { 148 | return _keysByPurpose[_purpose]; 149 | } 150 | 151 | /** 152 | * @dev See {IERC735-getClaimIdsByTopic}. 153 | * @notice Implementation of the getClaimIdsByTopic function from the ERC-735 standard. 154 | * used to get all the claims from the specified topic 155 | * @param _topic The identity of the claim i.e. keccak256(abi.encode(_issuer, _topic)) 156 | * @return claimIds Returns an array of claim IDs by topic. 157 | */ 158 | function getClaimIdsByTopic(uint256 _topic) 159 | external 160 | override 161 | view 162 | returns(bytes32[] memory claimIds) 163 | { 164 | return _claimsByTopic[_topic]; 165 | } 166 | 167 | /** 168 | * @notice implementation of the addKey function of the ERC-734 standard 169 | * Adds a _key to the identity. The _purpose specifies the purpose of key. Initially we propose four purposes: 170 | * 1: MANAGEMENT keys, which can manage the identity 171 | * 2: ACTION keys, which perform actions in this identities name (signing, logins, transactions, etc.) 172 | * 3: CLAIM signer keys, used to sign claims on other identities which need to be revokable. 173 | * 4: ENCRYPTION keys, used to encrypt data e.g. hold in claims. 174 | * MUST only be done by keys of purpose 1, or the identity itself. 175 | * If its the identity itself, the approval process will determine its approval. 176 | * @param _key keccak256 representation of an ethereum address 177 | * @param _type type of key used, which would be a uint256 for different key types. e.g. 1 = ECDSA, 2 = RSA, etc. 178 | * @param _purpose a uint256 specifying the key type, like 1 = MANAGEMENT, 2 = ACTION, 3 = CLAIM, 4 = ENCRYPTION 179 | * @return success Returns TRUE if the addition was successful and FALSE if not 180 | */ 181 | function addKey(bytes32 _key, uint256 _purpose, uint256 _type) 182 | public 183 | delegatedOnly 184 | onlyManager 185 | override 186 | returns (bool success) 187 | { 188 | if (_keys[_key].key == _key) { 189 | uint256[] memory _purposes = _keys[_key].purposes; 190 | for (uint keyPurposeIndex = 0; keyPurposeIndex < _purposes.length; keyPurposeIndex++) { 191 | uint256 purpose = _purposes[keyPurposeIndex]; 192 | 193 | if (purpose == _purpose) { 194 | revert("Conflict: Key already has purpose"); 195 | } 196 | } 197 | 198 | _keys[_key].purposes.push(_purpose); 199 | } else { 200 | _keys[_key].key = _key; 201 | _keys[_key].purposes = [_purpose]; 202 | _keys[_key].keyType = _type; 203 | } 204 | 205 | _keysByPurpose[_purpose].push(_key); 206 | 207 | emit KeyAdded(_key, _purpose, _type); 208 | 209 | return true; 210 | } 211 | 212 | /** 213 | * @dev See {IERC734-approve}. 214 | * @notice Approves an execution. 215 | * If the sender is an ACTION key and the destination address is not the identity contract itself, then the 216 | * approval is authorized and the operation would be performed. 217 | * If the destination address is the identity itself, then the execution would be authorized and performed only 218 | * if the sender is a MANAGEMENT key. 219 | */ 220 | function approve(uint256 _id, bool _approve) 221 | public 222 | delegatedOnly 223 | override 224 | returns (bool success) 225 | { 226 | require(_id < _executionNonce, "Cannot approve a non-existing execution"); 227 | require(!_executions[_id].executed, "Request already executed"); 228 | 229 | if(_executions[_id].to == address(this)) { 230 | require(keyHasPurpose(keccak256(abi.encode(msg.sender)), 1), "Sender does not have management key"); 231 | } 232 | else { 233 | require(keyHasPurpose(keccak256(abi.encode(msg.sender)), 2), "Sender does not have action key"); 234 | } 235 | 236 | emit Approved(_id, _approve); 237 | 238 | if (_approve == true) { 239 | _executions[_id].approved = true; 240 | 241 | // solhint-disable-next-line avoid-low-level-calls 242 | (success,) = _executions[_id].to.call{value:(_executions[_id].value)}(_executions[_id].data); 243 | 244 | if (success) { 245 | _executions[_id].executed = true; 246 | 247 | emit Executed( 248 | _id, 249 | _executions[_id].to, 250 | _executions[_id].value, 251 | _executions[_id].data 252 | ); 253 | 254 | return true; 255 | } else { 256 | emit ExecutionFailed( 257 | _id, 258 | _executions[_id].to, 259 | _executions[_id].value, 260 | _executions[_id].data 261 | ); 262 | 263 | return false; 264 | } 265 | } else { 266 | _executions[_id].approved = false; 267 | } 268 | return false; 269 | } 270 | 271 | /** 272 | * @dev See {IERC734-removeKey}. 273 | * @notice Remove the purpose from a key. 274 | */ 275 | function removeKey(bytes32 _key, uint256 _purpose) 276 | public 277 | delegatedOnly 278 | onlyManager 279 | override 280 | returns (bool success) 281 | { 282 | require(_keys[_key].key == _key, "NonExisting: Key isn't registered"); 283 | uint256[] memory _purposes = _keys[_key].purposes; 284 | 285 | uint purposeIndex = 0; 286 | while (_purposes[purposeIndex] != _purpose) { 287 | purposeIndex++; 288 | 289 | if (purposeIndex == _purposes.length) { 290 | revert("NonExisting: Key doesn't have such purpose"); 291 | } 292 | } 293 | 294 | _purposes[purposeIndex] = _purposes[_purposes.length - 1]; 295 | _keys[_key].purposes = _purposes; 296 | _keys[_key].purposes.pop(); 297 | 298 | uint keyIndex = 0; 299 | uint arrayLength = _keysByPurpose[_purpose].length; 300 | 301 | while (_keysByPurpose[_purpose][keyIndex] != _key) { 302 | keyIndex++; 303 | 304 | if (keyIndex >= arrayLength) { 305 | break; 306 | } 307 | } 308 | 309 | _keysByPurpose[_purpose][keyIndex] = _keysByPurpose[_purpose][arrayLength - 1]; 310 | _keysByPurpose[_purpose].pop(); 311 | 312 | uint keyType = _keys[_key].keyType; 313 | 314 | if (_purposes.length - 1 == 0) { 315 | delete _keys[_key]; 316 | } 317 | 318 | emit KeyRemoved(_key, _purpose, keyType); 319 | 320 | return true; 321 | } 322 | 323 | /** 324 | * @dev See {IERC735-addClaim}. 325 | * @notice Implementation of the addClaim function from the ERC-735 standard 326 | * Require that the msg.sender has claim signer key. 327 | * 328 | * @param _topic The type of claim 329 | * @param _scheme The scheme with which this claim SHOULD be verified or how it should be processed. 330 | * @param _issuer The issuers identity contract address, or the address used to sign the above signature. 331 | * @param _signature Signature which is the proof that the claim issuer issued a claim of topic for this identity. 332 | * it MUST be a signed message of the following structure: 333 | * keccak256(abi.encode(address identityHolder_address, uint256 _ topic, bytes data)) 334 | * @param _data The hash of the claim data, sitting in another 335 | * location, a bit-mask, call data, or actual data based on the claim scheme. 336 | * @param _uri The location of the claim, this can be HTTP links, swarm hashes, IPFS hashes, and such. 337 | * 338 | * @return claimRequestId Returns claimRequestId: COULD be 339 | * send to the approve function, to approve or reject this claim. 340 | * triggers ClaimAdded event. 341 | */ 342 | function addClaim( 343 | uint256 _topic, 344 | uint256 _scheme, 345 | address _issuer, 346 | bytes memory _signature, 347 | bytes memory _data, 348 | string memory _uri 349 | ) 350 | public 351 | delegatedOnly 352 | onlyClaimKey 353 | override 354 | returns (bytes32 claimRequestId) 355 | { 356 | if (_issuer != address(this)) { 357 | require(IClaimIssuer(_issuer).isClaimValid(IIdentity(address(this)), _topic, _signature, _data), "invalid claim"); 358 | } 359 | 360 | bytes32 claimId = keccak256(abi.encode(_issuer, _topic)); 361 | _claims[claimId].topic = _topic; 362 | _claims[claimId].scheme = _scheme; 363 | _claims[claimId].signature = _signature; 364 | _claims[claimId].data = _data; 365 | _claims[claimId].uri = _uri; 366 | 367 | if (_claims[claimId].issuer != _issuer) { 368 | _claimsByTopic[_topic].push(claimId); 369 | _claims[claimId].issuer = _issuer; 370 | 371 | emit ClaimAdded(claimId, _topic, _scheme, _issuer, _signature, _data, _uri); 372 | } 373 | else { 374 | emit ClaimChanged(claimId, _topic, _scheme, _issuer, _signature, _data, _uri); 375 | } 376 | return claimId; 377 | } 378 | 379 | /** 380 | * @dev See {IERC735-removeClaim}. 381 | * @notice Implementation of the removeClaim function from the ERC-735 standard 382 | * Require that the msg.sender has management key. 383 | * Can only be removed by the claim issuer, or the claim holder itself. 384 | * 385 | * @param _claimId The identity of the claim i.e. keccak256(abi.encode(_issuer, _topic)) 386 | * 387 | * @return success Returns TRUE when the claim was removed. 388 | * triggers ClaimRemoved event 389 | */ 390 | function removeClaim(bytes32 _claimId) 391 | public 392 | delegatedOnly 393 | onlyClaimKey 394 | override 395 | returns 396 | (bool success) { 397 | uint256 _topic = _claims[_claimId].topic; 398 | if (_topic == 0) { 399 | revert("NonExisting: There is no claim with this ID"); 400 | } 401 | 402 | uint claimIndex = 0; 403 | uint arrayLength = _claimsByTopic[_topic].length; 404 | while (_claimsByTopic[_topic][claimIndex] != _claimId) { 405 | claimIndex++; 406 | 407 | if (claimIndex >= arrayLength) { 408 | break; 409 | } 410 | } 411 | 412 | _claimsByTopic[_topic][claimIndex] = 413 | _claimsByTopic[_topic][arrayLength - 1]; 414 | _claimsByTopic[_topic].pop(); 415 | 416 | emit ClaimRemoved( 417 | _claimId, 418 | _topic, 419 | _claims[_claimId].scheme, 420 | _claims[_claimId].issuer, 421 | _claims[_claimId].signature, 422 | _claims[_claimId].data, 423 | _claims[_claimId].uri 424 | ); 425 | 426 | delete _claims[_claimId]; 427 | 428 | return true; 429 | } 430 | 431 | /** 432 | * @dev See {IERC735-getClaim}. 433 | * @notice Implementation of the getClaim function from the ERC-735 standard. 434 | * 435 | * @param _claimId The identity of the claim i.e. keccak256(abi.encode(_issuer, _topic)) 436 | * 437 | * @return topic Returns all the parameters of the claim for the 438 | * specified _claimId (topic, scheme, signature, issuer, data, uri) . 439 | * @return scheme Returns all the parameters of the claim for the 440 | * specified _claimId (topic, scheme, signature, issuer, data, uri) . 441 | * @return issuer Returns all the parameters of the claim for the 442 | * specified _claimId (topic, scheme, signature, issuer, data, uri) . 443 | * @return signature Returns all the parameters of the claim for the 444 | * specified _claimId (topic, scheme, signature, issuer, data, uri) . 445 | * @return data Returns all the parameters of the claim for the 446 | * specified _claimId (topic, scheme, signature, issuer, data, uri) . 447 | * @return uri Returns all the parameters of the claim for the 448 | * specified _claimId (topic, scheme, signature, issuer, data, uri) . 449 | */ 450 | function getClaim(bytes32 _claimId) 451 | public 452 | override 453 | view 454 | returns( 455 | uint256 topic, 456 | uint256 scheme, 457 | address issuer, 458 | bytes memory signature, 459 | bytes memory data, 460 | string memory uri 461 | ) 462 | { 463 | return ( 464 | _claims[_claimId].topic, 465 | _claims[_claimId].scheme, 466 | _claims[_claimId].issuer, 467 | _claims[_claimId].signature, 468 | _claims[_claimId].data, 469 | _claims[_claimId].uri 470 | ); 471 | } 472 | 473 | /** 474 | * @dev See {IERC734-keyHasPurpose}. 475 | * @notice Returns true if the key has MANAGEMENT purpose or the specified purpose. 476 | */ 477 | function keyHasPurpose(bytes32 _key, uint256 _purpose) 478 | public 479 | override 480 | view 481 | returns(bool result) 482 | { 483 | Key memory key = _keys[_key]; 484 | if (key.key == 0) return false; 485 | 486 | for (uint keyPurposeIndex = 0; keyPurposeIndex < key.purposes.length; keyPurposeIndex++) { 487 | uint256 purpose = key.purposes[keyPurposeIndex]; 488 | 489 | if (purpose == 1 || purpose == _purpose) return true; 490 | } 491 | 492 | return false; 493 | } 494 | 495 | /** 496 | * @dev Checks if a claim is valid. Claims issued by the identity are self-attested claims. They do not have a 497 | * built-in revocation mechanism and are considered valid as long as their signature is valid and they are still 498 | * stored by the identity contract. 499 | * @param _identity the identity contract related to the claim 500 | * @param claimTopic the claim topic of the claim 501 | * @param sig the signature of the claim 502 | * @param data the data field of the claim 503 | * @return claimValid true if the claim is valid, false otherwise 504 | */ 505 | function isClaimValid( 506 | IIdentity _identity, 507 | uint256 claimTopic, 508 | bytes memory sig, 509 | bytes memory data) 510 | public override virtual view returns (bool claimValid) 511 | { 512 | bytes32 dataHash = keccak256(abi.encode(_identity, claimTopic, data)); 513 | // Use abi.encodePacked to concatenate the message prefix and the message to sign. 514 | bytes32 prefixedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)); 515 | 516 | // Recover address of data signer 517 | address recovered = getRecoveredAddress(sig, prefixedHash); 518 | 519 | // Take hash of recovered address 520 | bytes32 hashedAddr = keccak256(abi.encode(recovered)); 521 | 522 | // Does the trusted identifier have they key which signed the user's claim? 523 | // && (isClaimRevoked(_claimId) == false) 524 | if (keyHasPurpose(hashedAddr, 3)) { 525 | return true; 526 | } 527 | 528 | return false; 529 | } 530 | 531 | /** 532 | * @dev returns the address that signed the given data 533 | * @param sig the signature of the data 534 | * @param dataHash the data that was signed 535 | * returns the address that signed dataHash and created the signature sig 536 | */ 537 | function getRecoveredAddress(bytes memory sig, bytes32 dataHash) 538 | public 539 | pure 540 | returns (address addr) 541 | { 542 | bytes32 ra; 543 | bytes32 sa; 544 | uint8 va; 545 | 546 | // Check the signature length 547 | if (sig.length != 65) { 548 | return address(0); 549 | } 550 | 551 | // Divide the signature in r, s and v variables 552 | // solhint-disable-next-line no-inline-assembly 553 | assembly { 554 | ra := mload(add(sig, 32)) 555 | sa := mload(add(sig, 64)) 556 | va := byte(0, mload(add(sig, 96))) 557 | } 558 | 559 | if (va < 27) { 560 | va += 27; 561 | } 562 | 563 | address recoveredAddress = ecrecover(dataHash, va, ra, sa); 564 | 565 | return (recoveredAddress); 566 | } 567 | 568 | /** 569 | * @notice Initializer internal function for the Identity contract. 570 | * 571 | * @param initialManagementKey The ethereum address to be set as the management key of the ONCHAINID. 572 | */ 573 | // solhint-disable-next-line func-name-mixedcase 574 | function __Identity_init(address initialManagementKey) internal { 575 | require(!_initialized || _isConstructor(), "Initial key was already setup."); 576 | _initialized = true; 577 | _canInteract = true; 578 | 579 | bytes32 _key = keccak256(abi.encode(initialManagementKey)); 580 | _keys[_key].key = _key; 581 | _keys[_key].purposes = [1]; 582 | _keys[_key].keyType = 1; 583 | _keysByPurpose[1].push(_key); 584 | emit KeyAdded(_key, 1, 1); 585 | } 586 | 587 | /** 588 | * @notice Computes if the context in which the function is called is a constructor or not. 589 | * 590 | * @return true if the context is a constructor. 591 | */ 592 | function _isConstructor() private view returns (bool) { 593 | address self = address(this); 594 | uint256 cs; 595 | // solhint-disable-next-line no-inline-assembly 596 | assembly { cs := extcodesize(self) } 597 | return cs == 0; 598 | } 599 | } 600 | -------------------------------------------------------------------------------- /contracts/Test.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | 4 | contract Test {} // solhint-disable-line 5 | -------------------------------------------------------------------------------- /contracts/_testContracts/VerifierUser.sol: -------------------------------------------------------------------------------- 1 | /* solhint-disable */ 2 | 3 | // SPDX-License-Identifier: GPL-3.0 4 | pragma solidity 0.8.17; 5 | 6 | import "../verifiers/Verifier.sol"; 7 | 8 | contract VerifierUser is Verifier { 9 | constructor() Verifier() {} 10 | 11 | function doSomething() onlyVerifiedSender public {} 12 | } 13 | -------------------------------------------------------------------------------- /contracts/factory/IIdFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | 4 | interface IIdFactory { 5 | 6 | /// events 7 | 8 | // event emitted whenever a single contract is deployed by the factory 9 | event Deployed(address indexed _addr); 10 | 11 | // event emitted when a wallet is linked to an ONCHAINID contract 12 | event WalletLinked(address indexed wallet, address indexed identity); 13 | 14 | // event emitted when a token is linked to an ONCHAINID contract 15 | event TokenLinked(address indexed token, address indexed identity); 16 | 17 | // event emitted when a wallet is unlinked from an ONCHAINID contract 18 | event WalletUnlinked(address indexed wallet, address indexed identity); 19 | 20 | // event emitted when an address is registered on the factory as a Token 21 | // factory address, granting this address the privilege to issue 22 | // Onchain identities for tokens 23 | event TokenFactoryAdded(address indexed factory); 24 | 25 | // event emitted when a previously recorded token factory address is removed 26 | event TokenFactoryRemoved(address indexed factory); 27 | 28 | /// functions 29 | 30 | /** 31 | * @dev function used to create a new Identity proxy from the factory 32 | * @param _wallet the wallet address of the primary owner of this ONCHAINID contract 33 | * @param _salt the salt used by create2 to issue the contract 34 | * requires a new salt for each deployment 35 | * _wallet cannot be linked to another ONCHAINID 36 | * only Owner can call => Owner is supposed to be a smart contract, managing the accessibility 37 | * of the function, including calls to oracles for multichain 38 | * deployment security (avoid identity theft), defining payment requirements, etc. 39 | */ 40 | function createIdentity(address _wallet, string memory _salt) external returns (address); 41 | 42 | /** 43 | * @dev function used to create a new Identity proxy from the factory, setting the wallet and listed keys as 44 | * MANAGEMENT keys. 45 | * @param _wallet the wallet address of the primary owner of this ONCHAINID contract 46 | * @param _salt the salt used by create2 to issue the contract 47 | * @param _managementKeys A list of keys hash (keccak256(abiEncoded())) to add as MANAGEMENT keys. 48 | * requires a new salt for each deployment 49 | * _wallet cannot be linked to another ONCHAINID 50 | * only Owner can call => Owner is supposed to be a smart contract, managing the accessibility 51 | * of the function, including calls to oracles for multichain 52 | * deployment security (avoid identity theft), defining payment requirements, etc. 53 | */ 54 | function createIdentityWithManagementKeys( 55 | address _wallet, 56 | string memory _salt, 57 | bytes32[] memory _managementKeys 58 | ) external returns (address); 59 | 60 | /** 61 | * @dev function used to create a new Token Identity proxy from the factory 62 | * @param _token the address of the token contract 63 | * @param _tokenOwner the owner address of the token 64 | * @param _salt the salt used by create2 to issue the contract 65 | * requires a new salt for each deployment 66 | * _token cannot be linked to another ONCHAINID 67 | * only Token factory or owner can call (owner should only use its privilege 68 | * for tokens not issued by a Token factory onchain 69 | */ 70 | function createTokenIdentity(address _token, address _tokenOwner, string memory _salt) external returns (address); 71 | 72 | /** 73 | * @dev function used to link a new wallet to an existing identity 74 | * @param _newWallet the address of the wallet to link 75 | * requires msg.sender to be linked to an existing onchainid 76 | * the _newWallet will be linked to the same OID contract as msg.sender 77 | * _newWallet cannot be linked to an OID yet 78 | * _newWallet cannot be address 0 79 | * cannot link more than 100 wallets to an OID, for gas consumption reason 80 | */ 81 | function linkWallet(address _newWallet) external; 82 | 83 | /** 84 | * @dev function used to unlink a wallet from an existing identity 85 | * @param _oldWallet the address of the wallet to unlink 86 | * requires msg.sender to be linked to the same onchainid as _oldWallet 87 | * msg.sender cannot be _oldWallet to keep at least 1 wallet linked to any OID 88 | * _oldWallet cannot be address 0 89 | */ 90 | function unlinkWallet(address _oldWallet) external; 91 | 92 | /** 93 | * @dev function used to register an address as a token factory 94 | * @param _factory the address of the token factory 95 | * can be called only by Owner 96 | * _factory cannot be registered yet 97 | * once the factory has been registered it can deploy token identities 98 | */ 99 | function addTokenFactory(address _factory) external; 100 | 101 | /** 102 | * @dev function used to unregister an address previously registered as a token factory 103 | * @param _factory the address of the token factory 104 | * can be called only by Owner 105 | * _factory has to be registered previously 106 | * once the factory has been unregistered it cannot deploy token identities anymore 107 | */ 108 | function removeTokenFactory(address _factory) external; 109 | 110 | /** 111 | * @dev getter for OID contract corresponding to a wallet/token 112 | * @param _wallet the wallet/token address 113 | */ 114 | function getIdentity(address _wallet) external view returns (address); 115 | 116 | /** 117 | * @dev getter to fetch the array of wallets linked to an OID contract 118 | * @param _identity the address of the OID contract 119 | * returns an array of addresses linked to the OID 120 | */ 121 | function getWallets(address _identity) external view returns (address[] memory); 122 | 123 | /** 124 | * @dev getter to fetch the token address linked to an OID contract 125 | * @param _identity the address of the OID contract 126 | * returns the address linked to the OID 127 | */ 128 | function getToken(address _identity) external view returns (address); 129 | 130 | /** 131 | * @dev getter to know if an address is registered as token factory or not 132 | * @param _factory the address of the factory 133 | * returns true if the address corresponds to a registered factory 134 | */ 135 | function isTokenFactory(address _factory) external view returns(bool); 136 | 137 | /** 138 | * @dev getter to know if a salt is taken for the create2 deployment 139 | * @param _salt the salt used for deployment 140 | */ 141 | function isSaltTaken(string calldata _salt) external view returns (bool); 142 | 143 | /** 144 | * @dev getter for the implementation authority used by this factory. 145 | */ 146 | function implementationAuthority() external view returns (address); 147 | } 148 | -------------------------------------------------------------------------------- /contracts/factory/IdFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | 4 | import "../proxy/IdentityProxy.sol"; 5 | import "./IIdFactory.sol"; 6 | import "../interface/IERC734.sol"; 7 | import "@openzeppelin/contracts/access/Ownable.sol"; 8 | 9 | contract IdFactory is IIdFactory, Ownable { 10 | 11 | mapping(address => bool) private _tokenFactories; 12 | 13 | // address of the _implementationAuthority contract making the link to the implementation contract 14 | address private immutable _implementationAuthority; 15 | 16 | // as it is not possible to deploy 2 times the same contract address, this mapping allows us to check which 17 | // salt is taken and which is not 18 | mapping(string => bool) private _saltTaken; 19 | 20 | // ONCHAINID of the wallet owner 21 | mapping(address => address) private _userIdentity; 22 | 23 | // wallets currently linked to an ONCHAINID 24 | mapping(address => address[]) private _wallets; 25 | 26 | // ONCHAINID of the token 27 | mapping(address => address) private _tokenIdentity; 28 | 29 | // token linked to an ONCHAINID 30 | mapping(address => address) private _tokenAddress; 31 | 32 | 33 | // setting 34 | constructor (address implementationAuthority) { 35 | require(implementationAuthority != address(0), "invalid argument - zero address"); 36 | _implementationAuthority = implementationAuthority; 37 | } 38 | 39 | /** 40 | * @dev See {IdFactory-addTokenFactory}. 41 | */ 42 | function addTokenFactory(address _factory) external override onlyOwner { 43 | require(_factory != address(0), "invalid argument - zero address"); 44 | require(!isTokenFactory(_factory), "already a factory"); 45 | _tokenFactories[_factory] = true; 46 | emit TokenFactoryAdded(_factory); 47 | } 48 | 49 | /** 50 | * @dev See {IdFactory-removeTokenFactory}. 51 | */ 52 | function removeTokenFactory(address _factory) external override onlyOwner { 53 | require(_factory != address(0), "invalid argument - zero address"); 54 | require(isTokenFactory(_factory), "not a factory"); 55 | _tokenFactories[_factory] = false; 56 | emit TokenFactoryRemoved(_factory); 57 | } 58 | 59 | /** 60 | * @dev See {IdFactory-createIdentity}. 61 | */ 62 | function createIdentity( 63 | address _wallet, 64 | string memory _salt) 65 | external onlyOwner override returns (address) { 66 | require(_wallet != address(0), "invalid argument - zero address"); 67 | require(keccak256(abi.encode(_salt)) != keccak256(abi.encode("")), "invalid argument - empty string"); 68 | string memory oidSalt = string.concat("OID",_salt); 69 | require (!_saltTaken[oidSalt], "salt already taken"); 70 | require (_userIdentity[_wallet] == address(0), "wallet already linked to an identity"); 71 | address identity = _deployIdentity(oidSalt, _implementationAuthority, _wallet); 72 | _saltTaken[oidSalt] = true; 73 | _userIdentity[_wallet] = identity; 74 | _wallets[identity].push(_wallet); 75 | emit WalletLinked(_wallet, identity); 76 | return identity; 77 | } 78 | 79 | /** 80 | * @dev See {IdFactory-createIdentityWithManagementKeys}. 81 | */ 82 | function createIdentityWithManagementKeys( 83 | address _wallet, 84 | string memory _salt, 85 | bytes32[] memory _managementKeys 86 | ) external onlyOwner override returns (address) { 87 | require(_wallet != address(0), "invalid argument - zero address"); 88 | require(keccak256(abi.encode(_salt)) != keccak256(abi.encode("")), "invalid argument - empty string"); 89 | string memory oidSalt = string.concat("OID",_salt); 90 | require (!_saltTaken[oidSalt], "salt already taken"); 91 | require (_userIdentity[_wallet] == address(0), "wallet already linked to an identity"); 92 | require(_managementKeys.length > 0, "invalid argument - empty list of keys"); 93 | 94 | address identity = _deployIdentity(oidSalt, _implementationAuthority, address(this)); 95 | 96 | for (uint i = 0; i < _managementKeys.length; i++) { 97 | require( 98 | _managementKeys[i] != keccak256(abi.encode(_wallet)) 99 | , "invalid argument - wallet is also listed in management keys"); 100 | IERC734(identity).addKey( 101 | _managementKeys[i], 102 | 1, 103 | 1 104 | ); 105 | } 106 | 107 | IERC734(identity).removeKey( 108 | keccak256(abi.encode(address(this))), 109 | 1 110 | ); 111 | 112 | _saltTaken[oidSalt] = true; 113 | _userIdentity[_wallet] = identity; 114 | _wallets[identity].push(_wallet); 115 | emit WalletLinked(_wallet, identity); 116 | 117 | return identity; 118 | } 119 | 120 | /** 121 | * @dev See {IdFactory-createTokenIdentity}. 122 | */ 123 | function createTokenIdentity( 124 | address _token, 125 | address _tokenOwner, 126 | string memory _salt) 127 | external override returns (address) { 128 | require(isTokenFactory(msg.sender) || msg.sender == owner(), "only Factory or owner can call"); 129 | require(_token != address(0), "invalid argument - zero address"); 130 | require(_tokenOwner != address(0), "invalid argument - zero address"); 131 | require(keccak256(abi.encode(_salt)) != keccak256(abi.encode("")), "invalid argument - empty string"); 132 | string memory tokenIdSalt = string.concat("Token",_salt); 133 | require(!_saltTaken[tokenIdSalt], "salt already taken"); 134 | require(_tokenIdentity[_token] == address(0), "token already linked to an identity"); 135 | address identity = _deployIdentity(tokenIdSalt, _implementationAuthority, _tokenOwner); 136 | _saltTaken[tokenIdSalt] = true; 137 | _tokenIdentity[_token] = identity; 138 | _tokenAddress[identity] = _token; 139 | emit TokenLinked(_token, identity); 140 | return identity; 141 | } 142 | 143 | /** 144 | * @dev See {IdFactory-linkWallet}. 145 | */ 146 | function linkWallet(address _newWallet) external override { 147 | require(_newWallet != address(0), "invalid argument - zero address"); 148 | require(_userIdentity[msg.sender] != address(0), "wallet not linked to an identity contract"); 149 | require(_userIdentity[_newWallet] == address(0), "new wallet already linked"); 150 | require(_tokenIdentity[_newWallet] == address(0), "invalid argument - token address"); 151 | address identity = _userIdentity[msg.sender]; 152 | require(_wallets[identity].length < 101, "max amount of wallets per ID exceeded"); 153 | _userIdentity[_newWallet] = identity; 154 | _wallets[identity].push(_newWallet); 155 | emit WalletLinked(_newWallet, identity); 156 | } 157 | 158 | /** 159 | * @dev See {IdFactory-unlinkWallet}. 160 | */ 161 | function unlinkWallet(address _oldWallet) external override { 162 | require(_oldWallet != address(0), "invalid argument - zero address"); 163 | require(_oldWallet != msg.sender, "cannot be called on sender address"); 164 | require(_userIdentity[msg.sender] == _userIdentity[_oldWallet], "only a linked wallet can unlink"); 165 | address _identity = _userIdentity[_oldWallet]; 166 | delete _userIdentity[_oldWallet]; 167 | uint256 length = _wallets[_identity].length; 168 | for (uint256 i = 0; i < length; i++) { 169 | if (_wallets[_identity][i] == _oldWallet) { 170 | _wallets[_identity][i] = _wallets[_identity][length - 1]; 171 | _wallets[_identity].pop(); 172 | break; 173 | } 174 | } 175 | emit WalletUnlinked(_oldWallet, _identity); 176 | } 177 | 178 | /** 179 | * @dev See {IdFactory-getIdentity}. 180 | */ 181 | function getIdentity(address _wallet) external override view returns (address) { 182 | if(_tokenIdentity[_wallet] != address(0)) { 183 | return _tokenIdentity[_wallet]; 184 | } 185 | else { 186 | return _userIdentity[_wallet]; 187 | } 188 | } 189 | 190 | /** 191 | * @dev See {IdFactory-isSaltTaken}. 192 | */ 193 | function isSaltTaken(string calldata _salt) external override view returns (bool) { 194 | return _saltTaken[_salt]; 195 | } 196 | 197 | /** 198 | * @dev See {IdFactory-getWallets}. 199 | */ 200 | function getWallets(address _identity) external override view returns (address[] memory) { 201 | return _wallets[_identity]; 202 | } 203 | 204 | /** 205 | * @dev See {IdFactory-getToken}. 206 | */ 207 | function getToken(address _identity) external override view returns (address) { 208 | return _tokenAddress[_identity]; 209 | } 210 | 211 | /** 212 | * @dev See {IdFactory-isTokenFactory}. 213 | */ 214 | function isTokenFactory(address _factory) public override view returns(bool) { 215 | return _tokenFactories[_factory]; 216 | } 217 | 218 | /** 219 | * @dev See {IdFactory-implementationAuthority}. 220 | */ 221 | function implementationAuthority() public override view returns (address) { 222 | return _implementationAuthority; 223 | } 224 | 225 | // deploy function with create2 opcode call 226 | // returns the address of the contract created 227 | function _deploy(string memory salt, bytes memory bytecode) private returns (address) { 228 | bytes32 saltBytes = bytes32(keccak256(abi.encodePacked(salt))); 229 | address addr; 230 | // solhint-disable-next-line no-inline-assembly 231 | assembly { 232 | let encoded_data := add(0x20, bytecode) // load initialization code. 233 | let encoded_size := mload(bytecode) // load init code's length. 234 | addr := create2(0, encoded_data, encoded_size, saltBytes) 235 | if iszero(extcodesize(addr)) { 236 | revert(0, 0) 237 | } 238 | } 239 | emit Deployed(addr); 240 | return addr; 241 | } 242 | 243 | // function used to deploy an identity using CREATE2 244 | function _deployIdentity 245 | ( 246 | string memory _salt, 247 | address implementationAuthority, 248 | address _wallet 249 | ) private returns (address){ 250 | bytes memory _code = type(IdentityProxy).creationCode; 251 | bytes memory _constructData = abi.encode(implementationAuthority, _wallet); 252 | bytes memory bytecode = abi.encodePacked(_code, _constructData); 253 | return _deploy(_salt, bytecode); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /contracts/gateway/Gateway.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 6 | import "../factory/IdFactory.sol"; 7 | 8 | using ECDSA for bytes32; 9 | 10 | /// A required parameter was set to the Zero address. 11 | error ZeroAddress(); 12 | /// The maximum number of signers was reached at deployment. 13 | error TooManySigners(); 14 | /// The signed attempted to add was already approved. 15 | error SignerAlreadyApproved(address signer); 16 | /// The signed attempted to remove was not approved. 17 | error SignerAlreadyNotApproved(address signer); 18 | /// A requested ONCHAINID deployment was requested without a valid signature while the Gateway requires one. 19 | error UnsignedDeployment(); 20 | /// A requested ONCHAINID deployment was requested and signer by a non approved signer. 21 | error UnapprovedSigner(address signer); 22 | /// A requested ONCHAINID deployment was requested with a signature revoked. 23 | error RevokedSignature(bytes signature); 24 | /// A requested ONCHAINID deployment was requested with a signature that expired. 25 | error ExpiredSignature(bytes signature); 26 | /// Attempted to revoke a signature that was already revoked. 27 | error SignatureAlreadyRevoked(bytes signature); 28 | /// Attempted to approve a signature that was not revoked. 29 | error SignatureNotRevoked(bytes signature); 30 | 31 | contract Gateway is Ownable { 32 | IdFactory public idFactory; 33 | mapping(address => bool) public approvedSigners; 34 | mapping(bytes => bool) public revokedSignatures; 35 | 36 | event SignerApproved(address indexed signer); 37 | event SignerRevoked(address indexed signer); 38 | event SignatureRevoked(bytes indexed signature); 39 | event SignatureApproved(bytes indexed signature); 40 | 41 | /** 42 | * @dev Constructor for the ONCHAINID Factory Gateway. 43 | * @param idFactoryAddress the address of the factory to operate (the Gateway must be owner of the Factory). 44 | */ 45 | constructor(address idFactoryAddress, address[] memory signersToApprove) Ownable() { 46 | if (idFactoryAddress == address(0)) { 47 | revert ZeroAddress(); 48 | } 49 | if (signersToApprove.length > 10) { 50 | revert TooManySigners(); 51 | } 52 | 53 | for (uint i = 0; i < signersToApprove.length; i++) { 54 | approvedSigners[signersToApprove[i]] = true; 55 | } 56 | 57 | idFactory = IdFactory(idFactoryAddress); 58 | } 59 | 60 | /** 61 | * @dev Approve a signer to sign ONCHAINID deployments. If the Gateway is setup to require signature, only 62 | * deployments requested with a valid signature from an approved signer will be accepted. 63 | * If the gateway does not require a signature, 64 | * @param signer the signer address to approve. 65 | */ 66 | function approveSigner(address signer) external onlyOwner { 67 | if (signer == address(0)) { 68 | revert ZeroAddress(); 69 | } 70 | 71 | if (approvedSigners[signer]) { 72 | revert SignerAlreadyApproved(signer); 73 | } 74 | 75 | approvedSigners[signer] = true; 76 | 77 | emit SignerApproved(signer); 78 | } 79 | 80 | /** 81 | * @dev Revoke a signer to sign ONCHAINID deployments. 82 | * @param signer the signer address to revoke. 83 | */ 84 | function revokeSigner(address signer) external onlyOwner { 85 | if (signer == address(0)) { 86 | revert ZeroAddress(); 87 | } 88 | 89 | if (!approvedSigners[signer]) { 90 | revert SignerAlreadyNotApproved(signer); 91 | } 92 | 93 | delete approvedSigners[signer]; 94 | 95 | emit SignerRevoked(signer); 96 | } 97 | 98 | /** 99 | * @dev Deploy an ONCHAINID using a factory. The operation must be signed by 100 | * an approved public key. This method allow to deploy an ONCHAINID using a custom salt. 101 | * @param identityOwner the address to set as a management key. 102 | * @param salt to use for the deployment. 103 | * @param signatureExpiry the block timestamp where the signature will expire. 104 | * @param signature the approval containing the salt and the identityOwner address. 105 | */ 106 | function deployIdentityWithSalt( 107 | address identityOwner, 108 | string memory salt, 109 | uint256 signatureExpiry, 110 | bytes calldata signature 111 | ) external returns (address) { 112 | if (identityOwner == address(0)) { 113 | revert ZeroAddress(); 114 | } 115 | 116 | if (signatureExpiry != 0 && signatureExpiry < block.timestamp) { 117 | revert ExpiredSignature(signature); 118 | } 119 | 120 | address signer = ECDSA.recover( 121 | keccak256( 122 | abi.encode( 123 | "Authorize ONCHAINID deployment", 124 | identityOwner, 125 | salt, 126 | signatureExpiry 127 | ) 128 | ).toEthSignedMessageHash(), 129 | signature 130 | ); 131 | 132 | if (!approvedSigners[signer]) { 133 | revert UnapprovedSigner(signer); 134 | } 135 | 136 | if (revokedSignatures[signature]) { 137 | revert RevokedSignature(signature); 138 | } 139 | 140 | return idFactory.createIdentity(identityOwner, salt); 141 | } 142 | 143 | /** 144 | * @dev Deploy an ONCHAINID using a factory. The operation must be signed by 145 | * an approved public key. This method allow to deploy an ONCHAINID using a custom salt and a custom list of 146 | * management keys. Note that the identity Owner address won't be added as a management keys, if this is desired, 147 | * the key hash must be listed in the managementKeys array. 148 | * @param identityOwner the address to set as a management key. 149 | * @param salt to use for the deployment. 150 | * @param managementKeys the list of management keys to add to the ONCHAINID. 151 | * @param signatureExpiry the block timestamp where the signature will expire. 152 | * @param signature the approval containing the salt and the identityOwner address. 153 | */ 154 | function deployIdentityWithSaltAndManagementKeys( 155 | address identityOwner, 156 | string memory salt, 157 | bytes32[] calldata managementKeys, 158 | uint256 signatureExpiry, 159 | bytes calldata signature 160 | ) external returns (address) { 161 | if (identityOwner == address(0)) { 162 | revert ZeroAddress(); 163 | } 164 | 165 | if (signatureExpiry != 0 && signatureExpiry < block.timestamp) { 166 | revert ExpiredSignature(signature); 167 | } 168 | 169 | address signer = ECDSA.recover( 170 | keccak256( 171 | abi.encode( 172 | "Authorize ONCHAINID deployment", 173 | identityOwner, 174 | salt, 175 | managementKeys, 176 | signatureExpiry 177 | ) 178 | ).toEthSignedMessageHash(), 179 | signature 180 | ); 181 | 182 | if (!approvedSigners[signer]) { 183 | revert UnapprovedSigner(signer); 184 | } 185 | 186 | if (revokedSignatures[signature]) { 187 | revert RevokedSignature(signature); 188 | } 189 | 190 | return idFactory.createIdentityWithManagementKeys(identityOwner, salt, managementKeys); 191 | } 192 | 193 | /** 194 | * @dev Deploy an ONCHAINID using a factory using the identityOwner address as salt. 195 | * @param identityOwner the address to set as a management key. 196 | */ 197 | function deployIdentityForWallet(address identityOwner) external returns (address) { 198 | if (identityOwner == address(0)) { 199 | revert ZeroAddress(); 200 | } 201 | 202 | return idFactory.createIdentity(identityOwner, Strings.toHexString(identityOwner)); 203 | } 204 | 205 | /** 206 | * @dev Revoke a signature, if the signature is used to deploy an ONCHAINID, the deployment would be rejected. 207 | * @param signature the signature to revoke. 208 | */ 209 | function revokeSignature(bytes calldata signature) external onlyOwner { 210 | if (revokedSignatures[signature]) { 211 | revert SignatureAlreadyRevoked(signature); 212 | } 213 | 214 | revokedSignatures[signature] = true; 215 | 216 | emit SignatureRevoked(signature); 217 | } 218 | 219 | /** 220 | * @dev Remove a signature from the revoke list. 221 | * @param signature the signature to approve. 222 | */ 223 | function approveSignature(bytes calldata signature) external onlyOwner { 224 | if (!revokedSignatures[signature]) { 225 | revert SignatureNotRevoked(signature); 226 | } 227 | 228 | delete revokedSignatures[signature]; 229 | 230 | emit SignatureApproved(signature); 231 | } 232 | 233 | /** 234 | * @dev Transfer the ownership of the factory to a new owner. 235 | * @param newOwner the new owner of the factory. 236 | */ 237 | function transferFactoryOwnership(address newOwner) external onlyOwner { 238 | idFactory.transferOwnership(newOwner); 239 | } 240 | 241 | /** 242 | * @dev Call a function on the factory. Only the owner of the Gateway can call this method. 243 | * @param data the data to call on the factory. 244 | */ 245 | function callFactory(bytes memory data) external onlyOwner { 246 | (bool success,) = address(idFactory).call(data); 247 | require(success, "Gateway: call to factory failed"); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /contracts/interface/IClaimIssuer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | 4 | import "./IIdentity.sol"; 5 | 6 | interface IClaimIssuer is IIdentity { 7 | 8 | /** 9 | * @dev Emitted when a claim is revoked. 10 | * 11 | * Specification: MUST be triggered when revoking a claim. 12 | */ 13 | event ClaimRevoked(bytes indexed signature); 14 | 15 | /** 16 | * @dev Revoke a claim previously issued, the claim is no longer considered as valid after revocation. 17 | * @notice will fetch the claim from the identity contract (unsafe). 18 | * @param _claimId the id of the claim 19 | * @param _identity the address of the identity contract 20 | * @return isRevoked true when the claim is revoked 21 | */ 22 | function revokeClaim(bytes32 _claimId, address _identity) external returns(bool); 23 | 24 | /** 25 | * @dev Revoke a claim previously issued, the claim is no longer considered as valid after revocation. 26 | * @param signature the signature of the claim 27 | */ 28 | function revokeClaimBySignature(bytes calldata signature) external; 29 | 30 | /** 31 | * @dev Returns revocation status of a claim. 32 | * @param _sig the signature of the claim 33 | * @return isRevoked true if the claim is revoked and false otherwise 34 | */ 35 | function isClaimRevoked(bytes calldata _sig) external view returns (bool); 36 | 37 | /** 38 | * @dev Checks if a claim is valid. 39 | * @param _identity the identity contract related to the claim 40 | * @param claimTopic the claim topic of the claim 41 | * @param sig the signature of the claim 42 | * @param data the data field of the claim 43 | * @return claimValid true if the claim is valid, false otherwise 44 | */ 45 | function isClaimValid( 46 | IIdentity _identity, 47 | uint256 claimTopic, 48 | bytes calldata sig, 49 | bytes calldata data) 50 | external view returns (bool); 51 | } 52 | -------------------------------------------------------------------------------- /contracts/interface/IERC734.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | 4 | /** 5 | * @dev interface of the ERC734 (Key Holder) standard as defined in the EIP. 6 | */ 7 | interface IERC734 { 8 | 9 | /** 10 | * @dev Emitted when an execution request was approved. 11 | * 12 | * Specification: MUST be triggered when approve was successfully called. 13 | */ 14 | event Approved(uint256 indexed executionId, bool approved); 15 | 16 | /** 17 | * @dev Emitted when an execute operation was approved and successfully performed. 18 | * 19 | * Specification: MUST be triggered when approve was called and the execution was successfully approved. 20 | */ 21 | event Executed(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data); 22 | 23 | /** 24 | * @dev Emitted when an execution request was performed via `execute`. 25 | * 26 | * Specification: MUST be triggered when execute was successfully called. 27 | */ 28 | event ExecutionRequested(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data); 29 | 30 | /** 31 | * @dev Emitted when an execute operation was called and failed 32 | * 33 | * Specification: MUST be triggered when execute call failed 34 | */ 35 | event ExecutionFailed(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data); 36 | 37 | /** 38 | * @dev Emitted when a key was added to the Identity. 39 | * 40 | * Specification: MUST be triggered when addKey was successfully called. 41 | */ 42 | event KeyAdded(bytes32 indexed key, uint256 indexed purpose, uint256 indexed keyType); 43 | 44 | /** 45 | * @dev Emitted when a key was removed from the Identity. 46 | * 47 | * Specification: MUST be triggered when removeKey was successfully called. 48 | */ 49 | event KeyRemoved(bytes32 indexed key, uint256 indexed purpose, uint256 indexed keyType); 50 | 51 | /** 52 | * @dev Adds a _key to the identity. The _purpose specifies the purpose of the key. 53 | * 54 | * Triggers Event: `KeyAdded` 55 | * 56 | * Specification: MUST only be done by keys of purpose 1, or the identity 57 | * itself. If it's the identity itself, the approval process will determine its approval. 58 | */ 59 | function addKey(bytes32 _key, uint256 _purpose, uint256 _keyType) external returns (bool success); 60 | 61 | /** 62 | * @dev Approves an execution. 63 | * 64 | * Triggers Event: `Approved` 65 | * Triggers on execution successful Event: `Executed` 66 | * Triggers on execution failure Event: `ExecutionFailed` 67 | */ 68 | function approve(uint256 _id, bool _approve) external returns (bool success); 69 | 70 | /** 71 | * @dev Removes _purpose for _key from the identity. 72 | * 73 | * Triggers Event: `KeyRemoved` 74 | * 75 | * Specification: MUST only be done by keys of purpose 1, or the identity itself. 76 | * If it's the identity itself, the approval process will determine its approval. 77 | */ 78 | function removeKey(bytes32 _key, uint256 _purpose) external returns (bool success); 79 | 80 | /** 81 | * @dev Passes an execution instruction to an ERC734 identity. 82 | * How the execution is handled is up to the identity implementation: 83 | * An execution COULD be requested and require `approve` to be called with one or more keys of purpose 1 or 2 to 84 | * approve this execution. 85 | * Execute COULD be used as the only accessor for `addKey` and `removeKey`. 86 | * 87 | * Triggers Event: ExecutionRequested 88 | * Triggers on direct execution Event: Executed 89 | */ 90 | function execute(address _to, uint256 _value, bytes calldata _data) external payable returns (uint256 executionId); 91 | 92 | /** 93 | * @dev Returns the full key data, if present in the identity. 94 | */ 95 | function getKey(bytes32 _key) external view returns (uint256[] memory purposes, uint256 keyType, bytes32 key); 96 | 97 | /** 98 | * @dev Returns the list of purposes associated with a key. 99 | */ 100 | function getKeyPurposes(bytes32 _key) external view returns(uint256[] memory _purposes); 101 | 102 | /** 103 | * @dev Returns an array of public key bytes32 held by this identity. 104 | */ 105 | function getKeysByPurpose(uint256 _purpose) external view returns (bytes32[] memory keys); 106 | 107 | /** 108 | * @dev Returns TRUE if a key is present and has the given purpose. If the key is not present it returns FALSE. 109 | */ 110 | function keyHasPurpose(bytes32 _key, uint256 _purpose) external view returns (bool exists); 111 | } 112 | -------------------------------------------------------------------------------- /contracts/interface/IERC735.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | 4 | /** 5 | * @dev interface of the ERC735 (Claim Holder) standard as defined in the EIP. 6 | */ 7 | interface IERC735 { 8 | 9 | /** 10 | * @dev Emitted when a claim was added. 11 | * 12 | * Specification: MUST be triggered when a claim was successfully added. 13 | */ 14 | event ClaimAdded( 15 | bytes32 indexed claimId, 16 | uint256 indexed topic, 17 | uint256 scheme, 18 | address indexed issuer, 19 | bytes signature, 20 | bytes data, 21 | string uri); 22 | 23 | /** 24 | * @dev Emitted when a claim was removed. 25 | * 26 | * Specification: MUST be triggered when removeClaim was successfully called. 27 | */ 28 | event ClaimRemoved( 29 | bytes32 indexed claimId, 30 | uint256 indexed topic, 31 | uint256 scheme, 32 | address indexed issuer, 33 | bytes signature, 34 | bytes data, 35 | string uri); 36 | 37 | /** 38 | * @dev Emitted when a claim was changed. 39 | * 40 | * Specification: MUST be triggered when addClaim was successfully called on an existing claimId. 41 | */ 42 | event ClaimChanged( 43 | bytes32 indexed claimId, 44 | uint256 indexed topic, 45 | uint256 scheme, 46 | address indexed issuer, 47 | bytes signature, 48 | bytes data, 49 | string uri); 50 | 51 | /** 52 | * @dev Add or update a claim. 53 | * 54 | * Triggers Event: `ClaimAdded`, `ClaimChanged` 55 | * 56 | * Specification: Add or update a claim from an issuer. 57 | * 58 | * _signature is a signed message of the following structure: 59 | * `keccak256(abi.encode(address identityHolder_address, uint256 topic, bytes data))`. 60 | * Claim IDs are generated using `keccak256(abi.encode(address issuer_address + uint256 topic))`. 61 | */ 62 | function addClaim( 63 | uint256 _topic, 64 | uint256 _scheme, 65 | address issuer, 66 | bytes calldata _signature, 67 | bytes calldata _data, 68 | string calldata _uri) 69 | external returns (bytes32 claimRequestId); 70 | 71 | /** 72 | * @dev Removes a claim. 73 | * 74 | * Triggers Event: `ClaimRemoved` 75 | * 76 | * Claim IDs are generated using `keccak256(abi.encode(address issuer_address, uint256 topic))`. 77 | */ 78 | function removeClaim(bytes32 _claimId) external returns (bool success); 79 | 80 | /** 81 | * @dev Get a claim by its ID. 82 | * 83 | * Claim IDs are generated using `keccak256(abi.encode(address issuer_address, uint256 topic))`. 84 | */ 85 | function getClaim(bytes32 _claimId) 86 | external view returns( 87 | uint256 topic, 88 | uint256 scheme, 89 | address issuer, 90 | bytes memory signature, 91 | bytes memory data, 92 | string memory uri); 93 | 94 | /** 95 | * @dev Returns an array of claim IDs by topic. 96 | */ 97 | function getClaimIdsByTopic(uint256 _topic) external view returns(bytes32[] memory claimIds); 98 | } 99 | -------------------------------------------------------------------------------- /contracts/interface/IIdentity.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | 4 | import "./IERC734.sol"; 5 | import "./IERC735.sol"; 6 | 7 | // solhint-disable-next-line no-empty-blocks 8 | interface IIdentity is IERC734, IERC735 { 9 | /** 10 | * @dev Checks if a claim is valid. 11 | * @param _identity the identity contract related to the claim 12 | * @param claimTopic the claim topic of the claim 13 | * @param sig the signature of the claim 14 | * @param data the data field of the claim 15 | * @return claimValid true if the claim is valid, false otherwise 16 | */ 17 | function isClaimValid( 18 | IIdentity _identity, 19 | uint256 claimTopic, 20 | bytes calldata sig, 21 | bytes calldata data) 22 | external view returns (bool); 23 | } 24 | -------------------------------------------------------------------------------- /contracts/interface/IImplementationAuthority.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.17; 4 | 5 | interface IImplementationAuthority { 6 | 7 | // event emitted when the implementation contract is updated 8 | event UpdatedImplementation(address newAddress); 9 | 10 | /** 11 | * @dev updates the address used as implementation by the proxies linked 12 | * to this ImplementationAuthority contract 13 | * @param _newImplementation the address of the new implementation contract 14 | * only Owner can call 15 | */ 16 | function updateImplementation(address _newImplementation) external; 17 | 18 | /** 19 | * @dev returns the address of the implementation 20 | */ 21 | function getImplementation() external view returns(address); 22 | } 23 | -------------------------------------------------------------------------------- /contracts/proxy/IdentityProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.17; 4 | 5 | import "../interface/IImplementationAuthority.sol"; 6 | 7 | contract IdentityProxy { 8 | 9 | /** 10 | * @dev constructor of the proxy Identity contract 11 | * @param _implementationAuthority the implementation Authority contract address 12 | * @param initialManagementKey the management key at deployment 13 | * the proxy is going to use the logic deployed on the implementation contract 14 | * deployed at an address listed in the ImplementationAuthority contract 15 | */ 16 | constructor(address _implementationAuthority, address initialManagementKey) { 17 | require(_implementationAuthority != address(0), "invalid argument - zero address"); 18 | require(initialManagementKey != address(0), "invalid argument - zero address"); 19 | 20 | // solhint-disable-next-line no-inline-assembly 21 | assembly { 22 | sstore(0x821f3e4d3d679f19eacc940c87acf846ea6eae24a63058ea750304437a62aafc, _implementationAuthority) 23 | } 24 | 25 | address logic = IImplementationAuthority(_implementationAuthority).getImplementation(); 26 | 27 | // solhint-disable-next-line avoid-low-level-calls 28 | (bool success,) = logic.delegatecall(abi.encodeWithSignature("initialize(address)", initialManagementKey)); 29 | require(success, "Initialization failed."); 30 | } 31 | 32 | /** 33 | * @dev fallback proxy function used for any transaction call that is made using 34 | * the Identity contract ABI and called on the proxy contract 35 | * The proxy will update its local storage depending on the behaviour requested 36 | * by the implementation contract given by the Implementation Authority 37 | */ 38 | // solhint-disable-next-line no-complex-fallback 39 | fallback() external payable { 40 | address logic = IImplementationAuthority(implementationAuthority()).getImplementation(); 41 | 42 | // solhint-disable-next-line no-inline-assembly 43 | assembly { 44 | calldatacopy(0x0, 0x0, calldatasize()) 45 | let success := delegatecall(sub(gas(), 10000), logic, 0x0, calldatasize(), 0, 0) 46 | let retSz := returndatasize() 47 | returndatacopy(0, 0, retSz) 48 | switch success 49 | case 0 { 50 | revert(0, retSz) 51 | } 52 | default { 53 | return(0, retSz) 54 | } 55 | } 56 | } 57 | 58 | function implementationAuthority() public view returns(address) { 59 | address implemAuth; 60 | // solhint-disable-next-line no-inline-assembly 61 | assembly { 62 | implemAuth := sload(0x821f3e4d3d679f19eacc940c87acf846ea6eae24a63058ea750304437a62aafc) 63 | } 64 | return implemAuth; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /contracts/proxy/ImplementationAuthority.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.17; 4 | 5 | import "../interface/IImplementationAuthority.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | contract ImplementationAuthority is IImplementationAuthority, Ownable { 9 | 10 | // the address of implementation of ONCHAINID 11 | address internal _implementation; 12 | 13 | constructor(address implementation) { 14 | require(implementation != address(0), "invalid argument - zero address"); 15 | _implementation = implementation; 16 | emit UpdatedImplementation(implementation); 17 | } 18 | 19 | /** 20 | * @dev See {IImplementationAuthority-updateImplementation}. 21 | */ 22 | function updateImplementation(address _newImplementation) external override onlyOwner { 23 | require(_newImplementation != address(0), "invalid argument - zero address"); 24 | _implementation = _newImplementation; 25 | emit UpdatedImplementation(_newImplementation); 26 | } 27 | 28 | /** 29 | * @dev See {IImplementationAuthority-getImplementation}. 30 | */ 31 | function getImplementation() external override view returns(address) { 32 | return _implementation; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contracts/storage/Storage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | import "./Structs.sol"; 4 | 5 | contract Storage is Structs { 6 | // nonce used by the execute/approve function 7 | uint256 internal _executionNonce; 8 | 9 | // keys as defined by IERC734 10 | mapping(bytes32 => Key) internal _keys; 11 | 12 | // keys for a given purpose 13 | // purpose 1 = MANAGEMENT 14 | // purpose 2 = ACTION 15 | // purpose 3 = CLAIM 16 | mapping(uint256 => bytes32[]) internal _keysByPurpose; 17 | 18 | // execution data 19 | mapping(uint256 => Execution) internal _executions; 20 | 21 | // claims held by the ONCHAINID 22 | mapping(bytes32 => Claim) internal _claims; 23 | 24 | // array of claims for a given topic 25 | mapping(uint256 => bytes32[]) internal _claimsByTopic; 26 | 27 | // status on initialization 28 | bool internal _initialized = false; 29 | 30 | // status on potential interactions with the contract 31 | bool internal _canInteract = false; 32 | 33 | /** 34 | * @dev This empty reserved space is put in place to allow future versions to add new 35 | * variables without shifting down storage in the inheritance chain. 36 | */ 37 | uint256[49] private __gap; 38 | } 39 | -------------------------------------------------------------------------------- /contracts/storage/Structs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity 0.8.17; 3 | 4 | contract Structs { 5 | 6 | /** 7 | * @dev Definition of the structure of a Key. 8 | * 9 | * Specification: Keys are cryptographic public keys, or contract addresses associated with this identity. 10 | * The structure should be as follows: 11 | * key: A public key owned by this identity 12 | * purposes: uint256[] Array of the key purposes, like 1 = MANAGEMENT, 2 = EXECUTION 13 | * keyType: The type of key used, which would be a uint256 for different key types. e.g. 1 = ECDSA, 2 = RSA, etc. 14 | * key: bytes32 The public key. // Its the Keccak256 hash of the key 15 | */ 16 | struct Key { 17 | uint256[] purposes; 18 | uint256 keyType; 19 | bytes32 key; 20 | } 21 | 22 | /** 23 | * @dev Definition of the structure of an Execution 24 | * 25 | * Specification: Executions are requests for transactions to be issued by the ONCHAINID 26 | * to: address of contract to interact with, can be address(this) 27 | * value: ETH to transfer with the transaction 28 | * data: payload of the transaction to execute 29 | * approved: approval status of the Execution 30 | * executed: execution status of the Execution (set as false when the Execution is created 31 | * and updated to true when the Execution is processed) 32 | */ 33 | struct Execution { 34 | address to; 35 | uint256 value; 36 | bytes data; 37 | bool approved; 38 | bool executed; 39 | } 40 | 41 | /** 42 | * @dev Definition of the structure of a Claim. 43 | * 44 | * Specification: Claims are information an issuer has about the identity holder. 45 | * The structure should be as follows: 46 | * claim: A claim published for the Identity. 47 | * topic: A uint256 number which represents the topic of the claim. (e.g. 1 biometric, 2 residence (ToBeDefined: 48 | * number schemes, sub topics based on number ranges??)) 49 | * scheme : The scheme with which this claim SHOULD be verified or how it should be processed. Its a uint256 for 50 | * different schemes. E.g. could 3 mean contract verification, where the data will be call data, and the issuer a 51 | * contract address to call (ToBeDefined). Those can also mean different key types e.g. 1 = ECDSA, 2 = RSA, etc. 52 | * (ToBeDefined) 53 | * issuer: The issuers identity contract address, or the address used to sign the above signature. If an 54 | * identity contract, it should hold the key with which the above message was signed, if the key is not present 55 | * anymore, the claim SHOULD be treated as invalid. The issuer can also be a contract address itself, at which the 56 | * claim can be verified using the call data. 57 | * signature: Signature which is the proof that the claim issuer issued a claim of topic for this identity. it 58 | * MUST be a signed message of the following structure: `keccak256(abi.encode(identityHolder_address, topic, data))` 59 | * data: The hash of the claim data, sitting in another location, a bit-mask, call data, or actual data based on 60 | * the claim scheme. 61 | * uri: The location of the claim, this can be HTTP links, swarm hashes, IPFS hashes, and such. 62 | */ 63 | struct Claim { 64 | uint256 topic; 65 | uint256 scheme; 66 | address issuer; 67 | bytes signature; 68 | bytes data; 69 | string uri; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /contracts/verifiers/Verifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.17; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "../interface/IClaimIssuer.sol"; 7 | 8 | contract Verifier is Ownable { 9 | /// @dev All topics of claims required to pass verification. 10 | uint256[] public requiredClaimTopics; 11 | 12 | /// @dev Array containing all TrustedIssuers identity contract address allowed to issue claims required. 13 | IClaimIssuer[] public trustedIssuers; 14 | 15 | /// @dev Mapping between a trusted issuer address and the topics of claims they are trusted for. 16 | mapping(address => uint256[]) public trustedIssuerClaimTopics; 17 | 18 | /// @dev Mapping between a claim topic and the trusted issuers trusted for it. 19 | mapping(uint256 => IClaimIssuer[]) public claimTopicsToTrustedIssuers; 20 | 21 | /** 22 | * this event is emitted when a claim topic has been added to the requirement list 23 | * the event is emitted by the 'addClaimTopic' function 24 | * `claimTopic` is the required claim topic added 25 | */ 26 | event ClaimTopicAdded(uint256 indexed claimTopic); 27 | 28 | /** 29 | * this event is emitted when a claim topic has been removed from the requirement list 30 | * the event is emitted by the 'removeClaimTopic' function 31 | * `claimTopic` is the required claim removed 32 | */ 33 | event ClaimTopicRemoved(uint256 indexed claimTopic); 34 | 35 | /** 36 | * this event is emitted when an issuer is added to the trusted list. 37 | * the event is emitted by the addTrustedIssuer function 38 | * `trustedIssuer` is the address of the trusted issuer's ClaimIssuer contract 39 | * `claimTopics` is the set of claims that the trusted issuer is allowed to emit 40 | */ 41 | event TrustedIssuerAdded(IClaimIssuer indexed trustedIssuer, uint256[] claimTopics); 42 | 43 | /** 44 | * this event is emitted when an issuer is removed from the trusted list. 45 | * the event is emitted by the removeTrustedIssuer function 46 | * `trustedIssuer` is the address of the trusted issuer's ClaimIssuer contract 47 | */ 48 | event TrustedIssuerRemoved(IClaimIssuer indexed trustedIssuer); 49 | 50 | /** 51 | * this event is emitted when the set of claim topics is changed for a given trusted issuer. 52 | * the event is emitted by the updateIssuerClaimTopics function 53 | * `trustedIssuer` is the address of the trusted issuer's ClaimIssuer contract 54 | * `claimTopics` is the set of claims that the trusted issuer is allowed to emit 55 | */ 56 | event ClaimTopicsUpdated(IClaimIssuer indexed trustedIssuer, uint256[] claimTopics); 57 | 58 | modifier onlyVerifiedSender() { 59 | require(verify(_msgSender()), "sender is not verified"); 60 | _; 61 | } 62 | 63 | /** 64 | * @dev See {IClaimTopicsRegistry-removeClaimTopic}. 65 | */ 66 | function addClaimTopic(uint256 claimTopic) public onlyOwner { 67 | uint256 length = requiredClaimTopics.length; 68 | require(length < 15, "cannot require more than 15 topics"); 69 | for (uint256 i = 0; i < length; i++) { 70 | require(requiredClaimTopics[i] != claimTopic, "claimTopic already exists"); 71 | } 72 | requiredClaimTopics.push(claimTopic); 73 | emit ClaimTopicAdded(claimTopic); 74 | } 75 | 76 | /** 77 | * @dev See {IClaimTopicsRegistry-getClaimTopics}. 78 | */ 79 | function removeClaimTopic(uint256 claimTopic) public onlyOwner { 80 | uint256 length = requiredClaimTopics.length; 81 | for (uint256 i = 0; i < length; i++) { 82 | if (requiredClaimTopics[i] == claimTopic) { 83 | requiredClaimTopics[i] = requiredClaimTopics[length - 1]; 84 | requiredClaimTopics.pop(); 85 | emit ClaimTopicRemoved(claimTopic); 86 | break; 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * @dev See {ITrustedIssuersRegistry-addTrustedIssuer}. 93 | */ 94 | function addTrustedIssuer(IClaimIssuer trustedIssuer, uint256[] calldata claimTopics) public onlyOwner { 95 | require(address(trustedIssuer) != address(0), "invalid argument - zero address"); 96 | require(trustedIssuerClaimTopics[address(trustedIssuer)].length == 0, "trusted Issuer already exists"); 97 | require(claimTopics.length > 0, "trusted claim topics cannot be empty"); 98 | require(claimTopics.length <= 15, "cannot have more than 15 claim topics"); 99 | require(trustedIssuers.length < 50, "cannot have more than 50 trusted issuers"); 100 | trustedIssuers.push(trustedIssuer); 101 | trustedIssuerClaimTopics[address(trustedIssuer)] = claimTopics; 102 | for (uint256 i = 0; i < claimTopics.length; i++) { 103 | claimTopicsToTrustedIssuers[claimTopics[i]].push(trustedIssuer); 104 | } 105 | emit TrustedIssuerAdded(trustedIssuer, claimTopics); 106 | } 107 | 108 | /** 109 | * @dev See {ITrustedIssuersRegistry-removeTrustedIssuer}. 110 | */ 111 | function removeTrustedIssuer(IClaimIssuer trustedIssuer) public onlyOwner { 112 | require(address(trustedIssuer) != address(0), "invalid argument - zero address"); 113 | require(trustedIssuerClaimTopics[address(trustedIssuer)].length != 0, "NOT a trusted issuer"); 114 | uint256 length = trustedIssuers.length; 115 | for (uint256 i = 0; i < length; i++) { 116 | if (trustedIssuers[i] == trustedIssuer) { 117 | trustedIssuers[i] = trustedIssuers[length - 1]; 118 | trustedIssuers.pop(); 119 | break; 120 | } 121 | } 122 | for ( 123 | uint256 claimTopicIndex = 0; 124 | claimTopicIndex < trustedIssuerClaimTopics[address(trustedIssuer)].length; 125 | claimTopicIndex++) { 126 | uint256 claimTopic = trustedIssuerClaimTopics[address(trustedIssuer)][claimTopicIndex]; 127 | uint256 topicsLength = claimTopicsToTrustedIssuers[claimTopic].length; 128 | for (uint256 i = 0; i < topicsLength; i++) { 129 | if (claimTopicsToTrustedIssuers[claimTopic][i] == trustedIssuer) { 130 | claimTopicsToTrustedIssuers[claimTopic][i] = 131 | claimTopicsToTrustedIssuers[claimTopic][topicsLength - 1]; 132 | claimTopicsToTrustedIssuers[claimTopic].pop(); 133 | break; 134 | } 135 | } 136 | } 137 | delete trustedIssuerClaimTopics[address(trustedIssuer)]; 138 | emit TrustedIssuerRemoved(trustedIssuer); 139 | } 140 | 141 | /** 142 | * @dev See {ITrustedIssuersRegistry-updateIssuerClaimTopics}. 143 | */ 144 | function updateIssuerClaimTopics(IClaimIssuer trustedIssuer, uint256[] calldata newClaimTopics) public onlyOwner { 145 | require(address(trustedIssuer) != address(0), "invalid argument - zero address"); 146 | require(trustedIssuerClaimTopics[address(trustedIssuer)].length != 0, "NOT a trusted issuer"); 147 | require(newClaimTopics.length <= 15, "cannot have more than 15 claim topics"); 148 | require(newClaimTopics.length > 0, "claim topics cannot be empty"); 149 | 150 | for (uint256 i = 0; i < trustedIssuerClaimTopics[address(trustedIssuer)].length; i++) { 151 | uint256 claimTopic = trustedIssuerClaimTopics[address(trustedIssuer)][i]; 152 | uint256 topicsLength = claimTopicsToTrustedIssuers[claimTopic].length; 153 | for (uint256 j = 0; j < topicsLength; j++) { 154 | if (claimTopicsToTrustedIssuers[claimTopic][j] == trustedIssuer) { 155 | claimTopicsToTrustedIssuers[claimTopic][j] = 156 | claimTopicsToTrustedIssuers[claimTopic][topicsLength - 1]; 157 | claimTopicsToTrustedIssuers[claimTopic].pop(); 158 | break; 159 | } 160 | } 161 | } 162 | trustedIssuerClaimTopics[address(trustedIssuer)] = newClaimTopics; 163 | for (uint256 i = 0; i < newClaimTopics.length; i++) { 164 | claimTopicsToTrustedIssuers[newClaimTopics[i]].push(trustedIssuer); 165 | } 166 | emit ClaimTopicsUpdated(trustedIssuer, newClaimTopics); 167 | } 168 | 169 | /** 170 | * @dev See {ITrustedIssuersRegistry-getTrustedIssuers}. 171 | */ 172 | function getTrustedIssuers() public view returns (IClaimIssuer[] memory) { 173 | return trustedIssuers; 174 | } 175 | 176 | /** 177 | * @dev See {ITrustedIssuersRegistry-getTrustedIssuersForClaimTopic}. 178 | */ 179 | function getTrustedIssuersForClaimTopic(uint256 claimTopic) public view returns (IClaimIssuer[] memory) { 180 | return claimTopicsToTrustedIssuers[claimTopic]; 181 | } 182 | 183 | /** 184 | * @dev See {ITrustedIssuersRegistry-isTrustedIssuer}. 185 | */ 186 | function isTrustedIssuer(address issuer) public view returns (bool) { 187 | if(trustedIssuerClaimTopics[issuer].length > 0) { 188 | return true; 189 | } 190 | return false; 191 | } 192 | 193 | /** 194 | * @dev See {ITrustedIssuersRegistry-getTrustedIssuerClaimTopics}. 195 | */ 196 | function getTrustedIssuerClaimTopics(IClaimIssuer trustedIssuer) public view returns (uint256[] memory) { 197 | require(trustedIssuerClaimTopics[address(trustedIssuer)].length != 0, "trusted Issuer doesn\'t exist"); 198 | return trustedIssuerClaimTopics[address(trustedIssuer)]; 199 | } 200 | 201 | /** 202 | * @dev See {ITrustedIssuersRegistry-hasClaimTopic}. 203 | */ 204 | function hasClaimTopic(address issuer, uint256 claimTopic) public view returns (bool) { 205 | uint256[] memory claimTopics = trustedIssuerClaimTopics[issuer]; 206 | uint256 length = claimTopics.length; 207 | for (uint256 i = 0; i < length; i++) { 208 | if (claimTopics[i] == claimTopic) { 209 | return true; 210 | } 211 | } 212 | return false; 213 | } 214 | 215 | function isClaimTopicRequired(uint256 claimTopic) public view returns (bool) { 216 | uint256 length = requiredClaimTopics.length; 217 | 218 | for (uint256 i = 0; i < length; i++) { 219 | if (requiredClaimTopics[i] == claimTopic) { 220 | return true; 221 | } 222 | } 223 | 224 | return false; 225 | } 226 | 227 | /** 228 | * @dev Verify an identity (ONCHAINID) by checking if the identity has at least one valid claim from a trusted 229 | * issuer for each required claim topic. Returns true if the identity is compliant, false otherwise. 230 | */ 231 | function verify(address identity) public view returns(bool isVerified) { 232 | if (requiredClaimTopics.length == 0) { 233 | return true; 234 | } 235 | 236 | uint256 foundClaimTopic; 237 | uint256 scheme; 238 | address issuer; 239 | bytes memory sig; 240 | bytes memory data; 241 | uint256 claimTopic; 242 | for (claimTopic = 0; claimTopic < requiredClaimTopics.length; claimTopic++) { 243 | IClaimIssuer[] memory trustedIssuersForClaimTopic = 244 | this.getTrustedIssuersForClaimTopic(requiredClaimTopics[claimTopic]); 245 | 246 | if (trustedIssuersForClaimTopic.length == 0) { 247 | return false; 248 | } 249 | 250 | bytes32[] memory claimIds = new bytes32[](trustedIssuersForClaimTopic.length); 251 | for (uint256 i = 0; i < trustedIssuersForClaimTopic.length; i++) { 252 | claimIds[i] = keccak256(abi.encode(trustedIssuersForClaimTopic[i], requiredClaimTopics[claimTopic])); 253 | } 254 | 255 | for (uint256 j = 0; j < claimIds.length; j++) { 256 | (foundClaimTopic, scheme, issuer, sig, data, ) = IIdentity(identity).getClaim(claimIds[j]); 257 | 258 | if (foundClaimTopic == requiredClaimTopics[claimTopic]) { 259 | try IClaimIssuer(issuer).isClaimValid(IIdentity(identity), requiredClaimTopics[claimTopic], sig, 260 | data) returns(bool _validity) { 261 | 262 | if ( 263 | _validity 264 | ) { 265 | j = claimIds.length; 266 | } 267 | if (!_validity && j == (claimIds.length - 1)) { 268 | return false; 269 | } 270 | } catch { 271 | if (j == (claimIds.length - 1)) { 272 | return false; 273 | } 274 | } 275 | } else if (j == (claimIds.length - 1)) { 276 | return false; 277 | } 278 | } 279 | } 280 | 281 | return true; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /contracts/version/Version.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | 3 | pragma solidity 0.8.17; 4 | 5 | /** 6 | * @dev Version contract gives the versioning information of the implementation contract 7 | */ 8 | contract Version { 9 | /** 10 | * @dev Returns the string of the current version. 11 | */ 12 | function version() external pure returns (string memory) { 13 | // version 2.2.0 14 | return "2.2.1"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "@nomicfoundation/hardhat-toolbox"; 2 | import { HardhatUserConfig } from "hardhat/config"; 3 | import 'solidity-coverage'; 4 | import "@nomiclabs/hardhat-solhint"; 5 | 6 | import "./tasks/add-claim.task"; 7 | import "./tasks/add-key.task"; 8 | import "./tasks/deploy-identity.task"; 9 | import "./tasks/deploy-proxy.task"; 10 | import "./tasks/remove-claim.task"; 11 | import "./tasks/remove-key.task"; 12 | import "./tasks/revoke.task"; 13 | 14 | const config: HardhatUserConfig = { 15 | solidity: "0.8.17", 16 | networks: { 17 | mumbai: { 18 | url: 'https://rpc-mumbai.maticvigil.com/v1/9cd3d6ce21f0a25bb8f33504a1820d616f700d24', 19 | accounts: ["1d79b7c95d2456a55f55a0e17f856412637fa6b3c332fa557ce2c8a89139ec74"], 20 | } 21 | } 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export namespace contracts { 2 | export const ClaimIssuer: any; 3 | export const Gateway: any; 4 | export const Identity: any; 5 | export const ImplementationAuthority: any; 6 | export const IdentityProxy: any; 7 | export const Factory: any; 8 | export const Verifier: any; 9 | } 10 | 11 | export namespace interfaces { 12 | export const IClaimIssuer: any; 13 | export const IERC734: any; 14 | export const IERC735: any; 15 | export const IIdentity: any; 16 | export const IImplementationAuthority: any; 17 | export const IFactory: any; 18 | } 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const IClaimIssuer = require('./artifacts/contracts/interface/IClaimIssuer.sol/IClaimIssuer.json'); 2 | const IERC734 = require('./artifacts/contracts/interface/IERC734.sol/IERC734.json'); 3 | const IERC735 = require('./artifacts/contracts/interface/IERC735.sol/IERC735.json'); 4 | const IFactory = require('./artifacts/contracts/factory/IIdFactory.sol/IIdFactory.json'); 5 | const IIdentity = require('./artifacts/contracts/interface/IIdentity.sol/IIdentity.json'); 6 | const IImplementationAuthority = require('./artifacts/contracts/interface/IImplementationAuthority.sol/IImplementationAuthority.json'); 7 | 8 | const ClaimIssuer = require('./artifacts/contracts/ClaimIssuer.sol/ClaimIssuer.json'); 9 | const Factory = require('./artifacts/contracts/factory/IdFactory.sol/IdFactory.json'); 10 | const Gateway = require('./artifacts/contracts/gateway/Gateway.sol/Gateway.json'); 11 | const Identity = require('./artifacts/contracts/Identity.sol/Identity.json'); 12 | const ImplementationAuthority = require('./artifacts/contracts/proxy/ImplementationAuthority.sol/ImplementationAuthority.json'); 13 | const IdentityProxy = require('./artifacts/contracts/proxy/IdentityProxy.sol/IdentityProxy.json'); 14 | const Verifier = require('./artifacts/contracts/verifiers/Verifier.sol/Verifier.json'); 15 | const Version = require('./artifacts/contracts/version/Version.sol/Version.json'); 16 | 17 | module.exports = { 18 | contracts: { 19 | ClaimIssuer, 20 | Factory, 21 | Gateway, 22 | Identity, 23 | ImplementationAuthority, 24 | IdentityProxy, 25 | Version, 26 | Verifier, 27 | }, 28 | interfaces: { 29 | IClaimIssuer, 30 | IERC734, 31 | IERC735, 32 | IIdentity, 33 | IImplementationAuthority, 34 | IFactory, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /onchainid_logo_final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onchain-id/solidity/a483cda6821e3883497972d36d0c4c1e1693e483/onchainid_logo_final.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@onchain-id/solidity", 3 | "version": "2.2.1", 4 | "description": "EVM solidity smart contracts for Blockchain OnchainID identities.", 5 | "files": [ 6 | "artifacts", 7 | "contracts", 8 | "index.js", 9 | "index.d.ts", 10 | "typechain-types" 11 | ], 12 | "scripts": { 13 | "build": "npx hardhat compile", 14 | "coverage": "npx hardhat coverage", 15 | "lint": "npx hardhat check", 16 | "test": "hardhat test" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/onchain-id/solidity.git" 21 | }, 22 | "keywords": [ 23 | "onchainid", 24 | "identity", 25 | "erc734", 26 | "erc735", 27 | "ethereum", 28 | "solidity", 29 | "smart contracts" 30 | ], 31 | "author": "OnchainID Organization", 32 | "license": "ISC", 33 | "bugs": { 34 | "url": "https://github.com/onchain-id/solidity/issues" 35 | }, 36 | "homepage": "https://github.com/onchain-id/solidity#readme", 37 | "devDependencies": { 38 | "@nomicfoundation/hardhat-toolbox": "^2.0.1", 39 | "@nomiclabs/hardhat-solhint": "^3.0.0", 40 | "@openzeppelin/contracts": "^4.8.3", 41 | "hardhat": "^2.12.6", 42 | "solhint-plugin-prettier": "^0.0.5", 43 | "solidity-coverage": "^0.8.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scripts/deploy-claim-issuer.ts: -------------------------------------------------------------------------------- 1 | import {ethers} from "hardhat"; 2 | 3 | async function main() { 4 | const [claimIssuerOwner] = await ethers.getSigners(); 5 | 6 | const claimIssuer = await ethers.deployContract("ClaimIssuer", [claimIssuerOwner.address]); 7 | 8 | console.log(`Deploying Claim Issuer at ${claimIssuer.address} ...`); 9 | 10 | await claimIssuer.deployed(); 11 | 12 | console.log(`Deployed Claim Issuer ${claimIssuer.address} !`); 13 | } 14 | 15 | main().catch((error) => { 16 | console.error(error); 17 | process.exitCode = 1; 18 | }); 19 | -------------------------------------------------------------------------------- /scripts/deploy-factory.ts: -------------------------------------------------------------------------------- 1 | import {ethers} from "hardhat"; 2 | 3 | async function main() { 4 | const [deployer] = await ethers.getSigners(); 5 | 6 | const identityImplementation = await ethers.deployContract("Identity", [deployer.address, true]); 7 | console.log(`Deploying identity implementation at ${identityImplementation.address} ... (tx hash: ${identityImplementation.deployTransaction.hash} )`); 8 | await identityImplementation.deployed(); 9 | console.log(`Deployed identity implementation at ${identityImplementation.address} (tx hash: ${identityImplementation.deployTransaction.hash} )`); 10 | 11 | const implementationAuthority = await ethers.deployContract("ImplementationAuthority", [identityImplementation.address]); 12 | console.log(`Deploying implementation authority at ${implementationAuthority.address} ... (tx hash: ${implementationAuthority.deployTransaction.hash} )`); 13 | await implementationAuthority.deployed(); 14 | console.log(`Deployed implementation authority at ${implementationAuthority.address} (tx hash: ${implementationAuthority.deployTransaction.hash} )`); 15 | 16 | const factory = await ethers.deployContract("IdFactory", [implementationAuthority.address]); 17 | console.log(`Deploying factory at ${factory.address} ... (tx hash: ${factory.deployTransaction.hash} )`); 18 | await factory.deployed(); 19 | console.log(`Deployed factory at ${factory.address} (tx hash: ${factory.deployTransaction.hash} )`); 20 | } 21 | 22 | main().catch((error) => { 23 | console.error(error); 24 | process.exitCode = 1; 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/deploy-identity.ts: -------------------------------------------------------------------------------- 1 | import {ethers} from "hardhat"; 2 | 3 | async function main() { 4 | const [identityOwner] = await ethers.getSigners(); 5 | 6 | const Identity = await ethers.getContractFactory("Identity"); 7 | const identity = await Identity.connect(identityOwner).deploy(identityOwner.address, false); 8 | 9 | console.log(`Deploying identity for ${identityOwner.address} at ${identity.address} ...`); 10 | 11 | await identity.deployed(); 12 | 13 | console.log(`Deployed identity for ${identityOwner.address} at ${identity.address} !`); 14 | } 15 | 16 | main().catch((error) => { 17 | console.error(error); 18 | process.exitCode = 1; 19 | }); 20 | -------------------------------------------------------------------------------- /tasks/add-claim.task.ts: -------------------------------------------------------------------------------- 1 | import {task} from "hardhat/config"; 2 | import {TaskArguments} from "hardhat/types"; 3 | 4 | task("add-claim", "Add a claim to an identity") 5 | .addParam("identity", "The address of the identity") 6 | .addParam("from", "A CLAIM key on the claim issuer") 7 | .addParam("claim", "The content of a claim as a JSON string") 8 | .setAction(async (args: TaskArguments, hre) => { 9 | const signer = await hre.ethers.getSigner(args.from); 10 | 11 | const identity = await hre.ethers.getContractAt('Identity', args.identity, signer); 12 | 13 | const claim = JSON.parse(args.claim); 14 | 15 | console.log(claim); 16 | 17 | const tx = await identity.addClaim( 18 | claim.topic, 19 | claim.scheme, 20 | claim.issuer, 21 | claim.signature, 22 | claim.data, 23 | claim.uri, 24 | ); 25 | 26 | console.log(`Add claim of topic ${claim.topic} on identity ${args.identity} tx: ${tx.hash}`); 27 | 28 | await tx.wait(); 29 | 30 | console.log(`Add claim of topic ${claim.topic} on identity ${args.identity} tx mined: ${tx.hash}`); 31 | }); 32 | -------------------------------------------------------------------------------- /tasks/add-key.task.ts: -------------------------------------------------------------------------------- 1 | import {task} from "hardhat/config"; 2 | import {TaskArguments} from "hardhat/types"; 3 | 4 | task("add-key", "Add a purpose to a key on an identity") 5 | .addParam("from", "A MANAGEMENT key on the claim issuer") 6 | .addParam("identity", "The address of the identity") 7 | .addParam("key", "The key (ethereum address)") 8 | .addParam("type", "The type of the key (ECDSA = 1)") 9 | .addParam("purpose", "The purpose to add (MANAGEMENT = 1, ACTION = 2, CLAIM = 3)") 10 | .setAction(async (args: TaskArguments, hre) => { 11 | const signer = await hre.ethers.getSigner(args.from); 12 | 13 | const identity = await hre.ethers.getContractAt('Identity', args.identity, signer); 14 | 15 | const keyHash = hre.ethers.utils.keccak256( 16 | hre.ethers.utils.defaultAbiCoder.encode(['address'], [args.key]), 17 | ); 18 | 19 | const tx = await identity.addKey( 20 | keyHash, 21 | args.purpose, 22 | args.type, 23 | ); 24 | 25 | console.log(`Add purpose ${args.purpose} to key ${args.key} (hash: ${keyHash}) on identity ${args.identity} tx: ${tx.hash}`); 26 | 27 | await tx.wait(); 28 | 29 | console.log(`Add purpose ${args.purpose} to key ${args.key} (hash: ${keyHash}) on identity ${args.identity} tx mined: ${tx.hash}`); 30 | }); 31 | -------------------------------------------------------------------------------- /tasks/deploy-identity.task.ts: -------------------------------------------------------------------------------- 1 | import {task} from "hardhat/config"; 2 | import {TaskArguments} from "hardhat/types"; 3 | 4 | task("deploy-identity", "Deploy an identity as a standalone contract") 5 | .addParam("from", "Will pay the gas for the transaction") 6 | .addParam("key", "The ethereum address that will own the identity (as a MANAGEMENT key)") 7 | .setAction(async (args: TaskArguments, hre) => { 8 | const signer = await hre.ethers.getSigner(args.from); 9 | 10 | const identity = await hre.ethers.deployContract('Identity', [args.key, false], signer); 11 | 12 | console.log(`Deploy a new identity at ${identity.address} . tx: ${identity.deployTransaction.hash}`); 13 | 14 | await identity.deployed(); 15 | 16 | console.log(`Deployed a new identity at ${identity.address} . tx: ${identity.deployTransaction.hash}`); 17 | }); 18 | -------------------------------------------------------------------------------- /tasks/deploy-proxy.task.ts: -------------------------------------------------------------------------------- 1 | import {task} from "hardhat/config"; 2 | import {TaskArguments} from "hardhat/types"; 3 | 4 | task("deploy-proxy", "Deploy an identity as a proxy using a factory") 5 | .addParam("from", "Will pay the gas for the transaction") 6 | .addParam("factory", "The address of the identity factory") 7 | .addParam("key", "The ethereum address that will own the identity (as a MANAGEMENT key)") 8 | .addOptionalParam("salt", "A salt to use when creating the identity") 9 | .setAction(async (args: TaskArguments, hre) => { 10 | const signer = await hre.ethers.getSigner(args.from); 11 | 12 | const factory = await hre.ethers.getContractAt('IdFactory', args.factory, signer); 13 | const tx = await factory.createIdentity(args.key, args.salt ?? args.key); 14 | 15 | console.log(`Deploy a new identity as a proxy using factory ${factory.address} . tx: ${tx.hash}`); 16 | 17 | await tx.wait(); 18 | 19 | const identityAddress = await factory.getIdentity(args.key); 20 | 21 | console.log(`Deployed a new identity at ${identityAddress} as a proxy using factory ${factory.address} . tx: ${tx.hash}`); 22 | }); 23 | -------------------------------------------------------------------------------- /tasks/remove-claim.task.ts: -------------------------------------------------------------------------------- 1 | import {task} from "hardhat/config"; 2 | import {TaskArguments} from "hardhat/types"; 3 | 4 | task("remove-claim", "Remove a cliam from an identity") 5 | .addParam("identity", "The address of the identity") 6 | .addParam("from", "A CLAIM key on the claim issuer") 7 | .addParam("claim", "The claim ID") 8 | .setAction(async (args: TaskArguments, hre) => { 9 | const signer = await hre.ethers.getSigner(args.from); 10 | 11 | const identity = await hre.ethers.getContractAt('Identity', args.identity, signer); 12 | 13 | const tx = await identity.removeClaim(args.claim); 14 | 15 | console.log(`Remove claim ${args.claim} from identity ${args.identity} tx: ${tx.hash}`); 16 | 17 | await tx.wait(); 18 | 19 | console.log(`Remove claim ${args.claim} from identity ${args.identity} tx mined: ${tx.hash}`); 20 | }); 21 | -------------------------------------------------------------------------------- /tasks/remove-key.task.ts: -------------------------------------------------------------------------------- 1 | import {task} from "hardhat/config"; 2 | import {TaskArguments} from "hardhat/types"; 3 | 4 | task("remove-key", "Remove a purpose from a key on an identity") 5 | .addParam("from", "A MANAGEMENT key on the claim issuer") 6 | .addParam("identity", "The address of the identity") 7 | .addParam("key", "The key (ethereum address)") 8 | .addParam("type", "The type of the key (ECDSA = 1)") 9 | .addParam("purpose", "The purpose to remove (MANAGEMENT = 1, ACTION = 2, CLAIM = 3)") 10 | .setAction(async (args: TaskArguments, hre) => { 11 | const signer = await hre.ethers.getSigner(args.from); 12 | 13 | const identity = await hre.ethers.getContractAt('Identity', args.identity, signer); 14 | 15 | const keyHash = hre.ethers.utils.keccak256( 16 | hre.ethers.utils.defaultAbiCoder.encode(['address'], [args.key]), 17 | ); 18 | 19 | const tx = await identity.removeKey( 20 | keyHash, 21 | args.purpose, 22 | ); 23 | 24 | console.log(`Remove purpose ${args.purpose} from key ${args.key} (hash: ${keyHash}) on identity ${args.identity} tx: ${tx.hash}`); 25 | 26 | await tx.wait(); 27 | 28 | console.log(`Remove purpose ${args.purpose} from key ${args.key} (hash: ${keyHash}) on identity ${args.identity} tx mined: ${tx.hash}`); 29 | }); 30 | -------------------------------------------------------------------------------- /tasks/revoke.task.ts: -------------------------------------------------------------------------------- 1 | import {task} from "hardhat/config"; 2 | import {TaskArguments} from "hardhat/types"; 3 | 4 | task("revoke", "Revoke a claim issued by a claim issuer") 5 | .addParam("from", "A MANAGEMENT key on the claim issuer") 6 | .addParam("issuer", "The address of the claim issuer") 7 | .addParam("signature", "The signature of the claim to revoke") 8 | .setAction(async (args: TaskArguments, hre) => { 9 | const signer = await hre.ethers.getSigner(args.from); 10 | 11 | const claimIssuer = await hre.ethers.getContractAt('ClaimIssuer', args.issuer, signer); 12 | 13 | const tx = await claimIssuer.revokeClaimBySignature(args.signature); 14 | 15 | console.log(`Revoke claim with signature ${args.signature} tx: ${tx.hash}`); 16 | 17 | await tx.wait(); 18 | 19 | console.log(`Revoke claim with signature ${args.signature} tx mined: ${tx.hash}`); 20 | }); 21 | -------------------------------------------------------------------------------- /test/claim-issuers/claim-issuer.test.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; 2 | import { expect } from "chai"; 3 | import {ethers} from "hardhat"; 4 | import {deployIdentityFixture} from "../fixtures"; 5 | 6 | describe('ClaimIssuer - Reference (with revoke)', () => { 7 | describe('revokeClaim (deprecated)', () => { 8 | describe('when calling as a non MANAGEMENT key', () => { 9 | it('should revert for missing permissions', async () => { 10 | const { claimIssuer, aliceWallet, aliceClaim666 } = await loadFixture(deployIdentityFixture); 11 | 12 | await expect(claimIssuer.connect(aliceWallet).revokeClaim(aliceClaim666.id, aliceClaim666.identity)).to.be.revertedWith('Permissions: Sender does not have management key'); 13 | }); 14 | }); 15 | 16 | describe("when calling as a MANAGEMENT key", () => { 17 | describe('when claim was already revoked', () => { 18 | it('should revert for conflict', async () => { 19 | const { claimIssuer, claimIssuerWallet, aliceClaim666 } = await loadFixture(deployIdentityFixture); 20 | 21 | await claimIssuer.connect(claimIssuerWallet).revokeClaim(aliceClaim666.id, aliceClaim666.identity); 22 | 23 | await expect(claimIssuer.connect(claimIssuerWallet).revokeClaim(aliceClaim666.id, aliceClaim666.identity)).to.be.revertedWith('Conflict: Claim already revoked'); 24 | }); 25 | }); 26 | 27 | describe('when is not revoked already', () => { 28 | it('should revoke the claim', async () => { 29 | const { claimIssuer, claimIssuerWallet, aliceClaim666 } = await loadFixture(deployIdentityFixture); 30 | 31 | expect(await claimIssuer.isClaimValid(aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.signature, aliceClaim666.data)).to.be.true; 32 | 33 | const tx = await claimIssuer.connect(claimIssuerWallet).revokeClaim(aliceClaim666.id, aliceClaim666.identity); 34 | 35 | await expect(tx).to.emit(claimIssuer, 'ClaimRevoked').withArgs(aliceClaim666.signature); 36 | 37 | expect(await claimIssuer.isClaimRevoked(aliceClaim666.signature)).to.be.true; 38 | expect(await claimIssuer.isClaimValid(aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.signature, aliceClaim666.data)).to.be.false; 39 | }); 40 | }); 41 | }); 42 | }); 43 | 44 | describe('revokeClaimBySignature', () => { 45 | describe('when calling as a non MANAGEMENT key', () => { 46 | it('should revert for missing permissions', async () => { 47 | const { claimIssuer, aliceWallet, aliceClaim666 } = await loadFixture(deployIdentityFixture); 48 | 49 | await expect(claimIssuer.connect(aliceWallet).revokeClaimBySignature(aliceClaim666.signature)).to.be.revertedWith('Permissions: Sender does not have management key'); 50 | }); 51 | }); 52 | 53 | describe("when calling as a MANAGEMENT key", () => { 54 | describe('when claim was already revoked', () => { 55 | it('should revert for conflict', async () => { 56 | const { claimIssuer, claimIssuerWallet, aliceClaim666 } = await loadFixture(deployIdentityFixture); 57 | 58 | await claimIssuer.connect(claimIssuerWallet).revokeClaimBySignature(aliceClaim666.signature); 59 | 60 | await expect(claimIssuer.connect(claimIssuerWallet).revokeClaimBySignature(aliceClaim666.signature)).to.be.revertedWith('Conflict: Claim already revoked'); 61 | }); 62 | }); 63 | 64 | describe('when is not revoked already', () => { 65 | it('should revoke the claim', async () => { 66 | const { claimIssuer, claimIssuerWallet, aliceClaim666 } = await loadFixture(deployIdentityFixture); 67 | 68 | expect(await claimIssuer.isClaimValid(aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.signature, aliceClaim666.data)).to.be.true; 69 | 70 | const tx = await claimIssuer.connect(claimIssuerWallet).revokeClaimBySignature(aliceClaim666.signature); 71 | 72 | await expect(tx).to.emit(claimIssuer, 'ClaimRevoked').withArgs(aliceClaim666.signature); 73 | 74 | expect(await claimIssuer.isClaimRevoked(aliceClaim666.signature)).to.be.true; 75 | expect(await claimIssuer.isClaimValid(aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.signature, aliceClaim666.data)).to.be.false; 76 | }); 77 | }); 78 | }); 79 | }); 80 | 81 | describe('getRecoveredAddress', () => { 82 | it('should return with a zero address with signature is not of proper length', async () => { 83 | const { claimIssuer, aliceClaim666 } = await loadFixture(deployIdentityFixture); 84 | 85 | expect(await claimIssuer.getRecoveredAddress(aliceClaim666.signature + "00", ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.data]))))).to.be.equal(ethers.constants.AddressZero); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/factory/factory.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | import {ethers} from "hardhat"; 3 | import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; 4 | 5 | import {deployIdentityFixture} from "../fixtures"; 6 | 7 | describe('IdFactory', () => { 8 | it('should revert because authority is Zero address', async () => { 9 | const [deployerWallet] = await ethers.getSigners(); 10 | 11 | const IdFactory = await ethers.getContractFactory('IdFactory'); 12 | await expect(IdFactory.connect(deployerWallet).deploy(ethers.constants.AddressZero)).to.be.revertedWith('invalid argument - zero address'); 13 | }); 14 | 15 | it('should revert because sender is not allowed to create identities', async () => { 16 | const {identityFactory, aliceWallet} = await loadFixture(deployIdentityFixture); 17 | 18 | await expect(identityFactory.connect(aliceWallet).createIdentity(ethers.constants.AddressZero, 'salt1')).to.be.revertedWith('Ownable: caller is not the owner'); 19 | }); 20 | 21 | it('should revert because wallet of identity cannot be Zero address', async () => { 22 | const {identityFactory, deployerWallet} = await loadFixture(deployIdentityFixture); 23 | 24 | await expect(identityFactory.connect(deployerWallet).createIdentity(ethers.constants.AddressZero, 'salt1')).to.be.revertedWith('invalid argument - zero address'); 25 | }); 26 | 27 | it('should revert because salt cannot be empty', async () => { 28 | const {identityFactory, deployerWallet, davidWallet} = await loadFixture(deployIdentityFixture); 29 | 30 | await expect(identityFactory.connect(deployerWallet).createIdentity(davidWallet.address, '')).to.be.revertedWith('invalid argument - empty string'); 31 | }); 32 | 33 | it('should revert because salt cannot be already used', async () => { 34 | const {identityFactory, deployerWallet, davidWallet, carolWallet} = await loadFixture(deployIdentityFixture); 35 | 36 | await identityFactory.connect(deployerWallet).createIdentity(carolWallet.address, 'saltUsed'); 37 | 38 | await expect(identityFactory.connect(deployerWallet).createIdentity(davidWallet.address, 'saltUsed')).to.be.revertedWith('salt already taken'); 39 | }); 40 | 41 | it('should revert because wallet is already linked to an identity', async () => { 42 | const {identityFactory, deployerWallet, aliceWallet} = await loadFixture(deployIdentityFixture); 43 | 44 | await expect(identityFactory.connect(deployerWallet).createIdentity(aliceWallet.address, 'newSalt')).to.be.revertedWith('wallet already linked to an identity'); 45 | }); 46 | 47 | describe('link/unlink wallet', () => { 48 | describe('linkWallet', () => { 49 | it('should revert for new wallet being zero address', async () => { 50 | const { identityFactory, aliceWallet } = await loadFixture(deployIdentityFixture); 51 | 52 | await expect(identityFactory.connect(aliceWallet).linkWallet(ethers.constants.AddressZero)).to.be.revertedWith('invalid argument - zero address'); 53 | }); 54 | 55 | it('should revert for sender wallet being not linked', async () => { 56 | const { identityFactory, davidWallet } = await loadFixture(deployIdentityFixture); 57 | 58 | await expect(identityFactory.connect(davidWallet).linkWallet(davidWallet.address)).to.be.revertedWith('wallet not linked to an identity contract'); 59 | }); 60 | 61 | it('should revert for new wallet being already linked', async () => { 62 | const { identityFactory, bobWallet, aliceWallet } = await loadFixture(deployIdentityFixture); 63 | 64 | await expect(identityFactory.connect(bobWallet).linkWallet(aliceWallet.address)).to.be.revertedWith('new wallet already linked'); 65 | }); 66 | 67 | it('should revert for new wallet being already to a token identity', async () => { 68 | const { identityFactory, bobWallet, tokenAddress } = await loadFixture(deployIdentityFixture); 69 | 70 | await expect(identityFactory.connect(bobWallet).linkWallet(tokenAddress)).to.be.revertedWith('invalid argument - token address'); 71 | }); 72 | 73 | it('should link the new wallet to the existing identity', async () => { 74 | const { identityFactory, aliceIdentity, aliceWallet, davidWallet } = await loadFixture(deployIdentityFixture); 75 | 76 | const tx = await identityFactory.connect(aliceWallet).linkWallet(davidWallet.address); 77 | await expect(tx).to.emit(identityFactory, 'WalletLinked').withArgs(davidWallet.address, aliceIdentity.address); 78 | 79 | expect(await identityFactory.getWallets(aliceIdentity.address)).to.deep.equal([aliceWallet.address, davidWallet.address]); 80 | }); 81 | }); 82 | 83 | describe('unlinkWallet', () => { 84 | it('should revert for wallet to unlink being zero address', async () => { 85 | const { identityFactory, aliceWallet } = await loadFixture(deployIdentityFixture); 86 | 87 | await expect(identityFactory.connect(aliceWallet).unlinkWallet(ethers.constants.AddressZero)).to.be.revertedWith('invalid argument - zero address'); 88 | }); 89 | 90 | it('should revert for sender wallet attemoting to unlink itself', async () => { 91 | const { identityFactory, aliceWallet } = await loadFixture(deployIdentityFixture); 92 | 93 | await expect(identityFactory.connect(aliceWallet).unlinkWallet(aliceWallet.address)).to.be.revertedWith('cannot be called on sender address'); 94 | }); 95 | 96 | it('should revert for sender wallet being not linked', async () => { 97 | const { identityFactory, aliceWallet, davidWallet } = await loadFixture(deployIdentityFixture); 98 | 99 | await expect(identityFactory.connect(davidWallet).unlinkWallet(aliceWallet.address)).to.be.revertedWith('only a linked wallet can unlink'); 100 | }); 101 | 102 | it('should unlink the wallet', async () => { 103 | const { identityFactory, aliceIdentity, aliceWallet, davidWallet } = await loadFixture(deployIdentityFixture); 104 | 105 | await identityFactory.connect(aliceWallet).linkWallet(davidWallet.address); 106 | const tx = await identityFactory.connect(aliceWallet).unlinkWallet(davidWallet.address); 107 | await expect(tx).to.emit(identityFactory, 'WalletUnlinked').withArgs(davidWallet.address, aliceIdentity.address); 108 | 109 | expect(await identityFactory.getWallets(aliceIdentity.address)).to.deep.equal([aliceWallet.address]); 110 | }); 111 | }); 112 | }); 113 | 114 | describe('createIdentityWithManagementKeys()', () => { 115 | describe('when no management keys are provided', () => { 116 | it('should revert', async () => { 117 | const {identityFactory, deployerWallet, davidWallet} = await loadFixture(deployIdentityFixture); 118 | 119 | await expect(identityFactory.connect(deployerWallet).createIdentityWithManagementKeys(davidWallet.address, 'salt1', [])).to.be.revertedWith('invalid argument - empty list of keys'); 120 | }); 121 | }); 122 | 123 | describe('when the wallet is included in the management keys listed', () => { 124 | it('should revert', async () => { 125 | const {identityFactory, deployerWallet, aliceWallet, davidWallet} = await loadFixture(deployIdentityFixture); 126 | 127 | await expect(identityFactory.connect(deployerWallet).createIdentityWithManagementKeys(davidWallet.address, 'salt1', [ 128 | ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address])), 129 | ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address'], [davidWallet.address])), 130 | ])).to.be.revertedWith('invalid argument - wallet is also listed in management keys'); 131 | }); 132 | }); 133 | 134 | describe('when other management keys are specified', () => { 135 | it('should deploy the identity proxy, set keys and wallet as management, and link wallet to identity', async () => { 136 | const {identityFactory, deployerWallet, aliceWallet, davidWallet} = await loadFixture(deployIdentityFixture); 137 | 138 | const tx = await identityFactory.connect(deployerWallet).createIdentityWithManagementKeys(davidWallet.address, 'salt1', [ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]))]); 139 | 140 | await expect(tx).to.emit(identityFactory, 'WalletLinked'); 141 | await expect(tx).to.emit(identityFactory, 'Deployed'); 142 | 143 | const identity = await ethers.getContractAt('Identity', await identityFactory.getIdentity(davidWallet.address)); 144 | 145 | await expect(tx).to.emit(identity, 'KeyAdded').withArgs(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address])), 1, 1); 146 | await expect(identity.keyHasPurpose( 147 | ethers.utils.defaultAbiCoder.encode(['address'], [identityFactory.address]), 148 | 1 149 | )).to.eventually.be.false; 150 | await expect(identity.keyHasPurpose( 151 | ethers.utils.defaultAbiCoder.encode(['address'], [davidWallet.address]), 152 | 1 153 | )).to.eventually.be.false; 154 | await expect(identity.keyHasPurpose( 155 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]), 156 | 1 157 | )).to.eventually.be.false; 158 | }); 159 | }); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /test/factory/token-oid.test.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; 2 | import { expect } from "chai"; 3 | import {ethers} from "hardhat"; 4 | 5 | import {deployFactoryFixture, deployIdentityFixture} from "../fixtures"; 6 | 7 | describe('IdFactory', () => { 8 | describe('add/remove Token factory', () => { 9 | it('should manipulate Token factory list', async () => { 10 | const { identityFactory, deployerWallet, aliceWallet, bobWallet } = await loadFixture(deployFactoryFixture); 11 | 12 | await expect(identityFactory.connect(aliceWallet).addTokenFactory(aliceWallet.address)).to.be.revertedWith('Ownable: caller is not the owner'); 13 | 14 | await expect(identityFactory.connect(deployerWallet).addTokenFactory(ethers.constants.AddressZero)).to.be.revertedWith('invalid argument - zero address'); 15 | 16 | const addTx = await identityFactory.connect(deployerWallet).addTokenFactory(aliceWallet.address); 17 | await expect(addTx).to.emit(identityFactory, 'TokenFactoryAdded').withArgs(aliceWallet.address); 18 | 19 | await expect(identityFactory.connect(deployerWallet).addTokenFactory(aliceWallet.address)).to.be.revertedWith('already a factory'); 20 | 21 | await expect(identityFactory.connect(aliceWallet).removeTokenFactory(bobWallet.address)).to.be.revertedWith('Ownable: caller is not the owner'); 22 | 23 | await expect(identityFactory.connect(deployerWallet).removeTokenFactory(ethers.constants.AddressZero)).to.be.revertedWith('invalid argument - zero address'); 24 | 25 | await expect(identityFactory.connect(deployerWallet).removeTokenFactory(bobWallet.address)).to.be.revertedWith('not a factory'); 26 | 27 | const removeTx = await identityFactory.connect(deployerWallet).removeTokenFactory(aliceWallet.address); 28 | await expect(removeTx).to.emit(identityFactory, 'TokenFactoryRemoved').withArgs(aliceWallet.address); 29 | }); 30 | }); 31 | 32 | describe('createTokenIdentity', () => { 33 | it('should revert for being not authorized to deploy token', async () => { 34 | const { identityFactory, aliceWallet } = await loadFixture(deployFactoryFixture); 35 | 36 | await expect(identityFactory.connect(aliceWallet).createTokenIdentity(aliceWallet.address, aliceWallet.address, 'TST')).to.be.revertedWith('only Factory or owner can call'); 37 | }); 38 | 39 | it('should revert for token address being zero address', async () => { 40 | const { identityFactory, deployerWallet, aliceWallet } = await loadFixture(deployFactoryFixture); 41 | 42 | await expect(identityFactory.connect(deployerWallet).createTokenIdentity(ethers.constants.AddressZero, aliceWallet.address, 'TST')).to.be.revertedWith('invalid argument - zero address'); 43 | }); 44 | 45 | it('should revert for owner being zero address', async () => { 46 | const { identityFactory, deployerWallet, aliceWallet } = await loadFixture(deployFactoryFixture); 47 | 48 | await expect(identityFactory.connect(deployerWallet).createTokenIdentity(aliceWallet.address, ethers.constants.AddressZero, 'TST')).to.be.revertedWith('invalid argument - zero address'); 49 | }); 50 | 51 | it('should revert for salt being empty', async () => { 52 | const { identityFactory, deployerWallet, aliceWallet } = await loadFixture(deployFactoryFixture); 53 | 54 | await expect(identityFactory.connect(deployerWallet).createTokenIdentity(aliceWallet.address, aliceWallet.address, '')).to.be.revertedWith('invalid argument - empty string'); 55 | }); 56 | 57 | it('should create one identity and then revert for salt/address being already used', async () => { 58 | const { identityFactory, deployerWallet, aliceWallet, bobWallet } = await loadFixture(deployFactoryFixture); 59 | 60 | expect(await identityFactory.isSaltTaken('Tokensalt1')).to.be.false; 61 | 62 | const tx = await identityFactory.connect(deployerWallet).createTokenIdentity(aliceWallet.address, bobWallet.address, 'salt1'); 63 | const tokenIdentityAddress = await identityFactory.getIdentity(aliceWallet.address); 64 | await expect(tx).to.emit(identityFactory, 'TokenLinked').withArgs(aliceWallet.address, tokenIdentityAddress); 65 | await expect(tx).to.emit(identityFactory, 'Deployed').withArgs(tokenIdentityAddress); 66 | 67 | expect(await identityFactory.isSaltTaken('Tokensalt1')).to.be.true; 68 | expect(await identityFactory.isSaltTaken('Tokensalt2')).to.be.false; 69 | expect(await identityFactory.getToken(tokenIdentityAddress)).to.deep.equal(aliceWallet.address); 70 | 71 | await expect(identityFactory.connect(deployerWallet).createTokenIdentity(aliceWallet.address, aliceWallet.address, 'salt1')).to.be.revertedWith('salt already taken'); 72 | await expect(identityFactory.connect(deployerWallet).createTokenIdentity(aliceWallet.address, aliceWallet.address, 'salt2')).to.be.revertedWith('token already linked to an identity'); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'hardhat'; 2 | 3 | export async function deployFactoryFixture() { 4 | const [deployerWallet, claimIssuerWallet, aliceWallet, bobWallet, carolWallet, davidWallet] = 5 | await ethers.getSigners(); 6 | 7 | const Identity = await ethers.getContractFactory('Identity'); 8 | const identityImplementation = await Identity.connect(deployerWallet).deploy(deployerWallet.address, true); 9 | 10 | const ImplementationAuthority = await ethers.getContractFactory( 11 | 'ImplementationAuthority' 12 | ); 13 | const implementationAuthority = await ImplementationAuthority.connect(deployerWallet).deploy( 14 | identityImplementation.address, 15 | ); 16 | 17 | const IdentityFactory = await ethers.getContractFactory('IdFactory'); 18 | const identityFactory = await IdentityFactory.connect(deployerWallet).deploy( 19 | implementationAuthority.address, 20 | ); 21 | 22 | return { 23 | identityFactory, 24 | identityImplementation, 25 | implementationAuthority, 26 | aliceWallet, 27 | bobWallet, 28 | carolWallet, 29 | davidWallet, 30 | deployerWallet, 31 | claimIssuerWallet, 32 | }; 33 | } 34 | 35 | export async function deployIdentityFixture() { 36 | const [deployerWallet, claimIssuerWallet, aliceWallet, bobWallet, carolWallet, davidWallet, tokenOwnerWallet] = 37 | await ethers.getSigners(); 38 | 39 | const { identityFactory, identityImplementation, implementationAuthority } = await deployFactoryFixture(); 40 | 41 | const ClaimIssuer = await ethers.getContractFactory('ClaimIssuer'); 42 | const claimIssuer = await ClaimIssuer.connect(claimIssuerWallet).deploy(claimIssuerWallet.address); 43 | await claimIssuer.connect(claimIssuerWallet).addKey( 44 | ethers.utils.keccak256( 45 | ethers.utils.defaultAbiCoder.encode(['address'], [claimIssuerWallet.address]) 46 | ), 47 | 3, 48 | 1, 49 | ); 50 | 51 | await identityFactory.connect(deployerWallet).createIdentity(aliceWallet.address, 'alice'); 52 | const aliceIdentity = await ethers.getContractAt('Identity', await identityFactory.getIdentity(aliceWallet.address)); 53 | await aliceIdentity.connect(aliceWallet).addKey(ethers.utils.keccak256( 54 | ethers.utils.defaultAbiCoder.encode(['address'], [carolWallet.address]) 55 | ), 3, 1); 56 | await aliceIdentity.connect(aliceWallet).addKey(ethers.utils.keccak256( 57 | ethers.utils.defaultAbiCoder.encode(['address'], [davidWallet.address]) 58 | ), 2, 1); 59 | const aliceClaim666 = { 60 | id: '', 61 | identity: aliceIdentity.address, 62 | issuer: claimIssuer.address, 63 | topic: 666, 64 | scheme: 1, 65 | data: '0x0042', 66 | signature: '', 67 | uri: 'https://example.com', 68 | }; 69 | aliceClaim666.id = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [aliceClaim666.issuer, aliceClaim666.topic])); 70 | aliceClaim666.signature = await claimIssuerWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.data])))); 71 | 72 | await aliceIdentity.connect(aliceWallet).addClaim(aliceClaim666.topic, aliceClaim666.scheme, aliceClaim666.issuer, aliceClaim666.signature, aliceClaim666.data, aliceClaim666.uri); 73 | 74 | await identityFactory.connect(deployerWallet).createIdentity(bobWallet.address, 'bob'); 75 | const bobIdentity = await ethers.getContractAt('Identity', await identityFactory.getIdentity(bobWallet.address)); 76 | 77 | const tokenAddress = '0xdEE019486810C7C620f6098EEcacA0244b0fa3fB'; 78 | await identityFactory.connect(deployerWallet).createTokenIdentity(tokenAddress, tokenOwnerWallet.address, 'tokenOwner'); 79 | 80 | return { 81 | identityFactory, 82 | identityImplementation, 83 | implementationAuthority, 84 | claimIssuer, 85 | aliceWallet, 86 | bobWallet, 87 | carolWallet, 88 | davidWallet, 89 | deployerWallet, 90 | claimIssuerWallet, 91 | tokenOwnerWallet, 92 | aliceIdentity, 93 | bobIdentity, 94 | aliceClaim666, 95 | tokenAddress 96 | }; 97 | } 98 | 99 | export async function deployVerifierFixture() { 100 | 101 | } 102 | -------------------------------------------------------------------------------- /test/identities/claims.test.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; 2 | import { expect } from "chai"; 3 | import {ethers} from "hardhat"; 4 | 5 | import { deployIdentityFixture } from '../fixtures'; 6 | 7 | describe('Identity', () => { 8 | describe('Claims', () => { 9 | describe('addClaim', () => { 10 | describe('when the claim is self-attested (issuer is identity address)', () => { 11 | describe('when the claim is not valid', () => { 12 | it('should add the claim anyway', async () => { 13 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 14 | 15 | const claim = { 16 | identity: aliceIdentity.address, 17 | issuer: aliceIdentity.address, 18 | topic: 42, 19 | scheme: 1, 20 | data: '0x0042', 21 | signature: '', 22 | uri: 'https://example.com', 23 | }; 24 | claim.signature = await aliceWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [claim.identity, claim.topic, '0x101010'])))); 25 | 26 | const tx = await aliceIdentity.connect(aliceWallet).addClaim(claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 27 | await expect(tx).to.emit(aliceIdentity, 'ClaimAdded').withArgs(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [claim.issuer, claim.topic])), claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 28 | await expect(aliceIdentity.isClaimValid(claim.identity, claim.topic, claim.signature, claim.data)).to.eventually.equal(false); 29 | }); 30 | }); 31 | 32 | describe('when the claim is valid', () => { 33 | let claim = { identity: '', issuer: '', topic: 0, scheme: 1, data: '', uri: '', signature: '' }; 34 | before(async () => { 35 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 36 | 37 | claim = { 38 | identity: aliceIdentity.address, 39 | issuer: aliceIdentity.address, 40 | topic: 42, 41 | scheme: 1, 42 | data: '0x0042', 43 | signature: '', 44 | uri: 'https://example.com', 45 | }; 46 | claim.signature = await aliceWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [claim.identity, claim.topic, claim.data])))); 47 | }); 48 | 49 | describe('when caller is the identity itself (execute)', () => { 50 | it('should add the claim', async () => { 51 | const { aliceIdentity, aliceWallet, bobWallet } = await loadFixture(deployIdentityFixture); 52 | 53 | const action = { 54 | to: aliceIdentity.address, 55 | value: 0, 56 | data: new ethers.utils.Interface([ 57 | 'function addClaim(uint256 topic, uint256 scheme, address issuer, bytes calldata signature, bytes calldata data, string calldata uri) external returns (bytes32 claimRequestId)' 58 | ]).encodeFunctionData('addClaim', [ 59 | claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri 60 | ]), 61 | }; 62 | 63 | await aliceIdentity.connect(bobWallet).execute(action.to, action.value, action.data); 64 | const tx = await aliceIdentity.connect(aliceWallet).approve(0, true); 65 | await expect(tx).to.emit(aliceIdentity, 'ClaimAdded').withArgs(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [claim.issuer, claim.topic])), claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 66 | await expect(tx).to.emit(aliceIdentity, 'Approved'); 67 | await expect(tx).to.emit(aliceIdentity, 'Executed'); 68 | await expect(aliceIdentity.isClaimValid(claim.identity, claim.topic, claim.signature, claim.data)).to.eventually.equal(true); 69 | }); 70 | }); 71 | 72 | describe('when caller is a CLAIM or MANAGEMENT key', () => { 73 | it('should add the claim', async () => { 74 | it('should add the claim anyway', async () => { 75 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 76 | 77 | const tx = await aliceIdentity.connect(aliceWallet).addClaim(claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 78 | await expect(tx).to.emit(aliceIdentity, 'ClaimAdded').withArgs(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [claim.issuer, claim.topic])), claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('when caller is not a CLAIM key', () => { 84 | it('should revert for missing permission', async () => { 85 | const { aliceIdentity, bobWallet } = await loadFixture(deployIdentityFixture); 86 | 87 | await expect(aliceIdentity.connect(bobWallet).addClaim(claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri)).to.be.revertedWith('Permissions: Sender does not have claim signer key'); 88 | }); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('when the claim is from a claim issuer', () => { 94 | describe('when the claim is not valid', () => { 95 | it('should revert for invalid claim', async () => { 96 | const { aliceIdentity, aliceWallet, claimIssuerWallet, claimIssuer } = await loadFixture(deployIdentityFixture); 97 | 98 | const claim = { 99 | identity: aliceIdentity.address, 100 | issuer: claimIssuer.address, 101 | topic: 42, 102 | scheme: 1, 103 | data: '0x0042', 104 | signature: '', 105 | uri: 'https://example.com', 106 | }; 107 | claim.signature = await claimIssuerWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [claim.identity, claim.topic, '0x10101010'])))); 108 | 109 | await expect(aliceIdentity.connect(aliceWallet).addClaim(claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri)).to.be.revertedWith('invalid claim'); 110 | }); 111 | }); 112 | 113 | describe('when the claim is valid', () => { 114 | let claim = { identity: '', issuer: '', topic: 0, scheme: 1, data: '', uri: '', signature: '' }; 115 | before(async () => { 116 | const { aliceIdentity, claimIssuer, claimIssuerWallet } = await loadFixture(deployIdentityFixture); 117 | 118 | claim = { 119 | identity: aliceIdentity.address, 120 | issuer: claimIssuer.address, 121 | topic: 42, 122 | scheme: 1, 123 | data: '0x0042', 124 | signature: '', 125 | uri: 'https://example.com', 126 | }; 127 | claim.signature = await claimIssuerWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [claim.identity, claim.topic, claim.data])))); 128 | }); 129 | 130 | describe('when caller is the identity itself (execute)', () => { 131 | it('should add the claim', async () => { 132 | const { aliceIdentity, aliceWallet, bobWallet } = await loadFixture(deployIdentityFixture); 133 | 134 | const action = { 135 | to: aliceIdentity.address, 136 | value: 0, 137 | data: new ethers.utils.Interface([ 138 | 'function addClaim(uint256 topic, uint256 scheme, address issuer, bytes calldata signature, bytes calldata data, string calldata uri) external returns (bytes32 claimRequestId)' 139 | ]).encodeFunctionData('addClaim', [ 140 | claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri 141 | ]), 142 | }; 143 | 144 | await aliceIdentity.connect(bobWallet).execute(action.to, action.value, action.data); 145 | const tx = await aliceIdentity.connect(aliceWallet).approve(0, true); 146 | await expect(tx).to.emit(aliceIdentity, 'ClaimAdded').withArgs(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [claim.issuer, claim.topic])), claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 147 | await expect(tx).to.emit(aliceIdentity, 'Approved'); 148 | await expect(tx).to.emit(aliceIdentity, 'Executed'); 149 | }); 150 | }); 151 | 152 | describe('when caller is a CLAIM or MANAGEMENT key', () => { 153 | it('should add the claim', async () => { 154 | it('should add the claim anyway', async () => { 155 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 156 | 157 | const tx = await aliceIdentity.connect(aliceWallet).addClaim(claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 158 | await expect(tx).to.emit(aliceIdentity, 'ClaimAdded').withArgs(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [claim.issuer, claim.topic])), claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 159 | }); 160 | }); 161 | }); 162 | 163 | describe('when caller is not a CLAIM key', () => { 164 | it('should revert for missing permission', async () => { 165 | const { aliceIdentity, bobWallet } = await loadFixture(deployIdentityFixture); 166 | 167 | await expect(aliceIdentity.connect(bobWallet).addClaim(claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri)).to.be.revertedWith('Permissions: Sender does not have claim signer key'); 168 | }); 169 | }); 170 | }); 171 | }); 172 | }); 173 | 174 | describe('updateClaim (addClaim)', () => { 175 | describe('when there is already a claim from this issuer and this topic', () => { 176 | let aliceIdentity: ethers.Contract; 177 | let aliceWallet: ethers.Wallet; 178 | let claimIssuer: ethers.Contract; 179 | let claimIssuerWallet: ethers.Wallet; 180 | before(async () => { 181 | const params = await loadFixture(deployIdentityFixture); 182 | aliceIdentity = params.aliceIdentity; 183 | aliceWallet = params.aliceWallet; 184 | claimIssuer = params.claimIssuer; 185 | claimIssuerWallet = params.claimIssuerWallet; 186 | 187 | const claim = { 188 | identity: aliceIdentity.address, 189 | issuer: claimIssuer.address, 190 | topic: 42, 191 | scheme: 1, 192 | data: '0x0042', 193 | signature: '', 194 | uri: 'https://example.com', 195 | }; 196 | claim.signature = await claimIssuerWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [claim.identity, claim.topic, claim.data])))); 197 | 198 | await aliceIdentity.connect(aliceWallet).addClaim( 199 | claim.topic, 200 | claim.scheme, 201 | claim.issuer, 202 | claim.signature, 203 | claim.data, 204 | claim.uri, 205 | ); 206 | }); 207 | 208 | it('should replace the existing claim', async () => { 209 | const claim = { 210 | identity: aliceIdentity.address, 211 | issuer: claimIssuer.address, 212 | topic: 42, 213 | scheme: 1, 214 | data: '0x004200101010', 215 | signature: '', 216 | uri: 'https://example.com', 217 | }; 218 | claim.signature = await claimIssuerWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [claim.identity, claim.topic, claim.data])))); 219 | 220 | const tx = await aliceIdentity.connect(aliceWallet).addClaim( 221 | claim.topic, 222 | claim.scheme, 223 | claim.issuer, 224 | claim.signature, 225 | claim.data, 226 | claim.uri, 227 | ); 228 | await expect(tx).to.emit(aliceIdentity, 'ClaimChanged').withArgs(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [claim.issuer, claim.topic])), claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 229 | }); 230 | }); 231 | }); 232 | 233 | describe('removeClaim', () => { 234 | describe('When caller is the identity itself (execute)', () => { 235 | it('should remove an existing claim', async () => { 236 | const { aliceIdentity, aliceWallet, bobWallet, claimIssuer, claimIssuerWallet } = await loadFixture(deployIdentityFixture); 237 | const claim = { 238 | identity: aliceIdentity.address, 239 | issuer: claimIssuer.address, 240 | topic: 42, 241 | scheme: 1, 242 | data: '0x0042', 243 | signature: '', 244 | uri: 'https://example.com', 245 | }; 246 | const claimId = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [claim.issuer, claim.topic])); 247 | claim.signature = await claimIssuerWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [claim.identity, claim.topic, claim.data])))); 248 | 249 | await aliceIdentity.connect(aliceWallet).addClaim(claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 250 | 251 | const action = { 252 | to: aliceIdentity.address, 253 | value: 0, 254 | data: new ethers.utils.Interface([ 255 | 'function removeClaim(bytes32 claimId) external returns (bool success)' 256 | ]).encodeFunctionData('removeClaim', [ 257 | claimId, 258 | ]), 259 | }; 260 | 261 | await aliceIdentity.connect(bobWallet).execute(action.to, action.value, action.data); 262 | const tx = await aliceIdentity.connect(aliceWallet).approve(0, true); 263 | await expect(tx).to.emit(aliceIdentity, 'ClaimRemoved').withArgs(claimId, claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 264 | }); 265 | }); 266 | 267 | describe('When caller is not a CLAIM key', () => { 268 | it('should revert for missing permission', async () => { 269 | const { aliceIdentity, bobWallet, claimIssuer } = await loadFixture(deployIdentityFixture); 270 | 271 | const claimId = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [claimIssuer.address, 42])); 272 | 273 | await expect(aliceIdentity.connect(bobWallet).removeClaim(claimId)).to.be.revertedWith('Permissions: Sender does not have claim signer key'); 274 | }); 275 | }); 276 | 277 | describe('When claim does not exist', () => { 278 | it('should revert for non existing claim', async () => { 279 | const { aliceIdentity, carolWallet, claimIssuer } = await loadFixture(deployIdentityFixture); 280 | 281 | const claimId = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [claimIssuer.address, 42])); 282 | 283 | await expect(aliceIdentity.connect(carolWallet).removeClaim(claimId)).to.be.revertedWith('NonExisting: There is no claim with this ID'); 284 | }); 285 | }); 286 | 287 | describe('When claim does exist', () => { 288 | it('should remove the claim', async () => { 289 | const { aliceIdentity, aliceWallet, claimIssuer, claimIssuerWallet } = await loadFixture(deployIdentityFixture); 290 | const claim = { 291 | identity: aliceIdentity.address, 292 | issuer: claimIssuer.address, 293 | topic: 42, 294 | scheme: 1, 295 | data: '0x0042', 296 | signature: '', 297 | uri: 'https://example.com', 298 | }; 299 | const claimId = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [claim.issuer, claim.topic])); 300 | claim.signature = await claimIssuerWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [claim.identity, claim.topic, claim.data])))); 301 | 302 | await aliceIdentity.connect(aliceWallet).addClaim(claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 303 | 304 | const tx = await aliceIdentity.connect(aliceWallet).removeClaim(claimId); 305 | await expect(tx).to.emit(aliceIdentity, 'ClaimRemoved').withArgs(claimId, claim.topic, claim.scheme, claim.issuer, claim.signature, claim.data, claim.uri); 306 | }); 307 | }); 308 | }); 309 | 310 | describe('getClaim', () => { 311 | describe('when claim does not exist', () => { 312 | it('should return an empty struct', async () => { 313 | const { aliceIdentity, claimIssuer } = await loadFixture(deployIdentityFixture); 314 | const claimId = ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256'], [claimIssuer.address, 42])); 315 | const found = await aliceIdentity.getClaim(claimId); 316 | expect(found.issuer).to.equal(ethers.constants.AddressZero); 317 | expect(found.topic).to.equal(0); 318 | expect(found.scheme).to.equal(0); 319 | expect(found.data).to.equal('0x'); 320 | expect(found.signature).to.equal('0x'); 321 | expect(found.uri).to.equal(''); 322 | }); 323 | }); 324 | 325 | describe('when claim does exist', () => { 326 | it('should return the claim', async () => { 327 | const { aliceIdentity, aliceClaim666 } = await loadFixture(deployIdentityFixture); 328 | 329 | const found = await aliceIdentity.getClaim(aliceClaim666.id); 330 | expect(found.issuer).to.equal(aliceClaim666.issuer); 331 | expect(found.topic).to.equal(aliceClaim666.topic); 332 | expect(found.scheme).to.equal(aliceClaim666.scheme); 333 | expect(found.data).to.equal(aliceClaim666.data); 334 | expect(found.signature).to.equal(aliceClaim666.signature); 335 | expect(found.uri).to.equal(aliceClaim666.uri); 336 | }); 337 | }); 338 | }); 339 | 340 | describe('getClaimIdsByTopic', () => { 341 | it('should return an empty array when there are no claims for the topic', async () => { 342 | const { aliceIdentity } = await loadFixture(deployIdentityFixture); 343 | 344 | await expect(aliceIdentity.getClaimIdsByTopic(101010)).to.eventually.deep.equal([]); 345 | }); 346 | 347 | it('should return an array of claim Id existing fo the topic', async () => { 348 | const { aliceIdentity, aliceClaim666 } = await loadFixture(deployIdentityFixture); 349 | 350 | await expect(aliceIdentity.getClaimIdsByTopic(aliceClaim666.topic)).to.eventually.deep.equal([aliceClaim666.id]); 351 | }); 352 | }); 353 | }); 354 | }); 355 | -------------------------------------------------------------------------------- /test/identities/executions.test.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; 2 | import { expect } from "chai"; 3 | import {ethers} from "hardhat"; 4 | 5 | import { deployIdentityFixture } from '../fixtures'; 6 | 7 | describe('Identity', () => { 8 | describe('Execute', () => { 9 | describe('when calling execute as a MANAGEMENT key', () => { 10 | describe('when execution is possible (transferring value with enough funds on the identity)', () => { 11 | it('should execute immediately the action', async () => { 12 | const { aliceIdentity, aliceWallet, carolWallet } = await loadFixture(deployIdentityFixture); 13 | 14 | const previousBalance = await ethers.provider.getBalance(carolWallet.address); 15 | const action = { 16 | to: carolWallet.address, 17 | value: 10, 18 | data: '0x', 19 | }; 20 | 21 | const tx = await aliceIdentity.connect(aliceWallet).execute(action.to, action.value, action.data, { value: action.value }); 22 | await expect(tx).to.emit(aliceIdentity, 'Approved'); 23 | await expect(tx).to.emit(aliceIdentity, 'Executed'); 24 | const newBalance = await ethers.provider.getBalance(carolWallet.address); 25 | 26 | expect(newBalance).to.equal(previousBalance.add(action.value)); 27 | }); 28 | }); 29 | 30 | describe('when execution is possible (successfull call)', () => { 31 | it('should emit Executed', async () => { 32 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 33 | 34 | const aliceKeyHash = ethers.utils.keccak256( 35 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 36 | ); 37 | 38 | const action = { 39 | to: aliceIdentity.address, 40 | value: 0, 41 | data: new ethers.utils.Interface(['function addKey(bytes32 key, uint256 purpose, uint256 keyType) returns (bool success)']).encodeFunctionData('addKey', [ 42 | aliceKeyHash, 43 | 3, 44 | 1, 45 | ]), 46 | }; 47 | 48 | const tx = await aliceIdentity.connect(aliceWallet).execute(action.to, action.value, action.data); 49 | await expect(tx).to.emit(aliceIdentity, 'Approved'); 50 | await expect(tx).to.emit(aliceIdentity, 'Executed'); 51 | 52 | const purposes = await aliceIdentity.getKeyPurposes(aliceKeyHash); 53 | expect(purposes).to.deep.equal([1, 3]); 54 | }); 55 | }); 56 | 57 | describe('when execution is not possible (failing call)', () => { 58 | it('should emit an ExecutionFailed event', async () => { 59 | const { aliceIdentity, aliceWallet, carolWallet } = await loadFixture(deployIdentityFixture); 60 | 61 | const previousBalance = await ethers.provider.getBalance(carolWallet.address); 62 | const action = { 63 | to: aliceIdentity.address, 64 | value: 0, 65 | data: new ethers.utils.Interface(['function addKey(bytes32 key, uint256 purpose, uint256 keyType) returns (bool success)']).encodeFunctionData('addKey', [ 66 | ethers.utils.keccak256( 67 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 68 | ), 69 | 1, 70 | 1, 71 | ]), 72 | }; 73 | 74 | const tx = await aliceIdentity.connect(aliceWallet).execute(action.to, action.value, action.data); 75 | await expect(tx).to.emit(aliceIdentity, 'Approved'); 76 | await expect(tx).to.emit(aliceIdentity, 'ExecutionFailed'); 77 | const newBalance = await ethers.provider.getBalance(carolWallet.address); 78 | 79 | expect(newBalance).to.equal(previousBalance.add(action.value)); 80 | }); 81 | }); 82 | }); 83 | 84 | describe('when calling execute as an ACTION key', () => { 85 | describe('when target is the identity contract', () => { 86 | it('should create an execution request', async () => { 87 | const { aliceIdentity, aliceWallet, bobWallet, carolWallet } = await loadFixture(deployIdentityFixture); 88 | 89 | const aliceKeyHash = ethers.utils.keccak256( 90 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 91 | ); 92 | const carolKeyHash = ethers.utils.keccak256( 93 | ethers.utils.defaultAbiCoder.encode(['address'], [carolWallet.address]) 94 | ); 95 | await aliceIdentity.connect(aliceWallet).addKey(carolKeyHash, 2, 1); 96 | 97 | const action = { 98 | to: aliceIdentity.address, 99 | value: 0, 100 | data: new ethers.utils.Interface(['function addKey(bytes32 key, uint256 purpose, uint256 keyType) returns (bool success)']).encodeFunctionData('addKey', [ 101 | aliceKeyHash, 102 | 2, 103 | 1, 104 | ]), 105 | }; 106 | 107 | const tx = await aliceIdentity.connect(carolWallet).execute(action.to, action.value, action.data, { value: action.value }); 108 | await expect(tx).to.emit(aliceIdentity, 'ExecutionRequested'); 109 | }); 110 | }); 111 | 112 | describe('when target is another address', () => { 113 | it('should emit ExecutionFailed for a failed execution', async () => { 114 | const { aliceIdentity, aliceWallet, carolWallet, davidWallet, bobIdentity } = await loadFixture(deployIdentityFixture); 115 | 116 | const carolKeyHash = ethers.utils.keccak256( 117 | ethers.utils.defaultAbiCoder.encode(['address'], [carolWallet.address]) 118 | ); 119 | await aliceIdentity.connect(aliceWallet).addKey(carolKeyHash, 2, 1); 120 | 121 | const aliceKeyHash = ethers.utils.keccak256( 122 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 123 | ); 124 | 125 | const action = { 126 | to: bobIdentity.address, 127 | value: 10, 128 | data: new ethers.utils.Interface(['function addKey(bytes32 key, uint256 purpose, uint256 keyType) returns (bool success)']).encodeFunctionData('addKey', [ 129 | aliceKeyHash, 130 | 3, 131 | 1, 132 | ]), 133 | }; 134 | 135 | const previousBalance = await ethers.provider.getBalance(bobIdentity.address); 136 | 137 | const tx = await aliceIdentity.connect(carolWallet).execute(action.to, action.value, action.data, { value: action.value }); 138 | await expect(tx).to.emit(aliceIdentity, 'Approved'); 139 | await expect(tx).to.emit(aliceIdentity, 'ExecutionFailed'); 140 | const newBalance = await ethers.provider.getBalance(bobIdentity.address); 141 | 142 | expect(newBalance).to.equal(previousBalance); 143 | }); 144 | 145 | it('should execute immediately the action', async () => { 146 | const { aliceIdentity, aliceWallet, carolWallet, davidWallet } = await loadFixture(deployIdentityFixture); 147 | 148 | const carolKeyHash = ethers.utils.keccak256( 149 | ethers.utils.defaultAbiCoder.encode(['address'], [carolWallet.address]) 150 | ); 151 | await aliceIdentity.connect(aliceWallet).addKey(carolKeyHash, 2, 1); 152 | 153 | const previousBalance = await ethers.provider.getBalance(davidWallet.address); 154 | const action = { 155 | to: davidWallet.address, 156 | value: 10, 157 | data: '0x', 158 | }; 159 | 160 | const tx = await aliceIdentity.connect(carolWallet).execute(action.to, action.value, action.data, { value: action.value }); 161 | await expect(tx).to.emit(aliceIdentity, 'Approved'); 162 | await expect(tx).to.emit(aliceIdentity, 'Executed'); 163 | const newBalance = await ethers.provider.getBalance(davidWallet.address); 164 | 165 | expect(newBalance).to.equal(previousBalance.add(action.value)); 166 | }); 167 | }); 168 | }); 169 | 170 | describe('when calling execute as a non-action key', () => { 171 | it('should create a pending execution request', async () => { 172 | const { aliceIdentity, bobWallet, carolWallet } = await loadFixture(deployIdentityFixture); 173 | 174 | const previousBalance = await ethers.provider.getBalance(carolWallet.address); 175 | const action = { 176 | to: carolWallet.address, 177 | value: 10, 178 | data: '0x', 179 | }; 180 | 181 | const tx = await aliceIdentity.connect(bobWallet).execute(action.to, action.value, action.data, { value: action.value }); 182 | await expect(tx).to.emit(aliceIdentity, 'ExecutionRequested'); 183 | const newBalance = await ethers.provider.getBalance(carolWallet.address); 184 | 185 | expect(newBalance).to.equal(previousBalance); 186 | }); 187 | }); 188 | }); 189 | 190 | describe('Approve', () => { 191 | describe('when calling a non-existing execution request', () => { 192 | it('should revert for execution request not found', async () => { 193 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 194 | 195 | await expect(aliceIdentity.connect(aliceWallet).approve(2, true)).to.be.revertedWith('Cannot approve a non-existing execution'); 196 | }); 197 | }); 198 | 199 | describe('when calling an already executed request', () => { 200 | it('should revert for execution request already executed', async () => { 201 | const { aliceIdentity, aliceWallet, bobWallet } = await loadFixture(deployIdentityFixture); 202 | 203 | await aliceIdentity.connect(aliceWallet).execute(bobWallet.address, 10, '0x', { value: 10 }); 204 | 205 | await expect(aliceIdentity.connect(aliceWallet).approve(0, true)).to.be.revertedWith('Request already executed'); 206 | }); 207 | }); 208 | 209 | describe('when calling approve for an execution targeting another address as a non-action key', () => { 210 | it('should revert for not authorized', async () => { 211 | const { aliceIdentity, bobWallet, carolWallet } = await loadFixture(deployIdentityFixture); 212 | 213 | await aliceIdentity.connect(bobWallet).execute(carolWallet.address, 10, '0x', { value: 10 }); 214 | 215 | await expect(aliceIdentity.connect(bobWallet).approve(0, true)).to.be.revertedWith('Sender does not have action key'); 216 | }); 217 | }); 218 | 219 | describe('when calling approve for an execution targeting another address as a non-management key', () => { 220 | it('should revert for not authorized', async () => { 221 | const { aliceIdentity, davidWallet, bobWallet } = await loadFixture(deployIdentityFixture); 222 | 223 | await aliceIdentity.connect(bobWallet).execute(aliceIdentity.address, 10, '0x', { value: 10 }); 224 | 225 | await expect(aliceIdentity.connect(davidWallet).approve(0, true)).to.be.revertedWith('Sender does not have management key'); 226 | }); 227 | }); 228 | 229 | describe('when calling approve as a MANAGEMENT key', () => { 230 | it('should approve the execution request', async () => { 231 | const { aliceIdentity, aliceWallet, bobWallet, carolWallet } = await loadFixture(deployIdentityFixture); 232 | 233 | const previousBalance = await ethers.provider.getBalance(carolWallet.address); 234 | await aliceIdentity.connect(bobWallet).execute(carolWallet.address, 10, '0x', { value: 10 }); 235 | 236 | const tx = await aliceIdentity.connect(aliceWallet).approve(0, true); 237 | await expect(tx).to.emit(aliceIdentity, 'Approved'); 238 | await expect(tx).to.emit(aliceIdentity, 'Executed'); 239 | const newBalance = await ethers.provider.getBalance(carolWallet.address); 240 | 241 | expect(newBalance).to.equal(previousBalance.add(10)); 242 | }); 243 | 244 | it('should leave approve to false', async () => { 245 | const { aliceIdentity, aliceWallet, bobWallet, carolWallet } = await loadFixture(deployIdentityFixture); 246 | 247 | const previousBalance = await ethers.provider.getBalance(carolWallet.address); 248 | await aliceIdentity.connect(bobWallet).execute(carolWallet.address, 10, '0x', { value: 10 }); 249 | 250 | const tx = await aliceIdentity.connect(aliceWallet).approve(0, false); 251 | await expect(tx).to.emit(aliceIdentity, 'Approved'); 252 | const newBalance = await ethers.provider.getBalance(carolWallet.address); 253 | 254 | expect(newBalance).to.equal(previousBalance); 255 | }); 256 | }); 257 | }); 258 | }); 259 | -------------------------------------------------------------------------------- /test/identities/init.test.ts: -------------------------------------------------------------------------------- 1 | import {loadFixture} from '@nomicfoundation/hardhat-network-helpers'; 2 | import {expect} from "chai"; 3 | import {ethers} from "hardhat"; 4 | 5 | import {deployIdentityFixture} from '../fixtures'; 6 | 7 | describe('Identity', () => { 8 | it('should revert when attempting to initialize an already deployed identity', async () => { 9 | const {aliceIdentity, aliceWallet} = await loadFixture(deployIdentityFixture); 10 | 11 | await expect(aliceIdentity.connect(aliceWallet).initialize(aliceWallet.address)).to.be.revertedWith('Initial key was already setup.'); 12 | }); 13 | 14 | it('should revert because interaction with library is forbidden', async () => { 15 | const {identityImplementation, aliceWallet, deployerWallet} = await loadFixture(deployIdentityFixture); 16 | 17 | await expect(identityImplementation.connect(deployerWallet).addKey( 18 | ethers.utils.keccak256( 19 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 20 | ), 21 | 3, 22 | 1, 23 | )).to.be.revertedWith('Interacting with the library contract is forbidden.'); 24 | 25 | await expect(identityImplementation.connect(aliceWallet).initialize(deployerWallet.address)) 26 | .to.be.revertedWith('Initial key was already setup.'); 27 | }); 28 | 29 | it('should prevent creating an identity with an invalid initial key', async () => { 30 | const [identityOwnerWallet] = await ethers.getSigners(); 31 | 32 | const Identity = await ethers.getContractFactory('Identity'); 33 | await expect(Identity.connect(identityOwnerWallet).deploy(ethers.constants.AddressZero, false)).to.be.revertedWith('invalid argument - zero address'); 34 | }); 35 | 36 | it('should return the version of the implementation', async () => { 37 | const {identityImplementation} = await loadFixture(deployIdentityFixture); 38 | 39 | expect(await identityImplementation.version()).to.equal('2.2.1'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/identities/keys.test.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; 2 | import { expect } from "chai"; 3 | import {ethers} from "hardhat"; 4 | 5 | import { deployIdentityFixture } from '../fixtures'; 6 | 7 | describe('Identity', () => { 8 | describe('Key Management', () => { 9 | describe('Read key methods', () => { 10 | it('should retrieve an existing key', async () => { 11 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 12 | 13 | const aliceKeyHash = ethers.utils.keccak256( 14 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 15 | ); 16 | const aliceKey = await aliceIdentity.getKey(aliceKeyHash); 17 | expect(aliceKey.key).to.equal(aliceKeyHash); 18 | expect(aliceKey.purposes).to.deep.equal([1]); 19 | expect(aliceKey.keyType).to.equal(1); 20 | }); 21 | 22 | it('should retrieve existing key purposes', async () => { 23 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 24 | 25 | const aliceKeyHash = ethers.utils.keccak256( 26 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 27 | ); 28 | const purposes = await aliceIdentity.getKeyPurposes(aliceKeyHash); 29 | expect(purposes).to.deep.equal([1]); 30 | }); 31 | 32 | it('should retrieve existing keys with given purpose', async () => { 33 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 34 | 35 | const aliceKeyHash = ethers.utils.keccak256( 36 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 37 | ); 38 | const keys = await aliceIdentity.getKeysByPurpose(1); 39 | expect(keys).to.deep.equal([aliceKeyHash]); 40 | }); 41 | 42 | it('should return true if a key has a given purpose', async () => { 43 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 44 | 45 | const aliceKeyHash = ethers.utils.keccak256( 46 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 47 | ); 48 | const hasPurpose = await aliceIdentity.keyHasPurpose(aliceKeyHash, 1); 49 | expect(hasPurpose).to.equal(true); 50 | }); 51 | 52 | it('should return false if a key has not a given purpose but is a MANAGEMENT key', async () => { 53 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 54 | 55 | const aliceKeyHash = ethers.utils.keccak256( 56 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 57 | ); 58 | const hasPurpose = await aliceIdentity.keyHasPurpose(aliceKeyHash, 2); 59 | expect(hasPurpose).to.equal(true); 60 | }); 61 | 62 | it('should return false if a key has not a given purpose', async () => { 63 | const { aliceIdentity, bobWallet } = await loadFixture(deployIdentityFixture); 64 | 65 | const bobKeyHash = ethers.utils.keccak256( 66 | ethers.utils.defaultAbiCoder.encode(['address'], [bobWallet.address]) 67 | ); 68 | const hasPurpose = await aliceIdentity.keyHasPurpose(bobKeyHash, 2); 69 | expect(hasPurpose).to.equal(false); 70 | }); 71 | }); 72 | 73 | describe('Add key methods', () => { 74 | describe('when calling as a non-MANAGEMENT key', () => { 75 | it('should revert because the signer is not a MANAGEMENT key', async () => { 76 | const { aliceIdentity, bobWallet } = await loadFixture(deployIdentityFixture); 77 | 78 | const bobKeyHash = ethers.utils.keccak256( 79 | ethers.utils.defaultAbiCoder.encode(['address'], [bobWallet.address]) 80 | ); 81 | await expect( 82 | aliceIdentity.connect(bobWallet).addKey(bobKeyHash, 1, 1) 83 | ).to.be.revertedWith('Permissions: Sender does not have management key'); 84 | }); 85 | }); 86 | 87 | describe('when calling as a MANAGEMENT key', () => { 88 | it('should add the purpose to the existing key', async () => { 89 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 90 | 91 | const aliceKeyHash = ethers.utils.keccak256( 92 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 93 | ); 94 | await aliceIdentity.connect(aliceWallet).addKey(aliceKeyHash, 2, 1); 95 | const aliceKey = await aliceIdentity.getKey(aliceKeyHash); 96 | expect(aliceKey.key).to.equal(aliceKeyHash); 97 | expect(aliceKey.purposes).to.deep.equal([1, 2]); 98 | expect(aliceKey.keyType).to.equal(1); 99 | }); 100 | 101 | it('should add a new key with a purpose', async () => { 102 | const { aliceIdentity, bobWallet, aliceWallet } = await loadFixture(deployIdentityFixture); 103 | 104 | const bobKeyHash = ethers.utils.keccak256( 105 | ethers.utils.defaultAbiCoder.encode(['address'], [bobWallet.address]) 106 | ); 107 | await aliceIdentity.connect(aliceWallet).addKey(bobKeyHash, 1, 1); 108 | const bobKey = await aliceIdentity.getKey(bobKeyHash); 109 | expect(bobKey.key).to.equal(bobKeyHash); 110 | expect(bobKey.purposes).to.deep.equal([1]); 111 | expect(bobKey.keyType).to.equal(1); 112 | }); 113 | 114 | it('should revert because key already has the purpose', async () => { 115 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 116 | 117 | const aliceKeyHash = ethers.utils.keccak256( 118 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 119 | ); 120 | await expect( 121 | aliceIdentity.connect(aliceWallet).addKey(aliceKeyHash, 1, 1) 122 | ).to.be.revertedWith('Conflict: Key already has purpose'); 123 | }); 124 | }); 125 | }); 126 | 127 | describe('Remove key methods', () => { 128 | describe('when calling as a non-MANAGEMENT key', () => { 129 | it('should revert because the signer is not a MANAGEMENT key', async () => { 130 | const { aliceIdentity, aliceWallet, bobWallet } = await loadFixture(deployIdentityFixture); 131 | 132 | const aliceKeyHash = ethers.utils.keccak256( 133 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 134 | ); 135 | await expect( 136 | aliceIdentity.connect(bobWallet).removeKey(aliceKeyHash, 1) 137 | ).to.be.revertedWith('Permissions: Sender does not have management key'); 138 | }); 139 | }); 140 | 141 | describe('when calling as a MANAGEMENT key', () => { 142 | it('should remove the purpose from the existing key', async () => { 143 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 144 | 145 | const aliceKeyHash = ethers.utils.keccak256( 146 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 147 | ); 148 | await aliceIdentity.connect(aliceWallet).removeKey(aliceKeyHash, 1); 149 | const aliceKey = await aliceIdentity.getKey(aliceKeyHash); 150 | expect(aliceKey.key).to.equal('0x0000000000000000000000000000000000000000000000000000000000000000'); 151 | expect(aliceKey.purposes).to.deep.equal([]); 152 | expect(aliceKey.keyType).to.equal(0); 153 | }); 154 | 155 | it('should revert because key does not exists', async () => { 156 | const { aliceIdentity, aliceWallet, bobWallet } = await loadFixture(deployIdentityFixture); 157 | 158 | const bobKeyHash = ethers.utils.keccak256( 159 | ethers.utils.defaultAbiCoder.encode(['address'], [bobWallet.address]) 160 | ); 161 | await expect( 162 | aliceIdentity.connect(aliceWallet).removeKey(bobKeyHash, 2) 163 | ).to.be.revertedWith("NonExisting: Key isn't registered"); 164 | }); 165 | 166 | it('should revert because key does not have the purpose', async () => { 167 | const { aliceIdentity, aliceWallet } = await loadFixture(deployIdentityFixture); 168 | 169 | const aliceKeyHash = ethers.utils.keccak256( 170 | ethers.utils.defaultAbiCoder.encode(['address'], [aliceWallet.address]) 171 | ); 172 | await expect( 173 | aliceIdentity.connect(aliceWallet).removeKey(aliceKeyHash, 2) 174 | ).to.be.revertedWith("NonExisting: Key doesn't have such purpose"); 175 | }); 176 | }); 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /test/proxy.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | import {ethers} from "hardhat"; 3 | import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; 4 | import {deployIdentityFixture} from "./fixtures"; 5 | 6 | describe('Proxy', () => { 7 | it('should revert because implementation is Zero address', async () => { 8 | const [deployerWallet, identityOwnerWallet] = await ethers.getSigners(); 9 | 10 | const IdentityProxy = await ethers.getContractFactory('IdentityProxy'); 11 | await expect(IdentityProxy.connect(deployerWallet).deploy(ethers.constants.AddressZero, identityOwnerWallet.address)).to.be.revertedWith('invalid argument - zero address'); 12 | }); 13 | 14 | it('should revert because implementation is not an identity', async () => { 15 | const [deployerWallet, identityOwnerWallet] = await ethers.getSigners(); 16 | 17 | const claimIssuer = await ethers.deployContract('Test'); 18 | 19 | const authority = await ethers.deployContract('ImplementationAuthority', [claimIssuer.address]); 20 | 21 | const IdentityProxy = await ethers.getContractFactory('IdentityProxy'); 22 | await expect(IdentityProxy.connect(deployerWallet).deploy(authority.address, identityOwnerWallet.address)).to.be.revertedWith('Initialization failed.'); 23 | }); 24 | 25 | it('should revert because initial key is Zero address', async () => { 26 | const [deployerWallet] = await ethers.getSigners(); 27 | 28 | const implementation = await ethers.deployContract('Identity', [deployerWallet.address, true]); 29 | const implementationAuthority = await ethers.deployContract('ImplementationAuthority', [implementation.address]); 30 | 31 | const IdentityProxy = await ethers.getContractFactory('IdentityProxy'); 32 | await expect(IdentityProxy.connect(deployerWallet).deploy(implementationAuthority.address, ethers.constants.AddressZero)).to.be.revertedWith('invalid argument - zero address'); 33 | }); 34 | 35 | it('should prevent creating an implementation authority with a zero address implementation', async () => { 36 | const [deployerWallet] = await ethers.getSigners(); 37 | 38 | const ImplementationAuthority = await ethers.getContractFactory('ImplementationAuthority'); 39 | await expect(ImplementationAuthority.connect(deployerWallet).deploy(ethers.constants.AddressZero)).to.be.revertedWith('invalid argument - zero address'); 40 | }); 41 | 42 | it('should prevent updating to a Zero address implementation', async () => { 43 | const {implementationAuthority, deployerWallet} = await loadFixture(deployIdentityFixture); 44 | 45 | await expect(implementationAuthority.connect(deployerWallet).updateImplementation(ethers.constants.AddressZero)).to.be.revertedWith('invalid argument - zero address'); 46 | }); 47 | 48 | it('should prevent updating when not owner', async () => { 49 | const {implementationAuthority, aliceWallet} = await loadFixture(deployIdentityFixture); 50 | 51 | await expect(implementationAuthority.connect(aliceWallet).updateImplementation(ethers.constants.AddressZero)).to.be.revertedWith('Ownable: caller is not the owner'); 52 | }); 53 | 54 | it('should update the implementation address', async () => { 55 | const [deployerWallet] = await ethers.getSigners(); 56 | 57 | const implementation = await ethers.deployContract('Identity', [deployerWallet.address, true]); 58 | const implementationAuthority = await ethers.deployContract('ImplementationAuthority', [implementation.address]); 59 | 60 | const newImplementation = await ethers.deployContract('Identity', [deployerWallet.address, true]); 61 | 62 | const tx = await implementationAuthority.connect(deployerWallet).updateImplementation(newImplementation.address); 63 | await expect(tx).to.emit(implementationAuthority, 'UpdatedImplementation').withArgs(newImplementation.address); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/verifiers/verifier-user.test.ts: -------------------------------------------------------------------------------- 1 | import {ethers} from "hardhat"; 2 | import {expect} from "chai"; 3 | 4 | describe('VerifierUser', () => { 5 | describe('when calling a verified function not as an identity', () => { 6 | it('should revert', async () => { 7 | const verifierUser = await ethers.deployContract('VerifierUser', []); 8 | 9 | await verifierUser.addClaimTopic(666); 10 | 11 | await expect(verifierUser.doSomething()).to.be.reverted; 12 | }); 13 | }); 14 | 15 | describe('when identity is verified', () => { 16 | it('should return', async () => { 17 | const [deployer, aliceWallet, claimIssuerWallet] = await ethers.getSigners(); 18 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [claimIssuerWallet.address]); 19 | const aliceIdentity = await ethers.deployContract('Identity', [aliceWallet.address, false]); 20 | const verifierUser = await ethers.deployContract('VerifierUser', []); 21 | 22 | await verifierUser.addClaimTopic(666); 23 | await verifierUser.addTrustedIssuer(claimIssuer.address, [666]); 24 | 25 | const aliceClaim666 = { 26 | id: '', 27 | identity: aliceIdentity.address, 28 | issuer: claimIssuer.address, 29 | topic: 666, 30 | scheme: 1, 31 | data: '0x0042', 32 | signature: '', 33 | uri: 'https://example.com', 34 | }; 35 | aliceClaim666.signature = await claimIssuerWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.data])))); 36 | await aliceIdentity.connect(aliceWallet).addClaim( 37 | aliceClaim666.topic, 38 | aliceClaim666.scheme, 39 | aliceClaim666.issuer, 40 | aliceClaim666.signature, 41 | aliceClaim666.data, 42 | aliceClaim666.uri, 43 | ); 44 | 45 | const action = { 46 | to: verifierUser.address, 47 | value: 0, 48 | data: new ethers.utils.Interface(['function doSomething()']).encodeFunctionData('doSomething'), 49 | }; 50 | 51 | const tx = await aliceIdentity.connect(aliceWallet).execute( 52 | action.to, 53 | action.value, 54 | action.data, 55 | ); 56 | expect(tx).to.emit(aliceIdentity, 'Executed'); 57 | }); 58 | }); 59 | 60 | describe('when identity is not verified', () => { 61 | it('should revert', async () => { 62 | const [deployer, aliceWallet, claimIssuerWallet] = await ethers.getSigners(); 63 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [claimIssuerWallet.address]); 64 | const aliceIdentity = await ethers.deployContract('Identity', [aliceWallet.address, false]); 65 | const verifierUser = await ethers.deployContract('VerifierUser', []); 66 | 67 | await verifierUser.addClaimTopic(666); 68 | await verifierUser.addTrustedIssuer(claimIssuer.address, [666]); 69 | 70 | const aliceClaim666 = { 71 | id: '', 72 | identity: aliceIdentity.address, 73 | issuer: claimIssuer.address, 74 | topic: 666, 75 | scheme: 1, 76 | data: '0x0042', 77 | signature: '', 78 | uri: 'https://example.com', 79 | }; 80 | aliceClaim666.signature = await claimIssuerWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.data])))); 81 | await aliceIdentity.connect(aliceWallet).addClaim( 82 | aliceClaim666.topic, 83 | aliceClaim666.scheme, 84 | aliceClaim666.issuer, 85 | aliceClaim666.signature, 86 | aliceClaim666.data, 87 | aliceClaim666.uri, 88 | ); 89 | 90 | await claimIssuer.connect(claimIssuerWallet).revokeClaimBySignature(aliceClaim666.signature); 91 | 92 | const action = { 93 | to: verifierUser.address, 94 | value: 0, 95 | data: new ethers.utils.Interface(['function doSomething()']).encodeFunctionData('doSomething'), 96 | }; 97 | 98 | const tx = await aliceIdentity.connect(aliceWallet).execute( 99 | action.to, 100 | action.value, 101 | action.data, 102 | ); 103 | expect(tx).to.emit(aliceIdentity, 'ExecutionFailed'); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/verifiers/verifier.test.ts: -------------------------------------------------------------------------------- 1 | import {ethers} from "hardhat"; 2 | import {expect} from "chai"; 3 | 4 | 5 | describe('Verifier', () => { 6 | describe('.verify()', () => { 7 | describe('when the Verifier does expect claim topics', () => { 8 | it('should return true', async () => { 9 | const [deployer, aliceWallet] = await ethers.getSigners(); 10 | const verifier = await ethers.deployContract('Verifier'); 11 | 12 | await expect(verifier.verify(aliceWallet.address)).to.eventually.be.true; 13 | }); 14 | }); 15 | 16 | describe('when the Verifier expect one claim topic but has no trusted issuers', () => { 17 | it('should return false', async () => { 18 | const [deployer, aliceWallet] = await ethers.getSigners(); 19 | const verifier = await ethers.deployContract('Verifier'); 20 | await verifier.addClaimTopic(ethers.utils.formatBytes32String('SOME_TOPIC')); 21 | 22 | await expect(verifier.verify(aliceWallet.address)).to.eventually.be.false; 23 | }); 24 | }); 25 | 26 | describe('when the Verifier expect one claim topic and a trusted issuer for another topic', () => { 27 | it('should return false', async () => { 28 | const [deployer, aliceWallet] = await ethers.getSigners(); 29 | const verifier = await ethers.deployContract('Verifier'); 30 | await verifier.addClaimTopic(ethers.utils.formatBytes32String('SOME_TOPIC')); 31 | await verifier.addTrustedIssuer(deployer.address, [ethers.utils.formatBytes32String('SOME_OTHER_TOPIC')]); 32 | 33 | await expect(verifier.verify(aliceWallet.address)).to.eventually.be.false; 34 | }); 35 | }); 36 | 37 | describe('when the Verifier expect one claim topic and a trusted issuer for the topic', () => { 38 | describe('when the identity does not have the claim', () => { 39 | it('should return false', async () => { 40 | const [deployer, aliceWallet, claimIssuerWallet] = await ethers.getSigners(); 41 | const verifier = await ethers.deployContract('Verifier'); 42 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [claimIssuerWallet.address]); 43 | const aliceIdentity = await ethers.deployContract('Identity', [aliceWallet.address, false]); 44 | await verifier.addClaimTopic(ethers.utils.formatBytes32String('SOME_TOPIC')); 45 | await verifier.addTrustedIssuer(claimIssuer.address, [ethers.utils.formatBytes32String('SOME_TOPIC')]); 46 | 47 | await expect(verifier.verify(aliceIdentity.address)).to.eventually.be.false; 48 | }); 49 | }); 50 | 51 | describe('when the identity does not have a valid expected claim', () => { 52 | it('should return false', async () => { 53 | const [deployer, aliceWallet, claimIssuerWallet] = await ethers.getSigners(); 54 | const verifier = await ethers.deployContract('Verifier'); 55 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [claimIssuerWallet.address]); 56 | const aliceIdentity = await ethers.deployContract('Identity', [aliceWallet.address, false]); 57 | 58 | await verifier.addClaimTopic(666); 59 | await verifier.addTrustedIssuer(claimIssuer.address, [666]); 60 | 61 | const aliceClaim666 = { 62 | id: '', 63 | identity: aliceIdentity.address, 64 | issuer: claimIssuer.address, 65 | topic: 666, 66 | scheme: 1, 67 | data: '0x0042', 68 | signature: '', 69 | uri: 'https://example.com', 70 | }; 71 | aliceClaim666.signature = await claimIssuerWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.data])))); 72 | await aliceIdentity.connect(aliceWallet).addClaim( 73 | aliceClaim666.topic, 74 | aliceClaim666.scheme, 75 | aliceClaim666.issuer, 76 | aliceClaim666.signature, 77 | aliceClaim666.data, 78 | aliceClaim666.uri, 79 | ); 80 | await claimIssuer.connect(claimIssuerWallet).revokeClaimBySignature( 81 | aliceClaim666.signature, 82 | ); 83 | 84 | await expect(verifier.verify(aliceIdentity.address)).to.eventually.be.false; 85 | }); 86 | }); 87 | 88 | describe('when the identity has the valid expected claim', () => { 89 | it('should return true', async () => { 90 | const [deployer, aliceWallet, claimIssuerWallet] = await ethers.getSigners(); 91 | const verifier = await ethers.deployContract('Verifier'); 92 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [claimIssuerWallet.address]); 93 | const aliceIdentity = await ethers.deployContract('Identity', [aliceWallet.address, false]); 94 | 95 | await verifier.addClaimTopic(666); 96 | await verifier.addTrustedIssuer(claimIssuer.address, [666]); 97 | 98 | const aliceClaim666 = { 99 | id: '', 100 | identity: aliceIdentity.address, 101 | issuer: claimIssuer.address, 102 | topic: 666, 103 | scheme: 1, 104 | data: '0x0042', 105 | signature: '', 106 | uri: 'https://example.com', 107 | }; 108 | aliceClaim666.signature = await claimIssuerWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.data])))); 109 | await aliceIdentity.connect(aliceWallet).addClaim( 110 | aliceClaim666.topic, 111 | aliceClaim666.scheme, 112 | aliceClaim666.issuer, 113 | aliceClaim666.signature, 114 | aliceClaim666.data, 115 | aliceClaim666.uri, 116 | ); 117 | 118 | await expect(verifier.verify(aliceIdentity.address)).to.eventually.be.true; 119 | }); 120 | }); 121 | }); 122 | 123 | describe('when the Verifier expect multiple claim topics and allow multiple trusted issuers', () => { 124 | describe('when identity is compliant', () => { 125 | it('should return true', async () => { 126 | const [deployer, aliceWallet, claimIssuerAWallet, claimIssuerBWallet, claimIssuerCWallet] = await ethers.getSigners(); 127 | const verifier = await ethers.deployContract('Verifier'); 128 | const claimIssuerA = await ethers.deployContract('ClaimIssuer', [claimIssuerAWallet.address]); 129 | const claimIssuerB = await ethers.deployContract('ClaimIssuer', [claimIssuerBWallet.address]); 130 | const claimIssuerC = await ethers.deployContract('ClaimIssuer', [claimIssuerCWallet.address]); 131 | const aliceIdentity = await ethers.deployContract('Identity', [aliceWallet.address, false]); 132 | 133 | await verifier.addClaimTopic(666); 134 | await verifier.addTrustedIssuer(claimIssuerA.address, [666]); 135 | await verifier.addClaimTopic(42); 136 | await verifier.addTrustedIssuer(claimIssuerB.address, [42, 666]); 137 | 138 | const aliceClaim666C = { 139 | id: '', 140 | identity: aliceIdentity.address, 141 | issuer: claimIssuerC.address, 142 | topic: 666, 143 | scheme: 1, 144 | data: '0x0042', 145 | signature: '', 146 | uri: 'https://example.com', 147 | }; 148 | aliceClaim666C.signature = await claimIssuerCWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim666C.identity, aliceClaim666C.topic, aliceClaim666C.data])))); 149 | await aliceIdentity.connect(aliceWallet).addClaim( 150 | aliceClaim666C.topic, 151 | aliceClaim666C.scheme, 152 | aliceClaim666C.issuer, 153 | aliceClaim666C.signature, 154 | aliceClaim666C.data, 155 | aliceClaim666C.uri, 156 | ); 157 | 158 | const aliceClaim666 = { 159 | id: '', 160 | identity: aliceIdentity.address, 161 | issuer: claimIssuerA.address, 162 | topic: 666, 163 | scheme: 1, 164 | data: '0x0042', 165 | signature: '', 166 | uri: 'https://example.com', 167 | }; 168 | aliceClaim666.signature = await claimIssuerAWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.data])))); 169 | await aliceIdentity.connect(aliceWallet).addClaim( 170 | aliceClaim666.topic, 171 | aliceClaim666.scheme, 172 | aliceClaim666.issuer, 173 | aliceClaim666.signature, 174 | aliceClaim666.data, 175 | aliceClaim666.uri, 176 | ); 177 | 178 | const aliceClaim666B = { 179 | id: '', 180 | identity: aliceIdentity.address, 181 | issuer: claimIssuerB.address, 182 | topic: 666, 183 | scheme: 1, 184 | data: '0x0066', 185 | signature: '', 186 | uri: 'https://example.com/B/666', 187 | }; 188 | aliceClaim666B.signature = await claimIssuerBWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim666B.identity, aliceClaim666B.topic, aliceClaim666B.data])))); 189 | await aliceIdentity.connect(aliceWallet).addClaim( 190 | aliceClaim666B.topic, 191 | aliceClaim666B.scheme, 192 | aliceClaim666B.issuer, 193 | aliceClaim666B.signature, 194 | aliceClaim666B.data, 195 | aliceClaim666B.uri, 196 | ); 197 | 198 | const aliceClaim42 = { 199 | id: '', 200 | identity: aliceIdentity.address, 201 | issuer: claimIssuerB.address, 202 | topic: 42, 203 | scheme: 1, 204 | data: '0x0010', 205 | signature: '', 206 | uri: 'https://example.com/42', 207 | }; 208 | aliceClaim42.signature = await claimIssuerBWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim42.identity, aliceClaim42.topic, aliceClaim42.data])))); 209 | await aliceIdentity.connect(aliceWallet).addClaim( 210 | aliceClaim42.topic, 211 | aliceClaim42.scheme, 212 | aliceClaim42.issuer, 213 | aliceClaim42.signature, 214 | aliceClaim42.data, 215 | aliceClaim42.uri, 216 | ); 217 | 218 | await claimIssuerB.connect(claimIssuerBWallet).revokeClaimBySignature(aliceClaim666B.signature); 219 | 220 | await expect(verifier.verify(aliceIdentity.address)).to.eventually.be.true; 221 | }); 222 | }); 223 | 224 | describe('when identity is not compliant', () => { 225 | it('should return false', async () => { 226 | const [deployer, aliceWallet, claimIssuerAWallet, claimIssuerBWallet] = await ethers.getSigners(); 227 | const verifier = await ethers.deployContract('Verifier'); 228 | const claimIssuerA = await ethers.deployContract('ClaimIssuer', [claimIssuerAWallet.address]); 229 | const claimIssuerB = await ethers.deployContract('ClaimIssuer', [claimIssuerBWallet.address]); 230 | const aliceIdentity = await ethers.deployContract('Identity', [aliceWallet.address, false]); 231 | 232 | await verifier.addClaimTopic(666); 233 | await verifier.addTrustedIssuer(claimIssuerA.address, [666]); 234 | await verifier.addClaimTopic(42); 235 | await verifier.addTrustedIssuer(claimIssuerB.address, [42, 666]); 236 | 237 | const aliceClaim666 = { 238 | id: '', 239 | identity: aliceIdentity.address, 240 | issuer: claimIssuerA.address, 241 | topic: 666, 242 | scheme: 1, 243 | data: '0x0042', 244 | signature: '', 245 | uri: 'https://example.com', 246 | }; 247 | aliceClaim666.signature = await claimIssuerAWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim666.identity, aliceClaim666.topic, aliceClaim666.data])))); 248 | await aliceIdentity.connect(aliceWallet).addClaim( 249 | aliceClaim666.topic, 250 | aliceClaim666.scheme, 251 | aliceClaim666.issuer, 252 | aliceClaim666.signature, 253 | aliceClaim666.data, 254 | aliceClaim666.uri, 255 | ); 256 | 257 | const aliceClaim666B = { 258 | id: '', 259 | identity: aliceIdentity.address, 260 | issuer: claimIssuerB.address, 261 | topic: 666, 262 | scheme: 1, 263 | data: '0x0066', 264 | signature: '', 265 | uri: 'https://example.com/B/666', 266 | }; 267 | aliceClaim666B.signature = await claimIssuerBWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim666B.identity, aliceClaim666B.topic, aliceClaim666B.data])))); 268 | await aliceIdentity.connect(aliceWallet).addClaim( 269 | aliceClaim666B.topic, 270 | aliceClaim666B.scheme, 271 | aliceClaim666B.issuer, 272 | aliceClaim666B.signature, 273 | aliceClaim666B.data, 274 | aliceClaim666B.uri, 275 | ); 276 | 277 | const aliceClaim42 = { 278 | id: '', 279 | identity: aliceIdentity.address, 280 | issuer: claimIssuerB.address, 281 | topic: 42, 282 | scheme: 1, 283 | data: '0x0010', 284 | signature: '', 285 | uri: 'https://example.com/42', 286 | }; 287 | aliceClaim42.signature = await claimIssuerBWallet.signMessage(ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(['address', 'uint256', 'bytes'], [aliceClaim42.identity, aliceClaim42.topic, aliceClaim42.data])))); 288 | await aliceIdentity.connect(aliceWallet).addClaim( 289 | aliceClaim42.topic, 290 | aliceClaim42.scheme, 291 | aliceClaim42.issuer, 292 | aliceClaim42.signature, 293 | aliceClaim42.data, 294 | aliceClaim42.uri, 295 | ); 296 | 297 | await claimIssuerB.connect(claimIssuerBWallet).revokeClaimBySignature(aliceClaim42.signature); 298 | 299 | await expect(verifier.verify(aliceIdentity.address)).to.eventually.be.false; 300 | }); 301 | }); 302 | }); 303 | }); 304 | 305 | describe('.removeClaimTopic', () => { 306 | describe('when not called by the owner', () => { 307 | it('should revert', async () => { 308 | const [deployer, aliceWallet] = await ethers.getSigners(); 309 | const verifier = await ethers.deployContract('Verifier'); 310 | 311 | await expect(verifier.connect(aliceWallet).removeClaimTopic(2)).to.be.revertedWith('Ownable: caller is not the owner'); 312 | }); 313 | }); 314 | 315 | describe('when called by the owner', () => { 316 | it('should remove the claim topic', async () => { 317 | const [deployer] = await ethers.getSigners(); 318 | const verifier = await ethers.deployContract('Verifier'); 319 | await verifier.addClaimTopic(1); 320 | await verifier.addClaimTopic(2); 321 | await verifier.addClaimTopic(3); 322 | 323 | const tx = await verifier.removeClaimTopic(2); 324 | await expect(tx).to.emit(verifier, 'ClaimTopicRemoved').withArgs(2); 325 | expect(await verifier.isClaimTopicRequired(1)).to.be.true; 326 | expect(await verifier.isClaimTopicRequired(2)).to.be.false; 327 | expect(await verifier.isClaimTopicRequired(3)).to.be.true; 328 | }); 329 | }); 330 | }); 331 | 332 | describe('.removeTrustedIssuer', () => { 333 | describe('when not called by the owner', () => { 334 | it('should revert', async () => { 335 | const [deployer, aliceWallet] = await ethers.getSigners(); 336 | const verifier = await ethers.deployContract('Verifier'); 337 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 338 | 339 | await expect(verifier.connect(aliceWallet).removeTrustedIssuer(claimIssuer.address)).to.be.revertedWith('Ownable: caller is not the owner'); 340 | }); 341 | }); 342 | 343 | describe('when called by the owner', () => { 344 | it('should remove the trusted issuer', async () => { 345 | const [deployer, aliceWallet] = await ethers.getSigners(); 346 | const verifier = await ethers.deployContract('Verifier'); 347 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 348 | const claimIssuerB = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 349 | await verifier.addTrustedIssuer(claimIssuer.address, [1]); 350 | await verifier.addTrustedIssuer(claimIssuerB.address, [2]); 351 | 352 | const tx = await verifier.removeTrustedIssuer(claimIssuer.address); 353 | await expect(tx).to.emit(verifier, 'TrustedIssuerRemoved').withArgs(claimIssuer.address); 354 | expect(await verifier.isTrustedIssuer(claimIssuer.address)).to.be.false; 355 | expect(await verifier.getTrustedIssuers()).to.be.deep.equal([claimIssuerB.address]); 356 | }); 357 | }); 358 | 359 | describe('when issuer address is zero', () => { 360 | it('should revert', async () => { 361 | const [deployer] = await ethers.getSigners(); 362 | const verifier = await ethers.deployContract('Verifier'); 363 | 364 | await expect(verifier.removeTrustedIssuer(ethers.constants.AddressZero)).to.be.revertedWith('invalid argument - zero address'); 365 | }); 366 | }); 367 | 368 | describe('when issuer is not trusted', () => { 369 | it('should revert', async () => { 370 | const [deployer, aliceWallet] = await ethers.getSigners(); 371 | const verifier = await ethers.deployContract('Verifier'); 372 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 373 | 374 | await expect(verifier.removeTrustedIssuer(claimIssuer.address)).to.be.revertedWith('NOT a trusted issuer'); 375 | }); 376 | }); 377 | }); 378 | 379 | describe('.addTrustedIssuer', () => { 380 | describe('when not called by the owner', () => { 381 | it('should revert', async () => { 382 | const [deployer, aliceWallet] = await ethers.getSigners(); 383 | const verifier = await ethers.deployContract('Verifier'); 384 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 385 | 386 | await expect(verifier.connect(aliceWallet).addTrustedIssuer(claimIssuer.address, [1])).to.be.revertedWith('Ownable: caller is not the owner'); 387 | }); 388 | }); 389 | 390 | describe('when issuer address is the zero', () => { 391 | it('should revert', async () => { 392 | const [deployer] = await ethers.getSigners(); 393 | const verifier = await ethers.deployContract('Verifier'); 394 | 395 | await expect(verifier.addTrustedIssuer(ethers.constants.AddressZero, [1])).to.be.revertedWith('invalid argument - zero address'); 396 | }); 397 | }); 398 | 399 | describe('when issuer is already trusted', () => { 400 | it('should revert', async () => { 401 | const [deployer, aliceWallet] = await ethers.getSigners(); 402 | const verifier = await ethers.deployContract('Verifier'); 403 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 404 | await verifier.addTrustedIssuer(claimIssuer.address, [1]); 405 | 406 | await expect(verifier.addTrustedIssuer(claimIssuer.address, [2])).to.be.revertedWith('trusted Issuer already exists'); 407 | }); 408 | }); 409 | 410 | describe('when claim topics array is empty', () => { 411 | it('should revert', async () => { 412 | const [deployer] = await ethers.getSigners(); 413 | const verifier = await ethers.deployContract('Verifier'); 414 | 415 | await expect(verifier.addTrustedIssuer(deployer.address, [])).to.be.revertedWith('trusted claim topics cannot be empty'); 416 | }); 417 | }); 418 | 419 | describe('when claim topics array contains more than 15 topics', () => { 420 | it('should revert', async () => { 421 | const [deployer] = await ethers.getSigners(); 422 | const verifier = await ethers.deployContract('Verifier'); 423 | 424 | await expect(verifier.addTrustedIssuer(deployer.address, [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16])).to.be.revertedWith('cannot have more than 15 claim topics'); 425 | }); 426 | }); 427 | 428 | describe('when adding a 51th trusted issuer', () => { 429 | it('should revert', async () => { 430 | const [deployer] = await ethers.getSigners(); 431 | const verifier = await ethers.deployContract('Verifier'); 432 | for (let i = 0; i < 50; i++) { 433 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [deployer.address]); 434 | await verifier.addTrustedIssuer(claimIssuer.address, [1]); 435 | } 436 | 437 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [deployer.address]); 438 | await expect(verifier.addTrustedIssuer(claimIssuer.address, [1])).to.be.revertedWith('cannot have more than 50 trusted issuers'); 439 | }); 440 | }); 441 | }); 442 | 443 | describe('.updateIssuerClaimTopics', () => { 444 | describe('when not called by the owner', () => { 445 | it('should revert', async () => { 446 | const [deployer, aliceWallet] = await ethers.getSigners(); 447 | const verifier = await ethers.deployContract('Verifier'); 448 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 449 | 450 | await expect(verifier.connect(aliceWallet).updateIssuerClaimTopics(claimIssuer.address, [1])).to.be.revertedWith('Ownable: caller is not the owner'); 451 | }); 452 | }); 453 | 454 | describe('when called by the owner', () => { 455 | it('should update the issuer claim topics', async () => { 456 | const [deployer, aliceWallet] = await ethers.getSigners(); 457 | const verifier = await ethers.deployContract('Verifier'); 458 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 459 | await verifier.addTrustedIssuer(claimIssuer.address, [1]); 460 | 461 | const tx = await verifier.updateIssuerClaimTopics(claimIssuer.address, [2, 3]); 462 | await expect(tx).to.emit(verifier, 'ClaimTopicsUpdated').withArgs(claimIssuer.address, [2, 3]); 463 | expect(await verifier.isTrustedIssuer(claimIssuer.address)).to.be.true; 464 | expect(await verifier.getTrustedIssuersForClaimTopic(1)).to.be.empty; 465 | expect(await verifier.getTrustedIssuerClaimTopics(claimIssuer.address)).to.be.deep.equal([2, 3]); 466 | expect(await verifier.hasClaimTopic(claimIssuer.address, 2)).to.be.true; 467 | expect(await verifier.hasClaimTopic(claimIssuer.address, 1)).to.be.false; 468 | }); 469 | }); 470 | 471 | describe('when issuer address is the zero address', () => { 472 | it('should revert', async () => { 473 | const verifier = await ethers.deployContract('Verifier'); 474 | 475 | await expect(verifier.updateIssuerClaimTopics(ethers.constants.AddressZero, [1])).to.be.revertedWith('invalid argument - zero address'); 476 | }); 477 | }); 478 | 479 | describe('when issuer is not trusted', () => { 480 | it('should revert', async () => { 481 | const [deployer, aliceWallet] = await ethers.getSigners(); 482 | const verifier = await ethers.deployContract('Verifier'); 483 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 484 | 485 | await expect(verifier.updateIssuerClaimTopics(claimIssuer.address, [1])).to.be.revertedWith('NOT a trusted issuer'); 486 | }); 487 | }); 488 | 489 | describe('when list of topics contains more than 15 topics', () => { 490 | it('should revert', async () => { 491 | const [deployer, aliceWallet] = await ethers.getSigners(); 492 | const verifier = await ethers.deployContract('Verifier'); 493 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 494 | await verifier.addTrustedIssuer(claimIssuer.address, [1]); 495 | 496 | await expect(verifier.updateIssuerClaimTopics(claimIssuer.address, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])).to.be.revertedWith('cannot have more than 15 claim topics'); 497 | }); 498 | }); 499 | 500 | describe('when list of topics is empty', () => { 501 | it('should revert', async () => { 502 | const [deployer, aliceWallet] = await ethers.getSigners(); 503 | const verifier = await ethers.deployContract('Verifier'); 504 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 505 | await verifier.addTrustedIssuer(claimIssuer.address, [1]); 506 | 507 | await expect(verifier.updateIssuerClaimTopics(claimIssuer.address, [])).to.be.revertedWith('claim topics cannot be empty'); 508 | }); 509 | }); 510 | }); 511 | 512 | describe('.getTrustedIssuerClaimTopic', () => { 513 | describe('when issuer is not trusted', () => { 514 | it('should revert', async () => { 515 | const [deployer, aliceWallet] = await ethers.getSigners(); 516 | const verifier = await ethers.deployContract('Verifier'); 517 | const claimIssuer = await ethers.deployContract('ClaimIssuer', [aliceWallet.address]); 518 | 519 | await expect(verifier.getTrustedIssuerClaimTopics(claimIssuer.address)).to.be.revertedWith('trusted Issuer doesn\'t exist'); 520 | }); 521 | }); 522 | }); 523 | }); 524 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------