mirror of
https://github.com/luxfi/onchain-id.git
synced 2026-10-30 15:23:49 +00:00
600 lines
20 KiB
Solidity
600 lines
20 KiB
Solidity
// SPDX-License-Identifier: GPL-3.0
|
|
pragma solidity ^0.8.17;
|
|
|
|
import "./interface/IIdentity.sol";
|
|
import "./interface/IClaimIssuer.sol";
|
|
import "./version/Version.sol";
|
|
import "./storage/Storage.sol";
|
|
|
|
/**
|
|
* @dev Implementation of the `IERC734` "KeyHolder" and the `IERC735` "ClaimHolder" interfaces
|
|
* into a common Identity Contract.
|
|
* This implementation has a separate contract were it declares all storage,
|
|
* allowing for it to be used as an upgradable logic contract.
|
|
*/
|
|
contract Identity is Storage, IIdentity, Version {
|
|
|
|
/**
|
|
* @notice Prevent any direct calls to the implementation contract (marked by _canInteract = false).
|
|
*/
|
|
modifier delegatedOnly() {
|
|
require(_canInteract == true, "Interacting with the library contract is forbidden.");
|
|
_;
|
|
}
|
|
|
|
/**
|
|
* @notice requires management key to call this function, or internal call
|
|
*/
|
|
modifier onlyManager() {
|
|
require(msg.sender == address(this) || keyHasPurpose(keccak256(abi.encode(msg.sender)), 1)
|
|
, "Permissions: Sender does not have management key");
|
|
_;
|
|
}
|
|
|
|
/**
|
|
* @notice requires claim key to call this function, or internal call
|
|
*/
|
|
modifier onlyClaimKey() {
|
|
require(msg.sender == address(this) || keyHasPurpose(keccak256(abi.encode(msg.sender)), 3)
|
|
, "Permissions: Sender does not have claim signer key");
|
|
_;
|
|
}
|
|
|
|
/**
|
|
* @notice constructor of the Identity contract
|
|
* @param initialManagementKey the address of the management key at deployment
|
|
* @param _isLibrary boolean value stating if the contract is library or not
|
|
* calls __Identity_init if contract is not library
|
|
*/
|
|
constructor(address initialManagementKey, bool _isLibrary) {
|
|
require(initialManagementKey != address(0), "invalid argument - zero address");
|
|
|
|
if (!_isLibrary) {
|
|
__Identity_init(initialManagementKey);
|
|
} else {
|
|
_initialized = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @notice When using this contract as an implementation for a proxy, call this initializer with a delegatecall.
|
|
*
|
|
* @param initialManagementKey The ethereum address to be set as the management key of the ONCHAINID.
|
|
*/
|
|
function initialize(address initialManagementKey) external {
|
|
require(initialManagementKey != address(0), "invalid argument - zero address");
|
|
__Identity_init(initialManagementKey);
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC734-execute}.
|
|
* @notice Passes an execution instruction to the keymanager.
|
|
* If the sender is an ACTION key and the destination address is not the identity contract itself, then the
|
|
* execution is immediately approved and performed.
|
|
* If the destination address is the identity itself, then the execution would be performed immediately only if
|
|
* the sender is a MANAGEMENT key.
|
|
* Otherwise the execution request must be approved via the `approve` method.
|
|
* @return executionId to use in the approve function, to approve or reject this execution.
|
|
*/
|
|
function execute(address _to, uint256 _value, bytes memory _data)
|
|
external
|
|
delegatedOnly
|
|
override
|
|
payable
|
|
returns (uint256 executionId)
|
|
{
|
|
uint256 _executionId = _executionNonce;
|
|
_executions[_executionId].to = _to;
|
|
_executions[_executionId].value = _value;
|
|
_executions[_executionId].data = _data;
|
|
_executionNonce++;
|
|
|
|
emit ExecutionRequested(_executionId, _to, _value, _data);
|
|
|
|
if (keyHasPurpose(keccak256(abi.encode(msg.sender)), 1)) {
|
|
approve(_executionId, true);
|
|
}
|
|
else if (_to != address(this) && keyHasPurpose(keccak256(abi.encode(msg.sender)), 2)){
|
|
approve(_executionId, true);
|
|
}
|
|
|
|
return _executionId;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC734-getKey}.
|
|
* @notice Implementation of the getKey function from the ERC-734 standard
|
|
* @param _key The public key. for non-hex and long keys, its the Keccak256 hash of the key
|
|
* @return purposes Returns the full key data, if present in the identity.
|
|
* @return keyType Returns the full key data, if present in the identity.
|
|
* @return key Returns the full key data, if present in the identity.
|
|
*/
|
|
function getKey(bytes32 _key)
|
|
external
|
|
override
|
|
view
|
|
returns(uint256[] memory purposes, uint256 keyType, bytes32 key)
|
|
{
|
|
return (_keys[_key].purposes, _keys[_key].keyType, _keys[_key].key);
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC734-getKeyPurposes}.
|
|
* @notice gets the purposes of a key
|
|
* @param _key The public key. for non-hex and long keys, its the Keccak256 hash of the key
|
|
* @return _purposes Returns the purposes of the specified key
|
|
*/
|
|
function getKeyPurposes(bytes32 _key)
|
|
external
|
|
override
|
|
view
|
|
returns(uint256[] memory _purposes)
|
|
{
|
|
return (_keys[_key].purposes);
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC734-getKeysByPurpose}.
|
|
* @notice gets all the keys with a specific purpose from an identity
|
|
* @param _purpose a uint256[] Array of the key types, like 1 = MANAGEMENT, 2 = ACTION, 3 = CLAIM, 4 = ENCRYPTION
|
|
* @return keys Returns an array of public key bytes32 hold by this identity and having the specified purpose
|
|
*/
|
|
function getKeysByPurpose(uint256 _purpose)
|
|
external
|
|
override
|
|
view
|
|
returns(bytes32[] memory keys)
|
|
{
|
|
return _keysByPurpose[_purpose];
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC735-getClaimIdsByTopic}.
|
|
* @notice Implementation of the getClaimIdsByTopic function from the ERC-735 standard.
|
|
* used to get all the claims from the specified topic
|
|
* @param _topic The identity of the claim i.e. keccak256(abi.encode(_issuer, _topic))
|
|
* @return claimIds Returns an array of claim IDs by topic.
|
|
*/
|
|
function getClaimIdsByTopic(uint256 _topic)
|
|
external
|
|
override
|
|
view
|
|
returns(bytes32[] memory claimIds)
|
|
{
|
|
return _claimsByTopic[_topic];
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
* @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
|
|
* @return success Returns TRUE if the addition was successful and FALSE if not
|
|
*/
|
|
function addKey(bytes32 _key, uint256 _purpose, uint256 _type)
|
|
public
|
|
delegatedOnly
|
|
onlyManager
|
|
override
|
|
returns (bool success)
|
|
{
|
|
if (_keys[_key].key == _key) {
|
|
uint256[] memory _purposes = _keys[_key].purposes;
|
|
for (uint keyPurposeIndex = 0; keyPurposeIndex < _purposes.length; keyPurposeIndex++) {
|
|
uint256 purpose = _purposes[keyPurposeIndex];
|
|
|
|
if (purpose == _purpose) {
|
|
revert("Conflict: Key already has purpose");
|
|
}
|
|
}
|
|
|
|
_keys[_key].purposes.push(_purpose);
|
|
} else {
|
|
_keys[_key].key = _key;
|
|
_keys[_key].purposes = [_purpose];
|
|
_keys[_key].keyType = _type;
|
|
}
|
|
|
|
_keysByPurpose[_purpose].push(_key);
|
|
|
|
emit KeyAdded(_key, _purpose, _type);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC734-approve}.
|
|
* @notice Approves an execution.
|
|
* If the sender is an ACTION key and the destination address is not the identity contract itself, then the
|
|
* approval is authorized and the operation would be performed.
|
|
* If the destination address is the identity itself, then the execution would be authorized and performed only
|
|
* if the sender is a MANAGEMENT key.
|
|
*/
|
|
function approve(uint256 _id, bool _approve)
|
|
public
|
|
delegatedOnly
|
|
override
|
|
returns (bool success)
|
|
{
|
|
require(_id < _executionNonce, "Cannot approve a non-existing execution");
|
|
require(!_executions[_id].executed, "Request already executed");
|
|
|
|
if(_executions[_id].to == address(this)) {
|
|
require(keyHasPurpose(keccak256(abi.encode(msg.sender)), 1), "Sender does not have management key");
|
|
}
|
|
else {
|
|
require(keyHasPurpose(keccak256(abi.encode(msg.sender)), 2), "Sender does not have action key");
|
|
}
|
|
|
|
emit Approved(_id, _approve);
|
|
|
|
if (_approve == true) {
|
|
_executions[_id].approved = true;
|
|
|
|
// solhint-disable-next-line avoid-low-level-calls
|
|
(success,) = _executions[_id].to.call{value:(_executions[_id].value)}(_executions[_id].data);
|
|
|
|
if (success) {
|
|
_executions[_id].executed = true;
|
|
|
|
emit Executed(
|
|
_id,
|
|
_executions[_id].to,
|
|
_executions[_id].value,
|
|
_executions[_id].data
|
|
);
|
|
|
|
return true;
|
|
} else {
|
|
emit ExecutionFailed(
|
|
_id,
|
|
_executions[_id].to,
|
|
_executions[_id].value,
|
|
_executions[_id].data
|
|
);
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
_executions[_id].approved = false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC734-removeKey}.
|
|
* @notice Remove the purpose from a key.
|
|
*/
|
|
function removeKey(bytes32 _key, uint256 _purpose)
|
|
public
|
|
delegatedOnly
|
|
onlyManager
|
|
override
|
|
returns (bool success)
|
|
{
|
|
require(_keys[_key].key == _key, "NonExisting: Key isn't registered");
|
|
uint256[] memory _purposes = _keys[_key].purposes;
|
|
|
|
uint purposeIndex = 0;
|
|
while (_purposes[purposeIndex] != _purpose) {
|
|
purposeIndex++;
|
|
|
|
if (purposeIndex == _purposes.length) {
|
|
revert("NonExisting: Key doesn't have such purpose");
|
|
}
|
|
}
|
|
|
|
_purposes[purposeIndex] = _purposes[_purposes.length - 1];
|
|
_keys[_key].purposes = _purposes;
|
|
_keys[_key].purposes.pop();
|
|
|
|
uint keyIndex = 0;
|
|
uint arrayLength = _keysByPurpose[_purpose].length;
|
|
|
|
while (_keysByPurpose[_purpose][keyIndex] != _key) {
|
|
keyIndex++;
|
|
|
|
if (keyIndex >= arrayLength) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
_keysByPurpose[_purpose][keyIndex] = _keysByPurpose[_purpose][arrayLength - 1];
|
|
_keysByPurpose[_purpose].pop();
|
|
|
|
uint keyType = _keys[_key].keyType;
|
|
|
|
if (_purposes.length - 1 == 0) {
|
|
delete _keys[_key];
|
|
}
|
|
|
|
emit KeyRemoved(_key, _purpose, keyType);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC735-addClaim}.
|
|
* @notice Implementation of the addClaim function from the ERC-735 standard
|
|
* Require that the msg.sender has claim signer key.
|
|
*
|
|
* @param _topic The type of claim
|
|
* @param _scheme The scheme with which this claim SHOULD be verified or how it should be processed.
|
|
* @param _issuer The issuers identity contract address, or the address used to sign the above signature.
|
|
* @param _signature Signature which is the proof that the claim issuer issued a claim of topic for this identity.
|
|
* it MUST be a signed message of the following structure:
|
|
* keccak256(abi.encode(address identityHolder_address, uint256 _ topic, bytes data))
|
|
* @param _data The hash of the claim data, sitting in another
|
|
* location, a bit-mask, call data, or actual data based on the claim scheme.
|
|
* @param _uri The location of the claim, this can be HTTP links, swarm hashes, IPFS hashes, and such.
|
|
*
|
|
* @return claimRequestId Returns claimRequestId: COULD be
|
|
* send to the approve function, to approve or reject this claim.
|
|
* triggers ClaimAdded event.
|
|
*/
|
|
function addClaim(
|
|
uint256 _topic,
|
|
uint256 _scheme,
|
|
address _issuer,
|
|
bytes memory _signature,
|
|
bytes memory _data,
|
|
string memory _uri
|
|
)
|
|
public
|
|
delegatedOnly
|
|
onlyClaimKey
|
|
override
|
|
returns (bytes32 claimRequestId)
|
|
{
|
|
if (_issuer != address(this)) {
|
|
require(IClaimIssuer(_issuer).isClaimValid(IIdentity(address(this)), _topic, _signature, _data), "invalid claim");
|
|
}
|
|
|
|
bytes32 claimId = keccak256(abi.encode(_issuer, _topic));
|
|
_claims[claimId].topic = _topic;
|
|
_claims[claimId].scheme = _scheme;
|
|
_claims[claimId].signature = _signature;
|
|
_claims[claimId].data = _data;
|
|
_claims[claimId].uri = _uri;
|
|
|
|
if (_claims[claimId].issuer != _issuer) {
|
|
_claimsByTopic[_topic].push(claimId);
|
|
_claims[claimId].issuer = _issuer;
|
|
|
|
emit ClaimAdded(claimId, _topic, _scheme, _issuer, _signature, _data, _uri);
|
|
}
|
|
else {
|
|
emit ClaimChanged(claimId, _topic, _scheme, _issuer, _signature, _data, _uri);
|
|
}
|
|
return claimId;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC735-removeClaim}.
|
|
* @notice Implementation of the removeClaim function from the ERC-735 standard
|
|
* Require that the msg.sender has management key.
|
|
* Can only be removed by the claim issuer, or the claim holder itself.
|
|
*
|
|
* @param _claimId The identity of the claim i.e. keccak256(abi.encode(_issuer, _topic))
|
|
*
|
|
* @return success Returns TRUE when the claim was removed.
|
|
* triggers ClaimRemoved event
|
|
*/
|
|
function removeClaim(bytes32 _claimId)
|
|
public
|
|
delegatedOnly
|
|
onlyClaimKey
|
|
override
|
|
returns
|
|
(bool success) {
|
|
uint256 _topic = _claims[_claimId].topic;
|
|
if (_topic == 0) {
|
|
revert("NonExisting: There is no claim with this ID");
|
|
}
|
|
|
|
uint claimIndex = 0;
|
|
uint arrayLength = _claimsByTopic[_topic].length;
|
|
while (_claimsByTopic[_topic][claimIndex] != _claimId) {
|
|
claimIndex++;
|
|
|
|
if (claimIndex >= arrayLength) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
_claimsByTopic[_topic][claimIndex] =
|
|
_claimsByTopic[_topic][arrayLength - 1];
|
|
_claimsByTopic[_topic].pop();
|
|
|
|
emit ClaimRemoved(
|
|
_claimId,
|
|
_topic,
|
|
_claims[_claimId].scheme,
|
|
_claims[_claimId].issuer,
|
|
_claims[_claimId].signature,
|
|
_claims[_claimId].data,
|
|
_claims[_claimId].uri
|
|
);
|
|
|
|
delete _claims[_claimId];
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC735-getClaim}.
|
|
* @notice Implementation of the getClaim function from the ERC-735 standard.
|
|
*
|
|
* @param _claimId The identity of the claim i.e. keccak256(abi.encode(_issuer, _topic))
|
|
*
|
|
* @return topic Returns all the parameters of the claim for the
|
|
* specified _claimId (topic, scheme, signature, issuer, data, uri) .
|
|
* @return scheme Returns all the parameters of the claim for the
|
|
* specified _claimId (topic, scheme, signature, issuer, data, uri) .
|
|
* @return issuer Returns all the parameters of the claim for the
|
|
* specified _claimId (topic, scheme, signature, issuer, data, uri) .
|
|
* @return signature Returns all the parameters of the claim for the
|
|
* specified _claimId (topic, scheme, signature, issuer, data, uri) .
|
|
* @return data Returns all the parameters of the claim for the
|
|
* specified _claimId (topic, scheme, signature, issuer, data, uri) .
|
|
* @return uri Returns all the parameters of the claim for the
|
|
* specified _claimId (topic, scheme, signature, issuer, data, uri) .
|
|
*/
|
|
function getClaim(bytes32 _claimId)
|
|
public
|
|
override
|
|
view
|
|
returns(
|
|
uint256 topic,
|
|
uint256 scheme,
|
|
address issuer,
|
|
bytes memory signature,
|
|
bytes memory data,
|
|
string memory uri
|
|
)
|
|
{
|
|
return (
|
|
_claims[_claimId].topic,
|
|
_claims[_claimId].scheme,
|
|
_claims[_claimId].issuer,
|
|
_claims[_claimId].signature,
|
|
_claims[_claimId].data,
|
|
_claims[_claimId].uri
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC734-keyHasPurpose}.
|
|
* @notice Returns true if the key has MANAGEMENT purpose or the specified purpose.
|
|
*/
|
|
function keyHasPurpose(bytes32 _key, uint256 _purpose)
|
|
public
|
|
override
|
|
view
|
|
returns(bool result)
|
|
{
|
|
Key memory key = _keys[_key];
|
|
if (key.key == 0) return false;
|
|
|
|
for (uint keyPurposeIndex = 0; keyPurposeIndex < key.purposes.length; keyPurposeIndex++) {
|
|
uint256 purpose = key.purposes[keyPurposeIndex];
|
|
|
|
if (purpose == 1 || purpose == _purpose) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @dev Checks if a claim is valid. Claims issued by the identity are self-attested claims. They do not have a
|
|
* built-in revocation mechanism and are considered valid as long as their signature is valid and they are still
|
|
* stored by the identity contract.
|
|
* @param _identity the identity contract related to the claim
|
|
* @param claimTopic the claim topic of the claim
|
|
* @param sig the signature of the claim
|
|
* @param data the data field of the claim
|
|
* @return claimValid true if the claim is valid, false otherwise
|
|
*/
|
|
function isClaimValid(
|
|
IIdentity _identity,
|
|
uint256 claimTopic,
|
|
bytes memory sig,
|
|
bytes memory data)
|
|
public override virtual view returns (bool claimValid)
|
|
{
|
|
bytes32 dataHash = keccak256(abi.encode(_identity, claimTopic, data));
|
|
// Use abi.encodePacked to concatenate the message prefix and the message to sign.
|
|
bytes32 prefixedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash));
|
|
|
|
// Recover address of data signer
|
|
address recovered = getRecoveredAddress(sig, prefixedHash);
|
|
|
|
// Take hash of recovered address
|
|
bytes32 hashedAddr = keccak256(abi.encode(recovered));
|
|
|
|
// Does the trusted identifier have they key which signed the user's claim?
|
|
// && (isClaimRevoked(_claimId) == false)
|
|
if (keyHasPurpose(hashedAddr, 3)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @dev returns the address that signed the given data
|
|
* @param sig the signature of the data
|
|
* @param dataHash the data that was signed
|
|
* returns the address that signed dataHash and created the signature sig
|
|
*/
|
|
function getRecoveredAddress(bytes memory sig, bytes32 dataHash)
|
|
public
|
|
pure
|
|
returns (address addr)
|
|
{
|
|
bytes32 ra;
|
|
bytes32 sa;
|
|
uint8 va;
|
|
|
|
// Check the signature length
|
|
if (sig.length != 65) {
|
|
return address(0);
|
|
}
|
|
|
|
// Divide the signature in r, s and v variables
|
|
// solhint-disable-next-line no-inline-assembly
|
|
assembly {
|
|
ra := mload(add(sig, 32))
|
|
sa := mload(add(sig, 64))
|
|
va := byte(0, mload(add(sig, 96)))
|
|
}
|
|
|
|
if (va < 27) {
|
|
va += 27;
|
|
}
|
|
|
|
address recoveredAddress = ecrecover(dataHash, va, ra, sa);
|
|
|
|
return (recoveredAddress);
|
|
}
|
|
|
|
/**
|
|
* @notice Initializer internal function for the Identity contract.
|
|
*
|
|
* @param initialManagementKey The ethereum address to be set as the management key of the ONCHAINID.
|
|
*/
|
|
// solhint-disable-next-line func-name-mixedcase
|
|
function __Identity_init(address initialManagementKey) internal {
|
|
require(!_initialized || _isConstructor(), "Initial key was already setup.");
|
|
_initialized = true;
|
|
_canInteract = true;
|
|
|
|
bytes32 _key = keccak256(abi.encode(initialManagementKey));
|
|
_keys[_key].key = _key;
|
|
_keys[_key].purposes = [1];
|
|
_keys[_key].keyType = 1;
|
|
_keysByPurpose[1].push(_key);
|
|
emit KeyAdded(_key, 1, 1);
|
|
}
|
|
|
|
/**
|
|
* @notice Computes if the context in which the function is called is a constructor or not.
|
|
*
|
|
* @return true if the context is a constructor.
|
|
*/
|
|
function _isConstructor() private view returns (bool) {
|
|
address self = address(this);
|
|
uint256 cs;
|
|
// solhint-disable-next-line no-inline-assembly
|
|
assembly { cs := extcodesize(self) }
|
|
return cs == 0;
|
|
}
|
|
}
|