pragma solidity >=0.5.4<0.6.0;

contract ERC20Detailed {
  string private _name;
  string private _symbol;
  uint8 private _decimals;

  constructor(string memory name, string memory symbol, uint8 decimals) public {
    _name = name;
    _symbol = symbol;
    _decimals = decimals;
  }

  function name() public view returns (string memory) {
    return _name;
  }

  function symbol() public view returns (string memory) {
    return _symbol;
  }

  function decimals() public view returns (uint8) {
    return _decimals;
  }
}


interface IERC20 {
  function transfer(address to, uint256 value) external returns (bool);

  function approve(address spender, uint256 value) external returns (bool);

  function transferFrom(address from, address to, uint256 value) external returns (bool);

  function totalSupply() external view returns (uint256);

  function balanceOf(address who) external view returns (uint256);

  function allowance(address owner, address spender) external view returns (uint256);

  event Transfer(address indexed from, address indexed to, uint256 value);

  event Approval(address indexed owner, address indexed spender, uint256 value);
}

contract ERC20 is IERC20 {
  using SafeMath for uint256;

  mapping(address => uint256) private _balances;

  mapping(address => mapping(address => uint256)) private _allowed;

  uint256 private _totalSupply;

  function totalSupply() public view returns (uint256) {
    return _totalSupply;
  }

  function balanceOf(address owner) public view returns (uint256) {
    return _balances[owner];
  }

  function allowance(address owner, address spender) public view returns (uint256) {
    return _allowed[owner][spender];
  }

  function transfer(address to, uint256 value) public returns (bool) {
    _transfer(msg.sender, to, value);
    return true;
  }

  function approve(address spender, uint256 value) public returns (bool) {
    _approve(msg.sender, spender, value);
    return true;
  }

  function transferFrom(address from, address to, uint256 value) public returns (bool) {
    _transfer(from, to, value);
    _approve(from, msg.sender, _allowed[from][msg.sender].sub(value));
    return true;
  }

  function _transfer(address from, address to, uint256 value) internal {
    require(to != address(0));

    _balances[from] = _balances[from].sub(value);
    _balances[to] = _balances[to].add(value);
    emit Transfer(from, to, value);
  }

  function _mint(address account, uint256 value) internal {
    require(account != address(0));

    _totalSupply = _totalSupply.add(value);
    _balances[account] = _balances[account].add(value);
    emit Transfer(address(0), account, value);
  }

  function _burn(address account, uint256 value) internal {
    require(account != address(0));

    _totalSupply = _totalSupply.sub(value);
    _balances[account] = _balances[account].sub(value);
    emit Transfer(account, address(0), value);
  }

  function _approve(address owner, address spender, uint256 value) internal {
    require(spender != address(0));
    require(owner != address(0));

    _allowed[owner][spender] = value;
    emit Approval(owner, spender, value);
  }
}


contract Ownable {
  address private _owner;

  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  constructor() internal {
    _owner = msg.sender;
    emit OwnershipTransferred(address(0), _owner);
  }

  function owner() public view returns (address) {
    return _owner;
  }

  modifier onlyOwner() {
    require(isOwner());
    _;
  }

  function isOwner() public view returns (bool) {
    return msg.sender == _owner;
  }

  function transferOwnership(address newOwner) public onlyOwner {
    _transferOwnership(newOwner);
  }

  function _transferOwnership(address newOwner) internal {
    require(newOwner != address(0));
    emit OwnershipTransferred(_owner, newOwner);
    _owner = newOwner;
  }
}

library MultiSigAction {
  struct Action {
    uint8 actionType;
    address callbackAddress;
    string callbackSig;
    bytes callbackData;
    uint8 quorum;
    address requestedBy;
    address rejectedBy;
    mapping(address => bool) approvedBy;
    uint8 numOfApprovals;
    bool rejected;
    bool failed;
  }

  function init(
    Action storage _self,
    uint8 _actionType,
    address _callbackAddress,
    string memory _callbackSig,
    bytes memory _callbackData,
    uint8 _quorum
  ) internal {
    _self.actionType = _actionType;
    _self.callbackAddress = _callbackAddress;
    _self.callbackSig = _callbackSig;
    _self.callbackData = _callbackData;
    _self.quorum = _quorum;
    _self.requestedBy = msg.sender;
  }

  function approve(Action storage _self) internal {
    require(!_self.rejected, "CANNOT_APPROVE_REJECTED");
    require(!_self.failed, "CANNOT_APPROVE_FAILED");
    require(!_self.approvedBy[msg.sender], "CANNOT_APPROVE_AGAIN");
    require(!isCompleted(_self), "CANNOT_APPROVE_COMPLETED");

    _self.approvedBy[msg.sender] = true;
    _self.numOfApprovals++;
  }

  function reject(Action storage _self) internal {
    require(!_self.approvedBy[msg.sender], "CANNOT_REJECT_APPROVED");
    require(!_self.failed, "CANNOT_REJECT_FAILED");
    require(!_self.rejected, "CANNOT_REJECT_REJECTED");
    require(!isCompleted(_self), "CANNOT_REJECT_COMPLETED");

    _self.rejectedBy = msg.sender;
    _self.rejected = true;
  }

  function complete(Action storage _self) internal {
    require(!_self.rejected, "CANNOT_COMPLETE_REJECTED");
    require(!_self.failed, "CANNOT_COMPLETE_FAILED");
    require(isCompleted(_self), "CANNNOT_COMPLETE_AGAIN");

    // solium-disable-next-line security/no-low-level-calls
    (bool _success, ) = _self.callbackAddress.call(
      abi.encodePacked(bytes4(keccak256(bytes(_self.callbackSig))), _self.callbackData)
    );

    if (!_success) {
      _self.failed = true;
    }
  }

  function isCompleted(Action storage _self) internal view returns (bool) {
    return _self.numOfApprovals >= _self.quorum && !_self.failed;
  }
}


library SafeMath {
  function sub(uint256 a, uint256 b) internal pure returns (uint256) {
    require(b <= a);
    uint256 c = a - b;

    return c;
  }

  function add(uint256 a, uint256 b) internal pure returns (uint256) {
    uint256 c = a + b;
    require(c >= a);

    return c;
  }
}

contract ERC20Extended is Ownable, ERC20, ERC20Detailed {
  constructor(string memory _name, string memory _symbol, uint8 _decimals)
    public
    ERC20Detailed(_name, _symbol, _decimals)
  {}

  function burn(uint256 _value) public onlyOwner returns (bool) {
    _burn(msg.sender, _value);
    return true;
  }

  function mint(address _to, uint256 _value) public onlyOwner returns (bool) {
    _mint(_to, _value);

    return true;
  }
}

contract MultiSigAdministration {
  event TenantRegistered(
    address indexed tenant,
    address[] creators,
    address[] admins,
    uint8 quorum
  );
  event ActionInitiated(address indexed tenant, uint256 indexed id, address initiatedBy);
  event ActionApproved(address indexed tenant, uint256 indexed id, address approvedBy);
  event ActionRejected(address indexed tenant, uint256 indexed id, address rejectedBy);
  event ActionCompleted(address indexed tenant, uint256 indexed id);
  event ActionFailed(address indexed tenant, uint256 indexed id);

  using MultiSigAction for MultiSigAction.Action;

  enum AdminAction {ADD_ADMIN, REMOVE_ADMIN, CHANGE_QUORUM, ADD_CREATOR, REMOVE_CREATOR}
  uint8 private constant OTHER_ACTION = uint8(AdminAction.REMOVE_CREATOR) + 1;

  mapping(address => uint256) public numOfActions;
  mapping(address => mapping(address => bool)) public isAdmin;
  mapping(address => uint8) public numOfAdmins;
  mapping(address => mapping(address => bool)) public isCreator;
  mapping(address => uint8) public quorums;
  mapping(address => bool) public isRegistered;
  mapping(address => uint256) public minValidActionId;

  mapping(address => mapping(uint256 => MultiSigAction.Action)) private actions;

  modifier onlyAdminOf(address _tenant) {
    require(isAdmin[_tenant][msg.sender], "ONLY_ADMIN_OF_TENANT");

    _;
  }

  modifier onlyAdminOrCreatorOf(address _tenant) {
    require(
      isAdmin[_tenant][msg.sender] || isCreator[_tenant][msg.sender],
      "ONLY_ADMIN_OR_CREATOR_OF_TENANT"
    );

    _;
  }

  modifier onlyRegistered(address _tenant) {
    require(isRegistered[_tenant], "ONLY_REGISTERED_TENANT");

    _;
  }

  modifier onlyMe() {
    require(msg.sender == address(this), "ONLY_INTERNAL");

    _;
  }

  modifier onlyExistingAction(address _tenant, uint256 _id) {
    require(_id <= numOfActions[_tenant], "ONLY_EXISTING_ACTION");
    require(_id > 0, "ONLY_EXISTING_ACTION");

    _;
  }

  constructor() public {}

  /* Public Functions - Start */
  function register(
    address _tenant,
    address[] memory _creators,
    address[] memory _admins,
    uint8 _quorum
  ) public returns (bool success) {
    require(
      msg.sender == _tenant || msg.sender == Ownable(_tenant).owner(),
      "ONLY_TENANT_OR_TENANT_OWNER"
    );

    return _register(_tenant, _creators, _admins, _quorum);
  }

  function initiateAdminAction(
    address _tenant,
    AdminAction _adminAction,
    bytes memory _callbackData
  ) public onlyRegistered(_tenant) onlyAdminOf(_tenant) returns (uint256 id) {
    string memory _callbackSig = _getAdminActionCallbackSig(_adminAction);

    uint256 _id = _initiateAction(
      uint8(_adminAction),
      _tenant,
      address(this),
      _callbackSig,
      abi.encodePacked(abi.encode(_tenant), _callbackData)
    );
    _approveAction(_tenant, _id);

    return _id;
  }

  function initiateAction(address _tenant, string memory _callbackSig, bytes memory _callbackData)
    public
    onlyRegistered(_tenant)
    onlyAdminOrCreatorOf(_tenant)
    returns (uint256 id)
  {
    uint256 _id = _initiateAction(OTHER_ACTION, _tenant, _tenant, _callbackSig, _callbackData);

    if (isAdmin[_tenant][msg.sender]) {
      _approveAction(_tenant, _id);
    }

    return _id;
  }

  function approveAction(address _tenant, uint256 _id)
    public
    onlyRegistered(_tenant)
    onlyAdminOf(_tenant)
    onlyExistingAction(_tenant, _id)
    returns (bool success)
  {
    return _approveAction(_tenant, _id);
  }

  function rejectAction(address _tenant, uint256 _id)
    public
    onlyRegistered(_tenant)
    onlyAdminOrCreatorOf(_tenant)
    onlyExistingAction(_tenant, _id)
    returns (bool success)
  {
    return _rejectAction(_tenant, _id);
  }

  function addAdmin(address _tenant, address _admin, bool _increaseQuorum) public onlyMe {
    minValidActionId[_tenant] = numOfActions[_tenant] + 1;
    _addAdmin(_tenant, _admin);

    if (_increaseQuorum) {
      uint8 _quorum = quorums[_tenant];
      uint8 _newQuorum = _quorum + 1;
      require(_newQuorum > _quorum, "OVERFLOW");

      _changeQuorum(_tenant, _newQuorum);
    }
  }

  function removeAdmin(address _tenant, address _admin, bool _decreaseQuorum) public onlyMe {
    uint8 _quorum = quorums[_tenant];

    if (_decreaseQuorum && _quorum > 1) {
      _changeQuorum(_tenant, _quorum - 1);
    }

    minValidActionId[_tenant] = numOfActions[_tenant] + 1;
    _removeAdmin(_tenant, _admin);
  }

  function changeQuorum(address _tenant, uint8 _quorum) public onlyMe {
    minValidActionId[_tenant] = numOfActions[_tenant] + 1;
    _changeQuorum(_tenant, _quorum);
  }

  function addCreator(address _tenant, address _creator) public onlyMe {
    _addCreator(_tenant, _creator);
  }

  function removeCreator(address _tenant, address _creator) public onlyMe {
    _removeCreator(_tenant, _creator);
  }

  function getAction(address _tenant, uint256 _id)
    public
    view
    returns (
    bool isAdminAction,
    string memory callbackSig,
    bytes memory callbackData,
    uint8 quorum,
    address requestedBy,
    address rejectedBy,
    uint8 numOfApprovals,
    bool rejected,
    bool failed,
    bool completed,
    bool valid
  )
  {
    MultiSigAction.Action storage _action = _getAction(_tenant, _id);

    isAdminAction = _action.callbackAddress == address(this);
    callbackSig = _action.callbackSig;
    callbackData = _action.callbackData;
    quorum = _action.quorum;
    requestedBy = _action.requestedBy;
    rejectedBy = _action.rejectedBy;
    numOfApprovals = _action.numOfApprovals;
    rejected = _action.rejected;
    failed = _action.failed;
    completed = _action.isCompleted();
    valid = _isActionValid(_tenant, _id);
  }

  function hasApprovedBy(address _tenant, uint256 _id, address _admin)
    public
    view
    returns (bool approvedBy)
  {
    approvedBy = _getAction(_tenant, _id).approvedBy[_admin];
  }
  /* Public Functions - End */

  /* Private Functions - Start */
  function _getAction(address _tenant, uint256 _id)
    private
    view
    returns (MultiSigAction.Action storage)
  {
    return actions[_tenant][_id];
  }

  function _isActionValid(address _tenant, uint256 _id) private view returns (bool) {
    return _id >= minValidActionId[_tenant];
  }

  function _getAdminActionCallbackSig(AdminAction _adminAction)
    private
    pure
    returns (string memory)
  {
    if (_adminAction == AdminAction.ADD_ADMIN) {
      return "addAdmin(address,address,bool)";
    }

    if (_adminAction == AdminAction.REMOVE_ADMIN) {
      return "removeAdmin(address,address,bool)";
    }

    if (_adminAction == AdminAction.CHANGE_QUORUM) {
      return "changeQuorum(address,uint8)";
    }

    if (_adminAction == AdminAction.ADD_CREATOR) {
      return "addCreator(address,address)";
    }

    return "removeCreator(address,address)";
  }

  function _addCreator(address _tenant, address _creator) private {
    require(_creator != address(this), "INVALID_CREATOR");
    require(!isAdmin[_tenant][_creator], "ALREADY_ADMIN");
    require(!isCreator[_tenant][_creator], "ALREADY_CREATOR");

    isCreator[_tenant][_creator] = true;
  }

  function _removeCreator(address _tenant, address _creator) private {
    require(isCreator[_tenant][_creator], "NOT_CREATOR");

    isCreator[_tenant][_creator] = false;
  }

  function _addAdmin(address _tenant, address _admin) private {
    require(_admin != address(this), "INVALID_ADMIN");
    require(!isAdmin[_tenant][_admin], "ALREADY_ADMIN");
    require(!isCreator[_tenant][_admin], "ALREADY_CREATOR");
    require(numOfAdmins[_tenant] + 1 > numOfAdmins[_tenant], "OVERFLOW");

    numOfAdmins[_tenant]++;
    isAdmin[_tenant][_admin] = true;
  }

  function _removeAdmin(address _tenant, address _admin) private {
    require(isAdmin[_tenant][_admin], "NOT_ADMIN");
    require(--numOfAdmins[_tenant] >= quorums[_tenant], "TOO_FEW_ADMINS");

    isAdmin[_tenant][_admin] = false;
  }

  function _changeQuorum(address _tenant, uint8 _quorum) private {
    require(_quorum <= numOfAdmins[_tenant], "QUORUM_TOO_BIG");
    require(_quorum > 0, "QUORUM_ZERO");

    quorums[_tenant] = _quorum;
  }

  function _register(
    address _tenant,
    address[] memory _creators,
    address[] memory _admins,
    uint8 _quorum
  ) private returns (bool) {
    require(_tenant != address(this), "INVALID_TENANT");
    require(!isRegistered[_tenant], "ALREADY_REGISTERED");

    for (uint8 i = 0; i < _admins.length; i++) {
      _addAdmin(_tenant, _admins[i]);
    }
    _changeQuorum(_tenant, _quorum);

    for (uint8 i = 0; i < _creators.length; i++) {
      _addCreator(_tenant, _creators[i]);
    }

    isRegistered[_tenant] = true;
    emit TenantRegistered(_tenant, _creators, _admins, _quorum);

    return true;
  }

  function _initiateAction(
    uint8 _actionType,
    address _tenant,
    address _callbackAddress,
    string memory _callbackSig,
    bytes memory _callbackData
  ) private returns (uint256) {
    uint256 _id = ++numOfActions[_tenant];
    uint8 _quorum = quorums[_tenant];

    if (_actionType == uint8(AdminAction.REMOVE_ADMIN)) {
      require(numOfAdmins[_tenant] > 1, "TOO_FEW_ADMINS");

      if (_quorum == numOfAdmins[_tenant] && _quorum > 2) {
        _quorum = numOfAdmins[_tenant] - 1;
      }
    }

    _getAction(_tenant, _id).init(
      _actionType,
      _callbackAddress,
      _callbackSig,
      _callbackData,
      _quorum
    );

    emit ActionInitiated(_tenant, _id, msg.sender);

    return _id;
  }

  function _approveAction(address _tenant, uint256 _id) private returns (bool) {
    require(_isActionValid(_tenant, _id), "ACTION_INVALIDATED");

    MultiSigAction.Action storage _action = _getAction(_tenant, _id);
    _action.approve();
    emit ActionApproved(_tenant, _id, msg.sender);

    if (_action.isCompleted()) {
      _action.complete();

      if (_action.failed) {
        emit ActionFailed(_tenant, _id);
      } else {
        emit ActionCompleted(_tenant, _id);
      }
    }

    return true;
  }

  function _rejectAction(address _tenant, uint256 _id) private returns (bool) {
    MultiSigAction.Action storage _action = _getAction(_tenant, _id);

    if (isCreator[_tenant][msg.sender]) {
      require(msg.sender == _action.requestedBy, "CREATOR_REJECT_NOT_REQUESTOR");
    }

    if (_action.actionType == uint8(AdminAction.REMOVE_ADMIN)) {
      (, address _admin, ) = abi.decode(_action.callbackData, (address, address, bool));

      require(_admin != msg.sender, "CANNOT_REJECT_ITS_OWN_REMOVAL");
    }

    _action.reject();

    emit ActionRejected(_tenant, _id, msg.sender);

    return true;
  }
  /* Private Functions - End */
}

contract MultiSigProxyOwner {
  event BurnRequested(address indexed owner, uint256 value);
  event BurnCanceled(address indexed owner);
  event BurnMinSet(uint256 burnMin);

  struct BurnRequest {
    uint256 actionId;
    uint256 value;
  }

  uint256 public burnMin;
  mapping(address => BurnRequest) public burnRequests;

  ERC20Extended private token;
  MultiSigAdministration private multiSigAdmin;
  address[] private creators;

  modifier onlyMultiSigAdministration {
    require(msg.sender == address(multiSigAdmin));

    _;
  }

  constructor(
    address _token,
    address _multiSigAdmin,
    address[] memory _admins,
    uint8 _quorum,
    uint256 _burnMin
  ) public {
    token = ERC20Extended(_token);
    multiSigAdmin = MultiSigAdministration(_multiSigAdmin);
    burnMin = _burnMin;

    creators.push(address(this));
    multiSigAdmin.register(address(this), creators, _admins, _quorum);

  }

  function requestBurn(uint256 _value) public returns (bool) {
    require(!_burnRequestExist(msg.sender), "BURN_REQUEST_EXISTS");
    require(_value >= burnMin, "SMALLER_THAN_MIN_BURN_AMOUNT");

    token.transferFrom(msg.sender, address(this), _value);
    burnRequests[msg.sender].value = _value;
    burnRequests[msg.sender].actionId = multiSigAdmin.initiateAction(
      address(this),
      "burn(address,uint256)",
      abi.encode(msg.sender, _value)
    );

    emit BurnRequested(msg.sender, _value);

    return true;
  }

  function cancelBurn() public returns (bool) {
    uint256 _actionId = burnRequests[msg.sender].actionId;
    uint256 _value = burnRequests[msg.sender].value;
    _deleteBurnRequest(msg.sender);

    // solium-disable-next-line security/no-low-level-calls
    (bool _success, ) = address(multiSigAdmin).call(
      abi.encodeWithSignature("rejectAction(address,uint256)", address(this), _actionId)
    );
    _success;
    token.transfer(msg.sender, _value);

    emit BurnCanceled(msg.sender);

    return true;
  }

  function burn(address _owner, uint256 _value) public onlyMultiSigAdministration returns (bool) {
    require(burnRequests[_owner].value == _value, "BURN_VALUE_MISMATCH");

    _deleteBurnRequest(_owner);
    token.burn(_value);

    return true;
  }

  function mint(address _to, uint256 _value) public onlyMultiSigAdministration returns (bool) {
    return token.mint(_to, _value);
  }

  function transferOwnership(address _newOwner) public onlyMultiSigAdministration returns (bool) {
    token.transferOwnership(_newOwner);

    return true;
  }

  function setBurnMin(uint256 _burnMin) public onlyMultiSigAdministration returns (bool) {
    return _setBurnMin(_burnMin);
  }

  function _setBurnMin(uint256 _burnMin) internal returns (bool) {
    burnMin = _burnMin;
    emit BurnMinSet(_burnMin);

    return true;
  }

  function _burnRequestExist(address _owner) internal view returns (bool) {
    return burnRequests[_owner].actionId != 0;
  }

  function _deleteBurnRequest(address _owner) internal returns (bool) {
    require(_burnRequestExist(_owner), "NO_BURN_REQUEST_EXISTS");

    burnRequests[_owner].actionId = 0;
    burnRequests[_owner].value = 0;

    return true;
  }
}