// File: @openzeppelin/contracts/proxy/Clones.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (proxy/Clones.sol)

pragma solidity ^0.8.0;

/**
 * @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for
 * deploying minimal proxy contracts, also known as "clones".
 *
 * > To simply and cheaply clone contract functionality in an immutable way, this standard specifies
 * > a minimal bytecode implementation that delegates all calls to a known, fixed address.
 *
 * The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2`
 * (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the
 * deterministic method.
 *
 * _Available since v3.4._
 */
library Clones {
    /**
     * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
     *
     * This function uses the create opcode, which should never revert.
     */
    function clone(address implementation) internal returns (address instance) {
        /// @solidity memory-safe-assembly
        assembly {
            // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
            // of the `implementation` address with the bytecode before the address.
            mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
            // Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
            mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
            instance := create(0, 0x09, 0x37)
        }
        require(instance != address(0), "ERC1167: create failed");
    }

    /**
     * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
     *
     * This function uses the create2 opcode and a `salt` to deterministically deploy
     * the clone. Using the same `implementation` and `salt` multiple time will revert, since
     * the clones cannot be deployed twice at the same address.
     */
    function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
        /// @solidity memory-safe-assembly
        assembly {
            // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
            // of the `implementation` address with the bytecode before the address.
            mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
            // Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
            mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
            instance := create2(0, 0x09, 0x37, salt)
        }
        require(instance != address(0), "ERC1167: create2 failed");
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
     */
    function predictDeterministicAddress(
        address implementation,
        bytes32 salt,
        address deployer
    ) internal pure returns (address predicted) {
        /// @solidity memory-safe-assembly
        assembly {
            let ptr := mload(0x40)
            mstore(add(ptr, 0x38), deployer)
            mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
            mstore(add(ptr, 0x14), implementation)
            mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
            mstore(add(ptr, 0x58), salt)
            mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
            predicted := keccak256(add(ptr, 0x43), 0x55)
        }
    }

    /**
     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
     */
    function predictDeterministicAddress(
        address implementation,
        bytes32 salt
    ) internal view returns (address predicted) {
        return predictDeterministicAddress(implementation, salt, address(this));
    }
}

// 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/FERC20.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

import "./lib/ERC20.sol";
import "./interfaces/IFERC20.sol";
import "./interfaces/IFERC721.sol";

contract FERC20 is ERC20, IFERC20 {
		address public immutable bridgeContract;

		constructor() {
			bridgeContract = msg.sender;
		}

    function initialize(address _ferc721Address) public {
				require(msg.sender == bridgeContract, "only bridge factory can initialize");
				name = string(abi.encodePacked(IFERC721(_ferc721Address).symbol(), " {ferc-20}"));
			  symbol = IFERC721(_ferc721Address).symbol();
				decimals = 18;
    }

    function mint(address account, uint256 amount) public {
				require(msg.sender == bridgeContract, "only bridge factory can mint");
				_mint(account, amount);
		}
}

// File: contracts/Ferc721Bridge.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
 
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/proxy/Clones.sol";
import "./interfaces/IFERC721.sol";
import "./interfaces/IFERC20.sol";
import "./FERC20.sol";

error NotCreatedByFactory();
error MintNotFinished();
error NotApprovedForAll();
error NotCreatedByBridge();
error AllowanceNotEnough();
error BalanceNotEnough();
error NotMultipleOfLimit();
error WithdrawAmountIsZero();

contract Ferc721Bridge {
		address public immutable factoryContractAddress;
		address public immutable tokenImplementation;

		mapping(address => uint64[]) public nftTokenIds;
		mapping(address => address) public ferc20Addresses; // ferc-721 => ferc-20 address
		mapping(address => address) public ferc721Addresses;  // ferc-20 => ferc-721 address

		event DeployERC20(address operator, address indexed from, address indexed ferc721Address, address indexed ferc20Address, uint256 limit);
		event ReceivedNFT(address operator, address indexed from, uint256 tokenId, address indexed ferc721Address, address indexed ferc20Address, uint256 limit);
		event ReceivedToken(address indexed from, uint256 ferc20Amount, uint256 ferc721Amount, address indexed ferc20Address, address indexed ferc721Address, uint256 limit);

		constructor(address _factoryContractAddress) {
			factoryContractAddress = _factoryContractAddress;
			tokenImplementation = address(new FERC20());
		}

    function withdraw(address ferc20Address, uint256 amount) public returns (bool) {
				IFERC20 ferc20 = IFERC20(ferc20Address);
				if(ferc20.bridgeContract() != address(this)) revert NotCreatedByBridge();
				if(ferc20.allowance(msg.sender, address(this)) < amount) revert AllowanceNotEnough();
				if(ferc20.balanceOf(msg.sender) < amount) revert BalanceNotEnough();

				address ferc721Address = ferc721Addresses[ferc20Address];
				IFERC721 ferc721 = IFERC721(ferc721Address);
				uint limit = ferc721.limit();
				if(amount / 1e18 % limit != 0) revert NotMultipleOfLimit();
				uint256 amountOfFERC721 = amount / limit / 1e18;
				if(amountOfFERC721 == 0) revert WithdrawAmountIsZero();
	
				// return back to depositer
				for(uint i; i < amountOfFERC721; i++) {
					ferc721.safeTransferFrom(address(this), msg.sender, nftTokenIds[ferc721Address][nftTokenIds[ferc721Address].length - 1]);
					nftTokenIds[ferc721Address].pop();
				}

				// burn ferc20
				ferc20.transferFrom(msg.sender, address(this), amount);
				ferc20.transfer(address(0x0), amount);

				emit ReceivedToken(msg.sender, amount, amountOfFERC721, msg.sender, ferc721Address, limit);
        return true;
    }

		function batchDeposit(address inscriptionAddress, uint[] calldata ids) public {
			IFERC721 inscription = IFERC721(inscriptionAddress);
			IFERC721.TokenData memory tokenData = inscription.tokenData();
			if(inscription.factoryContract() != factoryContractAddress) revert NotCreatedByFactory();
			if(tokenData.totalSupply < tokenData.max / tokenData.limit) revert MintNotFinished();
			if(!inscription.isApprovedForAll(msg.sender, address(this))) revert NotApprovedForAll();

			address ferc20Address = ferc20Addresses[inscriptionAddress];
			if(ferc20Address == address(0x0)) {
				ferc20Address = Clones.clone(tokenImplementation);
				FERC20(ferc20Address).initialize(inscriptionAddress);
				ferc721Addresses[ferc20Address] = inscriptionAddress;
				ferc20Addresses[inscriptionAddress] = ferc20Address;
				emit DeployERC20(msg.sender, msg.sender, inscriptionAddress, ferc20Address, tokenData.limit);
			}

			uint len = ids.length;
			for(uint i; i < len; i++) {
				inscription.transferFrom(msg.sender, address(this), ids[i]);
				nftTokenIds[inscriptionAddress].push(uint64(ids[i]));
				emit ReceivedNFT(msg.sender, msg.sender, ids[i], inscriptionAddress, ferc20Address, tokenData.limit);
			}
			FERC20(ferc20Address).mint(msg.sender, uint(tokenData.limit) * 1e18 * ids.length);
		}
		
		function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) public returns (bytes4) {
				if(IFERC721(msg.sender).factoryContract() != factoryContractAddress) revert NotCreatedByFactory();
				if(IFERC721(msg.sender).totalSupply() < IFERC721(msg.sender).max() / IFERC721(msg.sender).limit()) revert MintNotFinished();

				address ferc20Address = ferc20Addresses[msg.sender];
				if(ferc20Address == address(0x0)) {
					ferc20Address = Clones.clone(tokenImplementation);
					FERC20(ferc20Address).initialize(msg.sender);
					ferc721Addresses[ferc20Address] = msg.sender;
					ferc20Addresses[msg.sender] = ferc20Address;
					emit DeployERC20(operator, from, msg.sender, ferc20Address, IFERC721(msg.sender).limit());
				}
				nftTokenIds[msg.sender].push(uint64(tokenId));

				FERC20(ferc20Address).mint(from, IFERC721(msg.sender).limit() * 1e18);

				emit ReceivedNFT(operator, from, tokenId, msg.sender, ferc20Address, IFERC721(msg.sender).limit());
				return IERC721Receiver.onERC721Received.selector;
		}
}

// File: contracts/interfaces/IFERC20.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IFERC20 is IERC20 {
	function bridgeContract() external view returns(address);
	function mint(address account, uint256 amount) external;
}
// File: contracts/interfaces/IFERC721.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

interface IFERC721 is IERC721 {
	struct TokenData {
		uint64  max;
		uint64  totalSupply;
		bool    needFerc;
		uint24  inscriptionId;
		uint24  limit;
		bytes9  tick;
	}

	function factoryContract() external view returns(address);
	function swapContract() external view returns(address);
	function wethContract() external view returns(address);
	function name() external view returns(string memory);
	function symbol() external view returns(string memory);
	function totalSupply() external view returns(uint);
	function max() external view returns(uint);
	function limit() external view returns(uint);
	function needFerc() external view returns(bool);
	function inscriptionId() external view returns(uint);
	function tokenData() external view returns(TokenData memory);
	function lastMintTimestamp(address addr) external view returns (uint);
}

// File: contracts/lib/ERC20.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract ERC20 is IERC20 {
    uint public totalSupply;
    mapping(address => uint) public balanceOf;
    mapping(address => mapping(address => uint)) public allowance;
    string public name;
    string public symbol;
    uint8 public decimals;

    function transfer(address recipient, uint amount) external returns (bool) {
				balanceOf[msg.sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(msg.sender, recipient, amount);
        return true;
    }

    function approve(address spender, uint amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(
        address sender,
        address recipient,
        uint amount
    ) external returns (bool) {
        allowance[sender][msg.sender] -= amount;
        balanceOf[sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(sender, recipient, amount);
        return true;
    }

    function _mint(address account, uint256 amount) internal {
        require(account != address(0), "ERC20: mint to the zero address");
        totalSupply += amount;
        unchecked {
            // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above.
            balanceOf[account] += amount;
        }
        emit Transfer(address(0), account, amount);
    }
}