Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions contracts/Identity.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
30 changes: 13 additions & 17 deletions contracts/KeyManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions contracts/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
3 changes: 3 additions & 0 deletions contracts/libraries/KeyPurposes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

}
12 changes: 9 additions & 3 deletions test/claim-issuers/ClaimTo.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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";
Expand All @@ -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";
Expand Down
26 changes: 19 additions & 7 deletions test/identities/Claims.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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) =
Expand All @@ -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);
Expand Down Expand Up @@ -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);

Expand Down
Loading
Loading