diff --git a/contracts/Identity.sol b/contracts/Identity.sol index 3397105..e601e28 100644 --- a/contracts/Identity.sol +++ b/contracts/Identity.sol @@ -25,12 +25,6 @@ import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableS * - Separation of key and claim storage for better organization * - Upgradeable version management through ERC-7201 storage slots * - * The contract supports four key purposes: - * - MANAGEMENT: Keys that can manage the identity - * - ACTION: Keys that can perform actions on behalf of the identity - * - CLAIM_SIGNER: Keys that can sign claims for other identities - * - ENCRYPTION: Keys used for data encryption - * * @custom:security This contract uses ERC-7201 storage slots to prevent storage collision attacks * in upgradeable contracts. */ diff --git a/contracts/KeyManager.sol b/contracts/KeyManager.sol index 485e8fd..8bd65c9 100644 --- a/contracts/KeyManager.sol +++ b/contracts/KeyManager.sol @@ -78,14 +78,10 @@ contract KeyManager is IERC734 { } /** - * @dev See {IERC734-execute}. + * @inheritdoc IERC734 * @notice Passes an execution instruction to the keymanager. * - * Execution flow: - * 1. If the sender is an ACTION key and the destination is external, execution is auto-approved - * 2. If the sender is a MANAGEMENT key, execution is auto-approved for any destination - * 3. If the sender is a CLAIM_SIGNER key and the call is to addClaim, execution is auto-approved - * 4. Otherwise, the execution request must be approved via the `approve` method + * Access control: Only addresses with a MANAGEMENT, ACTION, or PROPOSER key can call this function. * * @param _to The destination address for the execution * @param _value The amount of ETH to send with the execution @@ -99,6 +95,14 @@ contract KeyManager is IERC734 { returns (uint256 executionId) { KeyStorage storage ks = _getKeyStorage(); + + bytes32 senderKey = keccak256(abi.encode(msg.sender)); + require( + keyHasPurpose(senderKey, KeyPurposes.MANAGEMENT) || keyHasPurpose(senderKey, KeyPurposes.ACTION) + || keyHasPurpose(senderKey, KeyPurposes.PROPOSER), + Errors.SenderCannotPropose() + ); + uint256 _executionId = ks.executionNonce; ks.executions[_executionId].to = _to; ks.executions[_executionId].value = _value; @@ -171,18 +175,10 @@ contract KeyManager is IERC734 { } /** - * @dev See {IERC734-addKey}. - * @notice implementation of the addKey function of the ERC-734 standard - * Adds a _key to the identity. The _purpose specifies the purpose of key. Initially we propose four purposes: - * 1: MANAGEMENT keys, which can manage the identity - * 2: ACTION keys, which perform actions in this identities name (signing, logins, transactions, etc.) - * 3: CLAIM signer keys, used to sign claims on other identities which need to be revokable. - * 4: ENCRYPTION keys, used to encrypt data e.g. hold in claims. - * MUST only be done by keys of purpose 1, or the identity itself. - * If its the identity itself, the approval process will determine its approval. + * @inheritdoc IERC734 * @param _key keccak256 representation of an ethereum address - * @param _type type of key used, which would be a uint256 for different key types. e.g. 1 = ECDSA, 2 = RSA, etc. - * @param _purpose a uint256 specifying the key type, like 1 = MANAGEMENT, 2 = ACTION, 3 = CLAIM, 4 = ENCRYPTION + * @param _type type of key used (see KeyTypes library) + * @param _purpose specifying the key purpose (see KeyPurposes library) * @return success Returns TRUE if the addition was successful and FALSE if not */ function addKey(bytes32 _key, uint256 _purpose, uint256 _type) diff --git a/contracts/libraries/Errors.sol b/contracts/libraries/Errors.sol index ddd01af..fc0eb0a 100644 --- a/contracts/libraries/Errors.sol +++ b/contracts/libraries/Errors.sol @@ -119,6 +119,9 @@ library Errors { /// @notice The sender does not have the action key. error SenderDoesNotHaveActionKey(); + /// @notice The sender does not have a key that allows proposing executions (MANAGEMENT, ACTION, or PROPOSER). + error SenderCannotPropose(); + /// @notice The initial key was already setup. error InitialKeyAlreadySetup(); diff --git a/contracts/libraries/KeyPurposes.sol b/contracts/libraries/KeyPurposes.sol index 53a0100..b8a464c 100644 --- a/contracts/libraries/KeyPurposes.sol +++ b/contracts/libraries/KeyPurposes.sol @@ -17,4 +17,7 @@ library KeyPurposes { /// @dev 4: ENCRYPTION keys, used to encrypt data e.g. hold in claims. uint256 internal constant ENCRYPTION = 4; + /// @dev 5: PROPOSER keys, which can propose execution requests but cannot approve or auto-execute them. + uint256 internal constant PROPOSER = 5; + } diff --git a/test/claim-issuers/ClaimTo.t.sol b/test/claim-issuers/ClaimTo.t.sol index b600e8a..c068204 100644 --- a/test/claim-issuers/ClaimTo.t.sol +++ b/test/claim-issuers/ClaimTo.t.sol @@ -7,6 +7,7 @@ import { IIdentity } from "contracts/interface/IIdentity.sol"; import { Errors } from "contracts/libraries/Errors.sol"; import { KeyPurposes } from "contracts/libraries/KeyPurposes.sol"; import { KeyTypes } from "contracts/libraries/KeyTypes.sol"; + import { Test as TestContract } from "test/mocks/Test.sol"; /// @notice Test suite for ClaimIssuer.addClaimTo functionality @@ -80,10 +81,11 @@ contract ClaimToTest is OnchainIDSetup { claimIssuer.addClaimTo(999, 1, hex"0099", hex"0099", "https://example.com/new-claim", IIdentity(address(0))); } - /// @notice Without key on aliceIdentity, addClaimTo creates pending execution requiring approval + /// @notice Without management key on aliceIdentity, addClaimTo creates pending execution requiring approval function test_addClaimTo_requiresApproval() public { - // ClaimIssuer does NOT have any key on aliceIdentity - // So the execution requires manual approval from alice + // Add claimIssuer as PROPOSER key so it can call execute (but not auto-approve) + vm.prank(alice); + aliceIdentity.addKey(ClaimSignerHelper.addressToKey(address(claimIssuer)), KeyPurposes.PROPOSER, KeyTypes.ECDSA); uint256 topic = 999; bytes memory data = hex"0099"; string memory uri = "https://example.com/new-claim"; @@ -109,6 +111,10 @@ contract ClaimToTest is OnchainIDSetup { /// @notice Pending execution then owner approves -- full verification of claim fields function test_addClaimTo_pendingThenOwnerApproves() public { + // Add claimIssuer as PROPOSER key so it can call execute (but not auto-approve) + vm.prank(alice); + aliceIdentity.addKey(ClaimSignerHelper.addressToKey(address(claimIssuer)), KeyPurposes.PROPOSER, KeyTypes.ECDSA); + uint256 topic = 999; bytes memory data = hex"0099"; string memory uri = "https://example.com/new-claim"; diff --git a/test/identities/Claims.t.sol b/test/identities/Claims.t.sol index 2a2aaf8..52f2d36 100644 --- a/test/identities/Claims.t.sol +++ b/test/identities/Claims.t.sol @@ -54,7 +54,11 @@ contract ClaimsTest is OnchainIDSetup { Identity.addClaim, (topic, Constants.CLAIM_SCHEME, address(aliceIdentity), signature, data, uri) ); - // Bob (ACTION key) executes + // Give bob a PROPOSER key so he can call execute (creates pending execution) + vm.prank(alice); + aliceIdentity.addKey(keccak256(abi.encode(bob)), KeyPurposes.PROPOSER, KeyTypes.ECDSA); + + // Bob (PROPOSER key) executes — creates pending request vm.prank(bob); uint256 executionId = aliceIdentity.execute(address(aliceIdentity), 0, actionData); @@ -128,7 +132,11 @@ contract ClaimsTest is OnchainIDSetup { Identity.addClaim, (topic, Constants.CLAIM_SCHEME, address(claimIssuer), signature, data, uri) ); - // Bob (ACTION key) executes + // Give bob a PROPOSER key so he can call execute (creates pending execution) + vm.prank(alice); + aliceIdentity.addKey(keccak256(abi.encode(bob)), KeyPurposes.PROPOSER, KeyTypes.ECDSA); + + // Bob (PROPOSER key) executes — creates pending request vm.prank(bob); uint256 executionId = aliceIdentity.execute(address(aliceIdentity), 0, actionData); @@ -211,7 +219,11 @@ contract ClaimsTest is OnchainIDSetup { // Encode removeClaim call bytes memory actionData = abi.encodeCall(Identity.removeClaim, (claimId)); - // Bob (ACTION key) executes + // Give bob a PROPOSER key so he can call execute (creates pending execution) + vm.prank(alice); + aliceIdentity.addKey(keccak256(abi.encode(bob)), KeyPurposes.PROPOSER, KeyTypes.ECDSA); + + // Bob (PROPOSER key) executes — creates pending request vm.prank(bob); uint256 executionId = aliceIdentity.execute(address(aliceIdentity), 0, actionData); @@ -334,7 +346,7 @@ contract ClaimsTest is OnchainIDSetup { // ============ getClaim ============ /// @notice When claim does not exist, should return empty struct - function test_getClaim_nonExistent_shouldReturnEmpty() public { + function test_getClaim_nonExistent_shouldReturnEmpty() public view { bytes32 claimId = ClaimSignerHelper.computeClaimId(address(claimIssuer), Constants.CLAIM_TOPIC_42); (uint256 topic, uint256 scheme, address issuer, bytes memory signature, bytes memory data, string memory uri) = @@ -349,7 +361,7 @@ contract ClaimsTest is OnchainIDSetup { } /// @notice When claim exists, should return correct data - function test_getClaim_existing_shouldReturnData() public { + function test_getClaim_existing_shouldReturnData() public view { // Use the pre-built aliceClaim666 from setup (uint256 topic, uint256 scheme, address issuer, bytes memory signature, bytes memory data, string memory uri) = aliceIdentity.getClaim(aliceClaim666.id); @@ -404,13 +416,13 @@ contract ClaimsTest is OnchainIDSetup { // ============ getClaimIdsByTopic ============ /// @notice When no claims exist for topic, should return empty array - function test_getClaimIdsByTopic_empty_shouldReturnEmpty() public { + function test_getClaimIdsByTopic_empty_shouldReturnEmpty() public view { bytes32[] memory claimIds = aliceIdentity.getClaimIdsByTopic(101010); assertEq(claimIds.length, 0, "Should return empty array"); } /// @notice When claims exist for topic, should return array of claim IDs - function test_getClaimIdsByTopic_hasClaims_shouldReturnIds() public { + function test_getClaimIdsByTopic_hasClaims_shouldReturnIds() public view { // Use the pre-built aliceClaim666 from setup bytes32[] memory claimIds = aliceIdentity.getClaimIdsByTopic(aliceClaim666.topic); diff --git a/test/identities/Executions.t.sol b/test/identities/Executions.t.sol index 2b244dd..7fb2f17 100644 --- a/test/identities/Executions.t.sol +++ b/test/identities/Executions.t.sol @@ -49,6 +49,10 @@ contract ExecutionsTest is OnchainIDSetup { function test_getExecutionData_pendingExecution() public { vm.deal(bob, 1 ether); + // Give bob a PROPOSER key on alice's identity so he can call execute + vm.prank(alice); + aliceIdentity.addKey(keccak256(abi.encode(bob)), KeyPurposes.PROPOSER, KeyTypes.ECDSA); + uint256 executionId = aliceIdentity.getCurrentNonce(); vm.prank(bob); aliceIdentity.execute{ value: 10 }(carol, 10, hex"123456"); @@ -103,7 +107,9 @@ contract ExecutionsTest is OnchainIDSetup { } function test_nestedExecute_claimIssuerNotManagementKey_pendingExecution() public { - // DON'T add claimIssuer as MANAGEMENT key + // Add claimIssuer as PROPOSER key (not MANAGEMENT) so it can call execute + vm.prank(alice); + aliceIdentity.addKey(keccak256(abi.encode(address(claimIssuer))), KeyPurposes.PROPOSER, KeyTypes.ECDSA); // Build claim ClaimSignerHelper.Claim memory claim = ClaimSignerHelper.buildClaim( @@ -132,7 +138,6 @@ contract ExecutionsTest is OnchainIDSetup { aliceIdentity.approve(0, true); // Verify claim was added - bytes32 claimId = keccak256(abi.encode(claim.issuer, claim.topic)); bytes32[] memory claimIds = aliceIdentity.getClaimIdsByTopic(42); assertEq(claimIds.length, 1); } @@ -237,19 +242,13 @@ contract ExecutionsTest is OnchainIDSetup { assertEq(david.balance, davidBalanceBefore + 10); } - function test_executeAsNonActionKey_pendingRequest() public { + function test_executeAsUnauthorizedKey_reverts() public { vm.deal(bob, 1 ether); - uint256 carolBalanceBefore = carol.balance; - // bob has no keys on aliceIdentity + // bob has no keys on aliceIdentity — should revert vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(Errors.SenderCannotPropose.selector)); aliceIdentity.execute{ value: 10 }(carol, 10, hex""); - - // Verify execution is pending and carol balance unchanged - Structs.Execution memory exec = aliceIdentity.getExecutionData(0); - assertFalse(exec.approved); - assertFalse(exec.executed); - assertEq(carol.balance, carolBalanceBefore); } function test_approveNonExistingExecution() public { @@ -272,17 +271,25 @@ contract ExecutionsTest is OnchainIDSetup { function test_approveAsNonActionKey_forExternalTarget() public { vm.deal(bob, 1 ether); + // Give bob a PROPOSER key so he can call execute + vm.prank(alice); + aliceIdentity.addKey(keccak256(abi.encode(bob)), KeyPurposes.PROPOSER, KeyTypes.ECDSA); + // bob creates pending execution vm.prank(bob); aliceIdentity.execute{ value: 10 }(carol, 10, hex""); - // bob tries to approve (bob has no ACTION key) + // bob tries to approve (bob has PROPOSER but not ACTION key) vm.prank(bob); vm.expectRevert(abi.encodeWithSelector(Errors.SenderDoesNotHaveActionKey.selector, bob)); aliceIdentity.approve(0, true); } function test_approveAsNonManagementKey_forIdentityTarget() public { + // Give bob a PROPOSER key so he can call execute + vm.prank(alice); + aliceIdentity.addKey(keccak256(abi.encode(bob)), KeyPurposes.PROPOSER, KeyTypes.ECDSA); + // bob creates pending execution targeting the identity itself bytes memory addKeyData = abi.encodeCall( KeyManager.addKey, (keccak256(abi.encode(makeAddr("newKey"))), KeyPurposes.ACTION, KeyTypes.ECDSA) @@ -302,6 +309,10 @@ contract ExecutionsTest is OnchainIDSetup { vm.deal(bob, 1 ether); uint256 carolBalanceBefore = carol.balance; + // Give bob a PROPOSER key so he can call execute + vm.prank(alice); + aliceIdentity.addKey(keccak256(abi.encode(bob)), KeyPurposes.PROPOSER, KeyTypes.ECDSA); + // bob creates pending execution vm.prank(bob); aliceIdentity.execute{ value: 10 }(carol, 10, hex""); @@ -317,6 +328,10 @@ contract ExecutionsTest is OnchainIDSetup { vm.deal(bob, 1 ether); uint256 carolBalanceBefore = carol.balance; + // Give bob a PROPOSER key so he can call execute + vm.prank(alice); + aliceIdentity.addKey(keccak256(abi.encode(bob)), KeyPurposes.PROPOSER, KeyTypes.ECDSA); + // bob creates pending execution vm.prank(bob); aliceIdentity.execute{ value: 10 }(carol, 10, hex""); @@ -334,10 +349,12 @@ contract ExecutionsTest is OnchainIDSetup { } function test_autoApprovalForAddClaimWithClaimSignerKey() public { - // Add bob as CLAIM_SIGNER + // Add bob as CLAIM_SIGNER and PROPOSER (PROPOSER needed to call execute, CLAIM_SIGNER for auto-approval) bytes32 bobKeyHash = keccak256(abi.encode(bob)); - vm.prank(alice); + vm.startPrank(alice); aliceIdentity.addKey(bobKeyHash, KeyPurposes.CLAIM_SIGNER, KeyTypes.ECDSA); + aliceIdentity.addKey(bobKeyHash, KeyPurposes.PROPOSER, KeyTypes.ECDSA); + vm.stopPrank(); // Build claim with claimIssuer as issuer ClaimSignerHelper.Claim memory claim = ClaimSignerHelper.buildClaim( @@ -360,6 +377,172 @@ contract ExecutionsTest is OnchainIDSetup { assertEq(claimIds[0], claimId); } + // ========= Proposer Key Tests ========= + + function test_executeAsProposer_createsPendingExternalRequest() public { + address proposer = makeAddr("proposer"); + bytes32 proposerKeyHash = keccak256(abi.encode(proposer)); + vm.prank(alice); + aliceIdentity.addKey(proposerKeyHash, KeyPurposes.PROPOSER, KeyTypes.ECDSA); + + vm.deal(proposer, 1 ether); + uint256 carolBalanceBefore = carol.balance; + + vm.prank(proposer); + aliceIdentity.execute{ value: 10 }(carol, 10, hex""); + + // Proposer key should NOT auto-approve — execution is pending + Structs.Execution memory exec = aliceIdentity.getExecutionData(0); + assertFalse(exec.approved); + assertFalse(exec.executed); + assertEq(carol.balance, carolBalanceBefore); + } + + function test_executeAsProposer_createsPendingInternalRequest() public { + address proposer = makeAddr("proposer"); + bytes32 proposerKeyHash = keccak256(abi.encode(proposer)); + vm.prank(alice); + aliceIdentity.addKey(proposerKeyHash, KeyPurposes.PROPOSER, KeyTypes.ECDSA); + + bytes memory addKeyData = abi.encodeCall( + KeyManager.addKey, (keccak256(abi.encode(makeAddr("newKey"))), KeyPurposes.ACTION, KeyTypes.ECDSA) + ); + + vm.prank(proposer); + aliceIdentity.execute(address(aliceIdentity), 0, addKeyData); + + // Proposer key should NOT auto-approve internal calls either + Structs.Execution memory exec = aliceIdentity.getExecutionData(0); + assertFalse(exec.approved); + assertFalse(exec.executed); + } + + function test_approveAsProposerOnly_revertsForInternal() public { + address proposer = makeAddr("proposer"); + bytes32 proposerKeyHash = keccak256(abi.encode(proposer)); + vm.prank(alice); + aliceIdentity.addKey(proposerKeyHash, KeyPurposes.PROPOSER, KeyTypes.ECDSA); + + // Proposer creates a pending internal execution + bytes memory addKeyData = abi.encodeCall( + KeyManager.addKey, (keccak256(abi.encode(makeAddr("newKey"))), KeyPurposes.ACTION, KeyTypes.ECDSA) + ); + vm.prank(proposer); + aliceIdentity.execute(address(aliceIdentity), 0, addKeyData); + + // Proposer tries to approve — should revert (no MANAGEMENT key) + vm.prank(proposer); + vm.expectRevert(abi.encodeWithSelector(Errors.SenderDoesNotHaveManagementKey.selector, proposer)); + aliceIdentity.approve(0, true); + } + + function test_approveAsProposerOnly_revertsForExternal() public { + address proposer = makeAddr("proposer"); + bytes32 proposerKeyHash = keccak256(abi.encode(proposer)); + vm.prank(alice); + aliceIdentity.addKey(proposerKeyHash, KeyPurposes.PROPOSER, KeyTypes.ECDSA); + + vm.deal(proposer, 1 ether); + vm.prank(proposer); + aliceIdentity.execute{ value: 10 }(carol, 10, hex""); + + // Proposer tries to approve — should revert (no ACTION key) + vm.prank(proposer); + vm.expectRevert(abi.encodeWithSelector(Errors.SenderDoesNotHaveActionKey.selector, proposer)); + aliceIdentity.approve(0, true); + } + + function test_proposerWithManagement_canAutoApprove() public { + address proposerManager = makeAddr("proposerManager"); + bytes32 keyHash = keccak256(abi.encode(proposerManager)); + vm.startPrank(alice); + aliceIdentity.addKey(keyHash, KeyPurposes.MANAGEMENT, KeyTypes.ECDSA); + aliceIdentity.addKey(keyHash, KeyPurposes.PROPOSER, KeyTypes.ECDSA); + vm.stopPrank(); + + vm.deal(proposerManager, 1 ether); + uint256 carolBalanceBefore = carol.balance; + + vm.prank(proposerManager); + aliceIdentity.execute{ value: 10 }(carol, 10, hex""); + + // MANAGEMENT takes precedence — should auto-approve + Structs.Execution memory exec = aliceIdentity.getExecutionData(0); + assertTrue(exec.approved); + assertTrue(exec.executed); + assertEq(carol.balance, carolBalanceBefore + 10); + } + + function test_proposerWithAction_canAutoApproveExternal() public { + address proposerAction = makeAddr("proposerAction"); + bytes32 keyHash = keccak256(abi.encode(proposerAction)); + vm.startPrank(alice); + aliceIdentity.addKey(keyHash, KeyPurposes.ACTION, KeyTypes.ECDSA); + aliceIdentity.addKey(keyHash, KeyPurposes.PROPOSER, KeyTypes.ECDSA); + vm.stopPrank(); + + vm.deal(proposerAction, 1 ether); + uint256 carolBalanceBefore = carol.balance; + + vm.prank(proposerAction); + aliceIdentity.execute{ value: 10 }(carol, 10, hex""); + + // ACTION key auto-approves external calls + Structs.Execution memory exec = aliceIdentity.getExecutionData(0); + assertTrue(exec.approved); + assertTrue(exec.executed); + assertEq(carol.balance, carolBalanceBefore + 10); + } + + function test_addAndRemoveProposerKey() public { + address proposer = makeAddr("proposer"); + bytes32 proposerKeyHash = keccak256(abi.encode(proposer)); + + // Add PROPOSER key + vm.prank(alice); + aliceIdentity.addKey(proposerKeyHash, KeyPurposes.PROPOSER, KeyTypes.ECDSA); + + // Verify key exists and has PROPOSER purpose + assertTrue(aliceIdentity.keyHasPurpose(proposerKeyHash, KeyPurposes.PROPOSER)); + bytes32[] memory proposerKeys = aliceIdentity.getKeysByPurpose(KeyPurposes.PROPOSER); + assertEq(proposerKeys.length, 1); + assertEq(proposerKeys[0], proposerKeyHash); + + // Verify getKey returns correct data + (uint256[] memory purposes, uint256 keyType, bytes32 key) = aliceIdentity.getKey(proposerKeyHash); + assertEq(key, proposerKeyHash); + assertEq(keyType, KeyTypes.ECDSA); + assertEq(purposes.length, 1); + assertEq(purposes[0], KeyPurposes.PROPOSER); + + // Remove PROPOSER key + vm.prank(alice); + aliceIdentity.removeKey(proposerKeyHash, KeyPurposes.PROPOSER); + + // Verify key is removed + assertFalse(aliceIdentity.keyHasPurpose(proposerKeyHash, KeyPurposes.PROPOSER)); + proposerKeys = aliceIdentity.getKeysByPurpose(KeyPurposes.PROPOSER); + assertEq(proposerKeys.length, 0); + } + + function test_executeAsClaimSignerOnly_reverts() public { + // carol already has CLAIM_SIGNER on aliceIdentity from setup + // CLAIM_SIGNER alone cannot call execute — needs PROPOSER, MANAGEMENT, or ACTION + vm.prank(carol); + vm.expectRevert(abi.encodeWithSelector(Errors.SenderCannotPropose.selector)); + aliceIdentity.execute(address(aliceIdentity), 0, hex""); + } + + function test_executeAsEncryptionOnly_reverts() public { + address encKey = makeAddr("encKey"); + vm.prank(alice); + aliceIdentity.addKey(keccak256(abi.encode(encKey)), KeyPurposes.ENCRYPTION, KeyTypes.ECDSA); + + vm.prank(encKey); + vm.expectRevert(abi.encodeWithSelector(Errors.SenderCannotPropose.selector)); + aliceIdentity.execute(carol, 0, hex""); + } + function test_multicallWithMixedApproveReject() public { // Add bob as ACTION key bytes32 bobKeyHash = keccak256(abi.encode(bob)); diff --git a/test/identities/Init.t.sol b/test/identities/Init.t.sol index 1ee5406..be48043 100644 --- a/test/identities/Init.t.sol +++ b/test/identities/Init.t.sol @@ -24,7 +24,7 @@ contract InitTest is OnchainIDSetup { new Identity(address(0), false); } - function test_versionInitializedWhenDeployedAsRegularContract() public { + function test_versionInitializedWhenDeployedAsRegularContract() public view { Identity identityImplementation = getIdentityImplementation(); assertEq(identityImplementation.version(), "3.0.0"); } @@ -43,7 +43,7 @@ contract InitTest is OnchainIDSetup { libraryImpl.addKey(keccak256(abi.encode(alice)), 1, 1); } - function test_supportsERC165InterfaceDetection() public { + function test_supportsERC165InterfaceDetection() public view { // ERC165 interface ID assertTrue(aliceIdentity.supportsInterface(0x01ffc9a7));