pragma solidity ^0.4.24;

contract Token {
    bytes32 public standard;
    bytes32 public name;
    bytes32 public symbol;
    uint256 public totalSupply;
    uint8 public decimals;
    bool public allowTransactions;
    mapping (address => uint256) public balanceOf;
    mapping (address => mapping (address => uint256)) public allowance;
    function transfer(address _to, uint256 _value) public returns (bool success);
    function approveAndCall(address _spender, uint256 _value, bytes _extraData) public returns (bool success);
    function approve(address _spender, uint256 _value) public returns (bool success);
    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success);
}

contract NescrowExchangeService {

    address owner = msg.sender;

    uint256 public feeRateMin = 200;
    uint256 public takerFeeRate = 0;
    uint256 public makerFeeRate = 0;
    address public feeAddress;

    mapping (address => bool) public admins;
    mapping (bytes32 => bool) public traded;
    mapping (bytes32 => uint256) public orderFills;
    mapping (bytes32 => bool) public withdrawn;
    mapping (bytes32 => bool) public transfers;
    mapping (address => mapping (address => uint256)) public balances;
    mapping (address => uint256) public tradesLocked;
    mapping (address => uint256) public disableFees;
    mapping (address => uint256) public tokenDecimals;
    mapping (address => bool) public tokenRegistered;

    event Deposit(address token, address user, uint256 amount, uint256 balance);
    event Withdraw(address token, address user, uint256 amount, uint256 balance);
    event TradesLock(address user);
    event TradesUnlock(address user);

    modifier onlyOwner {
        assert(msg.sender == owner);
        _;
    }

    modifier onlyAdmin {
        require(msg.sender == owner || admins[msg.sender]);
        _;
    }

    function setOwner(address newOwner) public onlyOwner {
        owner = newOwner;
    }

    function getOwner() public view returns (address out) {
        return owner;
    }

    function setAdmin(address admin, bool isAdmin) public onlyOwner {
        admins[admin] = isAdmin;
    }

    function deposit() public payable {
        uint amount = safeDiv(msg.value, 10**10);
        require(msg.value > 0);
        increaseBalance(msg.sender, address(0), amount);
        emit Deposit(address(0), msg.sender, amount, balances[address(0)][msg.sender]);
    }

    function depositToken(address token, uint256 amount) public {
        require(amount > 0);
        require(Token(token).transferFrom(msg.sender, this, toTokenAmount(token, amount)));
        increaseBalance(msg.sender, token, amount);
        emit Deposit(token, msg.sender, amount, balances[token][msg.sender]);
    }

    function sendTips() public payable {
        uint amount = safeDiv(msg.value, 10**10);
        require(msg.value > 0);
        increaseBalance(feeAddress, address(0), amount);
    }

    function sendTipsToken(address token, uint256 amount) public {
        require(amount > 0);
        require(Token(token).transferFrom(msg.sender, this, toTokenAmount(token, amount)));
        increaseBalance(feeAddress, token, amount);
    }

    function transferTips(address token, uint256 amount, address fromUser, uint nonce, uint8 v, bytes32 r, bytes32 s)
        public onlyAdmin {

        require(amount > 0);

        bytes32 hash = keccak256(abi.encodePacked(this, token, amount, fromUser, nonce));
        require(!transfers[hash]);
        transfers[hash] = true;

        address signer = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)), v, r, s);
        require(fromUser == signer);

        require(reduceBalance(fromUser, token, amount));
        increaseBalance(feeAddress, token, amount);
    }

    function transfer(address token, uint256 amount, address fromUser, address toUser, uint nonce, uint8 v, bytes32 r, bytes32 s)
        public onlyAdmin {

        require(amount > 0);

        bytes32 hash = keccak256(abi.encodePacked(this, token, amount, fromUser, toUser, nonce));
        require(!transfers[hash]);
        transfers[hash] = true;

        address signer = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)), v, r, s);
        require(fromUser == signer);

        require(reduceBalance(fromUser, token, amount));
        increaseBalance(toUser, token, amount);
    }

    function withdrawAdmin(address token, uint256 amount, address user, uint nonce, uint8 v, bytes32 r, bytes32 s)
        public onlyAdmin {

        require(amount > 0);

        bytes32 hash = keccak256(abi.encodePacked(this, token, amount, user, nonce));
        require(!withdrawn[hash]);
        withdrawn[hash] = true;

        address signer = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)), v, r, s);
        require(user == signer);

        require(reduceBalance(user, token, amount));
        if (token == address(0)) {
            require(user.send(toTokenAmount(address(0), amount)));
        } else {
            require(Token(token).transfer(user, toTokenAmount(token, amount)));
        }
        emit Withdraw(token, user, amount, balances[token][user]);
    }

    function withdraw(address token, uint256 amount) public {

        require(amount > 0);
        require(tradesLocked[msg.sender] > block.number);
        require(reduceBalance(msg.sender, token, amount));

        if (token == address(0)) {
            require(msg.sender.send(toTokenAmount(address(0), amount)));
        } else {
            require(Token(token).transfer(msg.sender, toTokenAmount(token, amount)));
        }
        emit Withdraw(token, msg.sender, amount, balances[token][msg.sender]);
    }

    function reduceBalance(address user, address token, uint256 amount) private returns(bool) {
        if (balances[token][user] < amount) return false;
        balances[token][user] = safeSub(balances[token][user], amount);
        return true;
    }

    function increaseBalance(address user, address token, uint256 amount) private returns(bool) {
        balances[token][user] = safeAdd(balances[token][user], amount);
        return true;
    }

    function toTokenAmount(address token, uint256 amount) private view returns (uint256) {

        require(tokenRegistered[token]);
        uint256 decimals = token == address(0)
            ? 18
            : tokenDecimals[token];

        if (decimals == 8) {
            return amount;
        }

        if (decimals > 8) {
            return safeMul(amount, 10**(decimals - 8));
        } else {
            return safeDiv(amount, 10**(8 - decimals));
        }
    }

    function setTakerFeeRate(uint256 feeRate) public onlyAdmin {
        require(feeRate == 0 || feeRate >= feeRateMin);
        takerFeeRate = feeRate;
    }

    function setMakerFeeRate(uint256 feeRate) public onlyAdmin {
        require(feeRate == 0 || feeRate >= feeRateMin);
        makerFeeRate = feeRate;
    }

    function setFeeAddress(address _feeAddress) public onlyAdmin {
        require(_feeAddress != address(0));
        feeAddress = _feeAddress;
    }

    function setDisableFees(address user, uint256 timestamp) public onlyAdmin {
        require(timestamp > block.timestamp);
        disableFees[user] = timestamp;
    }

    function invalidateOrder(address user, uint256 timestamp) public onlyAdmin {
        require(timestamp > block.timestamp);
        disableFees[user] = timestamp;
    }

    function setTokenDecimals(address token, uint256 decimals) public onlyAdmin {
        require(!tokenRegistered[token]);
        tokenRegistered[token] = true;
        tokenDecimals[token] = decimals;
    }

    function tradesLock(address user) public {
        require(user == msg.sender);
        tradesLocked[user] = block.number + 20000;
        emit TradesLock(user);
    }

    function tradesUnlock(address user) public {
        require(user == msg.sender);
        tradesLocked[user] = 0;
        emit TradesUnlock(user);
    }

    function isUserMakerFeeEnabled(address user) private view returns(bool) {
        return makerFeeRate > 0 && disableFees[user] < block.timestamp;
    }

    function isUserTakerFeeEnabled(address user) private view returns(bool) {
        return takerFeeRate > 0 && disableFees[user] < block.timestamp;
    }

    function trade(
        uint256[6] amounts,
        address[4] addresses,
        uint8[2] v,
        bytes32[4] rs
    ) public onlyAdmin {
        
        require(tradesLocked[addresses[0]] < block.number);
        require(block.timestamp <= amounts[3]);
        bytes32 orderHash = keccak256(abi.encodePacked(this, addresses[2], addresses[3], addresses[0], amounts[0], amounts[1], amounts[3], amounts[4]));

        require(ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", orderHash)), v[0], rs[0], rs[1]) == addresses[0]);

        bytes32 tradeHash = keccak256(abi.encodePacked(orderHash, amounts[2], addresses[1], amounts[5]));
        require(ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", tradeHash)), v[1], rs[2], rs[3]) == addresses[1]);

        require(!traded[tradeHash]);
        traded[tradeHash] = true;

        require(safeSub(amounts[0], orderFills[orderHash]) >= amounts[2]);

        uint256 wantAmountToTake = safeDiv(safeMul(amounts[2], amounts[1]), amounts[0]);
        require(wantAmountToTake > 0);

        require(reduceBalance(addresses[0], addresses[2], amounts[2]));
        require(reduceBalance(addresses[1], addresses[3], safeDiv(safeMul(amounts[2], amounts[1]), amounts[0])));

        if (isUserMakerFeeEnabled(addresses[0])) {
            increaseBalance(addresses[0], addresses[3], safeSub(wantAmountToTake, safeDiv(wantAmountToTake, makerFeeRate)));
            increaseBalance(feeAddress, addresses[3], safeDiv(wantAmountToTake, makerFeeRate));
        } else {
            increaseBalance(addresses[0], addresses[3], wantAmountToTake);
        }

        if (isUserTakerFeeEnabled(addresses[1])) {
            increaseBalance(addresses[1], addresses[2], safeSub(amounts[2], safeDiv(amounts[2], takerFeeRate)));
            increaseBalance(feeAddress, addresses[2], safeDiv(amounts[2], takerFeeRate));
        } else {
            increaseBalance(addresses[1], addresses[2], amounts[2]);
        }

        orderFills[orderHash] = safeAdd(orderFills[orderHash], amounts[2]);
    }

    function tradeWithTips(
        uint256[9] amounts,
        address[4] addresses,
        uint8[2] v,
        bytes32[4] rs
    ) public onlyAdmin {
        
        require(tradesLocked[addresses[0]] < block.number);
        require(block.timestamp <= amounts[3]);

        bytes32 orderHash;
        if (amounts[8] == 0) {
            orderHash = amounts[6] > 0
                ? keccak256(abi.encodePacked(this, addresses[2], addresses[3], addresses[0], amounts[0], amounts[1], amounts[3], amounts[4], amounts[6]))
                : keccak256(abi.encodePacked(this, addresses[2], addresses[3], addresses[0], amounts[0], amounts[1], amounts[3], amounts[4]));
        } else {
            orderHash = amounts[6] > 0
                ? keccak256(abi.encodePacked(this, addresses[2], addresses[3], addresses[0], addresses[1], amounts[0], amounts[1], amounts[3], amounts[4], amounts[6]))
                : keccak256(abi.encodePacked(this, addresses[2], addresses[3], addresses[0], addresses[1], amounts[0], amounts[1], amounts[3], amounts[4]));
        }

        require(ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", orderHash)), v[0], rs[0], rs[1]) == addresses[0]);

        bytes32 tradeHash = amounts[7] > 0
            ? keccak256(abi.encodePacked(orderHash, amounts[2], addresses[1], amounts[5], amounts[7]))
            : keccak256(abi.encodePacked(orderHash, amounts[2], addresses[1], amounts[5]));
        require(ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", tradeHash)), v[1], rs[2], rs[3]) == addresses[1]);

        require(!traded[tradeHash]);
        traded[tradeHash] = true;

        require(safeSub(amounts[0], orderFills[orderHash]) >= amounts[2]);

        uint256 wantAmountToTake = safeDiv(safeMul(amounts[2], amounts[1]), amounts[0]);
        require(wantAmountToTake > 0);

        require(reduceBalance(addresses[0], addresses[2], amounts[2]));
        require(reduceBalance(addresses[1], addresses[3], safeDiv(safeMul(amounts[2], amounts[1]), amounts[0])));

        if (amounts[6] > 0 && !isUserMakerFeeEnabled(addresses[0])) {
            increaseBalance(addresses[0], addresses[3], safeSub(wantAmountToTake, safeDiv(wantAmountToTake, amounts[6])));
            increaseBalance(feeAddress, addresses[3], safeDiv(wantAmountToTake, amounts[6]));
        } else if (amounts[6] == 0 && isUserMakerFeeEnabled(addresses[0])) {
            increaseBalance(addresses[0], addresses[3], safeSub(wantAmountToTake, safeDiv(wantAmountToTake, makerFeeRate)));
            increaseBalance(feeAddress, addresses[3], safeDiv(wantAmountToTake, makerFeeRate));
        } else if (amounts[6] > 0 && isUserMakerFeeEnabled(addresses[0])) {
            increaseBalance(addresses[0], addresses[3], safeSub(wantAmountToTake, safeAdd(safeDiv(wantAmountToTake, amounts[6]), safeDiv(wantAmountToTake, makerFeeRate))));
            increaseBalance(feeAddress, addresses[3], safeAdd(safeDiv(wantAmountToTake, amounts[6]), safeDiv(wantAmountToTake, makerFeeRate)));
        } else {
            increaseBalance(addresses[0], addresses[3], wantAmountToTake);
        }

        if (amounts[7] > 0 && !isUserTakerFeeEnabled(addresses[1])) {
            increaseBalance(addresses[1], addresses[2], safeSub(amounts[2], safeDiv(amounts[2], amounts[7])));
            increaseBalance(feeAddress, addresses[2], safeDiv(amounts[2], amounts[7]));
        } else if (amounts[7] == 0 && isUserTakerFeeEnabled(addresses[1])) {
            increaseBalance(addresses[1], addresses[2], safeSub(amounts[2], safeDiv(amounts[2], takerFeeRate)));
            increaseBalance(feeAddress, addresses[2], safeDiv(amounts[2], takerFeeRate));
        } else if (amounts[7] > 0 && isUserTakerFeeEnabled(addresses[1])) {
            increaseBalance(addresses[1], addresses[2], safeSub(amounts[2], safeAdd(safeDiv(amounts[2], amounts[7]), safeDiv(amounts[2], takerFeeRate))));
            increaseBalance(feeAddress, addresses[2], safeAdd(safeDiv(amounts[2], amounts[7]), safeDiv(amounts[2], takerFeeRate)));
        } else {
            increaseBalance(addresses[1], addresses[2], amounts[2]);
        }

        orderFills[orderHash] = safeAdd(orderFills[orderHash], amounts[2]);
    }

    function() external payable {
        revert();
    }

    function safeMul(uint a, uint b) internal pure returns (uint) {
        uint c = a * b;
        assert(a == 0 || c / a == b);
        return c;
    }

    function safeSub(uint a, uint b) internal pure returns (uint) {
        assert(b <= a);
        return a - b;
    }

    function safeAdd(uint a, uint b) internal pure returns (uint) {
        uint c = a + b;
        assert(c>=a && c>=b);
        return c;
    }

    function safeDiv(uint a, uint b) internal pure returns (uint) {
        assert(b > 0);
        uint c = a / b;
        assert(a == b * c + a % b);
        return c;
    }

}