pragma solidity ^0.5.0; import "./ConceptRegistry.sol"; import "./Assessment.sol"; import "./FathomToken.sol"; // NOTE // For a detailed explanation of the minting mechanism, see // https://fathom.network/blog/archive/distributing-tokens/ /** @notice Contract to manage the minting of new network tokens. New Tokens are minted once per a user-defined timeperiod (epoch) and are distributed in a lottery. Tickets can be submitted for assessors who have completed assessments as part of the majority, in the current epoch. */ contract Minter { address public owner; bool public initialized; // amount of new tokens generated each epoch uint public reward; uint public epochStart; // target hash for tickets to compare against uint public epochHash; uint public epochLength; uint public waitingPeriod; FathomToken public fathomToken; ConceptRegistry public conceptRegistry; address public winner; // current best ticket distance uint public closestDistance = 2**256 - 1; // participant => address that should receive winnings mapping (address => address) public coinbase; modifier onlyOwner() { require(msg.sender == owner, "Owner access only"); _; } event NewOwner(address oldOwner, address newOwner); event NewReward(uint oldReward, uint newReward); event NewEpochLength(uint oldLength, uint newLength); event TokensMinted(address recipient, uint amount); constructor(address _conceptRegistry, uint _epochLength, uint _waitingPeriod, uint _reward) public { conceptRegistry = ConceptRegistry(_conceptRegistry); epochLength = _epochLength; epochStart = now; require(2 * _waitingPeriod < _epochLength, 'waiting period must be at most half of epoch'); waitingPeriod = _waitingPeriod; epochHash = uint(blockhash(block.number - 1)); reward = _reward; owner = msg.sender; } /// @dev will be called during network deployment function init(address _fathomToken) public { if (!initialized) { fathomToken = FathomToken(_fathomToken); initialized = true; } } /* @notice Called to submit tickets for assessors who have completed an assessment as part of the majority in the current epoch @param _tokenSalt A number smaller or equal to the amount of tokens the assessor staked in the assessment */ function submitTicket (address _assessor, address _assessment, uint _tokenSalt) public { Assessment assessment = Assessment(_assessment); require(conceptRegistry.conceptExists(address(assessment.concept())), 'Assessed concept does not exist'); require(Concept(assessment.concept()).assessmentExists(_assessment), 'Assessment does not exist'); require(assessment.endTime() < epochStart + epochLength, 'Assessment must end before end of epoch'); require(assessment.endTime() > epochStart, 'Assessment must be from current epoch'); require(uint(assessment.assessmentStage()) == 4, 'Assessment must be completed'); // 4 -> Stage.Done require(uint(assessment.assessorStage(_assessor)) == 4, 'Assessor is not part of majority cluster'); require(_tokenSalt <= assessment.cost(), 'salt must be <= stake'); uint distance = getTicketDistance(_assessor, _assessment, _tokenSalt, assessment.salt()); if (distance < closestDistance) { closestDistance = distance; winner = _assessor; } } /// @param _tokenSalt A number smaller than the amount of tokens staked by the assessor /// @param _assessmentSalt Taken from assessment, generated by XOR-ing the salts of all revealing assessors /// @return the distance to the target hash of a given ticket function getTicketDistance( address _assessor, address _assessment, uint _tokenSalt, bytes32 _assessmentSalt ) public view returns(uint distance) { uint hash = uint(keccak256(abi.encodePacked(_assessor, _assessment, _tokenSalt, _assessmentSalt))); distance = epochHash > hash ? epochHash - hash : hash - epochHash; } /// @notice Called by anyone to end the current epoch, start the next one and trigger a payout to the winner function endEpoch() public { require(now > (epochStart + epochLength + waitingPeriod), 'Epoch and waiting period must be over'); if (winner != address(0)) { address mintAddress = coinbase[winner] != address(0) ? coinbase[winner] : winner; if (fathomToken.mint(mintAddress, reward)) { emit TokensMinted(winner, reward); } winner = address(0); } // increase epochStart until the current moment epochStart = epochStart + ((now - epochStart) / epochLength) * epochLength; epochHash = uint(blockhash(block.number - 1)); closestDistance = 2**256-1; } // ******************************************** // SETTER FUNCTIONS // ******************************************** /// @notice Called by participants to redirect their rewards function setCoinbase(address _user) public { coinbase[msg.sender] = _user; } function setOwner(address _owner) public onlyOwner() { owner = _owner; } function setReward(uint _reward) public onlyOwner() { reward = _reward; } function setEpochLength (uint _length) public onlyOwner() { epochLength = _length; } function setWaitingPeriod (uint _waitingPeriod) public onlyOwner() { require(2 * _waitingPeriod < epochLength, 'Waiting period must be at most half of epoch'); waitingPeriod = _waitingPeriod; } }