diff --git a/contracts/deploy/DelayedUpdatableInstance.sol b/contracts/deploy/DelayedUpdatableInstance.sol new file mode 100644 index 0000000..af12bca --- /dev/null +++ b/contracts/deploy/DelayedUpdatableInstance.sol @@ -0,0 +1,63 @@ +pragma solidity ^0.4.17; + +import "./DelayedUpdatableInstanceStorage.sol"; +import "./DelegatedCall.sol"; + +/** + * @title DelayedUpdatableInstance + * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) + * @dev Contract that can be updated by a call from itself. + */ +contract DelayedUpdatableInstance is DelayedUpdatableInstanceStorage, DelegatedCall { + + event UpdateRequested(address newKernel, uint256 activation); + event UpdateCancelled(); + event UpdateConfirmed(address oldKernel, address newKernel); + + function DelayedUpdatableInstance(address _kernel) public { + kernel = _kernel; + } + + /** + * @dev delegatecall everything (but declared functions) to `_target()` + * @notice Verify `kernel()` code to predict behavior + */ + function () external delegated { + //all goes to kernel + } + + /** + * @dev returns kernel if kernel that is configured + * @return kernel address + */ + function targetDelegatedCall() + internal + constant + returns(address) + { + return kernel; + } + + function updateRequestUpdatableInstance(address _kernel) external { + require(msg.sender == address(this)); + uint activation = block.timestamp + 30 days; + update = Update(_kernel, activation); + UpdateRequested(_kernel, activation); + } + + function updateConfirmUpdatableInstance(address _kernel) external { + require(msg.sender == address(this)); + Update memory pending = update; + require(pending.kernel == _kernel); + require(pending.activation < block.timestamp); + kernel = pending.kernel; + delete update; + UpdateConfirmed(kernel, pending.kernel); + } + + function updateCancelUpdatableInstance() external { + require(msg.sender == address(this)); + delete update; + } + +} \ No newline at end of file diff --git a/contracts/deploy/DelayedUpdatableInstanceStorage.sol b/contracts/deploy/DelayedUpdatableInstanceStorage.sol new file mode 100644 index 0000000..274da20 --- /dev/null +++ b/contracts/deploy/DelayedUpdatableInstanceStorage.sol @@ -0,0 +1,21 @@ +pragma solidity ^0.4.17; + +import "./InstanceStorage.sol"; + +/** + * @title InstanceStorage + * @author Ricardo Guilherme Schmidt (Status Research & Development GmbH) + * @dev Defines kernel vars that Kernel contract share with Instance. + * Important to avoid overwriting wrong storage pointers is that + * InstanceStorage should be always the first contract at heritance. + */ +contract DelayedUpdatableInstanceStorage is InstanceStorage { + // protected zone start (InstanceStorage vars) + Update update; + + struct Update { + address kernel; + uint256 activation; + } + // protected zone end +} \ No newline at end of file diff --git a/contracts/identity/ERC725.sol b/contracts/identity/ERC725.sol index 8d2d25e..17af37a 100644 --- a/contracts/identity/ERC725.sol +++ b/contracts/identity/ERC725.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.17; +pragma solidity ^0.4.18; contract ERC725 { @@ -6,19 +6,24 @@ contract ERC725 { uint256 constant ACTION_KEY = 2; uint256 constant CLAIM_SIGNER_KEY = 3; uint256 constant ENCRYPTION_KEY = 4; + + event KeyAdded(bytes32 indexed key, uint256 indexed purpose, uint256 indexed keyType); + event KeyRemoved(bytes32 indexed key, uint256 indexed purpose, uint256 indexed keyType); + event ExecutionRequested(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data); + event Executed(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data); + event Approved(uint256 indexed executionId, bool approved); - event KeyAdded(address indexed key, uint256 indexed keyType); - event KeyRemoved(address indexed key, uint256 indexed keyType); - event KeyReplaced(address indexed oldKey, address indexed newKey, uint256 indexed keyType); - event ExecutionRequested(bytes32 indexed executionId, address indexed to, uint256 indexed value, bytes data); - event Executed(bytes32 indexed executionId, address indexed to, uint256 indexed value, bytes data); - event Approved(bytes32 indexed executionId, bool approved); + struct Key { + uint256 purpose; //e.g., MANAGEMENT_KEY = 1, ACTION_KEY = 2, etc. + uint256 keyType; // e.g. 1 = ECDSA, 2 = RSA, etc. + bytes32 key; + } - function getKeyType(address _key) public constant returns(uint256 keyType); - function getKeysByType(uint256 _type) public constant returns(address[]); - function addKey(address _key, uint256 _type) public returns (bool success); - function removeKey(address _key) public returns (bool success); - function replaceKey(address _oldKey, address _newKey) public returns (bool success); - function execute(address _to, uint256 _value, bytes _data) public returns (bytes32 executionId); - function approve(bytes32 _id, bool _approve) public returns (bool success); + function getKey(bytes32 _key, uint256 _purpose) public constant returns(uint256 purpose, uint256 keyType, bytes32 key); + function getKeyPurpose(bytes32 _key) public constant returns(uint256[] purpose); + function getKeysByPurpose(uint256 _purpose) public constant returns(bytes32[] keys); + function addKey(bytes32 _key, uint256 _purpose, uint256 _keyType) public returns (bool success); + function removeKey(bytes32 _key, uint256 _purpose) public returns (bool success); + function execute(address _to, uint256 _value, bytes _data) public returns (uint256 executionId); + function approve(uint256 _id, bool _approve) public returns (bool success); } \ No newline at end of file diff --git a/contracts/identity/ERC735.sol b/contracts/identity/ERC735.sol index ea5f8f5..2109d28 100644 --- a/contracts/identity/ERC735.sol +++ b/contracts/identity/ERC735.sol @@ -1,23 +1,23 @@ -pragma solidity ^0.4.17; +pragma solidity ^0.4.18; contract ERC735 { - event ClaimRequested(bytes32 indexed claimId, uint256 indexed claimType, address indexed issuer, uint256 signatureType, bytes signature, bytes claim, string uri); - event ClaimAdded(bytes32 indexed claimId, uint256 indexed claimType, address indexed issuer, uint256 signatureType, bytes signature, bytes claim, string uri); - event ClaimRemoved(bytes32 indexed claimId, uint256 indexed claimType, address indexed issuer, uint256 signatureType, bytes signature, bytes claim, string uri); - event ClaimChanged(bytes32 indexed claimId, uint256 indexed claimType, address indexed issuer, uint256 signatureType, bytes signature, bytes claim, string uri); + event ClaimRequested(bytes32 indexed claimRequestId, uint256 indexed claimType, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); + event ClaimAdded(bytes32 indexed claimId, uint256 indexed claimType, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); + event ClaimRemoved(bytes32 indexed claimId, uint256 indexed claimType, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); + event ClaimChanged(bytes32 indexed claimId, uint256 indexed claimType, uint256 scheme, address indexed issuer, bytes signature, bytes data, string uri); struct Claim { uint256 claimType; + uint256 scheme; address issuer; // msg.sender - uint256 signatureType; // The type of signature - bytes signature; // this.address + claimType + claim - bytes claim; + bytes signature; // this.address + claimType + data + bytes data; string uri; } - function getClaim(bytes32 _claimId) public constant returns(uint256 claimType, address issuer, uint256 signatureType, bytes signature, bytes claim, string uri); - function getClaimsIdByType(uint256 _claimType) public constant returns(bytes32[]); - function addClaim(uint256 _claimType, address issuer, uint256 signatureType, bytes _signature, bytes _claim, string _uri) public returns (bytes32 claimId); + function getClaim(bytes32 _claimId) public constant returns(uint256 claimType, uint256 scheme, address issuer, bytes signature, bytes data, string uri); + function getClaimIdsByType(uint256 _claimType) public constant returns(bytes32[] claimIds); + function addClaim(uint256 _claimType, uint256 _scheme, address _issuer, bytes _signature, bytes _data, string _uri) public returns (bytes32 claimRequestId); function removeClaim(bytes32 _claimId) public returns (bool success); } \ No newline at end of file diff --git a/contracts/identity/Identity.sol b/contracts/identity/Identity.sol index 30f903f..86ff594 100644 --- a/contracts/identity/Identity.sol +++ b/contracts/identity/Identity.sol @@ -1,214 +1,621 @@ - pragma solidity ^0.4.17; +pragma solidity ^0.4.17; - import "./ERC725.sol"; - import "./ERC735.sol"; - - contract Identity is ERC725, ERC735 { - - mapping (address => uint256) keys; - mapping (bytes32 => Claim) claims; - mapping (uint256 => address[]) keysByType; - mapping (uint256 => bytes32[]) claimsByType; - mapping (bytes32 => uint256) indexes; - mapping (bytes32 => Transaction) txx; - - uint nonce = 0; - - struct Transaction { - address to; - uint value; - bytes data; - uint nonce; - } +import "./ERC725.sol"; +import "./ERC735.sol"; - modifier managerOnly { - require(keys[msg.sender] == MANAGEMENT_KEY); - _; - } +contract Identity is ERC725, ERC735 { - modifier managerOrSelf { - require(keys[msg.sender] == MANAGEMENT_KEY || msg.sender == address(this)); - _; - } + mapping (bytes32 => Key) keys; + mapping (bytes32 => Claim) claims; + mapping (uint256 => bytes32[]) keysByPurpose; + mapping (uint256 => bytes32[]) claimsByType; + mapping (bytes32 => uint256) indexes; + mapping (uint => Transaction) txx; + mapping (uint256 => uint256) minimumApprovalsByKeyPurpose; + bytes32[] pendingTransactions; + uint nonce = 0; + address recoveryContract; + address recoveryManager; - modifier actorOnly { - require(keys[msg.sender] == ACTION_KEY); - _; - } + struct Transaction { + address to; + uint value; + bytes data; + uint nonce; + uint approverCount; + mapping(bytes32 => bool) approvals; + } - modifier claimSignerOnly { - require(keys[msg.sender] == CLAIM_SIGNER_KEY); - _; - } + modifier managerOnly { + require( + isKeyType(bytes32(msg.sender), MANAGEMENT_KEY) + ); + _; + } + modifier selfOnly { + require( + msg.sender == address(this) + ); + _; + } - function Identity() public { - _addKey(msg.sender, MANAGEMENT_KEY); - } + modifier recoveryOnly { + require( + (recoveryContract != address(0) && msg.sender == address(recoveryContract)) + ); + _; + } + modifier actorOnly(bytes32 _key) { + require(isKeyType(_key, ACTION_KEY)); + _; + } + + modifier managerOrActor(bytes32 _key) { + require( + isKeyType(bytes32(msg.sender), MANAGEMENT_KEY) || + isKeyType(bytes32(msg.sender), ACTION_KEY) + ); + _; + } + + modifier validECDSAKey ( + bytes32 _key, + bytes32 _signHash, + uint8 _v, + bytes32 _r, + bytes32 _s + ) + { + require(address(_key) == ecrecover( + keccak256("\x19Ethereum Signed Message:\n32", _signHash), + _v, + _r, + _s + )); + require(keys[_key].purpose != 0); + _; + } - function addKey(address _key, uint256 _type) public managerOrSelf returns (bool success) { - _addKey(_key, _type); - } - - - function removeKey(address _key) public managerOrSelf returns (bool success) { - _removeKey(_key); - } - - - function replaceKey(address _oldKey, address _newKey) public managerOrSelf returns (bool success) { - _addKey(_newKey, getKeyType(_oldKey)); - _removeKey(_oldKey); - return true; - } - - - - function execute( - address _to, - uint256 _value, - bytes _data - ) - public - returns (bytes32 executionId) - { - uint256 senderKey = keys[msg.sender]; - require(senderKey == MANAGEMENT_KEY || senderKey == ACTION_KEY); - executionId = keccak256(_to, _value, _data, nonce); - ExecutionRequested(executionId, _to, _value, _data); - txx[executionId] = Transaction ( - { - to: _to, - value: _value, - data: _data, - nonce: nonce - }); - if (senderKey == MANAGEMENT_KEY) { - approve(executionId, true); - } - } - - function approve( - bytes32 _id, - bool _approve - ) - public - managerOnly - returns (bool success) - { - require(txx[_id].nonce == nonce); - nonce++; - if (_approve) { - success = txx[_id].to.call.value(txx[_id].value)(txx[_id].data); - } - } - - - function addClaim( - uint256 _claimType, - address _issuer, - uint256 _signatureType, - bytes _signature, - bytes _claim, - string _uri - ) - public - claimSignerOnly - returns (bytes32 claimId) - { - claimId = keccak256(_issuer, _claimType); - claims[claimId] = Claim( - { - claimType: _claimType, - issuer: _issuer, - signatureType: _signatureType, - signature: _signature, - claim: _claim, - uri: _uri - } - ); - indexes[keccak256(_issuer, _claimType)] = claimsByType[_claimType].length; - claimsByType[_claimType].push(claimId); - } - - - function removeClaim(bytes32 _claimId) public returns (bool success) { - Claim memory c = claims[_claimId]; - require(msg.sender == c.issuer || keys[msg.sender] == MANAGEMENT_KEY || msg.sender == address(this)); - uint claimIdTypePos = indexes[_claimId]; - delete indexes[_claimId]; - bytes32[] storage claimsTypeArr = claimsByType[c.claimType]; - bytes32 replacer = claimsTypeArr[claimsTypeArr.length-1]; - claimsTypeArr[claimIdTypePos] = replacer; - indexes[replacer] = claimIdTypePos; - delete claims[_claimId]; - claimsTypeArr.length--; - return true; - } - - - function _addKey(address _key, uint256 _type) private { - require(keys[_key] == 0); - KeyAdded(_key, _type); - keys[_key] = _type; - indexes[keccak256(_key, _type)] = keysByType[_type].length; - keysByType[_type].push(_key); - } - - - function _removeKey(address _key) private { - uint256 kType = keys[_key]; - KeyRemoved(_key, kType); - address[] storage keyArr = keysByType[kType]; - if (msg.sender != address(this) && kType == MANAGEMENT_KEY && keyArr.length == 1) { - revert(); - } - bytes32 oldIndex = keccak256(_key, kType); - uint index = indexes[oldIndex]; - delete indexes[oldIndex]; - address replacer = keyArr[keyArr.length-1]; - keyArr[index] = replacer; - indexes[keccak256(replacer, keys[replacer])] = index; - keyArr.length--; - delete keys[_key]; - } - - - function getKeyType(address _key) public constant returns(uint256 keyType) { - return keys[_key]; - } - - - function getKeysByType(uint256 _type) public constant returns(address[]) { - return keysByType[_type]; - } - - - function getClaim( - bytes32 _claimId - ) - public - constant - returns - (uint256 claimType, - address issuer, - uint256 signatureType, - bytes signature, - bytes claim, - string uri) - { - Claim memory _claim = claims[_claimId]; - return (_claim.claimType, _claim.issuer, _claim.signatureType, _claim.signature, _claim.claim, _claim.uri); - } - - - function getClaimsIdByType(uint256 _claimType) public constant returns(bytes32[]) { - return claimsByType[_claimType]; - } + function Identity() public { + _constructIdentity(msg.sender); + } + function () + public + payable + { } + function managerReset(address _newKey) + public + recoveryOnly + { + recoveryManager = _newKey; + _addKey(bytes32(recoveryManager), MANAGEMENT_KEY, 0); + minimumApprovalsByKeyPurpose[MANAGEMENT_KEY] = keysByPurpose[MANAGEMENT_KEY].length; + } + + function processManagerReset(uint256 limit) + public + { + require(recoveryManager != address(0)); + bytes32 newKey = bytes32(recoveryManager); + bytes32[] memory managers = keysByPurpose[MANAGEMENT_KEY]; + uint256 totalManagers = managers.length; + + if (limit == 0) { + limit = totalManagers; + } + + minimumApprovalsByKeyPurpose[MANAGEMENT_KEY] = totalManagers - limit + 1; + for (uint256 i = 0; i < limit; i++) { + bytes32 manager = managers[i]; + if (manager != newKey) { + _removeKey(manager, MANAGEMENT_KEY); + totalManagers--; + } + } + + if (totalManagers == 1) { + recoveryManager = address(0); + } + } + + function addKey( + bytes32 _key, + uint256 _purpose, + uint256 _type + ) + public + selfOnly + returns (bool success) + { + _addKey(_key, _purpose, _type); + return true; + } + + function replaceKey( + bytes32 _oldKey, + bytes32 _newKey, + uint256 _newType + ) + public + selfOnly + returns (bool success) + { + uint256 purpose = keys[_oldKey].purpose; + _addKey(_newKey, purpose, _newType); + _removeKey(_oldKey, purpose); + return true; + } + + function removeKey( + bytes32 _key, + uint256 _purpose + ) + public + selfOnly + returns (bool success) + { + _removeKey(_key, _purpose); + return true; + } + + function execute( + address _to, + uint256 _value, + bytes _data + ) + public + managerOrActor(bytes32(msg.sender)) + returns (uint256 executionId) + { + executionId = _execute(_to, _value, _data); + approve(executionId, true); + } + + function approve(uint256 _id, bool _approval) + public + managerOrActor(bytes32(msg.sender)) + returns (bool success) + { + return _approve(bytes32(msg.sender), _id, _approval); + } + + function setMinimumApprovalsByKeyType( + uint256 _purpose, + uint256 _minimumApprovals + ) + public + selfOnly + { + require(_minimumApprovals > 0); + require(_minimumApprovals <= keysByPurpose[_purpose].length); + minimumApprovalsByKeyPurpose[_purpose] = _minimumApprovals; + } + + + function addClaim( + uint256 _claimType, + uint256 _scheme, + address _issuer, + bytes _signature, + bytes _data, + string _uri + ) + public + returns (bytes32 claimHash) + { + claimHash = keccak256(_issuer, _claimType); + if (msg.sender == address(this)) { + if (claims[claimHash].claimType > 0) { + _modifyClaim(claimHash, _claimType, _scheme, _issuer, _signature, _data, _uri); + } else { + _includeClaim(claimHash, _claimType, _scheme, _issuer, _signature, _data, _uri); + } + } else { + require(_issuer == msg.sender); + require(isKeyType(bytes32(msg.sender), CLAIM_SIGNER_KEY)); + _execute(address(this), 0, msg.data); + ClaimRequested( + claimHash, + _claimType, + _scheme, + _issuer, + _signature, + _data, + _uri + ); + } + } + + function removeClaim(bytes32 _claimId) + public + returns (bool success) + { + Claim memory c = claims[_claimId]; + + require( + msg.sender == c.issuer || + msg.sender == address(this) + ); + + // MUST only be done by the issuer of the claim, or KEYS OF PURPOSE 1, or the identity itself. + // TODO If its the identity itself, the approval process will determine its approval. + + uint claimIdTypePos = indexes[_claimId]; + delete indexes[_claimId]; + bytes32[] storage claimsTypeArr = claimsByType[c.claimType]; + bytes32 replacer = claimsTypeArr[claimsTypeArr.length-1]; + claimsTypeArr[claimIdTypePos] = replacer; + indexes[replacer] = claimIdTypePos; + delete claims[_claimId]; + claimsTypeArr.length--; + return true; + } + + function getKey( + bytes32 _key, + uint256 _purpose + ) + public + constant + returns(uint256 purpose, uint256 keyType, bytes32 key) + { + Key storage myKey = keys[keccak256(_key, _purpose)]; + return (myKey.purpose, myKey.keyType, myKey.key); + } + + function isKeyType(bytes32 _key, uint256 _type) + public + constant + returns (bool) + { + return keys[keccak256(_key, _type)].purpose == _type; + } + + function getKeyPurpose(bytes32 _key) + public + constant + returns(uint256[] purpose) + { + + uint256[] memory purposeHolder = new uint256[](4); + uint8 counter = 0; + + if (isKeyType(_key, MANAGEMENT_KEY)) { + purposeHolder[counter] = MANAGEMENT_KEY; + counter++; + } + + if (isKeyType(_key, ACTION_KEY)) { + purposeHolder[counter] = ACTION_KEY; + counter++; + } + + if (isKeyType(_key, CLAIM_SIGNER_KEY)) { + purposeHolder[counter] = CLAIM_SIGNER_KEY; + counter++; + } + + if (isKeyType(_key, ENCRYPTION_KEY)) { + purposeHolder[counter] = ENCRYPTION_KEY; + counter++; + } + + uint256[] memory result = new uint256[](counter); + for (uint8 i = 0; i < counter; i++) { + result[i] = purposeHolder[i]; + } + + return result; + } + + function getKeysByPurpose(uint256 _purpose) + public + constant + returns(bytes32[]) + { + return keysByPurpose[_purpose]; + } + + function getClaim(bytes32 _claimId) + public + constant + returns( + uint256 claimType, + uint256 scheme, + address issuer, + bytes signature, + bytes data, + string uri + ) + { + Claim memory _claim = claims[_claimId]; + return (_claim.claimType, _claim.scheme, _claim.issuer, _claim.signature, _claim.data, _claim.uri); + } + + function getClaimIdsByType(uint256 _claimType) + public + constant + returns(bytes32[] claimIds) + { + return claimsByType[_claimType]; + } + + function approveECDSA( + uint256 _id, + bool _approval, + bytes32 _key, + uint8 _v, + bytes32 _r, + bytes32 _s + ) + public + validECDSAKey( + _key, + keccak256( + address(this), + bytes4(keccak256("approve(uint256,bool)")), + _id, + _approval + ), + _v, + _r, + _s + ) + managerOrActor(_key) + returns (bool success) + { + return _approve(_key, _id, _approval); + } + + function executeECDSA( + address _to, + uint256 _value, + bytes _data, + uint _nonce, + bytes32 _key, + uint8 _v, + bytes32 _r, + bytes32 _s + ) + public + validECDSAKey( + _key, + keccak256( + address(this), + bytes4( + keccak256("execute(address,uint256,bytes)")), + _to, + _value, + _data, + _nonce + ), + _v, + _r, + _s + ) + managerOrActor(_key) + returns (uint256 executionId) + { + executionId = _execute(_to, _value, _data); + _approve(_key, executionId, true); + } + + function setupRecovery(address _recoveryContract) + public + selfOnly + { + require(recoveryContract == address(0)); + recoveryContract = _recoveryContract; + } + + function _constructIdentity(address _manager) + internal + { + require(minimumApprovalsByKeyPurpose[MANAGEMENT_KEY] == 0); + _addKey(bytes32(_manager), MANAGEMENT_KEY, 0); + + minimumApprovalsByKeyPurpose[MANAGEMENT_KEY] = 1; + minimumApprovalsByKeyPurpose[ACTION_KEY] = 1; + } + + function _execute( + address _to, + uint256 _value, + bytes _data + ) + private + returns (uint256 executionId) + { + executionId = nonce; + ExecutionRequested(executionId, _to, _value, _data); + txx[executionId] = Transaction( + { + to: _to, + value: _value, + data: _data, + nonce: nonce, + approverCount: 0 + }); + nonce++; + } + + function _approve( + bytes32 _key, + uint256 _id, + bool _approval + ) + private + returns(bool success) + { + + Transaction storage trx = txx[_id]; + + uint256 approvalCount; + uint256 requiredKeyPurpose; + + Approved(_id, _approval); + + if (trx.to == address(this)) { + require(isKeyType(_key, MANAGEMENT_KEY)); + bytes32 managerKeyHash = keccak256(_key, MANAGEMENT_KEY); + requiredKeyPurpose = MANAGEMENT_KEY; + approvalCount = _calculateApprovals(managerKeyHash, _approval, trx); + } else { + require(isKeyType(_key, ACTION_KEY)); + bytes32 actorKeyHash = keccak256(_key, ACTION_KEY); + requiredKeyPurpose = ACTION_KEY; + approvalCount = _calculateApprovals(actorKeyHash, _approval, trx); + } + + if (approvalCount >= minimumApprovalsByKeyPurpose[requiredKeyPurpose]) { + Executed(_id, trx.to, trx.value, trx.data); + success = trx.to.call.value(trx.value)(trx.data); + } + } + + function _addKey( + bytes32 _key, + uint256 _purpose, + uint256 _type + ) + private + { + bytes32 keyHash = keccak256(_key, _purpose); + + require(keys[keyHash].purpose == 0); + require( + _purpose == MANAGEMENT_KEY || + _purpose == ACTION_KEY || + _purpose == CLAIM_SIGNER_KEY || + _purpose == ENCRYPTION_KEY + ); + KeyAdded(_key, _purpose, _type); + keys[keyHash] = Key(_purpose, _type, _key); + indexes[keyHash] = keysByPurpose[_purpose].push(_key) - 1; + } + + function _removeKey( + bytes32 _key, + uint256 _purpose + ) + private + { + bytes32 keyHash = keccak256(_key, _purpose); + Key storage myKey = keys[keyHash]; + KeyRemoved(myKey.key, myKey.purpose, myKey.keyType); + + uint index = indexes[keyHash]; + delete indexes[keyHash]; + bytes32 replacer = keysByPurpose[_purpose][keysByPurpose[_purpose].length - 1]; + keysByPurpose[_purpose][index] = replacer; + indexes[keccak256(replacer, _purpose)] = index; + keysByPurpose[_purpose].length--; + + if (_purpose == MANAGEMENT_KEY) { + require( + keysByPurpose[MANAGEMENT_KEY].length >= 1 && + keysByPurpose[MANAGEMENT_KEY].length >= minimumApprovalsByKeyPurpose[MANAGEMENT_KEY] + ); + + } + + delete keys[keyHash]; + } + + function _calculateApprovals( + bytes32 _keyHash, + bool _approval, + Transaction storage trx + ) + private + returns (uint256 approvalCount) + { + require(trx.approvals[_keyHash] != _approval); + + trx.approvals[_keyHash] = _approval; + if (_approval) { + trx.approverCount++; + } else { + trx.approverCount--; + } + + return trx.approverCount; + } + + + function _includeClaim( + bytes32 _claimHash, + uint256 _claimType, + uint256 _scheme, + address _issuer, + bytes _signature, + bytes _data, + string _uri + ) + private + { + claims[_claimHash] = Claim( + { + claimType: _claimType, + scheme: _scheme, + issuer: _issuer, + signature: _signature, + data: _data, + uri: _uri + } + ); + indexes[_claimHash] = claimsByType[_claimType].length; + claimsByType[_claimType].push(_claimHash); + ClaimAdded( + _claimHash, + _claimType, + _scheme, + _issuer, + _signature, + _data, + _uri + ); + } + + + function _modifyClaim( + bytes32 _claimHash, + uint256 _claimType, + uint256 _scheme, + address _issuer, + bytes _signature, + bytes _data, + string _uri + ) + private + { + require(msg.sender == _issuer); + ClaimChanged( + _claimHash, + _claimType, + _scheme, + _issuer, + _signature, + _data, + _uri + ); + claims[_claimHash] = Claim({ + claimType: _claimType, + scheme: _scheme, + issuer: _issuer, + signature: _signature, + data: _data, + uri: _uri + }); + } + + +} diff --git a/contracts/identity/IdentityFactory.sol b/contracts/identity/IdentityFactory.sol new file mode 100644 index 0000000..a0dac9a --- /dev/null +++ b/contracts/identity/IdentityFactory.sol @@ -0,0 +1,32 @@ +pragma solidity ^0.4.17; + +import "../deploy/Factory.sol"; +import "../deploy/DelayedUpdatableInstance.sol"; +import "./IdentityKernel.sol"; + + +contract IdentityFactory is Factory { + + event IdentityCreated(address instance); + + function IdentityFactory(bytes _infohash) + public + Factory(new IdentityKernel(), _infohash) + { + } + + function createIdentity() + external + { + createIdentity(msg.sender); + } + + function createIdentity(address _idOwner) + public + { + IdentityKernel instance = IdentityKernel(new DelayedUpdatableInstance(address(latestKernel))); + instance.initIdentity(_idOwner); + IdentityCreated(address(instance)); + } + +} diff --git a/contracts/identity/IdentityKernel.sol b/contracts/identity/IdentityKernel.sol new file mode 100644 index 0000000..d40056f --- /dev/null +++ b/contracts/identity/IdentityKernel.sol @@ -0,0 +1,11 @@ +pragma solidity ^0.4.17; + +import "../deploy/DelayedUpdatableInstanceStorage.sol"; +import "./Identity.sol"; + +contract IdentityKernel is DelayedUpdatableInstanceStorage, Identity { + + function initIdentity(address _caller) external { + _constructIdentity(_caller); + } +} diff --git a/contracts/tests/TestContract.sol b/contracts/tests/TestContract.sol new file mode 100644 index 0000000..3074bda --- /dev/null +++ b/contracts/tests/TestContract.sol @@ -0,0 +1,34 @@ +pragma solidity ^0.4.17; + + +contract TestContract { + + event TestFunctionExecuted(); + + function test() public { + TestFunctionExecuted(); + } + + + /* + Helper function to be used in unit testing due to error in web3 + web3.utils.soliditySha3([1, 2, 3]) + Error: Autodetection of array types is not supported. + at _processSoliditySha3Args (node_modules/web3-utils/src/soliditySha3.js:176:15) + */ + function hash( + address identity, + bytes32 _revealedSecret, + address _dest, + bytes _data, + bytes32 _newSecret, + bytes32[] _newFriendsHashes) + external + pure + returns(bytes32) + { + return keccak256(identity, _revealedSecret, _dest, _data, _newSecret, _newFriendsHashes); + + } + +} \ No newline at end of file diff --git a/contracts/tests/UpdatedIdentityKernel.sol b/contracts/tests/UpdatedIdentityKernel.sol new file mode 100644 index 0000000..23ad12b --- /dev/null +++ b/contracts/tests/UpdatedIdentityKernel.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.4.17; + +import "../identity/IdentityKernel.sol"; + + +contract UpdatedIdentityKernel is IdentityKernel { + + event TestFunctionExecuted(uint256 minApprovalsByManagementKeys); + + function test() public { + TestFunctionExecuted(minimumApprovalsByKeyPurpose[MANAGEMENT_KEY]); + } +} \ No newline at end of file diff --git a/test/factory.js b/test/factory.js new file mode 100644 index 0000000..84375c1 --- /dev/null +++ b/test/factory.js @@ -0,0 +1,102 @@ +const TestUtils = require("../utils/testUtils.js") +const idUtils = require("../utils/identityUtils.js") +const web3EthAbi = require("web3-eth-abi"); + +const Identity = artifacts.require("./identity/Identity.sol"); +const IdentityFactory = artifacts.require("./identity/IdentityFactory.sol"); +const UpdatableInstance = artifacts.require('./deploy/UpdatableInstance.sol'); +const UpdatedIdentityKernel = artifacts.require("./tests/UpdatedIdentityKernel.sol"); + +contract('IdentityFactory', function(accounts) { + + let identityFactory; + let identity; + let updatedIdentity; + let updatedIdentityKernel; + + before(async () => { + identityFactory = await IdentityFactory.new("0xaaa", {from: accounts[0]}); + }) + + describe("IdentityFactory()", () => { + it("Creates a new identity", async () => { + let tx = await identityFactory.createIdentity({from: accounts[0]}); + const logEntry = tx.logs[0]; + assert.strictEqual(logEntry.event, "IdentityCreated"); + + identity = await Identity.at(logEntry.args.instance, {from: accounts[0]}) + + assert.equal( + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])), + idUtils.purposes.MANAGEMENT, + identity.address + ".getKeyPurpose("+accounts[0]+") is not MANAGEMENT_KEY") + }); + + + it("Registers a updated identity contract", async() => { + const infoHash = "0xbbb"; + updatedIdentityKernel = await UpdatedIdentityKernel.new({from: accounts[0]}); + await identityFactory.setKernel(updatedIdentityKernel.address, infoHash); + + const newKernel = await TestUtils.listenForEvent(identityFactory.NewKernel()); + assert(newKernel.infohash, infoHash, "Infohash is not correct"); + }); + + + it("Creates a new identity using latest version", async() => { + let tx = await identityFactory.createIdentity({from: accounts[0]}); + const logEntry = tx.logs[0]; + assert.strictEqual(logEntry.event, "IdentityCreated"); + + updatedIdentity = await UpdatedIdentityKernel.at(logEntry.args.instance, {from: accounts[0]}) + tx = await updatedIdentity.test({from: accounts[0]}); + assert.strictEqual(tx.logs[0].event, "TestFunctionExecuted"); + + // Test if it still executes identity functions as expected + let baseIdentity = await Identity.at(updatedIdentity.address, {from: accounts[0]}) + assert.equal( + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])), + 1, + identity.address + ".getKeyPurpose("+accounts[0]+") is not MANAGEMENT_KEY") + }); + + + it("Updates an identity to the latest version", async() => { + let tx1 = await identity.execute( + identity.address, + 0, + idUtils.encode.updateRequestUpdatableInstance(updatedIdentityKernel.address), + {from: accounts[0]} + ); + + assert.strictEqual(tx1.logs[tx1.logs.length - 1].event, "Executed"); + + // Updating EVM timestamp to test delay + const plus31days = 60 * 60 * 24 * 31; + + web3.currentProvider.send({jsonrpc: "2.0", method: "evm_increaseTime", params: [plus31days], id: 0}); + web3.currentProvider.send({jsonrpc: "2.0", method: "evm_mine", params: [], id: 0}) + + // Confirm update + let tx2 = await identity.execute( + identity.address, + 0, + idUtils.encode.updateConfirmUpdatableInstance(updatedIdentityKernel.address), + {from: accounts[0]} + ); + + assert.strictEqual(tx2.logs[tx2.logs.length - 1].event, "Executed"); + + // Calling function available in updated identity kernel + let updatedIdentity1 = await UpdatedIdentityKernel.at(identity.address, {from: accounts[0]}) + let tx3 = await updatedIdentity1.test({from: accounts[0]}); + + assert.strictEqual(tx3.logs[tx3.logs.length - 1].event, "TestFunctionExecuted"); + assert.equal( + tx3.logs[tx3.logs.length - 1].args.minApprovalsByManagementKeys.toString(10), + 1, + identity.address + " wasn't updated to last version"); + }) + }); + +}); \ No newline at end of file diff --git a/test/identity.js b/test/identity.js index 55a046d..f240209 100644 --- a/test/identity.js +++ b/test/identity.js @@ -1,169 +1,265 @@ -const TestUtils = require("./TestUtils.js") +const TestUtils = require("../utils/testUtils.js"); +const web3EthAbi = require("web3-eth-abi"); +const idUtils = require('../utils/identityUtils.js'); + const Identity = artifacts.require("./identity/Identity.sol"); +const TestContract = artifacts.require("./test/TestContract.sol"); contract('Identity', function(accounts) { let identity; + beforeEach(async () => { identity = await Identity.new({from: accounts[0]}) }) - describe("Identity()", () => { - it("initialize with msg.sender as management key", async () => { assert.equal( - await identity.getKeyType(accounts[0]), - 1, - identity.address+".getKeyType("+accounts[0]+") is not MANAGEMENT_KEY") + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])), + idUtils.purposes.MANAGEMENT, + identity.address + ".getKeyPurpose("+accounts[0]+") is not MANAGEMENT_KEY") }); - }); describe("addKey(address _key, uint256 _type)", () => { - it("MANAGEMENT_KEY add a new address as ACTION_KEY", async () => { - await identity.addKey(accounts[1], 2, {from: accounts[0]}) - assert.equal( - await identity.getKeyType(accounts[1]), - 2, - identity.address+".getKeyType("+accounts[1]+") is not ACTION_KEY") - }); + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + assert.equal( + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), + idUtils.purposes.ACTION, + identity.address+".getKeyPurpose("+accounts[1]+") is not ACTION_KEY") + }); it("should not add key by non manager", async () => { try { - await identity.addKey(accounts[1], 1, {from: accounts[2]}) - }catch(e){ + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.MANAGEMENT, idUtils.types.ADDRESS), + {from: accounts[2]} + ); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); } + assert.equal( - await identity.getKeyType(accounts[1]), - 0, - identity.address+".getKeyType("+accounts[1]+") is not correct") + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), + idUtils.purposes.NONE, + identity.address+".getKeyPurpose("+accounts[1]+") is not correct") }); - it("should not add key type 1 by actor", async () => { - await identity.addKey(accounts[2], 2, {from: accounts[0]}) + it("should not add key type 1 by actor", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[2], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + try { - await identity.addKey(accounts[1], 1, {from: accounts[2]}) - } catch(e){ + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.MANAGEMENT, idUtils.types.ADDRESS), + {from: accounts[2]} + ); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); } + assert.equal( - await identity.getKeyType(accounts[1]), - 0, + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), + idUtils.purposes.NONE, identity.address+".getKeyType("+accounts[1]+") is not correct") }); - - it("fire KeyAdded(address indexed key, uint256 indexed type)", async () => { - identity.addKey(accounts[1], 2, {from: accounts[0]}) + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.MANAGEMENT, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + const keyAdded = await TestUtils.listenForEvent(identity.KeyAdded()) - assert(keyAdded.key, accounts[1], "Key is not correct") - assert(keyAdded.keyType, 2, "Type is not correct") + assert(keyAdded.key, TestUtils.addressToBytes32(accounts[1]), "Key is not correct") + assert(keyAdded.keyType, idUtils.types.ADDRESS, "Type is not correct") }); - }); describe("removeKey(address _key, uint256 _type)", () => { + it("MANAGEMENT_KEY should remove a key", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.MANAGEMENT, idUtils.types.ADDRESS), + {from: accounts[0]} + ); - it("MANAGEMENT_KEY should removes a key", async () => { - await identity.addKey(accounts[1], 1, {from: accounts[0]}) - await identity.removeKey(accounts[0], {from: accounts[1]}) + await identity.execute( + identity.address, + 0, + idUtils.encode.removeKey(accounts[1], idUtils.purposes.MANAGEMENT), + {from: accounts[0]} + ); + assert.equal( - await identity.getKeyType(accounts[0]), - 0, - identity.address+".getKeyType("+accounts[0]+") is not 0") + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), + idUtils.purposes.NONE, + identity.address+".getKeyPurpose("+accounts[1]+") is not 0") }); - - it("other key should not removes a key", async () => { - await identity.addKey(accounts[1], 1, {from: accounts[0]}) + it("other key should not remove a key", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.MANAGEMENT, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + try { - await identity.removeKey(accounts[1], {from: accounts[2]}) - }catch (e) { - + await identity.execute( + identity.address, + 0, + idUtils.encode.removeKey(accounts[1], idUtils.purposes.MANAGEMENT), + {from: accounts[2]} + ); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); } + assert.equal( - await identity.getKeyType(accounts[1]), - 1, - identity.address+".getKeyType("+accounts[1]+") is not 0") + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), + idUtils.purposes.MANAGEMENT, + identity.address+".getKeyPurpose("+accounts[1]+") is not 0") }); it("actor key should not remove key", async () => { - await identity.addKey(accounts[1], 2, {from: accounts[0]}) - await identity.addKey(accounts[2], 2, {from: accounts[0]}) - try { - await identity.removeKey(accounts[1], {from: accounts[2]}) - }catch (e) { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[2], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + + try { + await identity.execute( + identity.address, + 0, + idUtils.encode.removeKey(accounts[1], idUtils.purposes.ACTION), + {from: accounts[2]} + ); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); } + assert.equal( - await identity.getKeyType(accounts[1]), - 2, + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), + idUtils.purposes.ACTION, identity.address+".getKeyType("+accounts[1]+") is not 0") }); - it("MANAGEMENT_KEY should not remove itself MANAGEMENT_KEY when there is no other MANAGEMENT_KEY", async () => { - try { - await identity.removeKey(accounts[0], {from: accounts[0]}) - } catch(e) { - //nothing - } + await identity.execute( + identity.address, + 0, + idUtils.encode.removeKey(accounts[0], idUtils.purposes.MANAGEMENT), + {from: accounts[0]} + ); + assert.equal( - await identity.getKeyType(accounts[0]), - 1, - identity.address+".getKeyType("+accounts[0]+") is not MANAGEMENT_KEY") - + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])), + idUtils.purposes.MANAGEMENT, + identity.address+".getKeyType("+accounts[0]+") is not 1") }); it("fire KeyRemoved(address indexed key, uint256 indexed type)", async () => { - await identity.addKey(accounts[1], 2, {from: accounts[0]}) - identity.removeKey(accounts[1], {from: accounts[0]}) - const keyRemoved = await TestUtils.listenForEvent(identity.KeyRemoved()) - assert(keyRemoved.key, accounts[1], "Key is not correct") - assert(keyRemoved.keyType, 2, "Type is not correct") - }); + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + + await identity.execute( + identity.address, + 0, + idUtils.encode.removeKey(accounts[1], idUtils.purposes.ACTION), + {from: accounts[0]} + ); + const keyRemoved = await TestUtils.listenForEvent(identity.KeyRemoved()); + assert(keyRemoved.key, TestUtils.addressToBytes32(accounts[1]), "Key is not correct"); + assert(keyRemoved.keyType, idUtils.types.ADDRESS, "Type is not correct"); + }); }); - describe("getKeyType(address _key)", () => { + describe("getKeyPurpose(address _key)", () => { it("should start only with initializer as only key", async () => { assert.equal( - await identity.getKeyType(accounts[0]), - 1, - identity.address+".getKeyType("+accounts[0]+") is not correct") + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[0])), + idUtils.purposes.MANAGEMENT, + identity.address+".getKeyPurpose("+accounts[0]+") is not correct") assert.equal( - await identity.getKeyType(accounts[1]), - 0, - identity.address+".getKeyType("+accounts[1]+") is not correct") + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), + idUtils.purposes.NONE, + identity.address+".getKeyPurpose("+accounts[1]+") is not correct") }); it("should get type 2 after addKey type 2", async () => { - await identity.addKey(accounts[1], 2, {from: accounts[0]}) + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + assert.equal( - await identity.getKeyType(accounts[1]), - 2, - identity.address+".getKeyType("+accounts[1]+") is not correct") + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), + idUtils.purposes.ACTION, + identity.address+".getKeyPurpose("+accounts[1]+") is not correct") }); - it("should get type 999 after addKey type 999", async () => { - await identity.addKey(accounts[1], 999, {from: accounts[0]}) + it("should get type 3 after addKey type 3", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.CLAIM_SIGNER, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + assert.equal( - await identity.getKeyType(accounts[1]), - 999, - identity.address+".getKeyType("+accounts[1]+") is not correct") + await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])), + idUtils.purposes.CLAIM_SIGNER, + identity.address+".getKeyPurpose("+accounts[1]+") is not correct") }); }); - + /* describe("getKeysByType(uint256 _type)", () => { it("at initialization", async () => { @@ -177,60 +273,179 @@ contract('Identity', function(accounts) { it("after removeKey", async () => { }); - - it("after replaceKey", async () => { - - }); - - }); - - - describe("replaceKey(address _oldKey, address _newKey)", () => { - - it("MANAGEMENT_KEY replace itself (alone)", async () => { - - }); - - it("MANAGEMENT_KEY replace a key between others", async () => { - - }); - - it("MANAGEMENT_KEY replace the first key", async () => { - - }); - - it("MANAGEMENT_KEY replace the last key", async () => { - - }); - - it("fire KeyReplaced(address indexed oldKey, address indexed newKey, uint256 indexed type)", async () => { - - }); - }); +*/ describe("execute(address _to, uint256 _value, bytes _data)", () => { + let testContractInstance; + let functionPayload; + + it("Identity should receive ether", async() => { + + const amountToSend = web3.toWei(0.05, "ether"); + + let idBalance0 = web3.eth.getBalance(identity.address); + + await web3.eth.sendTransaction({from:accounts[0], to:identity.address, value: amountToSend}) + + let idBalance1 = web3.eth.getBalance(identity.address); + + assert.equal(idBalance0.toNumber() + amountToSend, idBalance1.toNumber(), identity.address + " did not receive ether"); + }); it("ACTOR_KEY execute arbitrary transaction", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + + testContractInstance = await TestContract.new({from: accounts[0]}); + + functionPayload = web3EthAbi.encodeFunctionCall({ + name: 'test', + type: 'function', + inputs: [] + }, []); + + await identity.execute( + testContractInstance.address, + 0, + functionPayload, + {from: accounts[1]} + ); + assert.notEqual( + await TestUtils.listenForEvent(testContractInstance.TestFunctionExecuted()), + undefined, + "Test function was not executed"); }); - it("MANAGEMENT_KEY execute arbitrary transaction", async () => { - + it("MANAGEMENT_KEY cannot execute arbitrary transaction", async () => { + try { + await identity.execute( + testContractInstance.address, + 0, + functionPayload, + {from: accounts[0]} + ); + } catch(error) { + TestUtils.assertJump(error); + } }); it("Other keys NOT execute arbitrary transaction", async () => { + try { + await identity.execute( + testContractInstance.address, + 0, + functionPayload, + {from: accounts[3]} + ); + assert.fail('should have reverted before'); + } catch(error) { + TestUtils.assertJump(error); + } + }); + + + it("ACTION_KEY should send ether from contract", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + + // Adding funds to contract + await web3.eth.sendTransaction({from:accounts[0], to:identity.address, value: web3.toWei(0.05, "ether")}) + + const amountToSend = web3.toWei(0.01, "ether"); + + let idBalance0 = web3.eth.getBalance(identity.address); + let a2Balance0 = web3.eth.getBalance(accounts[2]); + + await identity.execute( + accounts[2], + amountToSend, + '', + {from: accounts[1]} + ); + + let idBalance1 = web3.eth.getBalance(identity.address); + let a2Balance1 = web3.eth.getBalance(accounts[2]); + + assert(idBalance1.toNumber, idBalance0.toNumber - amountToSend, "Contract did not send ether"); + assert(a2Balance1.toNumber, a2Balance0.toNumber + amountToSend, accounts[2] + " did not receive ether"); + }); + + it("fire ExecutionRequested(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data)", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + await identity.execute( + testContractInstance.address, + 0, + functionPayload, + {from: accounts[1]} + ); + + const executionRequested = await TestUtils.listenForEvent(identity.ExecutionRequested()); + assert(executionRequested.to, testContractInstance.address, "To is not correct"); + assert(executionRequested.value, 0, "Value is not correct"); + assert(executionRequested.data, functionPayload, "Data is not correct"); }); it("fire Executed(uint256 indexed executionId, address indexed to, uint256 indexed value, bytes data)", async () => { + await identity.execute( + identity.address, + 0, + idUtils.encode.addKey(accounts[1], idUtils.purposes.ACTION, idUtils.types.ADDRESS), + {from: accounts[0]} + ); + await identity.execute( + testContractInstance.address, + 0, + functionPayload, + {from: accounts[1]} + ); + + const executed = await TestUtils.listenForEvent(identity.Executed()); + assert(executed.to, testContractInstance.address, "To is not correct"); + assert(executed.value, 0, "Value is not correct"); + assert(executed.data, functionPayload, "Data is not correct"); }); }); +/* + + describe("setMinimumApprovalsByKeyPurpose(uint256 _type, uint8 _minimumApprovals)", () => { + it("MANAGEMENT_KEY should set minimum approvals for MANAGEMENT_KEYs", async () => { + + }); + + it("MANAGEMENT_KEY should set minimum approvals for ACTION_KEYs", async () => { + + }); + + it("ACTION_KEY should not be able to set minimum approvals", async () => { + + }); + + it("Other keys should not be able to set minimum approvals", async () => { + + }); + }); + describe("approve(bytes32 _id, bool _approve)", () => { it("MANAGEMENT_KEY should approve a claim", async () => { @@ -241,6 +456,10 @@ contract('Identity', function(accounts) { }); + it("2 out of 3 MANAGEMENT_KEY should approve a transaction and execute it", async () => { + + }); + it("fire Approved(uint256 indexed executionId, bool approved)", async () => { }); @@ -282,5 +501,5 @@ contract('Identity', function(accounts) { }); }); - + */ }); diff --git a/test/identityExtended.js b/test/identityExtended.js new file mode 100644 index 0000000..0e2db9a --- /dev/null +++ b/test/identityExtended.js @@ -0,0 +1,64 @@ +const TestUtils = require("../utils/testUtils.js") +var ethUtils = require('ethereumjs-util') + +const Identity = artifacts.require("./identity/Identity.sol"); + +contract('Identity - Extended Functionality', function(accounts) { + + let identity; + + beforeEach(async () => { + identity = await Identity.new({from: accounts[0]}); + }) + + + describe("Identity()", () => { + + let privateKey = new Buffer('61bffea9215f65164ad18b45aff1436c0c165d0d5dd2087ef61b4232ba6d2c1a', 'hex') + let publicKey = ethUtils.privateToPublic(privateKey); + let pkSha = web3.sha3(publicKey.toString('hex'), {encoding: 'hex'}); + + it("Add ECDSA Management Key", async () => { + + await identity.addKey(pkSha, 2, 1, {from: accounts[0]}) + + await identity.addPublicKey(pkSha, '0x' + publicKey.toString('hex'), {from: accounts[0]}); + + assert.equal( + await identity.getPublicKey(pkSha, {from: accounts[0]}), + '0x' + publicKey.toString('hex'), + identity.address+".getPublicKey("+pkSha+") is not correct"); + + }); + + + it("Test Execution", async () => { + + let to = accounts[1]; + let value = 100; + let data = ''; + + let message = ethUtils.toBuffer("SignedMessage"); + let msgHash = ethUtils.hashPersonalMessage(message); + let sig = ethUtils.ecsign(msgHash, privateKey); + + let r = '0x' + sig.r.toString('hex'); + let s = '0x' + sig.s.toString('hex'); + let v = sig.v; + + + await identity.addKey(pkSha, 2, 1, {from: accounts[0]}) + + await identity.addPublicKey(pkSha, '0x' + publicKey.toString('hex'), {from: accounts[0]}); + + let tx = await identity.executeECDSA(to, value, data, pkSha, '0x' + msgHash.toString('hex'), v, r, s, {from: accounts[0]}); + + // TODO Assert ExecutionRequested Event + console.log(tx) + + }); + }); + + + +}); diff --git a/utils/identityUtils.js b/utils/identityUtils.js new file mode 100644 index 0000000..7b52d62 --- /dev/null +++ b/utils/identityUtils.js @@ -0,0 +1,191 @@ +const web3EthAbi = require("web3-eth-abi"); + +const _types = { + ADDRESS: 0, + ECDSA: 1, + RSA: 2 +} + +const _purposes = { + MANAGEMENT: 1, + ACTION: 2, + CLAIM_SIGNER: 3, + ENCRYPTION: 4, + + NONE: 0 +} + +const hexToBytes32 = (input) => { + input = input.replace(/^0x/i,''); + const stringed = "0000000000000000000000000000000000000000000000000000000000000000" + input; + return "0x" + stringed.substring(stringed.length - 64, stringed.length); +} + +const _addKey = function(key, purpose, type){ + if(!/^(0x)?[0-9a-f]{0,64}$/i.test(key)) + throw new Error('Key "'+ key +'" is not a valid hex string'); + + if (Object.values(_purposes).indexOf(purpose) == -1) + throw new Error('Purpose "'+ purpose +'" is not a valid purpose'); + + if (Object.values(_types).indexOf(type) == -1) + throw new Error('Type "'+ type +'" is not a valid type'); + + return web3EthAbi.encodeFunctionCall({ + name: 'addKey', + type: 'function', + inputs: [{ + type: 'bytes32', + name: '_key' + },{ + type: 'uint256', + name: '_purpose' + },{ + type: 'uint256', + name: '_type' + }] + }, [hexToBytes32(key), purpose, type]); +} + +const _removeKey = function(key, purpose){ + if(!/^(0x)?[0-9a-f]{0,64}$/i.test(key)) + throw new Error('Key "'+ key +'" is not a valid hex string'); + + if (Object.values(_purposes).indexOf(purpose) == -1) + throw new Error('Purpose "'+ purpose +'" is not a valid purpose'); + + return web3EthAbi.encodeFunctionCall({ + name: 'removeKey', + type: 'function', + inputs: [{ + type: 'bytes32', + name: '_key' + },{ + type: 'uint256', + name: '_purpose' + }] + }, [hexToBytes32(key), purpose]); +} + + +const _setMinimumApprovalsByKeyType = function(type, minimumApprovals) { + + if (Object.values(_types).indexOf(type) == -1) + throw new Error('Type "'+ type +'" is not a valid type'); + + // TODO valdate minimumApprovals + + return web3EthAbi.encodeFunctionCall({ + name: 'setMinimumApprovalsByKeyType', + type: 'function', + inputs: [{ + type: 'uint256', + name: '_type' + },{ + type: 'uint8', + name: '_minimumApprovals' + }] + }, arguments); +} + + +const _setupRecovery = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'setupRecovery', + type: 'function', + inputs: [{ + type: 'address', + name: '_recoveryContract' + }] + }, [address]); +} + +const _managerReset = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'managerReset', + type: 'function', + inputs: [{ + type: 'address', + name: '_newKey' + }] + }, [address]); +} + + +const _updateUpdatableInstance = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'updateUpdatableInstance', + type: 'function', + inputs: [{ + type: 'address', + name: '_kernel' + }] + }, [address]); +} + +const _updateRequestUpdatableInstance = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'updateRequestUpdatableInstance', + type: 'function', + inputs: [{ + type: 'address', + name: '_kernel' + }] + }, [address]); +} + +const _updateConfirmUpdatableInstance = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'updateConfirmUpdatableInstance', + type: 'function', + inputs: [{ + type: 'address', + name: '_kernel' + }] + }, [address]); +} + +const _updateCancelUpdatableInstance = function(address){ + if(!/^(0x)?[0-9a-f]{0,40}$/i.test(address)) + throw new Error('Address "'+ address +'" is not a valid Ethereum address.'); + + return web3EthAbi.encodeFunctionCall({ + name: 'updateCancelUpdatableInstance', + type: 'function', + inputs: [] + }, []); +} + + + + +module.exports = { + types: _types, + purposes: _purposes, + encode: { + addKey: _addKey, + removeKey: _removeKey, + setMinimumApprovalsByKeyType: _setMinimumApprovalsByKeyType, + setupRecovery: _setupRecovery, + managerReset: _managerReset, + updateUpdatableInstance: _updateUpdatableInstance, + updateRequestUpdatableInstance: _updateRequestUpdatableInstance, + updateConfirmUpdatableInstance: _updateConfirmUpdatableInstance, + updateCancelUpdatableInstance: _updateCancelUpdatableInstance + } +} \ No newline at end of file