contract COE is MintableToken, BurnableToken, RBAC {
  using SafeMath for uint256;

  string public constant name = "Coin Market Cap Coeval";
  string public constant symbol = "COE";
  uint8 public constant decimals = 18;

  // this variable represents the total amount of eth raised
  uint public ethRaised;

  string public constant ROLE_WHITELISTED = "whitelist";
  string public constant ROLE_ADMIN = "admin";
  string public constant ROLE_SUPER = "super";

  uint public swapLimit;
  uint public constant CYCLE_CAP = 100000 * (10 ** uint256(decimals));
  uint public constant BILLION = 10 ** 9;

  event SwapStarted(uint256 startTime);
  event MiningRestart(uint256 endTime);
  event CMCUpdate(string updateType, uint value);

  uint offset = 10**18;
  // these rates are offset. divide by 1e18 to get actual rate.
  uint public exchangeRateMNY;
  uint public exchangeRateFUTX;

  //initial state
  uint public cycleMintSupply = 0;
  bool public isMiningOpen = false;
  uint public CMC = 236346228108;
  uint public cycleEndTime;

  address public constant ZUR = 0x8218a33eB15901Ce71b3B8123E58b7E312ce638A;
  address public constant MNY = 0xD2354AcF1a2f06D69D8BC2e2048AaBD404445DF6;
  address public constant FUTX = 0x8b7d07b6ffB9364e97B89cEA8b84F94249bE459F;

  constructor() public {
    //only the contract itself can mint as the owner
    owner = this;
    totalSupply_ = 0;
    addRole(msg.sender, ROLE_ADMIN);
    addRole(msg.sender, ROLE_SUPER);
    addRole(msg.sender, ROLE_WHITELISTED);

    // initial market data to set rates.
    exchangeRateMNY = offset.mul(offset).div(CMC.mul(offset).div(BILLION)).mul(65).div(100);
    exchangeRateFUTX = offset.mul(offset).div(uint(29997535964).mul(offset).div(uint(123943034521)).mul(CMC).div(BILLION)).mul(65).div(100);
  }

  function () external payable {
    buyTokens(msg.sender);
  }

  function donateEth() external payable {
    //thanks! this will add to the swap back amount.
    ethRaised += msg.value;
  }

  uint public presaleFee = 0;
  uint8 public presaleLevel = 1;
  uint public coePerEthOffset = offset.div(presaleLevel).mul(650);
  bool public presaleOpen = false;
  uint public ethRateExpiration;
  uint public coeRemainingAtCurrentRate = 1000 ether;

  function startPresale() onlyAdmin public {
    require(!presaleOpen && presaleLevel == 1);
    ethRateExpiration = now + 1 days;
    presaleOpen = true;
  }

  function buyTokens(address _beneficiary) public payable {
    require(presaleOpen);
    require(msg.value > 0);
    ethRaised += msg.value;
    uint buyingPower = msg.value;
    presaleFee += buyingPower.mul(65).div(100);
    uint tokens = 0;
    uint zurFeed = 0;

    if (now > ethRateExpiration) {
      incrementLevel(buyingPower);
    }

    while(buyingPower > 0) {
      uint ethToFillLevel = coeRemainingAtCurrentRate.mul(65).div(100).mul(offset).div(coePerEthOffset);
      if (buyingPower >= ethToFillLevel) {
        buyingPower -= ethToFillLevel;
        tokens += coeRemainingAtCurrentRate.mul(65).div(100);
        zurFeed += coeRemainingAtCurrentRate.mul(35).div(100);
        coeRemainingAtCurrentRate = 0;
      } else {
        tokens += buyingPower.mul(coePerEthOffset).div(offset);
        zurFeed += buyingPower.mul(coePerEthOffset).div(offset).mul(35).div(65);
        coeRemainingAtCurrentRate = coeRemainingAtCurrentRate.sub(buyingPower.mul(coePerEthOffset).mul(100).div(65).div(offset));
        buyingPower = 0;
      }

      if (coeRemainingAtCurrentRate == 0) {
        incrementLevel(buyingPower);

        if (!presaleOpen) {
          // round any leftover dust to last buyer
          tokens += (CYCLE_CAP - cycleMintSupply - tokens - zurFeed);
          break;
        }
      }
    }

    cycleMintSupply += (tokens + zurFeed);
    if (!presaleOpen) {
      // start swap
      _startSwap();
    }
    MintableToken(this).mint(_beneficiary, tokens);
    MintableToken(this).mint(ZUR, zurFeed);
  }

  function incrementLevel(uint buyingPower) private {
    if (presaleLevel == 100) {
      if (buyingPower > 0) {
        // refund extra eth
        presaleFee -= buyingPower.mul(65).div(100);
        ethRaised -= buyingPower;
        msg.sender.transfer(buyingPower);
      }
      presaleOpen = false;
    } else {
      presaleLevel++;
      coeRemainingAtCurrentRate += 1000 ether;
      coePerEthOffset = offset.div(presaleLevel).mul(650);
      ethRateExpiration = now + 1 days;
    }
  }

  modifier canMine() {
    require(isMiningOpen);
    _;
  }

  // first call (mny address).approve(coe address, amount) for COE to transfer on your behalf.
  function mine(uint amount) canMine public {
    require(amount > 0);
    require(cycleMintSupply < CYCLE_CAP);
    require(ERC20(MNY).transferFrom(msg.sender, address(this), amount));

    uint refund = _mine(exchangeRateMNY, amount);
    if(refund > 0) {
      ERC20(MNY).transfer(msg.sender, refund);
    }
    if (cycleMintSupply == CYCLE_CAP) {
      //start swap
      _startSwap();
    }
  }

  // first call (futx address).approve(coe address, amount) for COE to transfer on your behalf.
  function whitelistMine(uint amount) canMine onlyIfWhitelisted public {
    require(amount > 0);
    require(cycleMintSupply < CYCLE_CAP);
    require(ERC20(FUTX).transferFrom(msg.sender, address(this), amount));

    uint refund = _mine(exchangeRateFUTX, amount);
    if(refund > 0) {
      ERC20(FUTX).transfer(msg.sender, refund);
    }
    if (cycleMintSupply == CYCLE_CAP) {
      //start swap
      _startSwap();
    }
  }

  function _mine(uint _rate, uint _inAmount) private returns (uint) {
    assert(_rate > 0);

    // took too long; return tokens and start swap.
    if (now > cycleEndTime && cycleMintSupply > 0) {
      _startSwap();
      return _inAmount;
    }
    uint tokens = _rate.mul(_inAmount).div(offset);
    uint refund = 0;

    // for every 65 tokens mined, we mine 35 for the zur contract.
    uint zurFeed = tokens.mul(35).div(65);

    if (tokens + zurFeed + cycleMintSupply > CYCLE_CAP) {
      uint overage = tokens + zurFeed + cycleMintSupply - CYCLE_CAP;
      uint tokenOverage = overage.mul(65).div(100);
      zurFeed -= (overage - tokenOverage);
      tokens -= tokenOverage;

      // refund token overage
      refund = tokenOverage.mul(offset).div(_rate);
    }
    cycleMintSupply += (tokens + zurFeed);
    require(zurFeed > 0, "Mining payment too small.");
    MintableToken(this).mint(msg.sender, tokens);
    MintableToken(this).mint(ZUR, zurFeed);

    return refund;
  }

  // swap data
  bool public swapOpen = false;
  uint public ethSwapRate;
  mapping(address => uint) public swapRates;

  function _startSwap() private {
    swapOpen = true;
    isMiningOpen = false;

    // set swap rates
    // 35% of holdings split among a number equal to 35% of newly minted coe
    swapLimit = cycleMintSupply.mul(35).div(100);
    ethSwapRate = (address(this).balance.sub(presaleFee)).mul(offset).mul(35).div(100).div(swapLimit);
    swapRates[FUTX] = ERC20(FUTX).balanceOf(address(this)).mul(offset).mul(35).div(100).div(swapLimit);
    swapRates[MNY] = ERC20(MNY).balanceOf(address(this)).mul(offset).mul(35).div(100).div(swapLimit);

    emit SwapStarted(now);
  }

  function swap(uint amt) public {
    require(swapOpen && swapLimit > 0);
    if (amt > swapLimit) {
      amt = swapLimit;
    }
    swapLimit -= amt;
    // burn verifies msg.sender has balance
    burn(amt);

    if (amt.mul(ethSwapRate) > 0) {
      msg.sender.transfer(amt.mul(ethSwapRate).div(offset));
    }

    if (amt.mul(swapRates[FUTX]) > 0) {
      ERC20(FUTX).transfer(msg.sender, amt.mul(swapRates[FUTX]).div(offset));
    }

    if (amt.mul(swapRates[MNY]) > 0) {
      ERC20(MNY).transfer(msg.sender, amt.mul(swapRates[MNY]).div(offset));
    }

    if (swapLimit == 0) {
      _restart();
    }
  }

  function _restart() private {
    require(swapOpen);
    require(swapLimit == 0);

    cycleMintSupply = 0;
    swapOpen = false;
    isMiningOpen = true;
    cycleEndTime = now + 100 days;

    emit MiningRestart(cycleEndTime);
  }

  function updateCMC(uint _cmc) private {
    require(_cmc > 0);
    CMC = _cmc;
    emit CMCUpdate("TOTAL_CMC", _cmc);
    exchangeRateMNY = offset.mul(offset).div(CMC.mul(offset).div(BILLION)).mul(65).div(100);
  }

  function updateCMC(uint _cmc, uint _btc, uint _eth) public onlyAdmin{
    require(_btc > 0 && _eth > 0);
    updateCMC(_cmc);
    emit CMCUpdate("BTC_CMC", _btc);
    emit CMCUpdate("ETH_CMC", _eth);
    exchangeRateFUTX = offset.mul(offset).div(_eth.mul(offset).div(_btc).mul(CMC).div(BILLION)).mul(65).div(100);
  }

  modifier onlyIfWhitelisted() {
    checkRole(msg.sender, ROLE_WHITELISTED);
    _;
  }

  modifier onlySuper() {
    checkRole(msg.sender, ROLE_SUPER);
    _;
  }

  modifier onlyAdmin() {
    checkRole(msg.sender, ROLE_ADMIN);
    _;
  }

  function addAdmin(address _addr) public onlySuper {
    addRole(_addr, ROLE_ADMIN);
  }

  function removeAdmin(address _addr) public onlySuper {
    removeRole(_addr, ROLE_ADMIN);
  }

  function changeSuper(address _addr) public onlySuper {
    addRole(_addr, ROLE_SUPER);
    removeRole(msg.sender, ROLE_SUPER);
  }

  function addAddressToWhitelist(address _operator)
    public
    onlySuper
  {
    addRole(_operator, ROLE_WHITELISTED);
  }

  function whitelist(address _operator)
    public
    view
    returns (bool)
  {
    return hasRole(_operator, ROLE_WHITELISTED);
  }

  function addAddressesToWhitelist(address[] _operators)
    public
    onlySuper
  {
    for (uint256 i = 0; i < _operators.length; i++) {
      addAddressToWhitelist(_operators[i]);
    }
  }

  function removeAddressFromWhitelist(address _operator)
    public
    onlySuper
  {
    removeRole(_operator, ROLE_WHITELISTED);
  }

  function removeAddressesFromWhitelist(address[] _operators)
    public
    onlySuper
  {
    for (uint256 i = 0; i < _operators.length; i++) {
      removeAddressFromWhitelist(_operators[i]);
    }
  }

  function payFees() public {
    require(presaleFee > 0);
    uint feeShare = presaleFee.div(13);
    if (feeShare > 0) {
      address(0x17F619855432168f2aB5A1B2133888d9ffCC3946).transfer(feeShare);
      address(0xAaf47A27BBd9B82ee0f1f77C7b437A36160c4242).transfer(feeShare * 4);
      address(0x6c18DCCDfFd4874Cb88b403637045f12f5a227e3).transfer(feeShare * 3);
      address(0x5d2b9f5345e69E2390cE4C26ccc9C2910A097520).transfer(feeShare * 2);
      address(0xcf5Ee528278a57Ba087684f685D99A6a5EC4c439).transfer(feeShare * 3);
    }
    presaleFee = 0;
  }
}
function whitelistMine(uint amount) canMine onlyIfWhitelisted public {
  require(amount > 0);
  require(cycleMintSupply < CYCLE_CAP);
  require(ERC20(FUTX).transferFrom(msg.sender, address(this), amount));

  uint refund = _mine(exchangeRateFUTX, amount);
  if(refund > 0) {
    ERC20(FUTX).transfer(msg.sender, refund);
  }
  if (cycleMintSupply == CYCLE_CAP) {
    //start swap
    _startSwap();
  }
}

// first call (futx address).approve(coe address, amount) for COE to transfer on your behalf.