pragma solidity ^0.5.0; import "./FathomToken.sol"; import "./ConceptRegistry.sol"; import "./Assessment.sol"; import "./AssessmentData.sol"; import "./Math.sol"; import "./ConceptData.sol"; /** @notice The concept contract that defines the concept, initiates assessments and stores data about its members @dev See ConceptProxy.sol for the constructor and ConceptData.sol for the storage layout */ contract Concept is ConceptData { modifier onlyOwner() { require(msg.sender == owner, "Owner access only"); _; } // ******************************************** // EXTERNAL FUNCTIONS // ******************************************** /// @notice Called by completed assessments to add the assessee as member to the concept function addMember(address _assessee, uint _weight) external { if (assessmentExists[msg.sender]) { memberData[_assessee].recentAssessment = msg.sender; addWeight(_assessee, _weight); } } /// @notice Called by cloned concepts to delete migrated members function migrateMember(address _member) external { require(conceptRegistry.conceptExists(msg.sender), "Concept access only"); toggleAvailability(false); delete memberData[_member]; } /** @notice Called by the distributor contract to directly add users (without an assessment) @dev This is only once used once on the mew-concept during the initialization of the network. */ function addInitialMember(address _user, uint _weight) external { require(msg.sender == conceptRegistry.distributorAddress(), "Distributor access only"); if (conceptRegistry.distributorAddress() == msg.sender) { addWeight(_user, _weight); uint startBucket = startBucketNumber(_user); for (uint i = 0; i < N_OF_BUCKETS; i++) { buckets[startBucket + i].push(_user); memberData[_user].bucketIndices[startBucket + i] = buckets[startBucket + i].length; } } } // ******************************************** // PUBLIC FUNCTIONS // ******************************************** /** @notice Called by anyone who wants to get assessed in this concept @param _size the number of assessors @param _cost the amount of tokens paid per assessor @param _waitTime how long to wait for enough assessors to accept @param _timeLimit how long until the assessment has to be finished @dev Also sends tokens from the msg.sender to the address of the created assessment */ function makeAssessment(uint _cost, uint _size, uint _waitTime, uint _timeLimit) public { require(_size >= 5, "Assessment size must be >= five"); require(fathomToken.balanceOf(msg.sender) >=_cost*_size, "Token balance too low"); address newAssessmentAddress = conceptRegistry.proxyFactory().createAssessment( msg.sender, _cost, _size, _waitTime, _timeLimit ); assessmentExists[newAssessmentAddress] = true; fathomToken.takeBalance(msg.sender, newAssessmentAddress, _cost * _size, address(this)); Assessment(newAssessmentAddress).setAssessorPool(uint(blockhash(block.number)), address(this), _size); } /** @notice Called by members to signal that they do/do not want to be called as assessor in this concept @dev See AssessmentData.sol for an explanation how buckets are used to discretize time. */ function toggleAvailability(bool _available) public { // startBucket: the first period of time (bucket) they caller is available in. uint startBucket = startBucketNumber(msg.sender); if (_available) { require(memberData[msg.sender].weight > 0, 'Must have a weight to toggle available'); require(memberData[msg.sender].assessmentDate + lifetime > now, 'Weight must not be expired to toggle available'); // REFACTOR this could be changed to only affect the buckets that are still live (not in the past) for (uint i; i < N_OF_BUCKETS; i++) { // add member to bucket, if they are not already in it if (memberData[msg.sender].bucketIndices[startBucket + i] == 0) { buckets[startBucket + i].push(msg.sender); memberData[msg.sender].bucketIndices[startBucket + i] = buckets[startBucket + i].length; } } return; } // Else set unavailable for (uint bi; bi < N_OF_BUCKETS; bi++) { uint index = memberData[msg.sender].bucketIndices[startBucket + bi]; // remove member from list if they are in it (index 0 says they are not!) if (index > 0) { address[] storage bucket = buckets[startBucket + bi]; address lastMember = bucket[bucket.length -1]; bucket[index - 1] = lastMember; memberData[lastMember].bucketIndices[startBucket + bi] = index; memberData[msg.sender].bucketIndices[startBucket + bi] = 0; bucket.length--; } } } /// @notice Called by members of the original concept who want to migrate to this one function integrateMember() public { Concept original = Concept(cloneOf); address recentAssessment; uint weight; uint assessmentDate; (recentAssessment, weight, assessmentDate) = original.memberData(msg.sender); require(weight > 0, "Must have a non-zero weight in original concept"); require(assessmentDate + lifetime > now, "Assessment must not be older than concept lifetime"); // Initial Members are not allowed to migrate /* require(recentAssessment != address(0x0), "Member must have an assessment in order to migrate") */ memberData[msg.sender].weight = weight; memberData[msg.sender].assessmentDate = assessmentDate; original.migrateMember(msg.sender); // by default, members are not available in the new concept // if (available) toggleAvailable(true); } // ******************************************** // SETTER FUNCTIONS // ******************************************** function transferOwnership(address _newOwner) public onlyOwner() { owner = _newOwner; } /// @dev The restrictions on parent-concepts here NEED to be identical to the ones in the constructor function changeParents( address[] memory _newParents, uint[] memory _newParentFactors ) public onlyOwner() { require(_newParents.length == _newParentFactors.length, "Number of parent concepts must match parent factors" ); require(_newParents.length < 6, "Too many parents"); // NOTE: this value is a working assumption uint sumOfParentFactors = 0; for (uint j=0; j < _newParents.length; j++) { require(_newParents[j] != address(this), "Concept cannot be parent to itself"); require(conceptRegistry.conceptExists(_newParents[j]), "Parent concept does not exist"); require(_newParentFactors[j] <= 1000, "Parent factor must be <=1000"); sumOfParentFactors += _newParentFactors[j]; } require(sumOfParentFactors == 1000, "Parent factors do not add up to 1000"); parents = _newParents; parentFactors = _newParentFactors; } function changeData(bytes memory _data) public onlyOwner() { data = _data; } function changeLifetime(uint _newLifetime) public onlyOwner() { lifetime = _newLifetime; } // ******************************************** // GETTER FUNCTIONS // ******************************************** /// @return the weight used for probabilistic calling of assessors function getWeight(address _member) public view returns(uint weight) { if (memberData[_member].assessmentDate + lifetime > now) { weight = memberData[_member].weight; } } /// @return how many members are currently available to act as assessor function getNumberOfAvailableMembers() public view returns(uint) { return buckets[activeBucketNumber()].length; } function getAvailableMembers () public view returns(address[] memory) { return buckets[activeBucketNumber()]; } function getNumberOfParents() public view returns(uint) { return parents.length; } /// @return the number of the period of time (bucket) the current moment is in function activeBucketNumber() public view returns(uint) { // resolution or bucketLength: how many seconds are discretized into one bucket uint bucketLength = lifetime / N_OF_BUCKETS; return now / bucketLength; } // ******************************************** // INTERNAL FUNCTIONS // ******************************************** /// @return the number of the first period of time (bucket) that a member can be available in function startBucketNumber(address _user) internal view returns(uint) { uint bucketLength = lifetime / N_OF_BUCKETS; return memberData[_user].assessmentDate / bucketLength; } function addWeight(address _member, uint _weight) internal { memberData[_member].weight = _weight; memberData[_member].assessmentDate = now; } }