contract DepositContractRegistry is Requestable {
  using DepositContractHelper for DepositContract;

  event CreatedDepositContract(address indexed owner, address indexed depositAddress);
  event UpgradedVersion(address indexed owner, address indexed depositAddress, address newVersion);
  event SetOperator(address indexed owner, address indexed operator, bool canOperate);

  bytes constant public DEPOSIT_CONTRACT_BYTECODE = type(DepositContract).creationCode;

  // =============================
  
  address public wethTokenAddress;
  mapping(address => address payable) public registry;
  mapping(address => address) public versions;
  mapping(address => mapping(address => bool)) operators;

  constructor(address _wethTokenAddress) public {
    wethTokenAddress = _wethTokenAddress;
  }

  /*
   * Get the deterministic address of the given address's deposit contract.
   * Whether this contract has been created yet or not, the address returned
   * here will be the address of the deposit address
   */
  function depositAddressOf(address owner) public view returns (address payable) {
    bytes32 codeHash = keccak256(_getCreationBytecode(owner));
    bytes32 addressHash = keccak256(abi.encodePacked(byte(0xff), address(this), uint256(owner), codeHash));
    return address(uint160(uint256(addressHash)));
  }

  function isDepositContractCreatedFor(address owner) public view returns (bool) {
    return registry[owner] != address(0x0);
  }

  /*
   * Get the version of the deposit contract for a specified owner address
   */
  function versionOf(address owner) public view returns (address) {
    return versions[owner];
  }

  function operatorOf(address owner, address operator) public view returns (bool) {
    return operators[owner][operator];
  }

  /*
   * Get the balance of the given token for the specified address's deposit address
   */
  function balanceOf(address owner, address token) public view returns (uint) {
    address depositAddress = depositAddressOf(owner);
    uint tokenBalance = IERC20(token).balanceOf(depositAddress);
    if (token == wethTokenAddress) tokenBalance = tokenBalance + depositAddress.balance;
    return tokenBalance;
  }

  /*
   * Create a deposit contract by providing a signed UpgradeRequest.
   * The deposit contract will be created and then immediately upgraded
   * to the version specified in the signed request
   */
  function createDepositContract(Types.Request memory request) public {
    validateRequest(request);
    _createDepositContract(request.owner);
    _upgradeVersion(request);
  }

  /*
   * Upgrade the version used by a deposit contract by providing a signed UpgradeRequest
   */
  function upgradeVersion(Types.Request memory request) public {
    validateRequest(request);
    _upgradeVersion(request);
  }

  function setOperator(Types.Request memory request) public {
    validateRequest(request);
    _setOperator(request);
    completeRequest(request);
  }

  function perform(Types.Request memory request) public {
    validateRequest(request);
    _perform(request);
    completeRequest(request);
  }

  // =============================
  // Internal functions

  function _payRequestFee(address owner, address feeToken, address feeRecipient, uint feeAmount) internal {
    DepositContract(registry[owner]).wrapAndTransferToken(feeToken, feeRecipient, feeAmount, wethTokenAddress);
  }

  function _getCreationBytecode(address owner) private view returns (bytes memory) {
    return abi.encodePacked(DEPOSIT_CONTRACT_BYTECODE, bytes12(0x000000000000000000000000), owner);
  }

  function _createDepositContract(address owner) private returns (address) {
    require(registry[owner] == address(0x0), "ALREADY_CREATED");

    address payable depositAddress;
    bytes memory code = _getCreationBytecode(owner);
    uint256 salt = uint256(owner);

    assembly {
      depositAddress := create2(0, add(code, 0x20), mload(code), salt)
      if iszero(extcodesize(depositAddress)) { revert(0, 0) }
    }

    emit CreatedDepositContract(owner, depositAddress);

    registry[owner] = depositAddress;
    return depositAddress;
  }

  function _upgradeVersion(Types.Request memory request) internal {
    require(registry[request.owner] != address(0x0), "NEEDS_CREATION");
    
    Types.UpdateRequest memory upgradeRequest = request.decodeUpdateRequest();
    address currentVersion = versions[request.owner];
    address payable depositAddress = registry[request.owner];

    // End usage of current version if one exists
    if (currentVersion != address(0x0)) {
      IVersionable(currentVersion).versionEndUsage(
        request.owner,
        depositAddress,
        upgradeRequest.version,
        upgradeRequest.additionalData
      );
    }

    // Payout request fee & set version before upgrading
    completeRequest(request);
    DepositContract(depositAddress).setVersion(upgradeRequest.version);
    versions[request.owner] = upgradeRequest.version;

    // Begin usage of new version
    IVersionable(upgradeRequest.version).versionBeginUsage(
      request.owner,
      depositAddress,
      currentVersion,
      upgradeRequest.additionalData
    );

    emit UpgradedVersion(request.owner, depositAddress, upgradeRequest.version);
  }

  function _setOperator(Types.Request memory request) internal {
    Types.ApproveRequest memory approveRequest = request.decodeApproveRequest();
    operators[request.owner][approveRequest.operator] = approveRequest.canOperate;
    emit SetOperator(request.owner, approveRequest.operator, approveRequest.canOperate);
  }

  function _perform(Types.Request memory request) internal {
    Types.PerformRequest memory performRequest = request.decodePerformRequest();
    require(registry[request.owner] != address(0x0), "NEEDS_CREATION");

    address payable depositAddress = registry[request.owner];
    DepositContract(depositAddress).perform(
      performRequest.to,
      performRequest.functionSignature,
      performRequest.encodedParams,
      performRequest.value
    );
  }
}
/**
 * @title DepositContractRegistry
 *
 * Factory for creating and upgrading DepositContracts using
 * user signatures to ensure non-custodianship. Uses CREATE-2 to
 * enable deposit addresses to safely receive ERC20 Tokens/Ether prior
 * to the deposit contract being deployed
 */
function upgradeVersion(Types.Request memory request) public {
  validateRequest(request);
  _upgradeVersion(request);
}

/*
 * Upgrade the version used by a deposit contract by providing a signed UpgradeRequest
 */