// File: @openzeppelin/contracts/token/ERC20/IERC20.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

// File: @openzeppelin/contracts/token/ERC721/IERC721.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/IERC721.sol)

pragma solidity ^0.8.0;

import "../../utils/introspection/IERC165.sol";

/**
 * @dev Required interface of an ERC721 compliant contract.
 */
interface IERC721 is IERC165 {
    /**
     * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
     */
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
     */
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
     */
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**
     * @dev Returns the number of tokens in ``owner``'s account.
     */
    function balanceOf(address owner) external view returns (uint256 balance);

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId) external;

    /**
     * @dev Transfers `tokenId` token from `from` to `to`.
     *
     * WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
     * or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
     * understand this adds an external call which potentially creates a reentrancy vulnerability.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 tokenId) external;

    /**
     * @dev Gives permission to `to` to transfer `tokenId` token to another account.
     * The approval is cleared when the token is transferred.
     *
     * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
     *
     * Requirements:
     *
     * - The caller must own the token or be an approved operator.
     * - `tokenId` must exist.
     *
     * Emits an {Approval} event.
     */
    function approve(address to, uint256 tokenId) external;

    /**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
     *
     * Requirements:
     *
     * - The `operator` cannot be the caller.
     *
     * Emits an {ApprovalForAll} event.
     */
    function setApprovalForAll(address operator, bool approved) external;

    /**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) external view returns (address operator);

    /**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
     *
     * See {setApprovalForAll}
     */
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

// File: @openzeppelin/contracts/token/ERC721/IERC721Receiver.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC721/IERC721Receiver.sol)

pragma solidity ^0.8.0;

/**
 * @title ERC721 token receiver interface
 * @dev Interface for any contract that wants to support safeTransfers
 * from ERC721 asset contracts.
 */
interface IERC721Receiver {
    /**
     * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
     * by `operator` from `from`, this function is called.
     *
     * It must return its Solidity selector to confirm the token transfer.
     * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
     *
     * The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`.
     */
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

// File: @openzeppelin/contracts/utils/introspection/IERC165.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

// File: contracts/DogelonPuzzleVault.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

/**
 * @title DogelonPuzzleVault
 * @dev This contract facilitates a riddle game where participants can win
 * ETH, ERC20, or ERC721 prizes by guessing correctly.
 */
contract DogelonPuzzleVault is IERC721Receiver {

  // Struct for storing ETH prizes
  struct EthVault {
    uint256 id;
    uint256 amount;
    address claimer;
  }

  // Struct for returning ETH vaults from view functions
  struct EthVaultWithAnswerHash {
    uint256 id;
    uint256 answerHash;
    uint256 amount;
    address claimer;
  }

  // Struct for storing ERC20 prizes
  struct Erc20Vault {
    uint256 id;
    uint256 amount;
    address addr; // Token address
    address claimer;
  }

  // Struct for returning ERC20 vaults from view functions
  struct Erc20VaultWithAnswerHash {
    uint256 id;
    uint256 answerHash;
    uint256 amount;
    address addr; // Token address
    address claimer;
  }

  // Struct for storing ERC721 prizes
  struct Erc721Vault {
    uint256 id;
    address addr; // Token address
    uint256 tokenId;
    address claimer;
  }

  // Struct for returning ERC20 vaults from view functions
  struct Erc721VaultWithAnswerHash {
    uint256 id;
    uint256 answerHash;
    address addr; // Token address
    uint256 tokenId;
    address claimer;
  }

  // Vault metadata using its hash and type (0 for ETH, 1 for ERC20, 2 for ERC721)
  struct VaultById {
    uint256 answerHash; // Hash of the correct answer
    uint256 vaultType;
  }

  // Commitment struct to handle the commit-reveal mechanism
  struct Commitment {
    bytes32 saltedHash; // Hash of the committed answer + salt
    address committer; // Address of the user who made the commitment
    uint256 blockNumber; // Block number when the commit was made
  }

  // Mapping to store ETH prize info using a combined key (answerHash + id)
  mapping(uint256 => EthVault) public ethVaults;

  // Mapping to store ERC20 prize info using a combined key (answerHash + id)
  mapping(uint256 => Erc20Vault) public erc20Vaults;

  // Mapping to store ERC721 prize info using a combined key (answerHash + id)
  mapping(uint256 => Erc721Vault) public erc721Vaults;

  // Array to store all vaults by ID for easy iteration
  VaultById[] public vaultsById;

  // Mapping to store user commitments
  mapping(address => Commitment) public commitments;

  // Contract owner's address
  address public owner;

  // Events
  event EthVaultCreated(uint256 indexed vaultId, uint256 amount);
  event Erc20VaultCreated(uint256 indexed vaultId, uint256 amount, address tokenAddress);
  event Erc721VaultCreated(uint256 indexed vaultId, address tokenAddress, uint256 tokenId);
  event VaultModified(uint256 indexed vaultId); // Split into three events, showing updated vault info
  event VaultDeleted(uint256 indexed vaultId);
  event VaultClaimed(uint256 vaultId, uint256 when, address by, uint256 vaultType);

  // Modifier to ensure only the contract owner can call a function
  modifier onlyOwner() {
    require(msg.sender == owner, "Must be owner");
    _;
  }

  /**
   * @dev Constructor initializes the contract setting the deployer as the owner.
   */
  constructor() {
    owner = msg.sender;
  }

  /**
   * @dev Helper function to get the combined key used in the vault mappings.
   * @param hash Hash of the answer.
   * @param id Vault ID.
   * @return Combined key.
   */
  function _getCombinedKey(uint256 hash, uint256 id) internal pure returns (uint256) {
    return uint256(keccak256(abi.encodePacked(hash, id)));
  }

  /**
   * @dev Create an ETH vault with a specific hash and amount.
   * @param answerHash Hash of the correct answer.
   * @param amount Amount of ETH to be won.
   */
  function createEthVault(uint256 answerHash, uint256 amount) external onlyOwner {
    EthVault memory vault = EthVault(vaultsById.length, amount, address(0));
    uint256 combinedKey = _getCombinedKey(answerHash, vaultsById.length);
    ethVaults[combinedKey] = vault;
    vaultsById.push(VaultById(answerHash, 0));
    emit EthVaultCreated(vaultsById.length - 1, amount);
  }

  /**
   * @dev Create an ERC20 vault with a specific hash, amount, and token address.
   * @param answerHash Hash of the correct answer.
   * @param amount Amount of the ERC20 token to be won.
   * @param addr Address of the ERC20 token.
   */
  function createErc20Vault(uint256 answerHash, uint256 amount, address addr) external onlyOwner {
    Erc20Vault memory vault = Erc20Vault(vaultsById.length, amount, addr, address(0));
    uint256 combinedKey = _getCombinedKey(answerHash, vaultsById.length);
    erc20Vaults[combinedKey] = vault;
    vaultsById.push(VaultById(answerHash, 1));
    emit Erc20VaultCreated(vaultsById.length - 1, amount, addr);
  }

  /**
   * @dev Create an ERC721 vault with a specific hash, token address, and token ID.
   * @param answerHash Hash of the correct answer.
   * @param addr Address of the ERC721 token.
   * @param tokenId ID of the ERC721 token to be won.
   */
  function createErc721Vault(uint256 answerHash, address addr, uint256 tokenId) external onlyOwner {
    Erc721Vault memory vault = Erc721Vault(vaultsById.length, addr, tokenId, address(0));
    uint256 combinedKey = _getCombinedKey(answerHash, vaultsById.length);
    erc721Vaults[combinedKey] = vault;
    vaultsById.push(VaultById(answerHash, 2));
    emit Erc721VaultCreated(vaultsById.length - 1, addr, tokenId);
  }

  /**
   * @dev Modify an existing ETH vault.
   * @param id ID of the vault to be modified.
   * @param newHash New hash for the answer.
   * @param newAmount New amount for the ETH prize.
   */
  function modifyEthVault(uint256 id, uint256 newHash, uint256 newAmount) external onlyOwner {
    require(id < vaultsById.length, "Invalid vault ID");
    require(vaultsById[id].vaultType == 0, "Not an ETH vault");
    uint256 combinedKey = _getCombinedKey(vaultsById[id].answerHash, id);
    EthVault storage vault = ethVaults[combinedKey];
    require(vault.claimer == address(0), "Cannot modify a claimed vault");
    require(!(vault.id == 0 && vault.amount == 0 && vault.claimer == address(0)), "Vault does not exist");

    vaultsById[id].answerHash = newHash;
    vault.amount = newAmount;

    uint256 newCombinedKey = _getCombinedKey(newHash, id);
    if (newCombinedKey != combinedKey) {
      ethVaults[newCombinedKey] = vault;
      delete ethVaults[combinedKey];
    }

    emit VaultModified(id);
  }

  /**
   * @dev Modify an existing ERC20 vault.
   * @param id ID of the vault to be modified.
   * @param newHash New hash for the answer.
   * @param newAmount New amount for the ERC20 prize.
   * @param newAddr New address for the ERC20 token.
   */
  function modifyErc20Vault(uint256 id, uint256 newHash, uint256 newAmount, address newAddr) external onlyOwner {
    require(id < vaultsById.length, "Invalid vault ID");
    require(vaultsById[id].vaultType == 1, "Not an ERC20 vault");
    uint256 combinedKey = _getCombinedKey(vaultsById[id].answerHash, id);
    Erc20Vault storage vault = erc20Vaults[combinedKey];
    require(vault.claimer == address(0), "Cannot modify a claimed vault");
    require(!(vault.id == 0 && vault.addr == address(0) && vault.amount == 0 && vault.claimer == address(0)), "Vault does not exist");
    vaultsById[id].answerHash = newHash;
    vault.amount = newAmount;
    vault.addr = newAddr;

    uint256 newCombinedKey = _getCombinedKey(newHash, id);
    if (newCombinedKey != combinedKey) {
      erc20Vaults[newCombinedKey] = vault;
      delete erc20Vaults[combinedKey];
    }

    emit VaultModified(id);
  }

  /**
   * @dev Modify an existing ERC721 vault.
   * @param id ID of the vault to be modified.
   * @param newHash New hash for the answer.
   * @param newAddr New address for the ERC721 token.
   * @param newTokenId New token ID for the ERC721 prize.
   */
  function modifyErc721Vault(uint256 id, uint256 newHash, address newAddr, uint256 newTokenId) external onlyOwner {
    require(id < vaultsById.length, "Invalid vault ID");
    require(vaultsById[id].vaultType == 2, "Not an ERC721 vault");
    uint256 combinedKey = _getCombinedKey(vaultsById[id].answerHash, id);
    Erc721Vault storage vault = erc721Vaults[combinedKey];
    require(vault.claimer == address(0), "Cannot modify a claimed vault");
    require(!(vault.id == 0 && vault.addr == address(0) && vault.tokenId == 0 && vault.claimer == address(0)), "Vault does not exist");
    vaultsById[id].answerHash = newHash;
    vault.addr = newAddr;
    vault.tokenId = newTokenId;

    uint256 newCombinedKey = _getCombinedKey(newHash, id);
    if (newCombinedKey != combinedKey) {
      erc721Vaults[newCombinedKey] = vault;
      delete erc721Vaults[combinedKey];
    }

    emit VaultModified(id);
  }

  /**
   * @dev Delete an existing ETH vault.
   * @param id ID of the vault to be deleted.
   */
  function deleteEthVault(uint256 id) external onlyOwner {
    require(id < vaultsById.length, "Invalid vault ID");
    require(vaultsById[id].vaultType == 0, "Not an ETH vault");
    uint256 combinedKey = _getCombinedKey(vaultsById[id].answerHash, id);
    EthVault storage vault = ethVaults[combinedKey];
    require(!(vault.id == 0 && vault.amount == 0 && vault.claimer == address(0)), "Vault does not exist");
    delete ethVaults[combinedKey];
    delete vaultsById[id];
    emit VaultDeleted(id);
  }

  /**
   * @dev Delete an existing ERC20 vault.
   * @param id ID of the vault to be deleted.
   */
  function deleteErc20Vault(uint256 id) external onlyOwner {
    require(id < vaultsById.length, "Invalid vault ID");
    require(vaultsById[id].vaultType == 1, "Not an ERC20 vault");
    uint256 combinedKey = _getCombinedKey(vaultsById[id].answerHash, id);
    Erc20Vault storage vault = erc20Vaults[combinedKey];
    require(!(vault.id == 0 && vault.addr == address(0) && vault.amount == 0 && vault.claimer == address(0)), "Vault does not exist");
    delete erc20Vaults[combinedKey];
    delete vaultsById[id];
    emit VaultDeleted(id);
  }

  /**
   * @dev Delete an existing ERC721 vault.
   * @param id ID of the vault to be deleted.
   */
  function deleteErc721Vault(uint256 id) external onlyOwner {
    require(id < vaultsById.length, "Invalid vault ID");
    require(vaultsById[id].vaultType == 2, "Not an ERC721 vault");
    uint256 combinedKey = _getCombinedKey(vaultsById[id].answerHash, id);
    Erc721Vault storage vault = erc721Vaults[combinedKey];
    require(!(vault.id == 0 && vault.addr == address(0) && vault.tokenId == 0 && vault.claimer == address(0)), "Vault does not exist");
    delete erc721Vaults[combinedKey];
    delete vaultsById[id];
    emit VaultDeleted(id);
  }

  /**
   * @dev Players use this function to commit their guess for a prize. This prevents front-running.
   * @param saltedHash Hash of the player's guess + a random salt.
   */
  function commitGuess(bytes32 saltedHash) external {
    commitments[msg.sender] = Commitment(saltedHash, msg.sender, block.number);
  }

  /**
   * @dev Players use this function to reveal their original guess and claim a prize.
   * @param guess The original guess made by the player.
   * @param salt The random salt used in the commit.
   * @param id ID of the vault.
   */
  function revealGuess(string memory guess, uint256 salt, uint256 id) external {
    require(id < vaultsById.length, "Invalid vault ID");

    bytes32 hashedGuess = keccak256(abi.encodePacked(guess, salt));
    Commitment memory userCommit = commitments[msg.sender];

    require(block.number > userCommit.blockNumber, "Trying to pull a fast one, eh?");
    require(msg.sender == userCommit.committer, "Imposter!");
    require(hashedGuess == userCommit.saltedHash, "There seems to be some kind of mix-up...");

    uint256 hash = uint256(keccak256(abi.encodePacked(guess)));
    VaultById memory targetVault = vaultsById[id];

    require(hash == targetVault.answerHash, "Wrong!");

    // Distribute prize
    uint256 combinedKey = _getCombinedKey(hash, id);
    if (targetVault.vaultType == 0) {
      EthVault storage vault = ethVaults[combinedKey];
      require(vault.claimer == address(0), "Vault already claimed!");
      payable(msg.sender).transfer(vault.amount);
      vault.claimer = msg.sender;
    } else if (targetVault.vaultType == 1) {
      Erc20Vault storage vault = erc20Vaults[combinedKey];
      require(vault.claimer == address(0), "Vault already claimed!");
      IERC20(vault.addr).transfer(msg.sender, vault.amount);
      vault.claimer = msg.sender;
    } else if (targetVault.vaultType == 2) {
      Erc721Vault storage vault = erc721Vaults[combinedKey];
      require(vault.claimer == address(0), "Vault already claimed!");
      IERC721(vault.addr).transferFrom(address(this), msg.sender, vault.tokenId);
      vault.claimer = msg.sender;
    }

    delete commitments[msg.sender];
    emit VaultClaimed(id, block.timestamp, msg.sender, targetVault.vaultType);
  }

  /**
   * @dev Allows the owner to retrieve any ETH that might be accidentally sent to the contract.
   */
  function sweepEth() external onlyOwner {
    payable(owner).transfer(address(this).balance);
  }

  /**
   * @dev Allows the owner to retrieve any ERC20 tokens that might be accidentally sent to the contract.
   * @param erc20s Array of ERC20 token addresses to be retrieved.
   */
  function sweepErc20(address[] memory erc20s) external onlyOwner {
    for (uint256 i = 0; i < erc20s.length; i++) {
      uint256 amount = IERC20(erc20s[i]).balanceOf(address(this));
      IERC20(erc20s[i]).transfer(owner, amount);
    }
  }

  /**
   * @dev Allows the owner to retrieve any ERC721 tokens that might be accidentally sent to the contract.
   * @param erc721s Array of ERC721 token addresses to be retrieved.
   * @param tokenIds 2D array of token IDs for each ERC721 token to be retrieved.
   */
  function sweepErc721(address[] memory erc721s, uint256[][] memory tokenIds) external onlyOwner {
    for (uint256 i = 0; i < erc721s.length; i++) {
      for (uint256 j = 0; j < tokenIds[i].length; j++) {
        IERC721(erc721s[i]).transferFrom(address(this), owner, tokenIds[i][j]);
      }
    }
  }

  /**
   * @dev Fetches the details of a range of vaults.
   * @param first The number of vaults to retrieve starting from `_after`.
   * @param _after The last vault ID to start from.
   * @return Arrays of EthVault, Erc20Vault, and Erc721Vault. Also returns a boolean indicating if there are more vaults.
   */
  function getVaults(uint256 first, uint256 _after) external view
  returns (
    EthVaultWithAnswerHash[] memory,
    Erc20VaultWithAnswerHash[] memory,
    Erc721VaultWithAnswerHash[] memory,
    bool hasMore
  ) {
    require(first > 0, "First must be greater than 0");

    uint256 ethCount = 0;
    uint256 erc20Count = 0;
    uint256 erc721Count = 0;

    // Count how many of each vault type we have in the specified range
    for (uint256 i = _after; i < _after + first && i < vaultsById.length; i++) {
      VaultById memory vaultInfo = vaultsById[i];
      if (vaultInfo.answerHash == 0) continue;
      uint256 combinedKey = _getCombinedKey(vaultInfo.answerHash, i);
      if (vaultInfo.vaultType == 0) {
        ethCount++;
      } else if (vaultInfo.vaultType == 1) {
        erc20Count++;
      } else if (vaultInfo.vaultType == 2) {
        erc721Count++;
      }
    }

    EthVaultWithAnswerHash[] memory _ethVaults = new EthVaultWithAnswerHash[](ethCount);
    Erc20VaultWithAnswerHash[] memory _erc20Vaults = new Erc20VaultWithAnswerHash[](erc20Count);
    Erc721VaultWithAnswerHash[] memory _erc721Vaults = new Erc721VaultWithAnswerHash[](erc721Count);

    uint256 ethCounter = 0;
    uint256 erc20Counter = 0;
    uint256 erc721Counter = 0;

    // Populate the arrays
    for (uint256 i = _after; i < _after + first && i < vaultsById.length; i++) {
      VaultById memory vaultInfo = vaultsById[i];
      uint256 combinedKey = _getCombinedKey(vaultInfo.answerHash, i);
      if (vaultInfo.vaultType == 0 && ethCounter < ethCount) {
        EthVault memory ethVault = ethVaults[combinedKey];
        _ethVaults[ethCounter++] = EthVaultWithAnswerHash(
          ethVault.id,
          vaultInfo.answerHash,
          ethVault.amount,
          ethVault.claimer
        );
      } else if (vaultInfo.vaultType == 1 && erc20Counter < erc20Count) {
        Erc20Vault memory erc20Vault = erc20Vaults[combinedKey];
        _erc20Vaults[erc20Counter++] = Erc20VaultWithAnswerHash(
          erc20Vault.id,
          vaultInfo.answerHash,
          erc20Vault.amount,
          erc20Vault.addr,
          erc20Vault.claimer
        );
      } else if (vaultInfo.vaultType == 2 && erc721Counter < erc721Count) {
        Erc721Vault memory erc721Vault = erc721Vaults[combinedKey];
        _erc721Vaults[erc721Counter++] = Erc721VaultWithAnswerHash(
          erc721Vault.id,
          vaultInfo.answerHash,
          erc721Vault.addr,
          erc721Vault.tokenId,
          erc721Vault.claimer
        );
      }
    }

    hasMore = _after + first < vaultsById.length;
    return (_ethVaults, _erc20Vaults, _erc721Vaults, hasMore);
  }

  /**
   * @notice External function to receive Ether.
   * @dev This function is intentionally left empty, allowing the contract to accept plain Ether transfers.
   */
  receive() external payable {}

  /**
   * @dev Handles the receipt of a single ERC721 token type.
   *
   * @param operator The address which called `safeTransferFrom` function
   * @param from The address which previously owned the token
   * @param tokenId The NFT identifier which is being transferred
   * @param data Additional data with no specified format
   * @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
   */
  function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) public override returns (bytes4) {
    return this.onERC721Received.selector;
  }
}