├── .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 | 
2 | ---
3 |
4 | 
5 | 
6 | 
7 | 
8 | 
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 |
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 |
--------------------------------------------------------------------------------