mirror of
https://github.com/status-im/contracts.git
synced 2025-02-24 04:28:51 +00:00
Merge of 27-ens-usernames with develop
This commit is contained in:
commit
0cc3554275
144
Identity.md
Normal file
144
Identity.md
Normal file
@ -0,0 +1,144 @@
|
||||
# Identity contracts
|
||||
|
||||
# Table of content
|
||||
- [Summary](#summary)
|
||||
- [Smart Contracts Overview](#smart-contracts-overview)
|
||||
- [Tutorials](#tutorials)
|
||||
- - [Identity Creation](#identity-creation)
|
||||
- - [Executions and Approvals](#executions-and-approvals)
|
||||
- - [Management Activities](#management-activities)
|
||||
- - [Constants / View functions](#constants--view-functions)
|
||||
- - [Adding new functionality to identitities](#adding-new-functionality-to-identitities)
|
||||
- - [Upgrade an `IdentityKernel` instance](#upgrade-an-identitykernel-instance)
|
||||
- - [Setting up Identity Recovery contract](#setting-up-identity-recovery-contract)
|
||||
- - [Recovering an Identity](#recovering-an-identity)
|
||||
## Summary
|
||||
This is a proposed proof of concept for the implementation of interfaces [ERC-725](https://github.com/ethereum/EIPs/issues/725) and [ERC735](https://github.com/ethereum/EIPs/issues/725), providing the following functionality:
|
||||
- Public key register, composed of Ethereum Addresses and ECDSA keys. These keys can perform management activities over the identity itself, as well as performing operations in other contracts and transfer of ether.
|
||||
- Claim holding, that can be used to add / verify claims against an identity.
|
||||
- Identity factory to simplify the process of instantiating an Identity contract, as well as handling the upgrade process of this instance, assuming there's a new version of this contract.
|
||||
- Identity recovery process that can be initiated using a shared secret among keys related to the identity.
|
||||
|
||||
## Smart Contracts Overview
|
||||
- `Controlled`. Keeps tracks of the controller or owner of the contract. Provides the modifier `onlyController` which can be used in child contracts.
|
||||
- `DelegatedCall`. Abstract contract that delegates calls using the `delegated` modifier to the result of `targetDelegatedCall()` function.
|
||||
- `InstanceStorage`.
|
||||
Defines kernel vars that an `IdentityKernel` contract share with a `Instance` contract. If you wish to reuse this contract, it is important to avoid overwriting wrong storage pointers, so `InstanceStorage` should be always the first contract to be inherited.
|
||||
- `Instance`. Contract that forwards everything through `delegatecall` to a defined `IdentityKernel`. This contracts inherits from `InstanceStorage` and `DelegatedCall`
|
||||
- `UpdatableInstance`. A contract that can be updated, if the contract itself calls `updateUpdatableInstance()`. This contract inherits from `Instance`
|
||||
- `DelayedUpdatableInstanceStorage`. This contract defines kernel vars that an `IdentityKernel` contract shares with and `Instance`. See `InstanceStorage` fro restrictions in case of reuse. This contract inherits from `InstanceStorage`
|
||||
- `DelayedUpdatableInstance`. Extending the functionality of `UpdatableInstance`, this contract introduces a delay functionality based in the `block.timestamp` in order to limit updates with a 30 days lock.
|
||||
- `Factory`. Contract used as a version control for child factories contracts
|
||||
- `ERC725` and `ERC735`. Interfaces based on EIPs [ERC-725: Identity](https://github.com/ethereum/EIPs/issues/725) and [ERC735: Claims Holder](https://github.com/ethereum/EIPs/issues/725)
|
||||
- `Identity`. Implementation of ERC725 and ERC735. Includes additional management functions to handle minimum required approvals for execution of transactions, as well as recovery related functions.
|
||||
- `IdentityKernel`. Represents a version of the identity contract that can be created with the `IdentityFactory`, as well as be updated with calls to itself. This contract inherits from `DelayedUpdatableInstanceStorage` and `Identity`
|
||||
- `FriendsRecovery`. Contract that handles the recovery process of an `Identity` in case the management keys are lost, or were compromised. A `FriendsRecovery` contract instance has an 1:1 association with an `Identity`
|
||||
- `IdentityFactory`. Deploys `DelayedUpdatableInstanceStorage` configured to a deployed `IdentityKernel` and initializes the Instance with `initIdentity` function from kernel.
|
||||
|
||||
## Tutorials
|
||||
|
||||
### Identity Creation
|
||||
We recommend to not create `Identity` instances directly, but create them through the `IdentityFactory` which is deployed in: `0x000000000000000000000000`, and provides the following functions:
|
||||
- `createIdentity()` - will create a new instance of an `IdentityKernel`, with `msg.sender` as a management key.
|
||||
- `createIdentity(address _idOwner)` - used to specify the owner / management key of a new identity.
|
||||
|
||||
The event `IdentityCreated` is triggered when the new identity is created successfully.
|
||||
|
||||
### Executions and Approvals
|
||||
Identities can perform management activities on themselves, as well as performing actions in other contracts. These operations are performed with the `execute()` and `approve()` functions.
|
||||
|
||||
`execute(address _to, uint256 _value, bytes _data)` is called when you wish to perform an operation. This function may be called by a management key or by an action key and triggers an `ExecutionRequested` event. Management keys are required when `_to` refers to the identity itself, otherwise, action keys are required.
|
||||
|
||||
The `_value` parameters refer to the amount in ether that this transaction will send to the contract/wallet address specified in `_to`. The identity contract should have funds if the value is greater than `0`.
|
||||
|
||||
`_data` refers to the byte encoding of the operations and parameters to be executed. `web3.eth.abi.encodeFunctionCall` is useful to generate the bytecode to be sent in this function. Here's an example on how to use it to call the `addKey` function of the identity contract:
|
||||
|
||||
```
|
||||
const web3EthAbi = require("web3-eth-abi");
|
||||
let data = web3EthAbi.encodeFunctionCall({
|
||||
name: 'addKey',
|
||||
type: 'function',
|
||||
inputs: [{
|
||||
type: 'bytes32',
|
||||
name: '_key'
|
||||
},{
|
||||
type: 'uint256',
|
||||
name: '_purpose'
|
||||
},{
|
||||
type: 'uint256',
|
||||
name: '_type'
|
||||
}]
|
||||
}, ["0x1234567", 1, 1]);
|
||||
|
||||
let receipt = await identityContractInstance.execute(identityContractInstance.address, 0, data).send({from: accounts[0]});
|
||||
```
|
||||
A javascript utils library is provided in `utils/identityUtils.js` which can be used to generate the payloads used in the `_data` parameter of the `execute` function
|
||||
|
||||
Once the `execute` function is executed, if the minimum required approvals by key purpose is one, the transaction is executed immediatly (Triggering the `Approved` and `Executed` events).
|
||||
|
||||
In case the minimum required approvals are greater than 1, `approve(uint256 _id, bool _approval)` needs to be called by N management or action keys depending on the operation to perform, each call triggering an `Approved` event. Once the minimum approvals required is reached, the transaction will be executed immediatly. This `approve` function requires an transaction execution id, which can be obtained by the `ExecutionRequested` event triggered by the `execute` function.
|
||||
|
||||
### Management Activities
|
||||
Identity management is limited to the addition, removal and setting of the minimum required approvals to perform a transaction. These activities will fall into the approval process requiring that N managers approve their execution before it is performed.
|
||||
- `addKey(bytes32 _key, uint256 _purpose, uint256 _type)`. Registers a key in the identity. Keys should have a defined purpose and type. The `_purpose` of a key can be `1` - Management keys, used only to perform management activities; `2` - Action keys, used only to perform calls to external contracts and sending ether; `3` - Claim signer key, which are keys that can add claims to the identity; and `4` - Encryption keys, at the moment used only for information purposes. The `_type` of keys supported at the moment are `0` for ethereum addresses, `1` for ECDSA. Keys are stored as a bytes32 value. Both `_purpose` and `_type` accepts `uint256` types, so they're not limited to the values described in here. It is worth mentioning that keys can have more than one purpose and `addKey` can be called for the same key more than once, assuming the purpose is different each time it is called. Triggers a `KeyAdded` event.
|
||||
- `removeKey(bytes32 _key, uint256 _purpose)`. Used to remove an existing key-purpose pair. It will fail if you're removing a management key, and there is only one management key registered in the identity. Triggers a `KeyRemoved` event.
|
||||
- `replaceKey(bytes32 _oldKey, bytes32 _newKey, uint256 _newType)`. Used to replace an existing key, for a new one that can have a different type. Triggers both a `KeyRemoved` and `KeyAdded` event.
|
||||
- `setMinimumApprovalsByKeyType(uint256 _purpose, uint256 _minimumApprovals)`. By default, an `Identity` has only one management key registered (the owner of the identity), however it is possible to have more than one management key registered with `addKey`, and require a N of M approvals (both for Management and Action keys). This is done with this function, where you have to specify number of minimum approvals required by key purpose.
|
||||
|
||||
### Claims
|
||||
Lorem Ipsum
|
||||
|
||||
### Constants / View functions
|
||||
The following functions are provided to access information about an identity:
|
||||
- `getKey(bytes32 _key, uint256 _purpose)`. Returns the key type, purpose and key for a given key-purpose pair
|
||||
- `isKeyPurpose(bytes32 _key, uint256 _purpose)` returns if a given key-purpose exists, and if it's purpose is actually what was specified.
|
||||
- `getKeyPurpose(bytes32 _key)` returns an array of purposes for a given key.
|
||||
- `function getKeysByPurpose(uint256 _purpose)` returns an array of keys for a given purpose.
|
||||
- `getClaim(bytes32 _claimId)` returns the claim information registered for a given claim Id.
|
||||
- `getClaimIdsByType(uint256 _claimType)` returns an array of claim Ids for a given claim type.
|
||||
|
||||
### Adding new functionality to identitities
|
||||
New versions of identities should extend from `IdentityKernel` and need to be registered in the `IdentityFactory`. This is done by creating a new instance of the new contract which inherits from `IdentityKernel`, and then calling the `setKernel` function of the `IdentityFactory` specifiying the address of the updated identity kernel, and a `bytes32` info hash with the description of the new version.
|
||||
Once updated, a `NewKernel` event is triggered.
|
||||
|
||||
### Upgrade an `IdentityKernel` instance
|
||||
When an identity instance needs to be upgraded, we can use the execute/approve process to upgrade it to an specific version. This upgrade process requires using `execute` to call two functions
|
||||
- `updateRequestUpdatableInstance(address _newKernel)`. This will generate a pending request for upgrading an instance `30 days` later and trigger an UpdateRequested event.
|
||||
- `updateConfirmUpdatableInstance(address _newKernel)`. After `30 days` pass, this function needs to be invoked to confirm the update process. Once the update process is completed a UpdateConfirmed event is triggered.
|
||||
- An request for update can be cancelled if it hasn't been approved yet. This is done using the function `updateCancelUpdatableInstance()` with the same execute/approve process
|
||||
|
||||
Kernel addresses could be obtained using the `getVersion` function of the `IdentityFactory`
|
||||
|
||||
### Setting up Identity Recovery contract
|
||||
After creating an identity with the `IdentityFactory`, an instance of `FriendsRecovery` need to be created. The constructor of this contract expects the following parameters:
|
||||
- `_identity`: The identity contract address
|
||||
- `_setupDelay`: Time for users to be able to change the selected friends for recovery.
|
||||
- `_threshold`: Minimum number of friends required to recover an identity
|
||||
- `_secret`: sha3 of the identity address + a secret word
|
||||
- `friendHashes`: an array of sha3 hashes compossed of the identity address + secret word + friend ethereum address.
|
||||
|
||||
Once this recovery contract is created, we need to associate it with the identity. This is done through the execute/approve mechanism of the identity, sending a payload to invoke the `setupRecovery` function of the identity, passing the recovery contract address as a parameter.
|
||||
|
||||
|
||||
### Recovering an Identity
|
||||
Recovery of an identity happens when you lose access to the management key(s) or Identity. The recovery is done having the friends sign a message. This message is a sha3 hash compossed of:
|
||||
|
||||
```
|
||||
recovery address + secret word + contract to invoke (identity) + the function and parameters of the function to invoke encoded (recover function of identity) +
|
||||
new secret word hash + new friend hashes.
|
||||
```
|
||||
|
||||
Where new `new secret word hash` is a sha3 of the identity address + secret word; and `new friend hashes` is an array of sha3 hashes compossed of the identity address + secret word + friend ethereum address).
|
||||
|
||||
Normally the function that is going to be encoded should be the identity `managerReset` with the address of the new management key.
|
||||
|
||||
A minimum of (threshold) friends should approve this recovery attempt, and this can be done by them spending gas, calling the `approve` function of the recovery contract; or by having a single address (probably the identity owner) calling `approvePreSigned`.
|
||||
|
||||
`approve` could be called sending the sha3 hashed message described previously through a regular transaction , and `approvePreSigned` needs gathering the signatures of the hashed message into different arrays (for v, r, and s)
|
||||
|
||||
To enhance privacy, any account can approve anything, however only revealed addresses will be used.
|
||||
|
||||
Once the approvation is complete, the `execute` function of the recovery contract needs to be called, with the parameters used to generate the hashed message, and after the recovery is completed, `processManagerReset` needs to be executedn on the identity to remove all the management keys different from the new management key used for the recovery
|
||||
|
||||
An example of how to use the recovery contract is available in `test/friendsRecovery.js`.
|
||||
|
@ -1 +1,5 @@
|
||||
# Contracts
|
||||
|
||||
### Identity
|
||||
- ENS subdomain username registrar #27 - [Link](https://github.com/status-im/ideas/issues/27)
|
||||
- Documentation: [Identity.md](Identity.md)
|
||||
|
@ -1,5 +1,4 @@
|
||||
pragma solidity ^0.4.11;
|
||||
|
||||
pragma solidity ^0.4.8;
|
||||
|
||||
contract Controlled {
|
||||
/// @notice The address of the controller is the only address that can call
|
||||
@ -11,11 +10,13 @@ contract Controlled {
|
||||
|
||||
address public controller;
|
||||
|
||||
function Controlled() { controller = msg.sender;}
|
||||
function Controlled() public {
|
||||
controller = msg.sender;
|
||||
}
|
||||
|
||||
/// @notice Changes the controller of the contract
|
||||
/// @param _newController The new controller of the contract
|
||||
function changeController(address _newController) onlyController {
|
||||
function changeController(address _newController) public onlyController {
|
||||
controller = _newController;
|
||||
}
|
||||
}
|
63
contracts/deploy/DelayedUpdatableInstance.sol
Normal file
63
contracts/deploy/DelayedUpdatableInstance.sol
Normal file
@ -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;
|
||||
}
|
||||
|
||||
}
|
21
contracts/deploy/DelayedUpdatableInstanceStorage.sol
Normal file
21
contracts/deploy/DelayedUpdatableInstanceStorage.sol
Normal file
@ -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
|
||||
}
|
57
contracts/deploy/Factory.sol
Normal file
57
contracts/deploy/Factory.sol
Normal file
@ -0,0 +1,57 @@
|
||||
pragma solidity ^0.4.17;
|
||||
|
||||
import "../common/Controlled.sol";
|
||||
|
||||
contract Factory is Controlled {
|
||||
|
||||
event NewKernel(address newKernel, bytes infohash);
|
||||
|
||||
struct Version {
|
||||
uint256 blockNumber;
|
||||
uint256 timestamp;
|
||||
address kernel;
|
||||
bytes infohash;
|
||||
}
|
||||
mapping (address => uint256) versionMap;
|
||||
|
||||
Version[] versionLog;
|
||||
uint256 latestUpdate;
|
||||
address latestKernel;
|
||||
|
||||
function Factory(address _kernel, bytes _infohash)
|
||||
public
|
||||
{
|
||||
_setKernel(_kernel, _infohash);
|
||||
}
|
||||
|
||||
function setKernel(address _kernel, bytes _infohash)
|
||||
external
|
||||
onlyController
|
||||
{
|
||||
_setKernel(_kernel, _infohash);
|
||||
}
|
||||
|
||||
function getVersion(uint256 index) public view
|
||||
returns(uint256 blockNumber,
|
||||
uint256 timestamp,
|
||||
address kernel,
|
||||
bytes infohash)
|
||||
{
|
||||
return (versionLog[index].blockNumber,
|
||||
versionLog[index].timestamp,
|
||||
versionLog[index].kernel,
|
||||
versionLog[index].infohash);
|
||||
}
|
||||
|
||||
function _setKernel(address _kernel, bytes _infohash)
|
||||
internal
|
||||
{
|
||||
require(_kernel != latestKernel);
|
||||
versionMap[_kernel] = versionLog.length;
|
||||
versionLog.push(Version({blockNumber: block.number, timestamp: block.timestamp, kernel: _kernel, infohash: _infohash}));
|
||||
latestUpdate = block.timestamp;
|
||||
latestKernel = _kernel;
|
||||
NewKernel(_kernel, _infohash);
|
||||
}
|
||||
|
||||
}
|
@ -10,6 +10,8 @@ import "./Instance.sol";
|
||||
*/
|
||||
contract UpdatableInstance is Instance {
|
||||
|
||||
event InstanceUpdated(address oldKernel, address newKernel);
|
||||
|
||||
function UpdatableInstance(address _kernel)
|
||||
Instance(_kernel)
|
||||
public
|
||||
@ -19,6 +21,7 @@ contract UpdatableInstance is Instance {
|
||||
|
||||
function updateUpdatableInstance(address _kernel) external {
|
||||
require(msg.sender == address(this));
|
||||
InstanceUpdated(kernel, _kernel);
|
||||
kernel = _kernel;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
pragma solidity ^0.4.17;
|
||||
pragma solidity ^0.4.18;
|
||||
|
||||
contract ERC725 {
|
||||
|
||||
@ -7,18 +7,23 @@ contract ERC725 {
|
||||
uint256 constant CLAIM_SIGNER_KEY = 3;
|
||||
uint256 constant ENCRYPTION_KEY = 4;
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
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);
|
||||
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 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);
|
||||
}
|
@ -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);
|
||||
}
|
177
contracts/identity/FriendsRecovery.sol
Normal file
177
contracts/identity/FriendsRecovery.sol
Normal file
@ -0,0 +1,177 @@
|
||||
pragma solidity ^0.4.17;
|
||||
|
||||
contract FriendsRecovery {
|
||||
|
||||
address private identity;
|
||||
bytes32 private secret;
|
||||
uint256 private threshold;
|
||||
uint256 private setupDelay;
|
||||
mapping(bytes32 => bool) private friendAllowed;
|
||||
mapping(bytes32 => bool) private revealed;
|
||||
mapping(bytes32 => mapping(address => bool)) private signed;
|
||||
NewRecovery private pendingSetup;
|
||||
|
||||
struct NewRecovery {
|
||||
uint256 addition;
|
||||
bytes32 secret;
|
||||
uint256 threshold;
|
||||
uint256 setupDelay;
|
||||
bytes32[] friends;
|
||||
}
|
||||
|
||||
event SetupRequested(uint256 activation);
|
||||
event Activated();
|
||||
event Approved(bytes32 indexed secretHash, address approver);
|
||||
event Execution(bool success);
|
||||
|
||||
modifier identityOnly() {
|
||||
require(msg.sender == identity);
|
||||
_;
|
||||
}
|
||||
modifier notRevealed(bytes32 secretHash) {
|
||||
require(!revealed[secretHash]);
|
||||
_;
|
||||
}
|
||||
|
||||
function FriendsRecovery(
|
||||
address _identity,
|
||||
uint256 _setupDelay,
|
||||
uint256 _threshold,
|
||||
bytes32 _secret,
|
||||
bytes32[] _friendHashes
|
||||
)
|
||||
public
|
||||
{
|
||||
identity = _identity;
|
||||
threshold = _threshold;
|
||||
secret = _secret;
|
||||
setupDelay = _setupDelay;
|
||||
addFriends(_friendHashes);
|
||||
}
|
||||
|
||||
function withdraw()
|
||||
external
|
||||
identityOnly
|
||||
{
|
||||
identity.transfer(this.balance);
|
||||
}
|
||||
|
||||
function cancelSetup()
|
||||
external
|
||||
identityOnly
|
||||
{
|
||||
delete pendingSetup;
|
||||
SetupRequested(0);
|
||||
}
|
||||
|
||||
function setup(
|
||||
bytes32 _newSecret,
|
||||
uint256 _setupDelay,
|
||||
uint256 _threshold,
|
||||
bytes32[] _newFriendsHashes
|
||||
)
|
||||
external
|
||||
identityOnly
|
||||
notRevealed(_newSecret)
|
||||
{
|
||||
pendingSetup.addition = block.timestamp;
|
||||
pendingSetup.secret = _newSecret;
|
||||
pendingSetup.friends = _newFriendsHashes;
|
||||
pendingSetup.threshold = _threshold;
|
||||
pendingSetup.setupDelay = _setupDelay;
|
||||
SetupRequested(block.timestamp + setupDelay);
|
||||
}
|
||||
|
||||
function activate()
|
||||
external
|
||||
{
|
||||
require(pendingSetup.addition > 0);
|
||||
require(pendingSetup.addition + setupDelay <= block.timestamp);
|
||||
threshold = pendingSetup.threshold;
|
||||
setupDelay = pendingSetup.setupDelay;
|
||||
secret = pendingSetup.secret;
|
||||
addFriends(pendingSetup.friends);
|
||||
delete pendingSetup;
|
||||
Activated();
|
||||
}
|
||||
|
||||
function approve(bytes32 _secretHash)
|
||||
external
|
||||
{
|
||||
require(!signed[_secretHash][msg.sender]);
|
||||
signed[_secretHash][msg.sender] = true;
|
||||
Approved(_secretHash, msg.sender);
|
||||
}
|
||||
|
||||
function approvePreSigned(bytes32 _secretHash, uint8[] _v, bytes32[] _r, bytes32[] _s)
|
||||
external
|
||||
{
|
||||
uint256 len = _v.length;
|
||||
require (_r.length == len);
|
||||
require (_s.length == len);
|
||||
require (_v.length == len);
|
||||
bytes32 signatureHash = getSignHash(_secretHash);
|
||||
for (uint256 i = 0; i < len; i++) {
|
||||
address recovered = ecrecover(signatureHash, _v[i], _r[i], _s[i]);
|
||||
require(!signed[_secretHash][recovered]);
|
||||
require(recovered != address(0));
|
||||
signed[_secretHash][recovered] = true;
|
||||
Approved(_secretHash, recovered);
|
||||
}
|
||||
}
|
||||
|
||||
function execute(
|
||||
bytes32 _revealedSecret,
|
||||
address _dest,
|
||||
bytes _data,
|
||||
address[] _friendList,
|
||||
bytes32 _newSecret,
|
||||
bytes32[] _newFriendsHashes
|
||||
)
|
||||
external
|
||||
notRevealed(_newSecret)
|
||||
{
|
||||
require(_friendList.length >= threshold);
|
||||
require(keccak256(identity, _revealedSecret) == secret);
|
||||
revealed[_newSecret] = true;
|
||||
bytes32 _secretHash = keccak256(identity, _revealedSecret, _dest, _data, _newSecret, _newFriendsHashes);
|
||||
|
||||
for (uint256 i = 0; i < threshold; i++) {
|
||||
address friend = _friendList[i];
|
||||
require(friend != address(0));
|
||||
require(friendAllowed[keccak256(identity, _revealedSecret, friend)]);
|
||||
require(signed[_secretHash][friend]);
|
||||
delete signed[_secretHash][friend];
|
||||
}
|
||||
|
||||
secret = _newSecret;
|
||||
addFriends(_newFriendsHashes);
|
||||
|
||||
Execution(_dest.call(_data));
|
||||
}
|
||||
|
||||
function addFriends(bytes32[] _newFriendsHashes) private {
|
||||
uint256 len = _newFriendsHashes.length;
|
||||
require(len >= threshold);
|
||||
for (uint256 i = 0; i < len; i++) {
|
||||
friendAllowed[_newFriendsHashes[i]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Hash a hash with `"\x19Ethereum Signed Message:\n32"`
|
||||
* @param _hash Sign to hash.
|
||||
* @return signHash Hash to be signed.
|
||||
*/
|
||||
function getSignHash(
|
||||
bytes32 _hash
|
||||
)
|
||||
pure
|
||||
public
|
||||
returns(bytes32 signHash)
|
||||
{
|
||||
signHash = keccak256("\x19Ethereum Signed Message:\n32", _hash);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -3,68 +3,167 @@
|
||||
import "./ERC725.sol";
|
||||
import "./ERC735.sol";
|
||||
|
||||
|
||||
contract Identity is ERC725, ERC735 {
|
||||
|
||||
mapping (address => uint256) keys;
|
||||
mapping (bytes32 => Key) keys;
|
||||
mapping (bytes32 => Claim) claims;
|
||||
mapping (uint256 => address[]) keysByType;
|
||||
mapping (uint256 => bytes32[]) keysByPurpose;
|
||||
mapping (uint256 => bytes32[]) claimsByType;
|
||||
mapping (bytes32 => uint256) indexes;
|
||||
mapping (bytes32 => Transaction) txx;
|
||||
|
||||
mapping (uint => Transaction) txx;
|
||||
mapping (uint256 => uint256) minimumApprovalsByKeyPurpose;
|
||||
bytes32[] pendingTransactions;
|
||||
uint nonce = 0;
|
||||
address recoveryContract;
|
||||
address recoveryManager;
|
||||
|
||||
struct Transaction {
|
||||
address to;
|
||||
uint value;
|
||||
bytes data;
|
||||
uint nonce;
|
||||
uint approverCount;
|
||||
mapping(bytes32 => bool) approvals;
|
||||
}
|
||||
|
||||
|
||||
modifier managerOnly {
|
||||
require(keys[msg.sender] == MANAGEMENT_KEY);
|
||||
require(
|
||||
isKeyType(bytes32(msg.sender), MANAGEMENT_KEY)
|
||||
);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier managerOrSelf {
|
||||
require(keys[msg.sender] == MANAGEMENT_KEY || msg.sender == address(this));
|
||||
modifier selfOnly {
|
||||
require(
|
||||
msg.sender == address(this)
|
||||
);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier actorOnly {
|
||||
require(keys[msg.sender] == ACTION_KEY);
|
||||
modifier recoveryOnly {
|
||||
require(
|
||||
(recoveryContract != address(0) && msg.sender == address(recoveryContract))
|
||||
);
|
||||
_;
|
||||
}
|
||||
|
||||
modifier claimSignerOnly {
|
||||
require(keys[msg.sender] == CLAIM_SIGNER_KEY);
|
||||
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 Identity() public {
|
||||
_addKey(msg.sender, MANAGEMENT_KEY);
|
||||
_constructIdentity(msg.sender);
|
||||
}
|
||||
|
||||
function ()
|
||||
public
|
||||
payable
|
||||
{
|
||||
|
||||
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 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;
|
||||
|
||||
function replaceKey(address _oldKey, address _newKey) public managerOrSelf returns (bool success) {
|
||||
_addKey(_newKey, getKeyType(_oldKey));
|
||||
_removeKey(_oldKey);
|
||||
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,
|
||||
@ -72,71 +171,82 @@
|
||||
bytes _data
|
||||
)
|
||||
public
|
||||
returns (bytes32 executionId)
|
||||
managerOrActor(bytes32(msg.sender))
|
||||
returns (uint256 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) {
|
||||
executionId = _execute(_to, _value, _data);
|
||||
approve(executionId, true);
|
||||
}
|
||||
}
|
||||
|
||||
function approve(
|
||||
bytes32 _id,
|
||||
bool _approve
|
||||
)
|
||||
function approve(uint256 _id, bool _approval)
|
||||
public
|
||||
managerOnly
|
||||
managerOrActor(bytes32(msg.sender))
|
||||
returns (bool success)
|
||||
{
|
||||
require(txx[_id].nonce == nonce);
|
||||
nonce++;
|
||||
if (_approve) {
|
||||
success = txx[_id].to.call.value(txx[_id].value)(txx[_id].data);
|
||||
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,
|
||||
uint256 _signatureType,
|
||||
bytes _signature,
|
||||
bytes _claim,
|
||||
bytes _data,
|
||||
string _uri
|
||||
)
|
||||
public
|
||||
claimSignerOnly
|
||||
returns (bytes32 claimId)
|
||||
returns (bytes32 claimHash)
|
||||
{
|
||||
claimId = keccak256(_issuer, _claimType);
|
||||
claims[claimId] = Claim(
|
||||
{
|
||||
claimType: _claimType,
|
||||
issuer: _issuer,
|
||||
signatureType: _signatureType,
|
||||
signature: _signature,
|
||||
claim: _claim,
|
||||
uri: _uri
|
||||
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
|
||||
);
|
||||
indexes[keccak256(_issuer, _claimType)] = claimsByType[_claimType].length;
|
||||
claimsByType[_claimType].push(claimId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function removeClaim(bytes32 _claimId) public returns (bool success) {
|
||||
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));
|
||||
|
||||
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];
|
||||
@ -148,67 +258,364 @@
|
||||
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
|
||||
function getKey(
|
||||
bytes32 _key,
|
||||
uint256 _purpose
|
||||
)
|
||||
public
|
||||
constant
|
||||
returns
|
||||
(uint256 claimType,
|
||||
address issuer,
|
||||
uint256 signatureType,
|
||||
bytes signature,
|
||||
bytes claim,
|
||||
string uri)
|
||||
returns(uint256 purpose, uint256 keyType, bytes32 key)
|
||||
{
|
||||
Claim memory _claim = claims[_claimId];
|
||||
return (_claim.claimType, _claim.issuer, _claim.signatureType, _claim.signature, _claim.claim, _claim.uri);
|
||||
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 getClaimsIdByType(uint256 _claimType) public constant returns(bytes32[]) {
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
32
contracts/identity/IdentityFactory.sol
Normal file
32
contracts/identity/IdentityFactory.sol
Normal file
@ -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));
|
||||
}
|
||||
|
||||
}
|
11
contracts/identity/IdentityKernel.sol
Normal file
11
contracts/identity/IdentityKernel.sol
Normal file
@ -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);
|
||||
}
|
||||
}
|
34
contracts/tests/TestContract.sol
Normal file
34
contracts/tests/TestContract.sol
Normal file
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
13
contracts/tests/UpdatedIdentityKernel.sol
Normal file
13
contracts/tests/UpdatedIdentityKernel.sol
Normal file
@ -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]);
|
||||
}
|
||||
}
|
5
migrations/2_deploy_contracts.js
Normal file
5
migrations/2_deploy_contracts.js
Normal file
@ -0,0 +1,5 @@
|
||||
let Identity = artifacts.require("./Identity.sol");
|
||||
|
||||
module.exports = function(deployer) {
|
||||
deployer.deploy(Identity);
|
||||
};
|
@ -15,6 +15,13 @@
|
||||
},
|
||||
"homepage": "https://github.com/status-im/contracts#readme",
|
||||
"devDependencies": {
|
||||
"solidity-coverage": "^0.4.4"
|
||||
"solidity-coverage": "^0.4.4",
|
||||
"elliptic": "^6.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"elliptic-curve": "^0.1.0",
|
||||
"solidity-coverage": "^0.4.4",
|
||||
"ethereumjs-util": "^5.1.5",
|
||||
"web3": "^1.0.0-beta.30"
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +0,0 @@
|
||||
|
||||
// This has been tested with the real Ethereum network and Testrpc.
|
||||
// Copied and edited from: https://gist.github.com/xavierlepretre/d5583222fde52ddfbc58b7cfa0d2d0a9
|
||||
exports.assertReverts = (contractMethodCall, maxGasAvailable) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
resolve(contractMethodCall())
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
.then(tx => {
|
||||
assert.equal(tx.receipt.gasUsed, maxGasAvailable, "tx successful, the max gas available was not consumed")
|
||||
})
|
||||
.catch(error => {
|
||||
if ((error + "").indexOf("invalid opcode") < 0 && (error + "").indexOf("out of gas") < 0) {
|
||||
// Checks if the error is from TestRpc. If it is then ignore it.
|
||||
// Otherwise relay/throw the error produced by the above assertion.
|
||||
// Note that no error is thrown when using a real Ethereum network AND the assertion above is true.
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
exports.listenForEvent = event => new Promise((resolve, reject) => {
|
||||
event.watch((error, response) => {
|
||||
if (!error) {
|
||||
resolve(response.args)
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
event.stopWatching()
|
||||
})
|
||||
})
|
102
test/factory.js
Normal file
102
test/factory.js
Normal file
@ -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");
|
||||
})
|
||||
});
|
||||
|
||||
});
|
122
test/friendsRecovery.js
Normal file
122
test/friendsRecovery.js
Normal file
@ -0,0 +1,122 @@
|
||||
const TestUtils = require("../utils/testUtils.js")
|
||||
const idUtils = require("../utils/identityUtils");
|
||||
|
||||
const Identity = artifacts.require("./identity/Identity.sol");
|
||||
const FriendsRecovery = artifacts.require("./identity/FriendsRecovery.sol");
|
||||
const TestContract = artifacts.require("./tests/TestContract.sol");
|
||||
|
||||
const web3Utils = require("web3-utils");
|
||||
const web3EthAbi = require("web3-eth-abi");
|
||||
const ethUtils = require('ethereumjs-util')
|
||||
|
||||
contract('FriendsRecovery', function(accounts) {
|
||||
|
||||
const friends = [
|
||||
{address: '0xe6fa5ca5836572e0da4804fe3599958dcfc6ac2a', private: '0xffe4e190cedfdff279f903701ac14b34e082c4e20bf600bcc73239486f24ea0e' },
|
||||
{address: '0xe9f16a2443208bbacc187b51f3bae88a49d31d5c', private: '0x8836fe516896c7888f9d7e0f3c0df14dd805634a1cfff15e2255f795c0456027' },
|
||||
{address: '0x17090e7674460bd8778b2378ea46238c26da372c', private: '0x6adaf080b209dabe64d72b937fb0708990c2b83aca8ccfb558d61421e3e3c5e5' },
|
||||
{address: '0xf2613d4eb15576f7b54b76a73ede4bb7cb8dceda', private: '0xb26484d0d645282a349950c36183d86e51a550fe3c67da3eb20c777cb779d695' },
|
||||
{address: '0x387a2d6f98b26094d05c2254106bdb9d11f23d6e', private: '0xa33ca4443deadd935a7524335cb6d546b4650199290c24a4d945ebe48cf889d0' },
|
||||
];
|
||||
|
||||
describe("FriendsRecovery()", () => {
|
||||
it("Execute a full recovery", async () => {
|
||||
let testContractInstance = await TestContract.new({from: accounts[0]});
|
||||
|
||||
let identity = await Identity.new({from: accounts[0]})
|
||||
|
||||
// A bytes32 string that represents some user data
|
||||
const secret = '0x0000000000000000000000000000000000000000000000000000000000123456';
|
||||
const hashedSecret = web3Utils.soliditySha3(identity.address, secret);
|
||||
|
||||
|
||||
const newController = accounts[9];
|
||||
|
||||
let threshold = 3;
|
||||
let friendHashes = [
|
||||
web3Utils.soliditySha3(identity.address, secret, friends[0].address),
|
||||
web3Utils.soliditySha3(identity.address, secret, friends[1].address),
|
||||
web3Utils.soliditySha3(identity.address, secret, friends[2].address),
|
||||
web3Utils.soliditySha3(identity.address, secret, friends[3].address),
|
||||
];
|
||||
|
||||
let recoveryContract = await FriendsRecovery.new(identity.address, 600, threshold, hashedSecret, friendHashes, {from: accounts[0]});
|
||||
|
||||
// Setting up recovery contract for identity
|
||||
let tx1 = await identity.execute(
|
||||
identity.address,
|
||||
0,
|
||||
idUtils.encode.setupRecovery(recoveryContract.address),
|
||||
{from: accounts[0]}
|
||||
);
|
||||
//console.log(tx1.logs);
|
||||
|
||||
const newSecret = '0x0000000000000000000000000000000000000000000000000000000000abcdef';
|
||||
const data = idUtils.encode.managerReset(newController);
|
||||
const newHashedSecret = web3Utils.soliditySha3(identity.address, newSecret);
|
||||
const newFriendHashes = [
|
||||
web3Utils.soliditySha3(accounts[3], newSecret),
|
||||
web3Utils.soliditySha3(accounts[4], newSecret),
|
||||
web3Utils.soliditySha3(accounts[5], newSecret)
|
||||
];
|
||||
|
||||
// Normaly we would use soliditySha3, but it doesn't like arrays
|
||||
const hashedMessageToSign = await testContractInstance.hash.call(identity.address, secret, identity.address, data, newHashedSecret, newFriendHashes);
|
||||
let msgHash = ethUtils.hashPersonalMessage(ethUtils.toBuffer(hashedMessageToSign, 'hex'));
|
||||
const friendSignatures = [
|
||||
ethUtils.ecsign(msgHash, ethUtils.toBuffer(friends[0].private, 'hex')),
|
||||
ethUtils.ecsign(msgHash, ethUtils.toBuffer(friends[1].private, 'hex')),
|
||||
ethUtils.ecsign(msgHash, ethUtils.toBuffer(friends[2].private, 'hex')),
|
||||
ethUtils.ecsign(msgHash, ethUtils.toBuffer(friends[3].private, 'hex'))
|
||||
];
|
||||
|
||||
let tx2 = await recoveryContract.approvePreSigned(
|
||||
hashedMessageToSign,
|
||||
[
|
||||
friendSignatures[0].v,
|
||||
friendSignatures[1].v,
|
||||
friendSignatures[2].v
|
||||
],
|
||||
[
|
||||
'0x' + friendSignatures[0].r.toString('hex'),
|
||||
'0x' + friendSignatures[1].r.toString('hex'),
|
||||
'0x' + friendSignatures[2].r.toString('hex')
|
||||
],
|
||||
[
|
||||
'0x' + friendSignatures[0].s.toString('hex'),
|
||||
'0x' + friendSignatures[1].s.toString('hex'),
|
||||
'0x' + friendSignatures[2].s.toString('hex')
|
||||
],
|
||||
{from: accounts[9]});
|
||||
|
||||
let tx3 = await recoveryContract.execute(
|
||||
secret,
|
||||
identity.address,
|
||||
data,
|
||||
[
|
||||
friends[0].address,
|
||||
friends[1].address,
|
||||
friends[2].address
|
||||
],
|
||||
newHashedSecret,
|
||||
newFriendHashes,
|
||||
{from: accounts[5]});
|
||||
|
||||
await identity.processManagerReset(0, {from: accounts[0]});
|
||||
|
||||
assert.equal(
|
||||
await identity.getKeyPurpose(TestUtils.addressToBytes32(newController)),
|
||||
idUtils.purposes.MANAGEMENT,
|
||||
identity.address + ".getKeyPurpose(" + newController + ") is not MANAGEMENT_KEY")
|
||||
|
||||
assert.equal(
|
||||
await identity.getKeyPurpose(TestUtils.addressToBytes32(newController)),
|
||||
idUtils.purposes.MANAGEMENT,
|
||||
identity.address+".getKeyPurpose("+newController+") is not correct")
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
463
test/identity.js
463
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){
|
||||
}
|
||||
assert.equal(
|
||||
await identity.getKeyType(accounts[1]),
|
||||
await identity.execute(
|
||||
identity.address,
|
||||
0,
|
||||
identity.address+".getKeyType("+accounts[1]+") is not correct")
|
||||
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.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]})
|
||||
try {
|
||||
await identity.addKey(accounts[1], 1, {from: accounts[2]})
|
||||
} catch(e){
|
||||
}
|
||||
assert.equal(
|
||||
await identity.getKeyType(accounts[1]),
|
||||
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.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.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]})
|
||||
const keyAdded = await TestUtils.listenForEvent(identity.KeyAdded())
|
||||
assert(keyAdded.key, accounts[1], "Key is not correct")
|
||||
assert(keyAdded.keyType, 2, "Type is not correct")
|
||||
});
|
||||
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, 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 removes a key", async () => {
|
||||
await identity.addKey(accounts[1], 1, {from: accounts[0]})
|
||||
await identity.removeKey(accounts[0], {from: accounts[1]})
|
||||
assert.equal(
|
||||
await identity.getKeyType(accounts[0]),
|
||||
it("MANAGEMENT_KEY should remove a key", async () => {
|
||||
await identity.execute(
|
||||
identity.address,
|
||||
0,
|
||||
identity.address+".getKeyType("+accounts[0]+") is not 0")
|
||||
idUtils.encode.addKey(accounts[1], idUtils.purposes.MANAGEMENT, idUtils.types.ADDRESS),
|
||||
{from: accounts[0]}
|
||||
);
|
||||
|
||||
await identity.execute(
|
||||
identity.address,
|
||||
0,
|
||||
idUtils.encode.removeKey(accounts[1], idUtils.purposes.MANAGEMENT),
|
||||
{from: accounts[0]}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
await identity.getKeyPurpose(TestUtils.addressToBytes32(accounts[1])),
|
||||
idUtils.purposes.NONE,
|
||||
identity.address+".getKeyPurpose("+accounts[1]+") is not 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]}
|
||||
);
|
||||
|
||||
it("other key should not removes a key", async () => {
|
||||
await identity.addKey(accounts[1], 1, {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
|
||||
}
|
||||
assert.equal(
|
||||
await identity.getKeyType(accounts[0]),
|
||||
1,
|
||||
identity.address+".getKeyType("+accounts[0]+") is not MANAGEMENT_KEY")
|
||||
await identity.execute(
|
||||
identity.address,
|
||||
0,
|
||||
idUtils.encode.removeKey(accounts[0], idUtils.purposes.MANAGEMENT),
|
||||
{from: accounts[0]}
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
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("ACTOR_KEY execute arbitrary transaction", async () => {
|
||||
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("MANAGEMENT_KEY execute arbitrary transaction", async () => {
|
||||
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 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) {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
*/
|
||||
});
|
||||
|
64
test/identityExtended.js
Normal file
64
test/identityExtended.js
Normal file
@ -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)
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
191
utils/identityUtils.js
Normal file
191
utils/identityUtils.js
Normal file
@ -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
|
||||
}
|
||||
}
|
71
utils/testUtils.js
Normal file
71
utils/testUtils.js
Normal file
@ -0,0 +1,71 @@
|
||||
|
||||
// This has been tested with the real Ethereum network and Testrpc.
|
||||
// Copied and edited from: https://gist.github.com/xavierlepretre/d5583222fde52ddfbc58b7cfa0d2d0a9
|
||||
exports.assertReverts = (contractMethodCall, maxGasAvailable) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
resolve(contractMethodCall())
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
.then(tx => {
|
||||
assert.equal(tx.receipt.gasUsed, maxGasAvailable, "tx successful, the max gas available was not consumed")
|
||||
})
|
||||
.catch(error => {
|
||||
if ((error + "").indexOf("invalid opcode") < 0 && (error + "").indexOf("out of gas") < 0) {
|
||||
// Checks if the error is from TestRpc. If it is then ignore it.
|
||||
// Otherwise relay/throw the error produced by the above assertion.
|
||||
// Note that no error is thrown when using a real Ethereum network AND the assertion above is true.
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
exports.listenForEvent = event => new Promise((resolve, reject) => {
|
||||
event.watch((error, response) => {
|
||||
if (!error) {
|
||||
resolve(response.args)
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
event.stopWatching()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
exports.addressToBytes32 = (address) => {
|
||||
const stringed = "0000000000000000000000000000000000000000000000000000000000000000" + address.slice(2);
|
||||
return "0x" + stringed.substring(stringed.length - 64, stringed.length);
|
||||
}
|
||||
|
||||
|
||||
// OpenZeppelin's expectThrow helper -
|
||||
// Source: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/expectThrow.js
|
||||
exports.expectThrow = async promise => {
|
||||
try {
|
||||
await promise;
|
||||
} catch (error) {
|
||||
// TODO: Check jump destination to destinguish between a throw
|
||||
// and an actual invalid jump.
|
||||
const invalidOpcode = error.message.search('invalid opcode') >= 0;
|
||||
// TODO: When we contract A calls contract B, and B throws, instead
|
||||
// of an 'invalid jump', we get an 'out of gas' error. How do
|
||||
// we distinguish this from an actual out of gas event? (The
|
||||
// testrpc log actually show an 'invalid jump' event.)
|
||||
const outOfGas = error.message.search('out of gas') >= 0;
|
||||
const revert = error.message.search('revert') >= 0;
|
||||
assert(
|
||||
invalidOpcode || outOfGas || revert,
|
||||
'Expected throw, got \'' + error + '\' instead',
|
||||
);
|
||||
return;
|
||||
}
|
||||
assert.fail('Expected throw not received');
|
||||
};
|
||||
|
||||
|
||||
|
||||
exports.assertJump = (error) => {
|
||||
assert.isAbove(error.message.search('revert'), -1, 'Revert should happen');
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user