pragma solidity ^0.5.0; import "./Math.sol"; import "./Concept.sol"; import "./FathomToken.sol"; import "./AssessmentData.sol"; /* @notice This contract implements the assessment game that adds new members to a concept. @dev See AssessmentProxy.sol for the constructor and AssessmentData.sol for the storage layout */ contract Assessment is AssessmentData { modifier onlyConcept() { require(msg.sender == address(concept), "Concept access only"); _; } modifier onlyInStage(Stage _stage) { require(assessmentStage == _stage, "Called during wrong assessment stage"); _; } // ******************************************** // PUBLIC FUNCTIONS // ******************************************** /** @notice Adds a called assessor to the assessment by taking their stake @dev The assessor's tokens are sent to the address of the assessment. Also, if the confirm-period is over, anyone's call to this function will cancel the assessment. */ function confirmAssessor() public onlyInStage(Stage.Called) { // cancel if the assessment is past its startTime // if time to confirm is over cancel and refund if (now > checkpoint) { return cancelAssessment(); } // withdraw stake, save to list and notify 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 sufficient, advance assessment if (assessors.length == size) { notifyAssessors(uint(Stage.Confirmed), FathomToken.Note.AssessmentHasBegun); fathomToken.emitNotification(assessee, FathomToken.Note.AssessmentHasBegun); assessmentStage = Stage.Confirmed; } } /** @notice Called by assessors to commit a (hashed) score @dev If this function is called (by anyone) after the end of the commit phase, it will burn all non-committed assessors and cancel or advance the assessment. */ function commit(bytes32 _hash) public onlyInStage(Stage.Confirmed) { // if commit-phase is over, burn stakes and cancel+refund if necessary bool continueAssessment = true; if (now > endTime) { continueAssessment = burnStakes(Stage.Confirmed); } if (continueAssessment == false) return; if (assessorStage[msg.sender] == Stage.Confirmed) { commits[msg.sender] = _hash; assessorStage[msg.sender] = Stage.Committed; done++; } // advance assessment, set checkpoint to earliest possible time to reveal if (done == size) { checkpoint = now + CHALLENGE_PERIOD; notifyAssessors(uint(Stage.Committed), FathomToken.Note.RevealScore); done = 0; assessmentStage = Stage.Committed; } } /** @notice Called by anyone to steal half of an assessor's stake and remove them from the assessment @dev The other half of the tokens will be burned when the payout is completed. */ 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--; done--; } if (size < MIN_ASSESSMENT_SIZE) { cancelAssessment(); } } } /** @notice Called by assessors to reveal their score @dev Also cancels the assessment if there have been to few reveals in time. */ function reveal(int128 _score, string memory _salt) public onlyInStage(Stage.Committed) { // See if challenge period is over: This is true at the earliest 12 // hours after the latest commit and at the latest 24 hours after the // desired end of the assessment. require(now > checkpoint, "Challenge period must be over"); // If reveal-phase has passed burn all unrevealed assessors bool continueAssessment = true; if (now > endTime + 24 hours) { continueAssessment = burnStakes(Stage.Committed); } if (continueAssessment == false) return; // assessment has been cancelled // reveal score 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++; } // advance assessment if (done == size) { assessmentStage = Stage.Done; calculateResult(); endTime = now; } } /// @notice Called by assessors/assessee to store data on the assessment 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); } /* @notice Assembles a pool of assessors from the assessed concept and its parents. @dev The number of assessor to draw per concept is limited by the amount of its available members (multiplied by a constant factor). For parent-concepts it is also proportional to the connection strength. */ function setAssessorPool(uint _seed, address _concept, uint _size) public onlyConcept() { // Total desired number of assessors for the pool to be big enough uint toBeCalled = (_size * ASSESSORPOOL_SIZEFACTOR) / 10; // Total number of distinct assessors that have successfully been called uint totalCalled; Concept assessedConcept = Concept(_concept); uint availableHere = (assessedConcept.getNumberOfAvailableMembers() * MEMBERCALL_CEILING_FACTOR) / 10; // Enough assessors or start calling from parents? if (availableHere >= toBeCalled) { // Actually try calling assessors from the assessed concept totalCalled += callRandomMembers(_seed, toBeCalled, _concept); } else { // Total number of assessors that could be called from the parent-concepts uint availableViaParents; // See how many are available AND actually try calling them 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)); } // Check if there are enough assessors in concept & parent combined require(availableHere + availableViaParents >= toBeCalled, "Not enough assessors in parent concepts"); // Then also try to actually calling the members from the assessed concept totalCalled += callRandomMembers(_seed + 4224, availableHere, _concept); } require(totalCalled >= MIN_ASSESSMENT_SIZE, "Too few assessors could be called"); assessmentStage = Stage.Called; } // ******************************************** // INTERNAL FUNCTIONS // ******************************************** /** @notice Tries to probabilistically call a given amount of members from a given concept and its parents. Members with higher weights having better chances of being called. @dev This assumes there are >=amount members in the concept. @return how many members have actually been added to the assessment pool. */ function callRandomMembers(uint _seed, uint _amount, address _concept) internal returns(uint successfullyCalled) { uint seed = _seed; Concept assessorConcept = Concept(_concept); address[] memory availableMembers = assessorConcept.getAvailableMembers(); // Select two random members and choose the one with higher weight // Also switch selected member with last address in array (to remove later) 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 = assessorConcept.getWeight(availableMembers[index1]); uint weight2 = assessorConcept.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; // Try adding the selected member. If they are the assessee or have already // been added to the pool from another concept, adding them again will fail if (addAssessorToPool(calledAssessor)) successfullyCalled += 1; seed += 10; // remove last address (calledAssessor) from list to not draw again length--; } } // ******************************************** // PRIVATE FUNCTIONS // ******************************************** /// @notice Called to cancel an assessment, refund all non-burned assessors 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); } } assessmentStage = Stage.Burned; fathomToken.destroyTokens(fathomToken.balanceOf(address(this))); } /** @notice Adds an address to the pool of potential assessors that will be allowed to accept (stake on) the assessment @return flag whether the address was added successfully */ 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; } } /** @dev Burns stakes of all assessors who are in a given stage and cancels the assessment if there are less than five assessors left @return continueAssessment flag whether there are enough assessors left to continue */ 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 < MIN_ASSESSMENT_SIZE) { cancelAssessment(); continueAssessment = false; } else { continueAssessment = true; } } /// @notice Triggers notification events with a given topic for all assessors in a given stage 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); } } } /** @notice Calculates the outcome (pass/fail/invalid) of the assessment, triggers the payout and ,upon positive outcome, adds the assessee as a member to the concept */ function calculateResult() private onlyInStage(Stage.Done) { // Store committed scores of all assessors who completed the assessment 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); // The finalScore must be the supported by a true majority (>50%) of assessors if (greatestClusterLength > done/2) { payout(greatestClusterLength); if (finalScore > 0) { concept.addMember(assessee, uint(finalScore) * greatestClusterLength); } } else { // no consensus, burn all tokens and mark assessment as incomplete fathomToken.destroyTokens(fathomToken.balanceOf(address(this))); assessmentStage = Stage.Burned; } fathomToken.emitNotification(assessee, FathomToken.Note.AssessmentFinished); } /** @notice Payout the assessors stake and proportional reward, redistribute from dissenting assessors and burn all remaining tokens. */ function payout(uint _greatestClusterLength) private onlyInStage(Stage.Done) { // Total amount of tokens from dissenting assessors (to be redistributed) uint dissentBonus = 0; // save whether an assessor is part of the winning-cluser (the majority) bool[] memory inAssessor = new bool[](assessors.length); // Save individual payouts + rewards 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 the winners 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); } } fathomToken.destroyTokens(fathomToken.balanceOf(address(this))); } }