pragma solidity ^0.5.0; import "./Math.sol"; import "./Concept.sol"; import "./FathomToken.sol"; import "./NewAssessmentData.sol"; /* This is a new masterAssessmentCopy to be used by the ProxyFactory. It is supposed to differ from Assessment.sol in no way. The difference lies in NewAssessmentData.sol constants. */ contract NewAssessment is NewAssessmentData { modifier onlyConcept() { require(msg.sender == address(concept), 'Concept access only'); _; } modifier onlyInStage(Stage _stage) { require(assessmentStage == _stage, 'Called during wrong assessment stage'); _; } // called by an assessor to confirm and stake function confirmAssessor() public onlyInStage(Stage.Called) { // cancel if the assessment is past its startTime if (now > checkpoint) { return cancelAssessment(); } if (assessorStage[msg.sender] == Stage.Called && assessors.length < size && fathomToken.takeBalance(msg.sender, address(this), cost, address(concept))) { assessors.push(msg.sender); assessorStage[msg.sender] = Stage.Confirmed; fathomToken.emitNotification(msg.sender, FathomToken.Note.ConfirmedAsAssessor); } if (assessors.length == size) { notifyAssessors(uint(Stage.Confirmed), FathomToken.Note.AssessmentHasBegun); fathomToken.emitNotification(assessee, FathomToken.Note.AssessmentHasBegun); assessmentStage = Stage.Confirmed; } } //called by an assessor to commit a hash of their score //TODO explain in more detail what's happening function commit(bytes32 _hash) public onlyInStage(Stage.Confirmed) { bool continueAssessment = true; if (now > endTime) { burnStakes(Stage.Confirmed); } if (continueAssessment == false) return; if (assessorStage[msg.sender] == Stage.Confirmed) { commits[msg.sender] = _hash; assessorStage[msg.sender] = Stage.Committed; done++; //Increases done by 1 to help progress to the next assessment stage. } if (done == size) { //set checkpoint to end of 12 hour challenge period after which scores can be revealed checkpoint = now + CHALLENGE_PERIOD; notifyAssessors(uint(Stage.Committed), FathomToken.Note.RevealScore); done = 0; //Resets the done counter assessmentStage = Stage.Committed; } } function steal(int128 _score, string memory _salt, address assessor) public { if (assessorStage[assessor] == Stage.Committed) { if (commits[assessor] == keccak256(abi.encodePacked(_score, _salt))) { fathomToken.transfer(msg.sender, cost/2); assessorStage[assessor] = Stage.Burned; size--; } } } //@purpose: called by assessors to reveal their own commits or others // must be called between 12 hours after the latest commit and 24 hours after the // end of the assessment. If the last commit happens during at the last possible // point in time (right before endtime), this period will be 12hours function reveal(int128 _score, string memory _salt) public onlyInStage(Stage.Committed) { // scores can only be revealed after the challenge period has passed require(now > checkpoint, 'Challenge period must be over'); // If the time to reveal has passed, burn all unrevealed assessors bool continueAssessment = true; if (now > endTime + 24 hours) { burnStakes(Stage.Committed); } if (continueAssessment == false) return; if (assessorStage[msg.sender] == Stage.Committed && commits[msg.sender] == keccak256(abi.encodePacked(_score, _salt))) { scores[msg.sender] = _score; salt = salt ^ (keccak256(bytes(_salt))); assessorStage[msg.sender] = Stage.Done; done++; } if (done == size) { assessmentStage = Stage.Done; calculateResult(); } } function addData(bytes memory _data) public { require(msg.sender == assessee || assessorStage[msg.sender] > Stage.Called, 'Must be assessee or confirmed assessor'); require(assessmentStage < Stage.Committed, 'Deadline for adding data has passed'); bytes memory oldData = data[msg.sender]; data[msg.sender] = _data; emit DataChanged(msg.sender, oldData, _data); } function payout(uint greatestClusterLength) public onlyInStage(Stage.Done) { uint dissentBonus = 0; bool[] memory inAssessor = new bool[](assessors.length); uint[] memory inAssessorPayout = new uint[](assessors.length); // pay out dissenting assessors their reduced stake, set their Stage to dissent // and save how much stake to redistribute to whom for (uint i = 0; i < assessors.length; i++) { if (assessorStage[assessors[i]] == Stage.Done) { uint payoutValue; bool dissenting; (payoutValue, dissenting) = Math.getPayout(Math.abs(scores[assessors[i]] - finalScore), cost, CONSENT_RADIUS); if (dissenting) { assessorStage[assessors[i]] = Stage.Dissent; dissentBonus += cost - payoutValue; if (payoutValue > 0) { fathomToken.transfer(assessors[i], payoutValue); } fathomToken.emitNotification(assessors[i], FathomToken.Note.ConsensusReached); } else { inAssessor[i] = true; inAssessorPayout[i] = payoutValue; } } } // pay out the majority-assessors their share of stake + the remainders of any dissenting assessors for (uint j = 0; j < inAssessorPayout.length; j++) { if (inAssessor[j]) { fathomToken.transfer(assessors[j], inAssessorPayout[j] + dissentBonus/greatestClusterLength); fathomToken.emitNotification(assessors[j], FathomToken.Note.ConsensusReached); } } } function callRandomMembers(uint _seed, uint amount, address _concept) internal returns(uint successfullyCalled){ uint seed = _seed; Concept concept = Concept(_concept); address[] memory availableMembers = concept.getAvailableMembers(); uint length = availableMembers.length; for(uint i = 0; i < amount; i++) { uint index1 = Math.getRandomNumber(seed, length - 1 ); uint index2 = Math.getRandomNumber(seed*2, length - 1); uint weight1 = concept.getWeight(availableMembers[index1]); uint weight2 = concept.getWeight(availableMembers[index2]); address calledAssessor; if (weight1 >= weight2) { calledAssessor = availableMembers[index1]; availableMembers[index1] = availableMembers[length - 1]; } else { calledAssessor = availableMembers[index2]; availableMembers[index2] = availableMembers[length - 1]; } availableMembers[length -1] = calledAssessor; if (addAssessorToPool(calledAssessor)) successfullyCalled += 1; seed += 10; length--; } } /* Assembles a pool of assessors from assessed concept @param: uint seed = the seed number for random number generation @param: address _concept = the concept being called from @param: uint _size = the desired size of the assessment */ function setAssessorPool(uint seed, address _concept, uint _size) public onlyConcept() { uint toBeCalled = (_size * ASSESSORPOOL_SIZEFACTOR) / 10; uint totalCalled; Concept assessedConcept = Concept(_concept); uint availableHere = (assessedConcept.getNumberOfAvailableMembers() * MEMBERCALL_CEILING_FACTOR) / 10; if (availableHere >= toBeCalled) { //Call from this concept num assessors totalCalled += callRandomMembers(seed, toBeCalled, _concept); } else { // check whether parents have enough members uint availableViaParents; for(uint k = 0; k < assessedConcept.getNumberOfParents(); k++) { Concept parent = Concept(assessedConcept.parents(k)); uint availableInParent = (((parent.getNumberOfAvailableMembers() * MEMBERCALL_CEILING_FACTOR) / 10) * assessedConcept.parentFactors(k)) / 1000; availableViaParents += availableInParent; totalCalled += callRandomMembers(seed + k, availableInParent, address(parent)); } require(availableHere + availableViaParents >= toBeCalled, 'Not enough assessors in parent concepts'); totalCalled += callRandomMembers(seed + 4224, availableHere, _concept); } require(totalCalled >= MIN_ASSESSMENT_SIZE, 'Too few assessors could be called'); assessmentStage = Stage.Called; } // ends the assessment, refunds the assessee and all assessors who have not been burned function cancelAssessment() private { uint assesseeRefund = assessmentStage == Stage.Called ? cost * size : cost * assessors.length; fathomToken.transfer(assessee, assesseeRefund); fathomToken.emitNotification(assessee, FathomToken.Note.AssessmentCancelled); for (uint i = 0; i < assessors.length; i++) { if (assessorStage[assessors[i]] != Stage.Burned) { fathomToken.transfer(assessors[i], cost); fathomToken.emitNotification(assessors[i], FathomToken.Note.AssessmentCancelled); } } } //adds a user to the pool eligible to accept an assessment function addAssessorToPool(address assessor) private returns(bool) { if (assessor != assessee && assessorStage[assessor] == Stage.None) { fathomToken.emitNotification(assessor, FathomToken.Note.CalledAsAssessor); assessorStage[assessor] = Stage.Called; return true; } } // burns stakes of all assessors who are in a certain stage Stage // if afterwards, the size is below 5, the assessment is cancelled function burnStakes(Stage _stage) private returns(bool continueAssessment){ for (uint i = 0; i < assessors.length; i++) { if (assessorStage[assessors[i]] == _stage) { assessorStage[assessors[i]] = Stage.Burned; size--; } } if (size < 5) { cancelAssessment(); continueAssessment = false; } else { continueAssessment = true; } } function notifyAssessors(uint _stage, FathomToken.Note _topic) private { for (uint i=0; i < assessors.length; i++) { if (uint(assessorStage[assessors[i]]) == _stage) { fathomToken.emitNotification(assessors[i], _topic); } } } function calculateResult() private onlyInStage(Stage.Done) { int[] memory finalScores = new int[](done); uint idx =0; for (uint j = 0; j < assessors.length; j++) { if (assessorStage[assessors[j]] == Stage.Done) { finalScores[idx] = scores[assessors[j]]; idx++; } } uint greatestClusterLength; (finalScore, greatestClusterLength) = Math.getFinalScore(finalScores, CONSENT_RADIUS); // check if a majority of assessors found a point of consensus if (greatestClusterLength > done/2) { payout(greatestClusterLength); if (finalScore > 0) { concept.addMember(assessee, uint(finalScore) * greatestClusterLength); } } else { // set final Score to zero to signal no consensus finalScore = 0; } fathomToken.emitNotification(assessee, FathomToken.Note.AssessmentFinished); } }