contract Fantastic12 {
  using SafeMath for uint256;
  using SafeERC20 for IERC20;
  using StandardBountiesWrapper for address;

  // Constants
  uint8   public constant MAX_MEMBERS = 12;
  string  public constant VERSION = "0.1.4";
  uint256 internal constant PRECISION = 10 ** 18;

  // Instance variables
  mapping(address => bool) public isMember;
  mapping(address => mapping(uint256 => bool)) public hasUsedSalt; // Stores whether a salt has been used for a member
  uint8 public memberCount;
  IERC20 public DAI;
  address payable[] public issuersOrFulfillers;
  address[] public approvers;
  uint256 public withdrawLimit;
  uint256 public withdrawnToday;
  uint256 public lastWithdrawTimestamp;
  uint256 public consensusThresholdPercentage; // at least (consensusThresholdPercentage * memberCount / PRECISION) approvers are needed to execute an action
  bool public initialized;

  // Modifiers
  modifier onlyMember {
    require(isMember[msg.sender], "Not member");
    _;
  }

  modifier withConsensus(
    bytes4           _funcSelector,
    bytes     memory _funcParams,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  ) {
    require(
      _consensusReached(
        consensusThreshold(),
        _funcSelector,
        _funcParams,
        _members,
        _signatures,
        _salts
      ), "No consensus");
    _;
  }

  modifier withUnanimity(
    bytes4           _funcSelector,
    bytes     memory _funcParams,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  ) {
    require(
      _consensusReached(
        memberCount,
        _funcSelector,
        _funcParams,
        _members,
        _signatures,
        _salts
      ), "No unanimity");
    _;
  }

  // Events
  event Shout(string message);
  event Declare(string message);
  event AddMember(address newMember, uint256 tribute);
  event RageQuit(address quitter);
  event PostBounty(uint256 bountyID, uint256 reward);
  event AddBountyReward(uint256 bountyID, uint256 reward);
  event RefundBountyReward(uint256 bountyID, uint256 refundAmount);
  event ChangeBountyData(uint256 bountyID);
  event ChangeBountyDeadline(uint256 bountyID);
  event AcceptBountySubmission(uint256 bountyID, uint256 fulfillmentID);
  event PerformBountyAction(uint256 bountyID);
  event FulfillBounty(uint256 bountyID);
  event UpdateBountyFulfillment(uint256 bountyID, uint256 fulfillmentID);

  // Initializer
  function init(
    address _summoner,
    address _DAI_ADDR,
    uint256 _withdrawLimit,
    uint256 _consensusThresholdPercentage
  ) public {
    require(! initialized, "Initialized");
    require(_consensusThresholdPercentage <= PRECISION, "Consensus threshold > 1");
    initialized = true;
    memberCount = 1;
    DAI = IERC20(_DAI_ADDR);
    issuersOrFulfillers = new address payable[](1);
    issuersOrFulfillers[0] = address(this);
    approvers = new address[](1);
    approvers[0] = address(this);
    withdrawLimit = _withdrawLimit;
    consensusThresholdPercentage = _consensusThresholdPercentage;

    // Add `_summoner` as the first member
    isMember[_summoner] = true;
  }

  // Functions

  /**
    Censorship-resistant messaging
   */
  function shout(string memory _message) public onlyMember {
    emit Shout(_message);
  }

  function declare(
    string memory _message,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.declare.selector,
      abi.encode(_message),
      _members,
      _signatures,
      _salts
    )
  {
    emit Declare(_message);
  }

  /**
    Member management
   */

  function addMembers(
    address[] memory _newMembers,
    uint256[] memory _tributes,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.addMembers.selector,
      abi.encode(_newMembers, _tributes),
      _members,
      _signatures,
      _salts
    )
  {
    require(_newMembers.length == _tributes.length, "_newMembers not same length as _tributes");
    for (uint256 i = 0; i < _newMembers.length; i = i.add(1)) {
      _addMember(_newMembers[i], _tributes[i]);
    }
  }

  function _addMember(
    address _newMember,
    uint256 _tribute
  )
    internal
  {
    require(_newMember != address(0), "Member cannot be zero address");
    require(!isMember[_newMember], "Member cannot be added twice");
    require(memberCount < MAX_MEMBERS, "Max member count reached");

    // Receive tribute from `_newMember`
    require(DAI.transferFrom(_newMember, address(this), _tribute), "Tribute transfer failed");

    // Add `_newMember` to squad
    isMember[_newMember] = true;
    memberCount += 1;

    emit AddMember(_newMember, _tribute);
  }

  function rageQuit() public onlyMember {
    // Give `msg.sender` their portion of the squad funds
    uint256 withdrawAmount;
    withdrawAmount = DAI.balanceOf(address(this)).div(memberCount);
    DAI.safeTransfer(msg.sender, withdrawAmount);

    // Remove `msg.sender` from squad
    isMember[msg.sender] = false;
    memberCount -= 1;
    emit RageQuit(msg.sender);
  }

  function rageQuitWithTokens(address[] memory _tokens) public onlyMember {
    // Give `msg.sender` their portion of the squad funds
    uint256 withdrawAmount;
    for (uint256 i = 0; i < _tokens.length; i = i.add(1)) {
      if (_tokens[i] == address(0)) {
        withdrawAmount = address(this).balance.div(memberCount);
        msg.sender.transfer(withdrawAmount);
      } else {
        IERC20 token = IERC20(_tokens[i]);
        withdrawAmount = token.balanceOf(address(this)).div(memberCount);
        token.safeTransfer(msg.sender, withdrawAmount);
      }
    }

    // Remove `msg.sender` from squad
    isMember[msg.sender] = false;
    memberCount -= 1;
    emit RageQuit(msg.sender);
  }

  /**
    Fund management
   */

  function transferDAI(
    address[] memory _dests,
    uint256[] memory _amounts,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.transferDAI.selector,
      abi.encode(_dests, _amounts),
      _members,
      _signatures,
      _salts
    )
  {
    require(_dests.length == _amounts.length, "_dests not same length as _amounts");
    for (uint256 i = 0; i < _dests.length; i = i.add(1)) {
      _transferDAI(_dests[i], _amounts[i]);
    }
  }

  function transferTokens(
    address payable[] memory _dests,
    uint256[] memory _amounts,
    address[] memory _tokens,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.transferTokens.selector,
      abi.encode(_dests, _amounts, _tokens),
      _members,
      _signatures,
      _salts
    )
  {
    require(_dests.length == _amounts.length && _dests.length == _tokens.length, "_dests, _amounts, _tokens not of same length");
    for (uint256 i = 0; i < _dests.length; i = i.add(1)) {
      if (_tokens[i] == address(0)) {
        _dests[i].transfer(_amounts[i]);
      } else if (_tokens[i] == address(DAI)) {
        _transferDAI(_dests[i], _amounts[i]);
      } else {
        IERC20 token = IERC20(_tokens[i]);
        token.safeTransfer(_dests[i], _amounts[i]);
      }
    }
  }
  
  function setWithdrawLimit(
    uint256 _newLimit,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withUnanimity(
      this.setWithdrawLimit.selector,
      abi.encode(_newLimit),
      _members,
      _signatures,
      _salts
    )
  {
    withdrawLimit = _newLimit;
  }

  function setConsensusThreshold(
    uint256 _newThresholdPercentage,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withUnanimity(
      this.setConsensusThreshold.selector,
      abi.encode(_newThresholdPercentage),
      _members,
      _signatures,
      _salts
    )
  {
    consensusThresholdPercentage = _newThresholdPercentage;
  }

  /**
    Posting bounties
   */

  function postBounty(
    string memory _dataIPFSHash,
    uint256 _deadline,
    uint256 _reward,
    address _standardBounties,
    uint256 _standardBountiesVersion,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.postBounty.selector,
      abi.encode(_dataIPFSHash, _deadline, _reward, _standardBounties, _standardBountiesVersion),
      _members,
      _signatures,
      _salts
    )
    returns (uint256 _bountyID)
  {
    return _postBounty(_dataIPFSHash, _deadline, _reward, _standardBounties, _standardBountiesVersion);
  }

  function _postBounty(
    string memory _dataIPFSHash,
    uint256 _deadline,
    uint256 _reward,
    address _standardBounties,
    uint256 _standardBountiesVersion
  )
    internal
    returns (uint256 _bountyID)
  {
    require(_reward > 0, "Reward can't be 0");

    // Approve DAI reward to bounties contract
    _approveDAI(_standardBounties, _reward);

    _bountyID = _standardBounties.issueAndContribute(
      _standardBountiesVersion,
      address(this),
      issuersOrFulfillers,
      approvers,
      _dataIPFSHash,
      _deadline,
      address(DAI),
      _reward
    );

    emit PostBounty(_bountyID, _reward);
  }

  function addBountyReward(
    uint256 _bountyID,
    uint256 _reward,
    address _standardBounties,
    uint256 _standardBountiesVersion,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.addBountyReward.selector,
      abi.encode(_bountyID, _reward, _standardBounties, _standardBountiesVersion),
      _members,
      _signatures,
      _salts
    )
  {
    // Approve DAI reward to bounties contract
    _approveDAI(_standardBounties, _reward);

    _standardBounties.contribute(
      _standardBountiesVersion,
      address(this),
      _bountyID,
      _reward
    );

    emit AddBountyReward(_bountyID, _reward);
  }

  function refundBountyReward(
    uint256 _bountyID,
    uint256[] memory _contributionIDs,
    address _standardBounties,
    uint256 _standardBountiesVersion,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.refundBountyReward.selector,
      abi.encode(_bountyID, _contributionIDs, _standardBounties, _standardBountiesVersion),
      _members,
      _signatures,
      _salts
    )
  {
    uint256 beforeBalance = DAI.balanceOf(address(this));
    _standardBounties.refundMyContributions(
      _standardBountiesVersion,
      address(this),
      _bountyID,
      _contributionIDs
    );

    emit RefundBountyReward(_bountyID, DAI.balanceOf(address(this)).sub(beforeBalance));
  }

  function changeBountyData(
    uint256 _bountyID,
    string memory _dataIPFSHash,
    address _standardBounties,
    uint256 _standardBountiesVersion,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.changeBountyData.selector,
      abi.encode(_bountyID, _dataIPFSHash, _standardBounties, _standardBountiesVersion),
      _members,
      _signatures,
      _salts
    )
  {
    _standardBounties.changeData(
      _standardBountiesVersion,
      address(this),
      _bountyID,
      _dataIPFSHash
    );
    emit ChangeBountyData(_bountyID);
  }

  function changeBountyDeadline(
    uint256 _bountyID,
    uint256 _deadline,
    address _standardBounties,
    uint256 _standardBountiesVersion,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.changeBountyDeadline.selector,
      abi.encode(_bountyID, _deadline, _standardBounties, _standardBountiesVersion),
      _members,
      _signatures,
      _salts
    )
  {
    _standardBounties.changeDeadline(
      _standardBountiesVersion,
      address(this),
      _bountyID,
      _deadline
    );
    emit ChangeBountyDeadline(_bountyID);
  }

  function acceptBountySubmission(
    uint256 _bountyID,
    uint256 _fulfillmentID,
    uint256[] memory _tokenAmounts,
    address _standardBounties,
    uint256 _standardBountiesVersion,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.acceptBountySubmission.selector,
      abi.encode(_bountyID, _fulfillmentID, _tokenAmounts, _standardBounties, _standardBountiesVersion),
      _members,
      _signatures,
      _salts
    )
  {
    _standardBounties.acceptFulfillment(
      _standardBountiesVersion,
      address(this),
      _bountyID,
      _fulfillmentID,
      _tokenAmounts
    );
    emit AcceptBountySubmission(_bountyID, _fulfillmentID);
  }

  /**
    Working on bounties
   */

  function performBountyAction(
    uint256 _bountyID,
    string memory _dataIPFSHash,
    address _standardBounties,
    uint256 _standardBountiesVersion,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.performBountyAction.selector,
      abi.encode(_bountyID, _dataIPFSHash, _standardBounties, _standardBountiesVersion),
      _members,
      _signatures,
      _salts
    )
  {
    _standardBounties.performAction(
      _standardBountiesVersion,
      address(this),
      _bountyID,
      _dataIPFSHash
    );
    emit PerformBountyAction(_bountyID);
  }

  function fulfillBounty(
    uint256 _bountyID,
    string memory _dataIPFSHash,
    address _standardBounties,
    uint256 _standardBountiesVersion,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.fulfillBounty.selector,
      abi.encode(_bountyID, _dataIPFSHash, _standardBounties, _standardBountiesVersion),
      _members,
      _signatures,
      _salts
    )
  {
    _standardBounties.fulfillBounty(
      _standardBountiesVersion,
      address(this),
      _bountyID,
      issuersOrFulfillers,
      _dataIPFSHash
    );
    emit FulfillBounty(_bountyID);
  }

  function updateBountyFulfillment(
    uint256 _bountyID,
    uint256 _fulfillmentID,
    string memory _dataIPFSHash,
    address _standardBounties,
    uint256 _standardBountiesVersion,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  )
    public
    withConsensus(
      this.updateBountyFulfillment.selector,
      abi.encode(_bountyID, _fulfillmentID, _dataIPFSHash, _standardBounties, _standardBountiesVersion),
      _members,
      _signatures,
      _salts
    )
  {
    _updateBountyFulfillment(_bountyID, _fulfillmentID, _dataIPFSHash, _standardBounties, _standardBountiesVersion);
  }

  function _updateBountyFulfillment(
    uint256 _bountyID,
    uint256 _fulfillmentID,
    string memory _dataIPFSHash,
    address _standardBounties,
    uint256 _standardBountiesVersion
  )
    internal
  {
    _standardBounties.updateFulfillment(
      _standardBountiesVersion,
      address(this),
      _bountyID,
      _fulfillmentID,
      issuersOrFulfillers,
      _dataIPFSHash
    );
    emit UpdateBountyFulfillment(_bountyID, _fulfillmentID);
  }

  /**
    Consensus
   */

  function naiveMessageHash(
    bytes4       _funcSelector,
    bytes memory _funcParams,
    uint256      _salt
  ) public view returns (bytes32) {
    // "|END|" is used to separate _funcParams from the rest, to prevent maliciously ambiguous signatures
    return keccak256(abi.encodeWithSelector(_funcSelector, _funcParams, "|END|", _salt, address(this)));
  }

  function consensusThreshold() public view returns (uint8) {
    uint8 blockingThresholdMemberCount = uint8(PRECISION.sub(consensusThresholdPercentage).mul(memberCount).div(PRECISION));
    return memberCount - blockingThresholdMemberCount;
  }

  function _consensusReached(
    uint256          _threshold,
    bytes4           _funcSelector,
    bytes     memory _funcParams,
    address[] memory _members,
    bytes[]   memory _signatures,
    uint256[] memory _salts
  ) internal returns (bool) {
    // Check if the number of signatures exceed the consensus threshold
    if (_members.length != _signatures.length || _members.length < _threshold) {
      return false;
    }
    // Check if each signature is valid and signed by a member
    for (uint256 i = 0; i < _members.length; i = i.add(1)) {
      address member = _members[i];
      uint256 salt = _salts[i];
      if (!isMember[member] || hasUsedSalt[member][salt]) {
        // Invalid member or salt already used
        return false;
      }

      bytes32 msgHash = ECDSA.toEthSignedMessageHash(naiveMessageHash(_funcSelector, _funcParams, salt));
      address recoveredAddress = ECDSA.recover(msgHash, _signatures[i]);
      if (recoveredAddress != member) {
        // Invalid signature
        return false;
      }

      // Signature valid, record use of salt
      hasUsedSalt[member][salt] = true;
    }

    return true;
  }

  function _timestampToDayID(uint256 _timestamp) internal pure returns (uint256) {
    return _timestamp.sub(_timestamp % 86400).div(86400);
  }

  // limits how much could be withdrawn each day, should be called before transfer() or approve()
  function _applyWithdrawLimit(uint256 _amount) internal {
    // check if the limit will be exceeded
    if (_timestampToDayID(now).sub(_timestampToDayID(lastWithdrawTimestamp)) >= 1) {
      // new day, don't care about existing limit
      withdrawnToday = 0;
    }
    uint256 newWithdrawnToday = withdrawnToday.add(_amount);
    require(newWithdrawnToday <= withdrawLimit, "Withdraw limit exceeded");
    withdrawnToday = newWithdrawnToday;
    lastWithdrawTimestamp = now;
  }

  function _transferDAI(address _to, uint256 _amount) internal {
    _applyWithdrawLimit(_amount);
    require(DAI.transfer(_to, _amount), "Failed DAI transfer");
  }

  function _approveDAI(address _to, uint256 _amount) internal {
    _applyWithdrawLimit(_amount);
    require(DAI.approve(_to, 0), "Failed to clear DAI approval");
    require(DAI.approve(_to, _amount), "Failed to approve DAI");
  }

  function() external payable {}
}
function transferDAI(
  address[] memory _dests,
  uint256[] memory _amounts,
  address[] memory _members,
  bytes[]   memory _signatures,
  uint256[] memory _salts
)
  public
  withConsensus(
    this.transferDAI.selector,
    abi.encode(_dests, _amounts),
    _members,
    _signatures,
    _salts
  )
{
  require(_dests.length == _amounts.length, "_dests not same length as _amounts");
  for (uint256 i = 0; i < _dests.length; i = i.add(1)) {
    _transferDAI(_dests[i], _amounts[i]);
  }
}

/**
 Fund management
 */