small tweak

small tweak

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "./LowLevelCallUtils.sol";
import "../registry/ENS.sol";
import "../resolvers/profiles/IExtendedResolver.sol";
import "../resolvers/Resolver.sol";
import "./NameEncoder.sol";
import "./BytesLib.sol";

error OffchainLookup(
    address sender,
    string[] urls,
    bytes callData,
    bytes4 callbackFunction,
    bytes extraData
);

/**
 * The Universal Resolver is a contract that handles the work of resolving a name entirely onchain,
 * making it possible to make a single smart contract call to resolve an ENS name.
 */
contract UniversalResolver is IExtendedResolver, ERC165 {
    using Address for address;
    using NameEncoder for string;
    using NameEncoder for bytes;
    using BytesLib for bytes;

    ENS public immutable registry;

    constructor(address _registry) {
        registry = ENS(_registry);
    }

    /**
     * @dev Performs ENS name resolution for the supplied name and resolution data.
     * @param name The name to resolve, in normalised and DNS-encoded form.
     * @param data The resolution data, as specified in ENSIP-10.
     * @return The result of resolving the name.
     */
    function resolve(bytes calldata name, bytes memory data)
        external
        view
        override
        returns (bytes memory, address)
    {
        (Resolver resolver, ) = findResolver(name);
        if (address(resolver) == address(0)) {
            return ("", address(0));
        }

        bool interfaceSupported = true;
        try
            resolver.supportsInterface(type(IExtendedResolver).interfaceId)
        returns (bool result) {
            interfaceSupported = result;
        } catch {
            interfaceSupported = false;
        }

        if (interfaceSupported) {
            return (
                callWithOffchainLookupPropagation(
                    address(resolver),
                    abi.encodeWithSelector(
                        IExtendedResolver.resolve.selector,
                        name,
                        data
                    ),
                    UniversalResolver.resolveCallback.selector
                ),
                address(resolver)
            );
        } else {
            return (
                callWithOffchainLookupPropagation(
                    address(resolver),
                    data,
                    UniversalResolver.resolveCallback.selector
                ),
                address(resolver)
            );
        }
    }

    /**
     * @dev Performs ENS name reverse resolution for the supplied address and resolution data.
     * @param reverseNode The reverse node to resolve, in normalised and DNS-encoded form.
     * @return The resolved name, and the resolved data.
     */
    function reverse(bytes memory reverseNode)
        external
        view
        returns (string memory, bytes memory)
    {
        (bool reverseSuccess, bytes memory resolvedReverseData) = address(this)
            .staticcall(
                abi.encodeCall(
                    this.resolve,
                    (
                        reverseNode,
                        abi.encodeCall(
                            INameResolver.name,
                            reverseNode.namehash(0)
                        )
                    )
                )
            );

        if (!reverseSuccess) {
            return ("", "");
        }

        string memory resolvedName = abi.decode(
            abi.decode(resolvedReverseData, (bytes)),
            (string)
        );

        (bytes memory encodedName, bytes32 namehash) = resolvedName
            .encodeAndHash();

        (bool success, bytes memory resolvedData) = address(this).staticcall(
            abi.encodeCall(
                this.resolve,
                (encodedName, abi.encodeCall(IAddrResolver.addr, namehash))
            )
        );

        if (!success) {
            return (resolvedName, "");
        }

        return (resolvedName, resolvedData);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override
        returns (bool)
    {
        return
            interfaceId == type(IExtendedResolver).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    /**
     * @dev Makes a call to `target` with `data`. If the call reverts with an `OffchainLookup` error, wraps
     *      the error with the data necessary to continue the request where it left off.
     * @param target The address to call.
     * @param data The data to call `target` with.
     * @param callbackFunction The function ID of a function on this contract to use as an EIP 3668 callback.
     *        This function's `extraData` argument will be passed `(address target, bytes4 innerCallback, bytes innerExtraData)`.
     * @return ret If `target` did not revert, contains the return data from the call to `target`.
     */
    function callWithOffchainLookupPropagation(
        address target,
        bytes memory data,
        bytes4 callbackFunction
    ) internal view returns (bytes memory ret) {
        bool result = LowLevelCallUtils.functionStaticCall(target, data);
        uint256 size = LowLevelCallUtils.returnDataSize();

        if (result) {
            return LowLevelCallUtils.readReturnData(0, size);
        }

        // Failure
        if (size >= 4) {
            bytes memory errorId = LowLevelCallUtils.readReturnData(0, 4);
            if (bytes4(errorId) == OffchainLookup.selector) {
                // Offchain lookup. Decode the revert message and create our own that nests it.
                bytes memory revertData = LowLevelCallUtils.readReturnData(
                    4,
                    size - 4
                );
                (
                    address sender,
                    string[] memory urls,
                    bytes memory callData,
                    bytes4 innerCallbackFunction,
                    bytes memory extraData
                ) = abi.decode(
                        revertData,
                        (address, string[], bytes, bytes4, bytes)
                    );
                if (sender == target) {
                    revert OffchainLookup(
                        address(this),
                        urls,
                        callData,
                        callbackFunction,
                        abi.encode(sender, innerCallbackFunction, extraData)
                    );
                }
            }
        }

        LowLevelCallUtils.propagateRevert();
    }

    /**
     * @dev Callback function for `resolve`.
     * @param response Response data returned by the target address that invoked the inner `OffchainData` revert.
     * @param extraData Extra data encoded by `callWithOffchainLookupPropagation` to allow completing the request.
     */
    function resolveCallback(bytes calldata response, bytes calldata extraData)
        external
        view
        returns (bytes memory)
    {
        (
            address target,
            bytes4 innerCallbackFunction,
            bytes memory innerExtraData
        ) = abi.decode(extraData, (address, bytes4, bytes));
        return
            abi.decode(
                target.functionStaticCall(
                    abi.encodeWithSelector(
                        innerCallbackFunction,
                        response,
                        innerExtraData
                    )
                ),
                (bytes)
            );
    }

    /**
     * @dev Finds a resolver by recursively querying the registry, starting at the longest name and progressively
     *      removing labels until it finds a result.
     * @param name The name to resolve, in DNS-encoded and normalised form.
     * @return The Resolver responsible for this name, and the namehash of the full name.
     */
    function findResolver(bytes calldata name)
        public
        view
        returns (Resolver, bytes32)
    {
        (address resolver, bytes32 labelhash) = findResolver(name, 0);
        return (Resolver(resolver), labelhash);
    }

    function findResolver(bytes calldata name, uint256 offset)
        internal
        view
        returns (address, bytes32)
    {
        uint256 labelLength = uint256(uint8(name[offset]));
        if (labelLength == 0) {
            return (address(0), bytes32(0));
        }
        uint256 nextLabel = offset + labelLength + 1;
        bytes32 labelHash = keccak256(name[offset + 1:nextLabel]);
        (address parentresolver, bytes32 parentnode) = findResolver(
            name,
            nextLabel
        );
        bytes32 node = keccak256(abi.encodePacked(parentnode, labelHash));
        address resolver = registry.resolver(node);
        if (resolver != address(0)) {
            return (resolver, node);
        }
        return (parentresolver, node);
    }
}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "./LowLevelCallUtils.sol";
import "../registry/ENS.sol";
import "../resolvers/profiles/IExtendedResolver.sol";
import "../resolvers/Resolver.sol";
import "./NameEncoder.sol";
import "./BytesLib.sol";

error OffchainLookup(
    address sender,
    string[] urls,
    bytes callData,
    bytes4 callbackFunction,
    bytes extraData
);

/**
 * The Universal Resolver is a contract that handles the work of resolving a name entirely onchain,
 * making it possible to make a single smart contract call to resolve an ENS name.
 */
contract UniversalResolver is IExtendedResolver, ERC165 {
    using Address for address;
    using NameEncoder for string;
    using NameEncoder for bytes;
    using BytesLib for bytes;

    ENS public immutable registry;

    constructor(address _registry) {
        registry = ENS(_registry);
    }

    /**
     * @dev Performs ENS name resolution for the supplied name and resolution data.
     * @param name The name to resolve, in normalised and DNS-encoded form.
     * @param data The resolution data, as specified in ENSIP-10.
     * @return The result of resolving the name.
     */
    function resolve(bytes calldata name, bytes memory data)
        external
        view
        override
        returns (bytes memory, address)
    {
        (Resolver resolver, ) = findResolver(name);
        if (address(resolver) == address(0)) {
            return ("", address(0));
        }

        bool success = true;
        bool interfaceSupported = true;
        try
            resolver.supportsInterface(type(IExtendedResolver).interfaceId)
        returns (bool result) {
            interfaceSupported = result;
            success = true;
        } catch {
            interfaceSupported = false;
            success = false;
        }

        if (interfaceSupported) {
            return (
                callWithOffchainLookupPropagation(
                    address(resolver),
                    abi.encodeWithSelector(
                        IExtendedResolver.resolve.selector,
                        name,
                        data
                    ),
                    UniversalResolver.resolveCallback.selector
                ),
                address(resolver)
            );
        } else {
            return (
                callWithOffchainLookupPropagation(
                    address(resolver),
                    data,
                    UniversalResolver.resolveCallback.selector
                ),
                address(resolver)
            );
        }
    }

    /**
     * @dev Performs ENS name reverse resolution for the supplied address and resolution data.
     * @param reverseNode The reverse node to resolve, in normalised and DNS-encoded form.
     * @return The resolved name, and the resolved data.
     */
    function reverse(bytes memory reverseNode)
        external
        view
        returns (string memory, bytes memory)
    {
        (bool reverseSuccess, bytes memory resolvedReverseData) = address(this)
            .staticcall(
                abi.encodeCall(
                    this.resolve,
                    (
                        reverseNode,
                        abi.encodeCall(
                            INameResolver.name,
                            reverseNode.namehash(0)
                        )
                    )
                )
            );

        if (!reverseSuccess) {
            return ("", "");
        }

        string memory resolvedName = abi.decode(
            abi.decode(resolvedReverseData, (bytes)),
            (string)
        );

        (bytes memory encodedName, bytes32 namehash) = resolvedName
            .encodeAndHash();

        (bool success, bytes memory resolvedData) = address(this).staticcall(
            abi.encodeCall(
                this.resolve,
                (encodedName, abi.encodeCall(IAddrResolver.addr, namehash))
            )
        );

        if (!success) {
            return (resolvedName, "");
        }

        return (resolvedName, resolvedData);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override
        returns (bool)
    {
        return
            interfaceId == type(IExtendedResolver).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    /**
     * @dev Makes a call to `target` with `data`. If the call reverts with an `OffchainLookup` error, wraps
     *      the error with the data necessary to continue the request where it left off.
     * @param target The address to call.
     * @param data The data to call `target` with.
     * @param callbackFunction The function ID of a function on this contract to use as an EIP 3668 callback.
     *        This function's `extraData` argument will be passed `(address target, bytes4 innerCallback, bytes innerExtraData)`.
     * @return ret If `target` did not revert, contains the return data from the call to `target`.
     */
    function callWithOffchainLookupPropagation(
        address target,
        bytes memory data,
        bytes4 callbackFunction
    ) internal view returns (bytes memory ret) {
        bool result = LowLevelCallUtils.functionStaticCall(target, data);
        uint256 size = LowLevelCallUtils.returnDataSize();

        if (result) {
            return LowLevelCallUtils.readReturnData(0, size);
        }

        // Failure
        if (size >= 4) {
            bytes memory errorId = LowLevelCallUtils.readReturnData(0, 4);
            if (bytes4(errorId) == OffchainLookup.selector) {
                // Offchain lookup. Decode the revert message and create our own that nests it.
                bytes memory revertData = LowLevelCallUtils.readReturnData(
                    4,
                    size - 4
                );
                (
                    address sender,
                    string[] memory urls,
                    bytes memory callData,
                    bytes4 innerCallbackFunction,
                    bytes memory extraData
                ) = abi.decode(
                        revertData,
                        (address, string[], bytes, bytes4, bytes)
                    );
                if (sender == target) {
                    revert OffchainLookup(
                        address(this),
                        urls,
                        callData,
                        callbackFunction,
                        abi.encode(sender, innerCallbackFunction, extraData)
                    );
                }
            }
        }

        LowLevelCallUtils.propagateRevert();
    }

    /**
     * @dev Callback function for `resolve`.
     * @param response Response data returned by the target address that invoked the inner `OffchainData` revert.
     * @param extraData Extra data encoded by `callWithOffchainLookupPropagation` to allow completing the request.
     */
    function resolveCallback(bytes calldata response, bytes calldata extraData)
        external
        view
        returns (bytes memory)
    {
        (
            address target,
            bytes4 innerCallbackFunction,
            bytes memory innerExtraData
        ) = abi.decode(extraData, (address, bytes4, bytes));
        return
            abi.decode(
                target.functionStaticCall(
                    abi.encodeWithSelector(
                        innerCallbackFunction,
                        response,
                        innerExtraData
                    )
                ),
                (bytes)
            );
    }

    /**
     * @dev Finds a resolver by recursively querying the registry, starting at the longest name and progressively
     *      removing labels until it finds a result.
     * @param name The name to resolve, in DNS-encoded and normalised form.
     * @return The Resolver responsible for this name, and the namehash of the full name.
     */
    function findResolver(bytes calldata name)
        public
        view
        returns (Resolver, bytes32)
    {
        (address resolver, bytes32 labelhash) = findResolver(name, 0);
        return (Resolver(resolver), labelhash);
    }

    function findResolver(bytes calldata name, uint256 offset)
        internal
        view
        returns (address, bytes32)
    {
        uint256 labelLength = uint256(uint8(name[offset]));
        if (labelLength == 0) {
            return (address(0), bytes32(0));
        }
        uint256 nextLabel = offset + labelLength + 1;
        bytes32 labelHash = keccak256(name[offset + 1:nextLabel]);
        (address parentresolver, bytes32 parentnode) = findResolver(
            name,
            nextLabel
        );
        bytes32 node = keccak256(abi.encodePacked(parentnode, labelHash));
        address resolver = registry.resolver(node);
        if (resolver != address(0)) {
            return (resolver, node);
        }
        return (parentresolver, node);
    }
}


change finalAssessors comment

change finalAssessors comment

import "lib/random.sol";
import "userMaster.sol";
import "concept.sol";
import "user.sol";
import "conceptMaster.sol";

/*
@type: contract
@name: Assessment
@purpose: To facilitate the assessment process
*/
contract Assessment
{
  address assessee; //The address of the user being assessed
  address[] public assessorPool; //The addresses of the user's that the assessors are randomly selected from
  mapping (address => uint) assessors; //A mapping of the assessors to their current status
  uint calledAssessors = 0; //The number of assessors that have been called to assess
  address[] finalAssessors; //The addresses of the final set of assessors who have agreed to the assessment process
  address concept; //The address of the concept that is being assessed
  address userMaster; //The address of the UserMaster
  address conceptMaster; //The address of the ConceptMaster
  address random; //The address of the Random contract
  uint public poolSizeRemaining; //The remaining number of users to be added to the assessor pool
  uint public numberOfAssessors; //The number of assessors requested for this assessment
  mapping(address => int) assessmentResults; //Pass/Fail and Score given by assessors
  mapping(int => bool) inRewardCluster; //A bool that reflects whether or not a score is within the largest cluster of scores
  mapping(address => string[]) public data; //IFFS hashes of data that can be passed between the assessors and the assessee for the assessment to occur
  address[] potentialAssessors; //The addresses of those randomly selected to be assessors (but may not have confirmed yes)
  int finalScore; //The final score of the assessee
  uint public referenceTime; //Time used as reference to determine how much time has passed for a certain portion of the assessment
  uint numberCancelled = 0; //The number of assessors who have refused to join the assessment
  uint doneAssessors = 0; //The number of assessors that are done assessing
  uint resultsSet = 0; //The number of assessors who have set their score for the assessee
  uint assessmentTime; //The amount of time allotted for the assessors to judge the assessee and determine their score

  /*
  @type: modifier
  @name: onlyConcept
  @purpose: to only allow the Concept contract that spawned this assessment to call a function to which this modifier is applied
  */
  modifier onlyConcept()
  {
    if(msg.sender != concept) //checks if msg.sender is the concept that spawned this assessment
    {
      throw; //throws the function call if not
    }
    _;
  }

  /*
  @type: modifier
  @name: onlyThis
  @purpose: to only allow the this contract to call a function to which this modifier is applied
  */
  modifier onlyThis()
  {
    if(msg.sender != address(this)) //Checks if msg.sender is this contract
    {
      throw; //Throws out the function call if it isn't
    }
    _;
  }

  /*
  @type: modifier
  @name: onlyAssessorAssessee
  @purpose: to only allow the assessors and assessee to call a function to which this modifier is applied
  */
  modifier onlyAssessorAssessee()
  {
    if(msg.sender != assessee && assessors[msg.sender] == 0) //Checks if msg.sender has the same address as either the assessee or an assessor
    {
      throw; //Throws the function call if not
    }
    _;
  }

  /*
  @type: modifier
  @name: onlyConceptAssessment
  @purpose: to only allow the this contract or the Concept contract that spawned it to call a function to which this modifier is applied
  */
  modifier onlyConceptAssessment()
  {
    if(msg.sender != address(this) && msg.sender != concept) //Checks if msg.sender has the same address as this contract or the Concept that spawned it
    {
      throw; //Throws the function call if not
    }
    _;
  }

  function Assessment(address assesseeAddress, address conceptAddress, address userMasterAddress, address conceptMasterAddress, address randomAddress, uint time)
  {
    assessee = assesseeAddress;
    concept = conceptAddress;
    userMaster = userMasterAddress;
    conceptMaster = conceptMasterAddress;
    random = randomAddress;
    referenceTime = block.timestamp;
    assessmentTime = time;
    User(assessee).notification("Assessment made", concept, 0);
  }

  event PotentialAssessorSet(address _potentialAssessor);
  event DataSet(address _dataSetter, uint _index);

  function setNumberOfAssessors(uint number) onlyConcept()
  {
    numberOfAssessors = number;
  }

  function setAssessmentPoolSize(uint sizeRemaining) onlyConcept()
  {
    if(poolSizeRemaining > 0)
    {
      poolSizeRemaining = sizeRemaining;
    }
  }

  function addToAssessorPool(address potentialAddress) onlyConcept()
  {
    assessorPool.push(potentialAddress);
  }

  function setPotentialAssessor(uint needed)
  {
    if(assessorPool.length - calledAssessors < needed)
    {
      cancelAssessment();
    }
    else if(msg.sender == address(this) || msg.sender == concept)
    {
      bool potentialAssessorSet = false;
      numberCancelled = 0;
      uint randomNumber = Random(random).getRandom(assessorPool.length, assessorPool.length);
      for(uint i = 0; i < needed; i++)
      {
        while(potentialAssessorSet == false)
        {
          address randomAssessor = assessorPool[randomNumber];
          if(assessors[randomAssessor] == 0)
          {
            assessors[randomAssessor] = 3;
            potentialAssessors.push(randomAssessor);
            calledAssessors++;
            User(randomAssessor).notification("Called As A Potential Assessor",concept, 1);
            randomNumber = Random(random).getRandom(assessorPool.length, randomNumber);
            PotentialAssessorSet(randomAssessor);
            potentialAssessorSet = true;
          }
        }
        potentialAssessorSet = false;
      }
      referenceTime = now;
    }
  }

  function confirmAssessor(uint confirm)
  {
    if(assessors[msg.sender] != 0 && now - referenceTime <= 300)
    {
      assessors[msg.sender] = confirm;
      if(confirm == 1)
      {
        User(msg.sender).notification("Confirmed As Assessing", concept, 2);
        finalAssessors.push(msg.sender);
        referenceTime = now;
      }
      if(confirm == 2)
      {
        User(msg.sender).notification("Confirmed As Not Assessing", concept, 3);
        numberCancelled ++;
      }
    }
    if(now - referenceTime > 300)
    {
      for(uint i = 0; i < potentialAssessors.length; i++)
      {
        if(assessors[potentialAssessors[i]] == 3)
        {
          User(potentialAssessors[i]).notification("Did Not Respond In Time To Be Assessor", concept, 4);
          assessors[potentialAssessors[i]] = 4;
          numberCancelled++;
        }
      }
      setPotentialAssessor(numberCancelled);
    }
    if(numberCancelled == 0)
    {
      notifyStart();
    }
  }

  function notifyStart() onlyThis()
  {
    for(uint i = 0; i < finalAssessors.length; i++)
    {
      User(finalAssessors[i]).notification("Assessment Has Started", concept, 17);
    }
    User(assessee).notification("Assessment Has Started", concept, 17);
    referenceTime = now;
  }

  function setData(string newData) onlyAssessorAssessee()
  {
    data[msg.sender].push(newData);
    DataSet(msg.sender, data[msg.sender].length - 1);
  }

  function cancelAssessment() onlyConceptAssessment()
  {
    User(assessee).notification("Assessment Cancled", concept, 8);
    Concept(concept).pay(assessee, UserMaster(userMaster).getTokenBalance(assessee) + int(assessmentTime*numberOfAssessors));
    User(assessee).notification("You have been refunded for your assessment", concept, 20);
    for(uint i = 0; i < finalAssessors.length; i++)
    {
      User(finalAssessors[i]).notification("Assessment Cancled", concept, 8);
    }
    suicide(conceptMaster);
  }

  function doneAssessing()
  {
    if(assessors[msg.sender] == 1)
    {
      assessors[msg.sender] = 5;
      doneAssessors++;
    }
    if(referenceTime - now > assessmentTime && doneAssessors != finalAssessors.length)
    {
      for(uint i = 0; i < finalAssessors.length; i++)
      {
        if(assessors[finalAssessors[i]] == 1)
        {
          assessors[finalAssessors[i]] = 5;
          doneAssessors++;
        }
      }
    }
    if(doneAssessors == finalAssessors.length)
    {
      for(uint n = 0; n < finalAssessors.length; n++)
      {
        User(finalAssessors[n]).notification("Send in Score", concept, 18);
      }
      referenceTime = block.number; //use referenceTime to refer to the block number instead of timestamp
    }
  }

  function setResult(int score)
  {
    if(doneAssessors == finalAssessors.length && assessors[msg.sender] == 5 && block.number - referenceTime == 5)
    {
      assessmentResults[msg.sender] = score;
      assessors[msg.sender] = 6;
      resultsSet++;
    }
    if(resultsSet < finalAssessors.length && block.number - referenceTime > 5)
    {
      for(uint i = 0; i < finalAssessors.length; i++)
      {
        if(assessors[finalAssessors[i]] == 5)
        {
          assessmentResults[finalAssessors[i]] = 0;
          assessors[finalAssessors[i]] = 6;
          resultsSet++;
        }
      }
    }
    if(resultsSet == finalAssessors.length)
    {
      calculateResult();
    }
  }

  function calculateMAD(int[] scores, int n) onlyThis() constant returns(int)
  {
    int meanScore;
    int totalRelativeDistance;
    int meanAbsoluteDeviation;
    for(uint j = 0; j < scores.length; j++)
    {
      meanScore += scores[j];
      inRewardCluster[scores[j]] = false;
    }
    meanScore /= n;
    for(uint k = 0; k < scores.length; k++)
    {
      int distanceFromMean = scores[k] - meanScore;
      if(distanceFromMean < 0)
      {
        distanceFromMean *= -1;
      }
      totalRelativeDistance += distanceFromMean;
    }
    meanAbsoluteDeviation = totalRelativeDistance/n;
    return meanAbsoluteDeviation;
  }

  function calculateResult() onlyThis()
  {
    mapping(uint => int[]) clusters;
    int[] memory scores = new int[] (finalAssessors.length);
    uint largestClusterIndex = 0;
    int averageScore;
    for(uint i = 0; i < finalAssessors.length; i++)
    {
      scores[i] = assessmentResults[finalAssessors[i]];
    }
    int meanAbsoluteDeviation = calculateMAD(scores,int(numberOfAssessors));
    for(uint l = 0; l < scores.length; l++)
    {
      for(uint m = 0; m < scores.length; m++)
      {
        if(scores[l] - scores[m] <= meanAbsoluteDeviation)
        {
          clusters[l].push(scores[m]);
        }
      }
      if(clusters[l].length > clusters[largestClusterIndex].length)
      {
        largestClusterIndex = l;
      }
    }
    for(uint o = 0; o < clusters[largestClusterIndex].length; o++)
    {
      averageScore += clusters[largestClusterIndex][o];
      inRewardCluster[clusters[largestClusterIndex][o]] = true;
    }
    averageScore /= int(clusters[largestClusterIndex].length);
    finalScore = averageScore;
    payout(clusters[largestClusterIndex].length);
  }

  function payout(uint largestSize) onlyThis()
  {
    for(uint i = 0; i < finalAssessors.length; i++)
    {
      int score = assessmentResults[finalAssessors[i]];
      int scoreDistance = ((score - finalScore)*100)/finalScore;
      if(scoreDistance < 0)
      {
        scoreDistance *= -1;
      }
      int payoutValue = (int(assessmentTime*finalAssessors.length)/(100 - scoreDistance)) * int(finalAssessors.length/largestSize);
      if(inRewardCluster[score] == true)
      {
        Concept(concept).pay(finalAssessors[i], UserMaster(userMaster).getTokenBalance(finalAssessors[i]) + payoutValue);
        User(finalAssessors[i]).notification("You Have Received Payment For Your Assessment", concept, 15);
      }
      if(inRewardCluster[score] == false)
      {
        Concept(concept).pay(finalAssessors[i], UserMaster(userMaster).getTokenBalance(finalAssessors[i]) - payoutValue);
        User(finalAssessors[i]).notification("You Have Received A Fine For Your Assessment", concept, 16);
      }
    }
    returnResults();
  }

  function returnResults() onlyThis()
  {
    Concept(concept).finishAssessment(finalScore, assessee, address(this));
  }
}

import "lib/random.sol";
import "userMaster.sol";
import "concept.sol";
import "user.sol";
import "conceptMaster.sol";

/*
@type: contract
@name: Assessment
@purpose: To facilitate the assessment process
*/
contract Assessment
{
  address assessee; //The address of the user being assessed
  address[] public assessorPool; //The addresses of the user's that the assessors are randomly selected from
  mapping (address => uint) assessors; //A mapping of the assessors to their current status
  uint calledAssessors = 0; //The number of assessors that have been called to assess
  address[] finalAssessors; //The addresses of the final set of assessors who have completed the assessment process
  address concept; //The address of the concept that is being assessed
  address userMaster; //The address of the UserMaster
  address conceptMaster; //The address of the ConceptMaster
  address random; //The address of the Random contract
  uint public poolSizeRemaining; //The remaining number of users to be added to the assessor pool
  uint public numberOfAssessors; //The number of assessors requested for this assessment
  mapping(address => int) assessmentResults; //Pass/Fail and Score given by assessors
  mapping(int => bool) inRewardCluster; //A bool that reflects whether or not a score is within the largest cluster of scores
  mapping(address => string[]) public data; //IFFS hashes of data that can be passed between the assessors and the assessee for the assessment to occur
  address[] potentialAssessors; //The addresses of those randomly selected to be assessors (but may not have confirmed yes)
  int finalScore; //The final score of the assessee
  uint public referenceTime; //Time used as reference to determine how much time has passed for a certain portion of the assessment
  uint numberCancelled = 0; //The number of assessors who have refused to join the assessment
  uint doneAssessors = 0; //The number of assessors that are done assessing
  uint resultsSet = 0; //The number of assessors who have set their score for the assessee
  uint assessmentTime; //The amount of time allotted for the assessors to judge the assessee and determine their score

  /*
  @type: modifier
  @name: onlyConcept
  @purpose: to only allow the Concept contract that spawned this assessment to call a function to which this modifier is applied
  */
  modifier onlyConcept()
  {
    if(msg.sender != concept) //checks if msg.sender is the concept that spawned this assessment
    {
      throw; //throws the function call if not
    }
    _;
  }

  /*
  @type: modifier
  @name: onlyThis
  @purpose: to only allow the this contract to call a function to which this modifier is applied
  */
  modifier onlyThis()
  {
    if(msg.sender != address(this)) //Checks if msg.sender is this contract
    {
      throw; //Throws out the function call if it isn't
    }
    _;
  }

  /*
  @type: modifier
  @name: onlyAssessorAssessee
  @purpose: to only allow the assessors and assessee to call a function to which this modifier is applied
  */
  modifier onlyAssessorAssessee()
  {
    if(msg.sender != assessee && assessors[msg.sender] == 0) //Checks if msg.sender has the same address as either the assessee or an assessor
    {
      throw; //Throws the function call if not
    }
    _;
  }

  /*
  @type: modifier
  @name: onlyConceptAssessment
  @purpose: to only allow the this contract or the Concept contract that spawned it to call a function to which this modifier is applied
  */
  modifier onlyConceptAssessment()
  {
    if(msg.sender != address(this) && msg.sender != concept) //Checks if msg.sender has the same address as this contract or the Concept that spawned it
    {
      throw; //Throws the function call if not
    }
    _;
  }

  function Assessment(address assesseeAddress, address conceptAddress, address userMasterAddress, address conceptMasterAddress, address randomAddress, uint time)
  {
    assessee = assesseeAddress;
    concept = conceptAddress;
    userMaster = userMasterAddress;
    conceptMaster = conceptMasterAddress;
    random = randomAddress;
    referenceTime = block.timestamp;
    assessmentTime = time;
    User(assessee).notification("Assessment made", concept, 0);
  }

  event PotentialAssessorSet(address _potentialAssessor);
  event DataSet(address _dataSetter, uint _index);

  function setNumberOfAssessors(uint number) onlyConcept()
  {
    numberOfAssessors = number;
  }

  function setAssessmentPoolSize(uint sizeRemaining) onlyConcept()
  {
    if(poolSizeRemaining > 0)
    {
      poolSizeRemaining = sizeRemaining;
    }
  }

  function addToAssessorPool(address potentialAddress) onlyConcept()
  {
    assessorPool.push(potentialAddress);
  }

  function setPotentialAssessor(uint needed)
  {
    if(assessorPool.length - calledAssessors < needed)
    {
      cancelAssessment();
    }
    else if(msg.sender == address(this) || msg.sender == concept)
    {
      bool potentialAssessorSet = false;
      numberCancelled = 0;
      uint randomNumber = Random(random).getRandom(assessorPool.length, assessorPool.length);
      for(uint i = 0; i < needed; i++)
      {
        while(potentialAssessorSet == false)
        {
          address randomAssessor = assessorPool[randomNumber];
          if(assessors[randomAssessor] == 0)
          {
            assessors[randomAssessor] = 3;
            potentialAssessors.push(randomAssessor);
            calledAssessors++;
            User(randomAssessor).notification("Called As A Potential Assessor",concept, 1);
            randomNumber = Random(random).getRandom(assessorPool.length, randomNumber);
            PotentialAssessorSet(randomAssessor);
            potentialAssessorSet = true;
          }
        }
        potentialAssessorSet = false;
      }
      referenceTime = now;
    }
  }

  function confirmAssessor(uint confirm)
  {
    if(assessors[msg.sender] != 0 && now - referenceTime <= 300)
    {
      assessors[msg.sender] = confirm;
      if(confirm == 1)
      {
        User(msg.sender).notification("Confirmed As Assessing", concept, 2);
        finalAssessors.push(msg.sender);
        referenceTime = now;
      }
      if(confirm == 2)
      {
        User(msg.sender).notification("Confirmed As Not Assessing", concept, 3);
        numberCancelled ++;
      }
    }
    if(now - referenceTime > 300)
    {
      for(uint i = 0; i < potentialAssessors.length; i++)
      {
        if(assessors[potentialAssessors[i]] == 3)
        {
          User(potentialAssessors[i]).notification("Did Not Respond In Time To Be Assessor", concept, 4);
          assessors[potentialAssessors[i]] = 4;
          numberCancelled++;
        }
      }
      setPotentialAssessor(numberCancelled);
    }
    if(numberCancelled == 0)
    {
      notifyStart();
    }
  }

  function notifyStart() onlyThis()
  {
    for(uint i = 0; i < finalAssessors.length; i++)
    {
      User(finalAssessors[i]).notification("Assessment Has Started", concept, 17);
    }
    User(assessee).notification("Assessment Has Started", concept, 17);
    referenceTime = now;
  }

  function setData(string newData) onlyAssessorAssessee()
  {
    data[msg.sender].push(newData);
    DataSet(msg.sender, data[msg.sender].length - 1);
  }

  function cancelAssessment() onlyConceptAssessment()
  {
    User(assessee).notification("Assessment Cancled", concept, 8);
    Concept(concept).pay(assessee, UserMaster(userMaster).getTokenBalance(assessee) + int(assessmentTime*numberOfAssessors));
    User(assessee).notification("You have been refunded for your assessment", concept, 20);
    for(uint i = 0; i < finalAssessors.length; i++)
    {
      User(finalAssessors[i]).notification("Assessment Cancled", concept, 8);
    }
    suicide(conceptMaster);
  }

  function doneAssessing()
  {
    if(assessors[msg.sender] == 1)
    {
      assessors[msg.sender] = 5;
      doneAssessors++;
    }
    if(referenceTime - now > assessmentTime && doneAssessors != finalAssessors.length)
    {
      for(uint i = 0; i < finalAssessors.length; i++)
      {
        if(assessors[finalAssessors[i]] == 1)
        {
          assessors[finalAssessors[i]] = 5;
          doneAssessors++;
        }
      }
    }
    if(doneAssessors == finalAssessors.length)
    {
      for(uint n = 0; n < finalAssessors.length; n++)
      {
        User(finalAssessors[n]).notification("Send in Score", concept, 18);
      }
      referenceTime = block.number; //use referenceTime to refer to the block number instead of timestamp
    }
  }

  function setResult(int score)
  {
    if(doneAssessors == finalAssessors.length && assessors[msg.sender] == 5 && block.number - referenceTime == 5)
    {
      assessmentResults[msg.sender] = score;
      assessors[msg.sender] = 6;
      resultsSet++;
    }
    if(resultsSet < finalAssessors.length && block.number - referenceTime > 5)
    {
      for(uint i = 0; i < finalAssessors.length; i++)
      {
        if(assessors[finalAssessors[i]] == 5)
        {
          assessmentResults[finalAssessors[i]] = 0;
          assessors[finalAssessors[i]] = 6;
          resultsSet++;
        }
      }
    }
    if(resultsSet == finalAssessors.length)
    {
      calculateResult();
    }
  }

  function calculateMAD(int[] scores, int n) onlyThis() constant returns(int)
  {
    int meanScore;
    int totalRelativeDistance;
    int meanAbsoluteDeviation;
    for(uint j = 0; j < scores.length; j++)
    {
      meanScore += scores[j];
      inRewardCluster[scores[j]] = false;
    }
    meanScore /= n;
    for(uint k = 0; k < scores.length; k++)
    {
      int distanceFromMean = scores[k] - meanScore;
      if(distanceFromMean < 0)
      {
        distanceFromMean *= -1;
      }
      totalRelativeDistance += distanceFromMean;
    }
    meanAbsoluteDeviation = totalRelativeDistance/n;
    return meanAbsoluteDeviation;
  }

  function calculateResult() onlyThis()
  {
    mapping(uint => int[]) clusters;
    int[] memory scores = new int[] (finalAssessors.length);
    uint largestClusterIndex = 0;
    int averageScore;
    for(uint i = 0; i < finalAssessors.length; i++)
    {
      scores[i] = assessmentResults[finalAssessors[i]];
    }
    int meanAbsoluteDeviation = calculateMAD(scores,int(numberOfAssessors));
    for(uint l = 0; l < scores.length; l++)
    {
      for(uint m = 0; m < scores.length; m++)
      {
        if(scores[l] - scores[m] <= meanAbsoluteDeviation)
        {
          clusters[l].push(scores[m]);
        }
      }
      if(clusters[l].length > clusters[largestClusterIndex].length)
      {
        largestClusterIndex = l;
      }
    }
    for(uint o = 0; o < clusters[largestClusterIndex].length; o++)
    {
      averageScore += clusters[largestClusterIndex][o];
      inRewardCluster[clusters[largestClusterIndex][o]] = true;
    }
    averageScore /= int(clusters[largestClusterIndex].length);
    finalScore = averageScore;
    payout(clusters[largestClusterIndex].length);
  }

  function payout(uint largestSize) onlyThis()
  {
    for(uint i = 0; i < finalAssessors.length; i++)
    {
      int score = assessmentResults[finalAssessors[i]];
      int scoreDistance = ((score - finalScore)*100)/finalScore;
      if(scoreDistance < 0)
      {
        scoreDistance *= -1;
      }
      int payoutValue = (int(assessmentTime*finalAssessors.length)/(100 - scoreDistance)) * int(finalAssessors.length/largestSize);
      if(inRewardCluster[score] == true)
      {
        Concept(concept).pay(finalAssessors[i], UserMaster(userMaster).getTokenBalance(finalAssessors[i]) + payoutValue);
        User(finalAssessors[i]).notification("You Have Received Payment For Your Assessment", concept, 15);
      }
      if(inRewardCluster[score] == false)
      {
        Concept(concept).pay(finalAssessors[i], UserMaster(userMaster).getTokenBalance(finalAssessors[i]) - payoutValue);
        User(finalAssessors[i]).notification("You Have Received A Fine For Your Assessment", concept, 16);
      }
    }
    returnResults();
  }

  function returnResults() onlyThis()
  {
    Concept(concept).finishAssessment(finalScore, assessee, address(this));
  }
}


Improved final contract;

Improved final contract;

/*******************************************************************************
**************************     BATTLE CONTRACT v1.1  ***************************
********************************************************************************

Battle smart contract is a platform/ecosystem for gaming built on top of a
decentralized smart contract, allowing anyone to use a Warrior tokens: entities
which exists on the Ethereum Network, which can be traded or used to enter the
battle with other Warrior tokens holders, for profit or just for fun!

********************************************************************************
**************************           RULES           ***************************
********************************************************************************

- This first battle contract accepts Persians, Spartans (300 Tokens), Immortals
  and Athenians as warriors.
- Every warrior token has a proper value in **Battle Point (BP)** that represent
  his strength on the battle contract.
- Persians and Immortals represent the Persian faction, Spartans and Athenians
  the Greek one.
- During the first phase players send tokens to the battle contract
  (NOTE: before calling the proper contract's function that assigning warriors
  to the battlefiled, players NEED TO CALL APPROVE on their token contract to
  allow Battle contract to move their tokens.
- Once sent, troops can't be retired form the battlefield
- The battle will last for several days
- When the battle period is over, following results can happpen:
    -- When the battle ends in a draw:
        (*) 10% of main troops of both sides lie on the ground
        (*) 90% of them can be retrieved by each former owner
        (*) No slaves are assigned
    -- When the battle ends with a winning factions:
        (*) 10% of main troops of both sides lie on the ground
        (*) 90% of them can be retrieved by each former owner
        (*) Surviving warriors of the loosing faction are assigned as slaves
            to winners
        (*) Slaves are computed based on the BP contributed by each sender
- Persians and Spartans are main troops.
- Immortals and Athenians are support troops: there will be no casualties in
  their row, and they will be retrieved without losses by original senders.
- Only Persians and Spartans can be slaves. Immortals and Athenians WILL NOT
  be sent back as slaves to winners.

********************************************************************************
**************************      TOKEN ADDRESSES      ***************************
********************************************************************************

        0x163733bcc28dbf26B41a8CfA83e369b5B3af741b      Persians    (PRS)
        0x22E5F62D0FA19974749faa194e3d3eF6d89c08d7      Immortals   (IMT)
        0xaEc98A708810414878c3BCDF46Aad31dEd4a4557      Spartans    (300)
        0x17052d51E954592C1046320c2371AbaB6C73Ef10      Athenians   (ATH)

*******************************************************************************/
pragma solidity ^0.4.15;

import "./TokenERC20.sol";
import "./Timed.sol";
import "./SafeMathLib.sol";
import "./Upgradable.sol";

contract Battle is Timed, Upgradable {
    using SafeMathLib for uint;

    uint    public constant MAX_PERSIANS            = 300000 * 10**18;  // 300.000
    uint    public constant MAX_SPARTANS            = 300 * 10**18;     // 300
    uint    public constant MAX_IMMORTALS           = 100;              // 100
    uint    public constant MAX_ATHENIANS           = 100 * 10**18;     // 100

    uint    public constant WAD                     = 10**18;           // Common decimal positions
    uint8   public constant BP_PERSIAN              = 1;                // Each Persian worths 1 Battle Point
    uint8   public constant BP_IMMORTAL             = 100;              // Each Immortal worths 100 Battle Points
    uint16  public constant BP_SPARTAN              = 1000;             // Each Spartan worths 1000 Battle Points
    uint8   public constant BP_ATHENIAN             = 100;              // Each Athenians worths 100 Battle Points

    uint8   public constant BATTLE_POINT_DECIMALS   = 18;
    uint8   public constant BATTLE_CASUALTIES       = 10;               // Percentage of Persian and Spartan casualties

    address public persians;                                            // Address of the Persian Tokens
    address public immortals;                                           // Address of the Immortal Tokens
    address public spartans;                                            // Address of the 300 Tokens
    address public athenians;                                           // Address of the Athenian Tokens

    mapping (address => mapping (address => uint))   public  warriorsByPlayer;               // Troops currently allocated by each player
    mapping (address => uint)                        public  warriorsOnTheBattlefield;       // Total troops fighting in the battle

    event WarriorsAssignedToBattlefield (address indexed _from, address _faction, uint _battlePointsIncrementForecast);
    event WarriorsBackToHome            (address indexed _to, address _faction, uint _survivedWarriors);

    function Battle(uint _startTime, uint _life, uint8 _avarageBlockTime, address _persians, address _immortals, address _spartans, address _athenians) Timed(_startTime, _life, _avarageBlockTime) Upgradable("1.0.0") {
        persians = _persians;
        immortals = _immortals;
        spartans = _spartans;
        athenians = _athenians;
    }

    /**** PHASE #1 ******/

    function assignPersiansToBattle(uint _warriors) onlyIfInTime external returns (bool success) {
        assignWarriorsToBattle(msg.sender, persians, _warriors, MAX_PERSIANS);
        // Persians are divisible with 18 decimals and their value is 1 BP
        WarriorsAssignedToBattlefield(msg.sender, persians, _warriors / WAD);
        return true;
    }

    function assignSpartansToBattle(uint _warriors) onlyIfInTime external returns (bool success) {
        assignWarriorsToBattle(msg.sender, spartans, _warriors, MAX_SPARTANS);
        // Spartans are divisible with 18 decimals and their value is 1.000 BP
        WarriorsAssignedToBattlefield(msg.sender, spartans, (_warriors / WAD).mul(BP_SPARTAN));
        return true;
    }

    function assignImmortalsToBattle(uint _warriors) onlyIfInTime external returns (bool success) {
        assignWarriorsToBattle(msg.sender, immortals, _warriors, MAX_IMMORTALS);
        // Immortals are not divisible and their value is 100 BP
        WarriorsAssignedToBattlefield(msg.sender, immortals, _warriors.mul(BP_IMMORTAL));
        return true;
    }

    function assignAtheniansToBattle(uint _warriors) onlyIfInTime external returns (bool success) {
        assignWarriorsToBattle(msg.sender, athenians, _warriors, MAX_ATHENIANS);
        // Athenians are divisible with 18 decimals and their value is 100 BP
        WarriorsAssignedToBattlefield(msg.sender, athenians, (_warriors / WAD).mul(BP_ATHENIAN));
        return true;
    }

    /**** PHASE #2 ******/

    function redeemWarriors() onlyIfTimePassed external returns (bool success) {
        if (getPersiansBattlePoints() > getGreeksBattlePoints()) {
            // Persians won, compute slaves
            uint spartanSlaves = computeSlaves(msg.sender, spartans);
            if (spartanSlaves > 0) {
                // Send back Spartan slaves to winner
                sendWarriors(msg.sender, spartans, spartanSlaves);
            }
            // Send back Persians but casualties
            retrieveWarriors(msg.sender, persians, BATTLE_CASUALTIES);
        } else if (getPersiansBattlePoints() < getGreeksBattlePoints()) {
            //Greeks won, send back Persian slaves
            uint persianSlaves = computeSlaves(msg.sender, persians);
            if (persianSlaves > 0) {
                // Send back Persians slaves to winner
                sendWarriors(msg.sender, persians, persianSlaves);
            }
            // Send back Spartans but casualties
            retrieveWarriors(msg.sender, spartans, BATTLE_CASUALTIES);
        } else {
            // It's a draw, send back Persians and Spartans but casualties
            retrieveWarriors(msg.sender, persians, BATTLE_CASUALTIES);
            retrieveWarriors(msg.sender, spartans, BATTLE_CASUALTIES);
        }
        // Send back Immortals untouched
        retrieveWarriors(msg.sender, immortals, 0);
        // Send back Athenians untouched
        retrieveWarriors(msg.sender, athenians, 0);
        return true;
    }

    /*** PRIVATE FUNCTIONS ***/

    function assignWarriorsToBattle(address _player, address _faction, uint _warriors, uint _maxWarriors) private {
        require(warriorsOnTheBattlefield[_faction].add(_warriors) <= _maxWarriors);
        assert(TokenERC20(_faction).transferFrom(_player, address(this), _warriors));
        warriorsByPlayer[_player][_faction] = warriorsByPlayer[_player][_faction].add(_warriors);
        warriorsOnTheBattlefield[_faction] = warriorsOnTheBattlefield[_faction].add(_warriors);
    }

    function retrieveWarriors(address _player, address _faction, uint8 _deadPercentage) private {
        if (warriorsByPlayer[_player][_faction] > 0) {
            uint _warriors = warriorsByPlayer[_player][_faction];
            if (_deadPercentage > 0) {
                _warriors = _warriors.sub(_warriors.wper(_deadPercentage));
            }
            warriorsByPlayer[_player][_faction] = 0;
            sendWarriors(_player, _faction, _warriors);
            WarriorsBackToHome(_player, _faction, _warriors);
        }
    }

    function sendWarriors(address _player, address _faction, uint _warriors) private {
        assert(TokenERC20(_faction).transfer(_player, _warriors));
    }

    /*** CONSTANT FUNCTIONS AND DAPP HELPERS ***/

    function getPersiansOnTheBattlefield(address _player) constant returns (uint persiansOnTheBattlefield) {
        return warriorsByPlayer[_player][persians];
    }

    function getImmortalsOnTheBattlefield(address _player) constant returns (uint immortalsOnTheBattlefield) {
        return warriorsByPlayer[_player][immortals];
    }

    function getSpartansOnTheBattlefield(address _player) constant returns (uint spartansOnTheBattlefield) {
        return warriorsByPlayer[_player][spartans];
    }

    function getAtheniansOnTheBattlefield(address _player) constant returns (uint atheniansOnTheBattlefield) {
        return warriorsByPlayer[_player][athenians];
    }

    function getPersiansBattlePoints() constant returns (uint persiansBattlePoints) {
        return (warriorsOnTheBattlefield[persians].mul(BP_PERSIAN) + warriorsOnTheBattlefield[immortals].mul(WAD).mul(BP_IMMORTAL));
    }

    function getGreeksBattlePoints() constant returns (uint greeksBattlePoints) {
        return (warriorsOnTheBattlefield[spartans].mul(BP_SPARTAN) + warriorsOnTheBattlefield[athenians].mul(BP_ATHENIAN));
    }

    function getPersiansBattlePointsBy(address _player) constant returns (uint playerBattlePoints) {
        return (getPersiansOnTheBattlefield(_player).mul(BP_PERSIAN) + getImmortalsOnTheBattlefield(_player).mul(WAD).mul(BP_IMMORTAL));
    }

    function getGreeksBattlePointsBy(address _player) constant returns (uint playerBattlePoints) {
        return (getSpartansOnTheBattlefield(_player).mul(BP_SPARTAN) + getAtheniansOnTheBattlefield(_player).mul(BP_ATHENIAN));
    }

    function computeSlaves(address _player, address _loosingMainTroops) constant returns (uint slaves) {
        if (_loosingMainTroops == spartans) {
            return getPersiansBattlePointsBy(_player).wdiv(getPersiansBattlePoints()).wmul(getTotalSlaves(spartans));
        } else {
            return getGreeksBattlePointsBy(_player).wdiv(getGreeksBattlePoints()).wmul(getTotalSlaves(persians));
        }
    }

    function getTotalSlaves(address _faction) constant returns (uint slaves) {
        return warriorsOnTheBattlefield[_faction].sub(warriorsOnTheBattlefield[_faction].wper(BATTLE_CASUALTIES));
    }

    function isInProgress() constant returns (bool inProgress) {
        return !isTimeExpired();
    }

    function isEnded() constant returns (bool ended) {
        return isTimeExpired();
    }

    function isDraw() constant returns (bool draw) {
        return (getPersiansBattlePoints() == getGreeksBattlePoints());
    }

    function getTemporaryWinningFaction() constant returns (string temporaryWinningFaction) {
        if (isDraw()) {
            return "It's currently a draw, but the battle is still in progress!";
        }
        return getPersiansBattlePoints() > getGreeksBattlePoints() ?
            "Persians are winning, but the battle is still in progress!" : "Greeks are winning, but the battle is still in progress!";
    }

    function getWinningFaction() constant returns (string winningFaction) {
        if (isInProgress()) {
            return "The battle is still in progress";
        }
        if (isDraw()) {
            return "The battle ended in a draw!";
        }
        return getPersiansBattlePoints() > getGreeksBattlePoints() ? "Persians" : "Greeks";
    }

    /****           DEV FUNCTIONS                   *******/
    /**** REMOVE FOLLOWING FUNCTIONS BEFORE DEPLOY  *******/
    /**** REMOVE FOLLOWING FUNCTIONS BEFORE DEPLOY  *******/
    /**** REMOVE FOLLOWING FUNCTIONS BEFORE DEPLOY  *******/
    /**** REMOVE FOLLOWING FUNCTIONS BEFORE DEPLOY  *******/
    /****             REALLY!                       *******/

    function setTime(uint _startTime, uint life, uint8 _avarageBlockTime) onlyOwner {
        startTime = _startTime;
        endTime = _startTime + life;
        avarageBlockTime = _avarageBlockTime;
    }
}

/*******************************************************************************
**************************     BATTLE CONTRACT v1    ***************************
********************************************************************************

Battle smart contract is a platform/ecosystem for gaming built on top of a
decentralized smart contract, allowing anyone to use a Warrior tokens: entities
which exists on the Ethereum Network, which can be traded or used to enter the
battle with other Warrior tokens holders, for profit or just for fun!

********************************************************************************
**************************           RULES           ***************************
********************************************************************************

- This first battle contract accepts Persians, Spartans (300 Tokens), Immortals
  and Athenians as warriors.
- Every warrior token has a proper value in **Battle Point (BP)** that represent
  his strength on the battle contract.
- Persians and Immortals represent the Persian faction, Spartans and Athenians
  the Greek one.
- During the first phase players send tokens to the battle contract
  (NOTE: before calling the proper contract's function that assigning warriors
  to the battlefiled, players NEED TO CALL APPROVE on their token contract to
  allow Battle contract to move their tokens.
- Once sent, troops can't be retired form the battlefield
- The battle will last for several days
- When the battle period is over, following results can happpen:
    -- When the battle ends in a draw:
        (*) 10% of main troops of both sides lie on the ground
        (*) 90% of them can be retrieved by each former owner
        (*) No slaves are assigned
    -- When the battle ends with a winning factions:
        (*) 10% of main troops of both sides lie on the ground
        (*) 90% of them can be retrieved by each former owner
        (*) Surviving warriors of the loosing faction are assigned as slaves
            to winners
        (*) Slaves are computed based on the BP contributed by each sender
- Persians and Spartans are main troops.
- Immortals and Athenians are support troops: there will be no casualties in
  their row, and they will be retrieved without losses by original senders.
- Only Persians and Spartans can be slaves. Immortals and Athenians WILL NOT
  be sent back as slaves to winners.

********************************************************************************
**************************      TOKEN ADDRESSES      ***************************
********************************************************************************

        0xaEc98A708810414878c3BCDF46Aad31dEd4a4557      Persians
        0x22E5F62D0FA19974749faa194e3d3eF6d89c08d7      Immortals
        0x163733bcc28dbf26B41a8CfA83e369b5B3af741b      Spartans
        0x17052d51E954592C1046320c2371AbaB6C73Ef10      Athenians

*******************************************************************************/
pragma solidity ^0.4.15;

import "./TokenERC20.sol";
import "./Timed.sol";
import "./SafeMathLib.sol";
import "./Upgradable.sol";

contract Battle is Timed, Upgradable {
    using SafeMathLib for uint;

    uint    public constant MAX_PERSIANS            = 300000 * 10**18;  // 300.000
    uint    public constant MAX_SPARTANS            = 300 * 10**18;     // 300
    uint    public constant MAX_IMMORTALS           = 100;              // 100
    uint    public constant MAX_ATHENIANS           = 100 * 10**18;     // 100

    uint    public constant WAD                     = 10**18;           // Common decimal positions
    uint8   public constant BP_PERSIAN              = 1;                // Each Persian worths 1 Battle Point
    uint8   public constant BP_IMMORTAL             = 100;              // Each Immortal worths 100 Battle Points
    uint16  public constant BP_SPARTAN              = 1000;             // Each Spartan worths 1000 Battle Points
    uint8   public constant BP_ATHENIAN             = 100;              // Each Athenians worths 100 Battle Points

    uint8   public constant BATTLE_POINT_DECIMALS   = 18;
    uint8   public constant BATTLE_CASUALTIES       = 10;               // Percentage of Persian and Spartan casualties

    address public persians;                                            // Address of the Persian Tokens
    address public immortals;                                           // Address of the Immortal Tokens
    address public spartans;                                            // Address of the 300 Tokens
    address public athenians;                                           // Address of the Athenian Tokens

    mapping (address => mapping (address => uint))   public  warriorsByPlayer;               // Troops currently allocated by each player
    mapping (address => uint)                        public  warriorsOnTheBattlefield;       // Total troops fighting in the battle

    event WarriorsAssignedToBattlefield (address indexed _from, address _faction, uint _battlePointsIncrementForecast);
    event WarriorsBackToHome            (address indexed _to, address _faction, uint _survivedWarriors);

    function Battle(uint _startTime, uint _life, uint8 _avarageBlockTime, address _persians, address _immortals, address _spartans, address _athenians) Timed(_startTime, _life, _avarageBlockTime) Upgradable("1.0.0") {
        persians = _persians;
        immortals = _immortals;
        spartans = _spartans;
        athenians = _athenians;
    }

    /**** PHASE #1 ******/

    function assignPersiansToBattle(uint _warriors) onlyIfInTime external returns (bool success) {
        assignWarriorsToBattle(msg.sender, persians, _warriors, MAX_PERSIANS);
        // Persians are divisible with 18 decimals and their value is 1 BP
        WarriorsAssignedToBattlefield(msg.sender, persians, _warriors / WAD);
        return true;
    }

    function assignSpartansToBattle(uint _warriors) onlyIfInTime external returns (bool success) {
        assignWarriorsToBattle(msg.sender, spartans, _warriors, MAX_SPARTANS);
        // Spartans are divisible with 18 decimals and their value is 1.000 BP
        WarriorsAssignedToBattlefield(msg.sender, spartans, (_warriors / WAD).mul(BP_SPARTAN));
        return true;
    }

    function assignImmortalsToBattle(uint _warriors) onlyIfInTime external returns (bool success) {
        assignWarriorsToBattle(msg.sender, immortals, _warriors, MAX_IMMORTALS);
        // Immortals are not divisible and their value is 100 BP
        WarriorsAssignedToBattlefield(msg.sender, immortals, _warriors.mul(BP_IMMORTAL));
        return true;
    }

    function assignAtheniansToBattle(uint _warriors) onlyIfInTime external returns (bool success) {
        assignWarriorsToBattle(msg.sender, athenians, _warriors, MAX_ATHENIANS);
        // Athenians are divisible with 18 decimals and their value is 100 BP
        WarriorsAssignedToBattlefield(msg.sender, athenians, (_warriors / WAD).mul(BP_ATHENIAN));
        return true;
    }

    /**** PHASE #2 ******/

    function redeemWarriors() onlyIfTimePassed external returns (bool success) {
        if (getPersiansBattlePoints() > getGreeksBattlePoints()) {
            // Persians won, compute slaves
            uint spartanSlaves = computeSlaves(msg.sender, spartans);
            if (spartanSlaves > 0) {
                // Send back Spartan slaves to winner
                sendWarriors(msg.sender, spartans, spartanSlaves);
            }
            // Send back Persians but casualties
            retrieveWarriors(msg.sender, persians, BATTLE_CASUALTIES);
        } else if (getPersiansBattlePoints() < getGreeksBattlePoints()) {
            //Greeks won, send back Persian slaves
            uint persianSlaves = computeSlaves(msg.sender, persians);
            if (persianSlaves > 0) {
                // Send back Persians slaves to winner
                sendWarriors(msg.sender, persians, persianSlaves);
            }
            // Send back Spartans but casualties
            retrieveWarriors(msg.sender, spartans, BATTLE_CASUALTIES);
        } else {
            // It's a draw, send back Persians and Spartans but casualties
            retrieveWarriors(msg.sender, persians, BATTLE_CASUALTIES);
            retrieveWarriors(msg.sender, spartans, BATTLE_CASUALTIES);
        }
        // Send back Immortals untouched
        retrieveWarriors(msg.sender, immortals, 0);
        // Send back Athenians untouched
        retrieveWarriors(msg.sender, athenians, 0);
        return true;
    }

    /*** PRIVATE FUNCTIONS ***/

    function assignWarriorsToBattle(address _player, address _faction, uint _warriors, uint _maxWarriors) private {
        require(warriorsOnTheBattlefield[_faction].add(_warriors) <= _maxWarriors);
        assert(TokenERC20(_faction).transferFrom(_player, address(this), _warriors));
        warriorsByPlayer[_player][_faction] = warriorsByPlayer[_player][_faction].add(_warriors);
        warriorsOnTheBattlefield[_faction] = warriorsOnTheBattlefield[_faction].add(_warriors);
    }

    function retrieveWarriors(address _player, address _faction, uint8 _deadPercentage) private {
        if (warriorsByPlayer[_player][_faction] > 0) {
            uint _warriors = warriorsByPlayer[_player][_faction];
            if (_deadPercentage > 0) {
                _warriors = _warriors.sub(_warriors.wper(_deadPercentage));
            }
            warriorsByPlayer[_player][_faction] = 0;
            sendWarriors(_player, _faction, _warriors);
            WarriorsBackToHome(_player, _faction, _warriors);
        }
    }

    function sendWarriors(address _player, address _faction, uint _warriors) private {
        assert(TokenERC20(_faction).transfer(_player, _warriors));
    }

    /*** CONSTANT FUNCTIONS AND DAPP HELPERS ***/

    function getPersiansOnTheBattlefield(address _player) constant returns (uint persiansOnTheBattlefield) {
        return warriorsByPlayer[_player][persians];
    }

    function getImmortalsOnTheBattlefield(address _player) constant returns (uint immortalsOnTheBattlefield) {
        return warriorsByPlayer[_player][immortals];
    }

    function getSpartansOnTheBattlefield(address _player) constant returns (uint spartansOnTheBattlefield) {
        return warriorsByPlayer[_player][spartans];
    }

    function getAtheniansOnTheBattlefield(address _player) constant returns (uint atheniansOnTheBattlefield) {
        return warriorsByPlayer[_player][athenians];
    }

    function getPersiansBattlePoints() constant returns (uint persiansBattlePoints) {
        return (warriorsOnTheBattlefield[persians].mul(BP_PERSIAN) + warriorsOnTheBattlefield[immortals].mul(WAD).mul(BP_IMMORTAL));
    }

    function getGreeksBattlePoints() constant returns (uint greeksBattlePoints) {
        return (warriorsOnTheBattlefield[spartans].mul(BP_SPARTAN) + warriorsOnTheBattlefield[athenians].mul(BP_ATHENIAN));
    }

    function getPersiansBattlePointsBy(address _player) constant returns (uint playerBattlePoints) {
        return (getPersiansOnTheBattlefield(_player).mul(BP_PERSIAN) + getImmortalsOnTheBattlefield(_player).mul(WAD).mul(BP_IMMORTAL));
    }

    function getGreeksBattlePointsBy(address _player) constant returns (uint playerBattlePoints) {
        return (getSpartansOnTheBattlefield(_player).mul(BP_SPARTAN) + getAtheniansOnTheBattlefield(_player).mul(BP_ATHENIAN));
    }

    function computeSlaves(address _player, address _loosingMainTroops) constant returns (uint slaves) {
        if (_loosingMainTroops == spartans) {
            return getPersiansBattlePointsBy(_player).wdiv(getPersiansBattlePoints()).wmul(getTotalSlaves(spartans));
        } else {
            return getGreeksBattlePointsBy(_player).wdiv(getGreeksBattlePoints()).wmul(getTotalSlaves(persians));
        }
    }

    function getTotalSlaves(address _faction) constant returns (uint slaves) {
        return warriorsOnTheBattlefield[_faction].sub(warriorsOnTheBattlefield[_faction].wper(BATTLE_CASUALTIES));
    }

    function isInProgress() constant returns (bool inProgress) {
        return !isTimeExpired();
    }

    function isEnded() constant returns (bool ended) {
        return isTimeExpired();
    }

    function isDraw() constant returns (bool draw) {
        return (getPersiansBattlePoints() == getGreeksBattlePoints());
    }

    function getTemporaryWinningFaction() constant returns (string temporaryWinningFaction) {
        if (isDraw()) {
            return "It's currently a draw, but the battle is still in progress!";
        }
        return getPersiansBattlePoints() > getGreeksBattlePoints() ?
            "Persians are winning, but the battle is still in progress!" : "Greeks are winning, but the battle is still in progress!";
    }

    function getWinningFaction() constant returns (string winningFaction) {
        if (isInProgress()) {
            return "The battle is still in progress";
        }
        if (isDraw()) {
            return "The battle ended in a draw!";
        }
        return getPersiansBattlePoints() > getGreeksBattlePoints() ? "Persians" : "Greeks";
    }

    /****           DEV FUNCTIONS                   *******/
    /**** REMOVE FOLLOWING FUNCTIONS BEFORE DEPLOY  *******/
    /**** REMOVE FOLLOWING FUNCTIONS BEFORE DEPLOY  *******/
    /**** REMOVE FOLLOWING FUNCTIONS BEFORE DEPLOY  *******/
    /**** REMOVE FOLLOWING FUNCTIONS BEFORE DEPLOY  *******/
    /****             REALLY!                       *******/

    function setTime(uint _startTime, uint life, uint8 _avarageBlockTime) {
        startTime = _startTime;
        endTime = _startTime + life;
        avarageBlockTime = _avarageBlockTime;
    }

    event Debug (uint value);
}


remove ws

remove ws

import './PullPaymentCapable.sol';
import './Token.sol';

/*
 * Bounty
 * This bounty will pay out if you can cause a Token's balance
 * to be lower than its totalSupply, which would mean that it doesn't
 * have sufficient ether for everyone to withdraw.
 */
contract Bounty is PullPaymentCapable {

  bool public claimed;
  mapping(address => address) public researchers;

  function() {
    if (claimed) {
      throw;
    }
  }

  function createTarget() returns(Token) {
    Token target = new Token(0);
    researchers[target] = msg.sender;
    return target;
  }

  function claim(Token target) {
    address researcher = researchers[target];
    if (researcher == 0) {
      throw;
    }
    // check Token contract invariants
    if (target.totalSupply() == target.balance) {
      throw;
    }
    asyncSend(researcher, this.balance);
    claimed = true;
  }

}


Separate bounty contracts from general pattern contracts

pragma solidity ^0.4.0;
import './PullPayment.sol';
import './token/SimpleToken.sol';

/*
 * Bounty
 * This bounty will pay out if you can cause a SimpleToken's balance
 * to be lower than its totalSupply, which would mean that it doesn't
 * have sufficient ether for everyone to withdraw.
 */
contract Bounty is PullPayment {

  bool public claimed;
  mapping(address => address) public researchers;

  function() {
    if (claimed) throw;
  }

  function createTarget() returns(SimpleToken) {
    SimpleToken target = new SimpleToken();
    researchers[target] = msg.sender;
    return target;
  }

  function claim(SimpleToken target) {
    address researcher = researchers[target];
    if (researcher == 0) throw;
    // Check SimpleToken contract invariants
    // Customize this to the specifics of your contract
    if (target.totalSupply() == target.balance) {
      throw;
    }
    asyncSend(researcher, this.balance);
    claimed = true;
  }

}


contracts/KetherSortition.sol: Update natspec comments, generalize _nominate

//SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;

import "@openzeppelin/contracts/access/Ownable.sol";

import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";

interface IKetherHomepage {
  function ads(uint _idx) external view returns (address,uint,uint,uint,uint,string memory,string memory,string memory,bool,bool);
  function getAdsLength() view external returns (uint);
}

interface IERC721 {
  function ownerOf(uint256) external view returns (address);
  function balanceOf(address) external view returns (uint256);
  function tokenOfOwnerByIndex(address, uint256) external view returns (uint256);
}

interface IERC20 {
  function balanceOf(address account) external view returns (uint256);
  function transfer(address recipient, uint256 amount) external returns (bool);
}

library Errors {
  string constant MustHaveBalance = "must have tokens to nominate";
  string constant OnlyMagistrate = "only active magistrate can do this";
  string constant MustHaveEntropy = "waiting for entropy";
  string constant MustHaveNominations = "must have nominations";
  string constant AlreadyStarted = "election already started";
  string constant NotExecuted = "election not executed";
  string constant TermNotExpired = "term not expired";
  string constant NotEnoughLink = "not enough LINK";
}

contract KetherSortition is Ownable, VRFConsumerBase {
  event Nominated(
      uint256 indexed termNumber,
      address nominator,
      uint256 pixels
  );

  event ElectionExecuting(
    uint256 indexed termNumber
  );

  event ElectionCompleted(
    uint256 indexed termNumber,
    uint256 magistrateToken,
    address currentTokenOwner
  );

  event StepDown(
    uint256 indexed termNumber,
    uint256 magistrateToken,
    address currentTokenOwner
  );

  struct Nomination{
    uint256 termNumber;
    uint256 nominatedToken;
  }

  /// tokenId of an NFT whose owner controls the royalties purse for this term.
  uint256 public magistrateToken;

  uint256 constant MIN_ELECTION_DURATION = 2 days;
  uint256 constant TERM_DURATION = 6 weeks;
  uint256 constant PIXELS_PER_CELL = 100;

  uint256 public termStarted;
  uint256 public termExpires;
  uint256 public termNumber = 0;

  IERC721 ketherNFTContract;
  IKetherHomepage ketherContract;

  uint256[] public nominatedTokens;
  uint256 public nominatedPixels = 0;
  mapping(uint256 => Nomination) nominations; // mapping of tokenId => {termNumber, nominatedToken}

  uint256 public electionEntropy; // Provided by Chainlink

  // nominating -[term expired & startElection() calls]> waitingForEntropy -[Chainlink calls into fulfillrandomness()]> gotEntropy -[completeElection()] -> nominating
  enum StateMachine { NOMINATING, WAITING_FOR_ENTROPY, GOT_ENTROPY }
  StateMachine state = StateMachine.NOMINATING;

  // Chainlink values
  bytes32 private s_keyHash;
  uint256 private s_fee;

  constructor(address _ketherNFTContract, address _ketherContract, address vrfCoordinator, address link, bytes32 keyHash, uint256 fee)
  public
  VRFConsumerBase(vrfCoordinator, link) {
    s_keyHash = keyHash;
    s_fee = fee;

    ketherNFTContract = IERC721(_ketherNFTContract);
    ketherContract = IKetherHomepage(_ketherContract);

    termExpires = block.timestamp + MIN_ELECTION_DURATION;
  }

  // Internal helpers:

  /**
   * @notice Only callable by Chainlink VRF, async triggered via startElection().
   */
  function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
    require(state == StateMachine.WAITING_FOR_ENTROPY, Errors.NotExecuted);
    electionEntropy = randomness;
    state = StateMachine.GOT_ENTROPY;
  }

  /// @dev _nominate does not check token ownership, must already be checked.
  /// @param _ownedTokenId Token to nominate from
  /// @param _nominateTokenId Token to nominate to, defaults to _ownedTokenId if does not exist.
  function _nominate(uint256 _ownedTokenId, uint256 _nominateTokenId) internal returns (uint256 pixels) {
    if (ketherContract.getAdsLength() < _nominateTokenId) {
      _nominateTokenId = _ownedTokenId;
    }

    // Only push the ad and update pixel count if it's not been nominated before
    if (!isNominated(_ownedTokenId)) {
      pixels += getAdPixels(_ownedTokenId);
      nominatedTokens.push(_ownedTokenId);
    }

    nominations[_ownedTokenId] = Nomination(termNumber + 1, _nominateTokenId);

    return pixels;
  }

  // Views:

  function getMagistrate() public view returns (address) {
    return getAdOwner(magistrateToken);
  }

  function getAdOwner(uint256 _idx) public view returns (address) {
    return ketherNFTContract.ownerOf(_idx);
  }

  function getAdPixels(uint256 _idx) public view returns (uint256) {
    (,,,uint width,uint height,,,,,) = ketherContract.ads(_idx);
    return width * height * PIXELS_PER_CELL;
  }

  function isNominated(uint256 _idx) public view returns (bool) {
    return nominations[_idx].termNumber > termNumber;
  }

  function getNominatedToken(uint256 _idx) public view returns (uint256) {
    require(isNominated(_idx), "XXX: TODO");

    return nominations[_idx].nominatedToken;
  }

  function getNextMagistrateToken() public view returns (uint256) {
    require(state == StateMachine.GOT_ENTROPY, Errors.MustHaveEntropy);
    require(nominatedTokens.length > 0, Errors.MustHaveNominations);

    uint256 pixelChosen = electionEntropy % nominatedPixels;
    uint256 curPixel = 0;

    for(uint256 i = 0; i < nominatedTokens.length; i++) {
      uint256 idx = nominatedTokens[i];
      curPixel += getAdPixels(idx);
      if (curPixel > pixelChosen) {
        return getNominatedToken(idx);
      }
    }
    return 0;
  }

  // External interface:

  /**
   * @notice Nominate tokens held by the sender as candidates for magistrate in the next term.
   *      Nominations of tokens are independent of their owner.
   * @return Number of nominated pixels.
   *
   * Emits {Nominated} event.
   */
  function nominate(uint256 _ownedTokenId, uint256 _nominateTokenId) external returns (uint256) {
    require(state == StateMachine.NOMINATING, Errors.AlreadyStarted);
    address sender = _msgSender();
    require(ketherNFTContract.ownerOf(_ownedTokenId) == sender, Errors.MustHaveBalance);

    uint256 pixels = _nominate(_ownedTokenId, _nominateTokenId);
    emit Nominated(termNumber+1, sender, pixels);

    return pixels;
  }

  /**
   * @notice Nominate tokens held by the sender as candidates towards a specific `_nominateTokenId` as magistrate in the next term
   * @param _nominateTokenId tokenId to count nominations towards. If doesn't exist, then tokens nominate themselves.
   * @return Number of nominated pixels.
   *
   * Emits {Nominated} event.
   */
  function nominateAll(uint256 _nominateTokenId) public returns (uint256) {
    require(state == StateMachine.NOMINATING, Errors.AlreadyStarted);
    address sender = _msgSender();
    require(ketherNFTContract.balanceOf(sender) > 0, Errors.MustHaveBalance);

    uint256 pixels = 0;
    for (uint256 i = 0; i < ketherNFTContract.balanceOf(sender); i++) {
      uint256 idx = ketherNFTContract.tokenOfOwnerByIndex(sender, i);
      pixels += _nominate(idx, _nominateTokenId);
    }

    nominatedPixels += pixels;

    emit Nominated(termNumber+1, sender, pixels);
    return pixels;
  }

  /**
   * @notice Nominate tokens held by the sender as candidates towards a specific `_nominateTokenId` as magistrate in the next term
   * @return Number of nominated pixels.
   *
   * Emits {Nominated} event.
   */
  function nominateSelf() public returns (uint256) {
    return nominateAll(2 ** 255); // Pass in a tokenId that does not exist.
  }

  /**
   * @notice Stop accepting nominations, start election.
   *
   * Emits {ElectionExecuting} event.
   */
  function startElection() external {
    // FIXME: check that term expired
    require(state == StateMachine.NOMINATING, Errors.AlreadyStarted);
    require(nominatedTokens.length > 0, Errors.MustHaveNominations);
    require(termExpires <= block.timestamp, Errors.TermNotExpired);
    require(LINK.balanceOf(address(this)) >= s_fee, Errors.NotEnoughLink);

    // TODO: check if this is a re-entry vector
    state = StateMachine.WAITING_FOR_ENTROPY;
    requestRandomness(s_keyHash, s_fee);

    emit ElectionExecuting(termNumber);
  }

  /**
   * @notice Assign new magistrate and open up for nominations for next election.
   *
   * Emits {ElectionCompleted} event.
   */
  function completeElection() external {
    require(state == StateMachine.GOT_ENTROPY, Errors.MustHaveEntropy);
    magistrateToken = getNextMagistrateToken();

    // FIXME: This is going to stagger terms, do we want to align them to
    // royalty payouts? Or maybe we could assign payouts to terms on a payment
    // receiver.
    termNumber += 1;
    termStarted = block.timestamp;
    termExpires = termStarted + TERM_DURATION;

    delete nominatedTokens;
    nominatedPixels = 0;
    state = StateMachine.NOMINATING;

    emit ElectionCompleted(termNumber, magistrateToken, getMagistrate());
  }

  // Only magistrate:

  /// @notice Transfer balance controlled by magistrate.
  function withdraw(address payable to) public {
    require(_msgSender() == getMagistrate(), Errors.OnlyMagistrate);
    // NOTE: you have exclusive rights to withdraw until the end of your term.
    // afterwards it becomes a race as someone call call an election.

    // FIXME: Would it be fun if this required having a >2 LINK balance to
    // withdraw? If we wanna be super cute, could automagically buy LINK from
    // the proceeds before transferring the remaining balance.

    to.transfer(address(this).balance);
  }

  /// @notice Cut the term short, leaving enough time for new nominations.
  /// Emits {StepDown} event.
  function stepDown() public {
    require(_msgSender() == getMagistrate(), Errors.OnlyMagistrate);

    uint256 timeRemaining = termExpires - block.timestamp;
    if (timeRemaining > MIN_ELECTION_DURATION) {
      termExpires = block.timestamp + MIN_ELECTION_DURATION;
    }

    emit StepDown(termNumber, magistrateToken, _msgSender());
  }

  // Only owner (admin helpers):

  // TODO: adminUpdateChainlinkValues(bytes32 keyHash, uint256 fee) external onlyOwner

  function adminDelayNextEpoch(uint256 nextEpoch) external onlyOwner {
    // XXX: Implement this.
    // TODO: Should this also allow adjusting the interval?
  }

  /**
   * @notice Withdraw ERC20 tokens, primarily for rescuing remaining LINK once the experiment is over.
   */
  function adminWithdrawToken(IERC20 token, address to) external onlyOwner {
    // XXX: Add require to confirm that the contract is suspended?
    token.transfer(to, token.balanceOf(address(this)));
  }
}

//SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;

import "@openzeppelin/contracts/access/Ownable.sol";

import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";

interface IKetherHomepage {
  function ads(uint _idx) external view returns (address,uint,uint,uint,uint,string memory,string memory,string memory,bool,bool);
  function getAdsLength() view external returns (uint);
}

interface IERC721 {
  function ownerOf(uint256) external view returns (address);
  function balanceOf(address) external view returns (uint256);
  function tokenOfOwnerByIndex(address, uint256) external view returns (uint256);
}

interface IERC20 {
  function balanceOf(address account) external view returns (uint256);
  function transfer(address recipient, uint256 amount) external returns (bool);
}

library Errors {
  string constant MustHaveBalance = "must have tokens to nominate";
  string constant OnlyMagistrate = "only active magistrate can do this";
  string constant MustHaveEntropy = "waiting for entropy";
  string constant MustHaveNominations = "must have nominations";
  string constant AlreadyStarted = "election already started";
  string constant NotExecuted = "election not executed";
  string constant TermNotExpired = "term not expired";
  string constant NotEnoughLink = "not enough LINK";
}

contract KetherSortition is Ownable, VRFConsumerBase {
  event Nominated(
      uint256 indexed termNumber,
      address nominator,
      uint256 pixels
  );

  event ElectionExecuting(
    uint256 indexed termNumber
  );

  event ElectionCompleted(
    uint256 indexed termNumber,
    uint256 magistrateToken,
    address currentTokenOwner
  );

  event StepDown(
    uint256 indexed termNumber,
    uint256 magistrateToken,
    address currentTokenOwner
  );

  struct Nomination{
    uint256 termNumber;
    uint256 nominatedToken;
  }

  // MagistrateToken is tokenId of an NFT whose owner controls the royalties purse for this term.
  uint256 public magistrateToken;

  uint256 constant MIN_ELECTION_DURATION = 2 days;
  uint256 constant TERM_DURATION = 6 weeks;
  uint256 constant PIXELS_PER_CELL = 100;

  uint256 public termStarted;
  uint256 public termExpires;
  uint256 public termNumber = 0;

  IERC721 ketherNFTContract;
  IKetherHomepage ketherContract;

  uint256[] public nominatedTokens;
  uint256 public nominatedPixels = 0;
  mapping(uint256 => Nomination) nominations; // mapping of tokenId => termNumber
  // TODO: mapping(uint256 => uint256) electedCount;

  uint256 public electionEntropy; // Provided by Chainlink

  // nominating -[term expired & startElection() calls]> waitingForEntropy -[Chainlink calls into fulfillrandomness()]> gotEntropy -[completeElection()] -> nominating
  enum StateMachine { NOMINATING, WAITING_FOR_ENTROPY, GOT_ENTROPY }
  StateMachine state = StateMachine.NOMINATING;

  // Chainlink values
  bytes32 private s_keyHash;
  uint256 private s_fee;

  constructor(address _ketherNFTContract, address _ketherContract, address vrfCoordinator, address link, bytes32 keyHash, uint256 fee)
  public
  VRFConsumerBase(vrfCoordinator, link) {
    s_keyHash = keyHash;
    s_fee = fee;

    ketherNFTContract = IERC721(_ketherNFTContract);
    ketherContract = IKetherHomepage(_ketherContract);

    termExpires = block.timestamp + MIN_ELECTION_DURATION;
  }

  // Internal helpers:

  /**
   * @dev Only callable by Chainlink VRF, async triggered via startElection().
   */
  function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
    require(state == StateMachine.WAITING_FOR_ENTROPY, Errors.NotExecuted);
    electionEntropy = randomness;
    state = StateMachine.GOT_ENTROPY;
  }

  // Views:

  function getMagistrate() public view returns (address) {
    return getAdOwner(magistrateToken);
  }

  function getAdOwner(uint256 _idx) public view returns (address) {
    return ketherNFTContract.ownerOf(_idx);
  }

  function getAdPixels(uint256 _idx) public view returns (uint256) {
    (,,,uint width,uint height,,,,,) = ketherContract.ads(_idx);
    return width * height * PIXELS_PER_CELL;
  }

  function isNominated(uint256 _idx) public view returns (bool) {
    return nominations[_idx].termNumber > termNumber;
  }

  function getNominatedToken(uint256 _idx) public view returns (uint256) {
    require(isNominated(_idx), "erorr todo");

    return nominations[_idx].nominatedToken;
  }

  function getNextMagistrateToken() public view returns (uint256) {
    require(state == StateMachine.GOT_ENTROPY, Errors.MustHaveEntropy);
    require(nominatedTokens.length > 0, Errors.MustHaveNominations);

    uint256 pixelChosen = electionEntropy % nominatedPixels;
    uint256 curPixel = 0;

    for(uint256 i = 0; i < nominatedTokens.length; i++) {
      uint256 idx = nominatedTokens[i];
      curPixel += getAdPixels(idx);
      if (curPixel > pixelChosen) {
        return getNominatedToken(idx);
      }
    }
    return 0;
  }

  // External interface:

  // TODO: function nominateAll() external
  // TODO: function nominate(ownedTokenId) external

  /**
   * @dev Nominate tokens held by the sender as candidates for magistrate in the next term.
   *      Nominations of tokens are independent of their owner.
   * @return Number of nominated pixels
   *
   * TODO
   */
  function nominate(uint256 _ownedTokenId, uint256 _nominateTokenId) external returns (uint256) {
    require(state == StateMachine.NOMINATING, Errors.AlreadyStarted);
    address sender = _msgSender();
    require(ketherNFTContract.ownerOf(_ownedTokenId) == sender, Errors.MustHaveBalance);

    uint256 pixels = _nominate(_ownedTokenId, _nominateTokenId);
    emit Nominated(termNumber+1, sender, pixels);

    return pixels;
  }

  /**
   * @dev Nominate tokens held by the sender as candidates for magistrate in the next term
   *      Nominations of tokens are independent of their owner.
   * @return Number of nominated pixels
   * TODO
   */
  function nominateAll(uint256 _nominateTokenId) external returns (uint256) {
    require(state == StateMachine.NOMINATING, Errors.AlreadyStarted);
    address sender = _msgSender();
    require(ketherNFTContract.balanceOf(sender) > 0, Errors.MustHaveBalance);

    uint256 pixels = 0;
    for (uint256 i = 0; i < ketherNFTContract.balanceOf(sender); i++) {
      uint256 idx = ketherNFTContract.tokenOfOwnerByIndex(sender, i);
      pixels += _nominate(idx, _nominateTokenId);
    }

    nominatedPixels += pixels;

    emit Nominated(termNumber+1, sender, pixels);
    return pixels;
  }

  function nominateSelf() external returns (uint256) {
    require(state == StateMachine.NOMINATING, Errors.AlreadyStarted);
    address sender = _msgSender();
    require(ketherNFTContract.balanceOf(sender) > 0, Errors.MustHaveBalance);

    uint256 pixels = 0;
    for (uint256 i = 0; i < ketherNFTContract.balanceOf(sender); i++) {
      uint256 idx = ketherNFTContract.tokenOfOwnerByIndex(sender, i);
      pixels += _nominate(idx, idx);
    }

    nominatedPixels += pixels;

    emit Nominated(termNumber+1, sender, pixels);
    return pixels;
  }

  function _nominate(uint256 _ownedTokenId, uint256 _nominateTokenId) internal returns (uint256 pixels) {
      // Only push the ad and update pixel count if it's not been nominated before
      if (!isNominated(_ownedTokenId)) {
        pixels += getAdPixels(_ownedTokenId);
        nominatedTokens.push(_ownedTokenId);
      }

      nominations[_ownedTokenId] = Nomination(termNumber + 1, _nominateTokenId);

      return pixels;
  }

  // TODO: Do we want nominateOther, so people can easily delegate their nominations?

  // TODO: Do we want to have a unNominateSelf()

  /**
   * @dev Stop accepting nominations, start election.
   */
  function startElection() external {
    // FIXME: check that term expired
    require(state == StateMachine.NOMINATING, Errors.AlreadyStarted);
    require(nominatedTokens.length > 0, Errors.MustHaveNominations);
    require(termExpires <= block.timestamp, Errors.TermNotExpired);
    require(LINK.balanceOf(address(this)) >= s_fee, Errors.NotEnoughLink);

    // TODO: check if this is a re-entry vector
    state = StateMachine.WAITING_FOR_ENTROPY;
    requestRandomness(s_keyHash, s_fee);

    emit ElectionExecuting(termNumber);
  }

  /**
   * @dev Assign new magistrate and open up for nominations for next election.
   */
  function completeElection() external {
    require(state == StateMachine.GOT_ENTROPY, Errors.MustHaveEntropy);
    magistrateToken = getNextMagistrateToken();

    // FIXME: This is going to stagger terms, do we want to align them to
    // royalty payouts? Or maybe we could assign payouts to terms on a payment
    // receiver.
    termNumber += 1;
    termStarted = block.timestamp;
    termExpires = termStarted + TERM_DURATION;

    delete nominatedTokens;
    nominatedPixels = 0;
    state = StateMachine.NOMINATING;

    emit ElectionCompleted(termNumber, magistrateToken, getMagistrate());
  }

  // Only magistrate:

  /// @dev Transfer balance controlled by magistrate.
  function withdraw(address payable to) public {
    require(_msgSender() == getMagistrate(), Errors.OnlyMagistrate);
    // NOTE: you have exclusive rights to withdraw until the end of your term.
    // afterwards it becomes a race as someone call call an election.

    // FIXME: Would it be fun if this required having a >2 LINK balance to
    // withdraw? If we wanna be super cute, could automagically buy LINK from
    // the proceeds before transferring the remaining balance.

    to.transfer(address(this).balance);
  }

  /// @dev Cut the term short, leaving enough time for new nominations.
  function stepDown() public {
    require(_msgSender() == getMagistrate(), Errors.OnlyMagistrate);

    uint256 timeRemaining = termExpires - block.timestamp;
    if (timeRemaining > MIN_ELECTION_DURATION) {
      termExpires = block.timestamp + MIN_ELECTION_DURATION;
    }

    emit StepDown(termNumber, magistrateToken, _msgSender());
  }

  // Only owner (admin helpers):

  // TODO: adminUpdateChainlinkValues(bytes32 keyHash, uint256 fee) external onlyOwner

  function adminDelayNextEpoch(uint256 nextEpoch) external onlyOwner {
    // XXX: Implement this.
    // TODO: Should this also allow adjusting the interval?
  }

  /**
   * @dev Withdraw ERC20 tokens, primarily for rescuing remaining LINK once the
   *      experiment is over.
   */
  function adminWithdrawToken(IERC20 token, address to) external onlyOwner {
    // XXX: Add require to confirm that the contract is suspended?
    token.transfer(to, token.balanceOf(address(this)));
  }
}