// File: contracts/ERC20Locker.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.17;

interface IERC20 {
    function totalSupply() external view returns (uint256);

    function balanceOf(address account) external view returns (uint256);

    function transfer(
        address recipient,
        uint256 amount
    ) external returns (bool);

    function allowance(
        address owner,
        address spender
    ) external view returns (uint256);

    function approve(address spender, uint256 amount) external returns (bool);

    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );
}

interface PriceFeedInterface {
    function latestRoundData()
        external
        view
        returns (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answeredInRound
        );
}

pragma solidity 0.8.17;

import "./LockerPass.sol";

contract ERC20Locker {
    mapping(uint256 => DepositData) public deposits;
    LockerPass public lockerPass;

    struct DepositData {
        address token;
        uint256 createdAt;
        uint256 lockForDays;
        int256 minExpectedPrice;
        int256 pricePrecision;
        address priceFeed;
        uint256 balance;
    }

    modifier onlyDepositOwner(address _account, uint256 _depositId) {
        require(lockerPass.ownerOf(_depositId) == _account, "Access denied");
        require(deposits[_depositId].createdAt != 0, ERRNOTCONFIGURED);
        _;
    }

    modifier onlyConfigured(uint256 _depositId) {
        require(deposits[_depositId].createdAt != 0, ERRNOTCONFIGURED);
        _;
    }

    address private constant ZERO_ADDRESS = address(0x0);
    string private constant ERRBADCONFIG = "Invalid price configuration";
    string private constant ERRNOTCONFIGURED = "Deposit not configured.";

    constructor() {
        lockerPass = new LockerPass(address(this), "ERC20LockerPass", "LOP");
    }

    function configureDepositWithPrice(
        address _token,
        uint256 _lockForDays,
        address _priceFeed,
        int256 _minExpectedPrice,
        int256 _pricePrecision
    ) public {
        require(_minExpectedPrice >= 0, "Invalid minExpectedPrice value.");
        require(_lockForDays > 0, "Invalid lockForDays value.");

        // check feed address interface
        if (_minExpectedPrice == 0) {
            require(_priceFeed == ZERO_ADDRESS, ERRBADCONFIG);
        } else {
            require(_priceFeed != ZERO_ADDRESS, ERRBADCONFIG);
            // check feed address interface
            PriceFeedInterface(_priceFeed).latestRoundData();
        }

        DepositData memory newDeposit = DepositData({
            token: _token,
            createdAt: block.timestamp,
            minExpectedPrice: _minExpectedPrice,
            pricePrecision: _pricePrecision,
            priceFeed: _priceFeed,
            balance: 0,
            lockForDays: _lockForDays
        });

        uint256 newDepositId = lockerPass.nextId();
        lockerPass.mint(msg.sender);
        deposits[newDepositId] = newDeposit;
    }

    function configureDeposit(address _token, uint256 _lockForDays) external {
        configureDepositWithPrice(_token, _lockForDays, ZERO_ADDRESS, 0, 0);
    }

    function deposit(
        address _token,
        uint256 _amount,
        uint256 _depositId
    ) external onlyDepositOwner(msg.sender, _depositId) {
        DepositData storage depositData = deposits[_depositId];

        IERC20(_token).transferFrom(msg.sender, address(this), _amount);
        depositData.balance = depositData.balance + _amount;
    }

    function canWithdraw(
        uint256 _depositId
    ) public view onlyConfigured(_depositId) returns (bool) {
        DepositData memory depositData = deposits[_depositId];

        uint256 releaseAt = depositData.createdAt +
            (depositData.lockForDays * 1 days);

        if (releaseAt < block.timestamp) {
            return true;
        } else if (depositData.minExpectedPrice == 0) {
            return false;
        } else if (depositData.minExpectedPrice < getPrice(_depositId)) {
            return true;
        } else return false;
    }

    function withdraw(
        uint256 _depositId
    ) external onlyDepositOwner(msg.sender, _depositId) {
        require(canWithdraw(_depositId), "You cannot withdraw yet!");

        DepositData memory depositData = deposits[_depositId];
        uint256 balance = depositData.balance;

        delete deposits[_depositId];
        lockerPass.burn(_depositId);

        IERC20(depositData.token).transfer(msg.sender, balance);
    }

    function increaseMinExpectedPrice(
        int256 _newMinExpectedPrice,
        uint256 _depositId
    ) external onlyDepositOwner(msg.sender, _depositId) {
        DepositData storage depositData = deposits[_depositId];

        require(
            depositData.minExpectedPrice != 0,
            "minExpectedPrice not configured!"
        );

        require(
            depositData.minExpectedPrice < _newMinExpectedPrice,
            "New price value invalid!"
        );

        depositData.minExpectedPrice = _newMinExpectedPrice;
    }

    function increaseLockForDays(
        uint256 _newLockForDays,
        uint256 _depositId
    ) external onlyDepositOwner(msg.sender, _depositId) {
        DepositData storage depositData = deposits[_depositId];

        require(
            depositData.lockForDays < _newLockForDays,
            "New lockForDays value invalid!"
        );

        depositData.lockForDays = _newLockForDays;
    }

    function checkPriceFeed(
        address _feedAddress,
        int256 _precision
    ) external view returns (int256) {
        (, int256 price, , , ) = PriceFeedInterface(_feedAddress)
            .latestRoundData();
        return price / _precision;
    }

    function getPrice(
        uint256 _depositId
    ) public view onlyConfigured(_depositId) returns (int256) {
        DepositData memory depositData = deposits[_depositId];

        if (depositData.priceFeed == ZERO_ADDRESS) {
            return 0;
        }

        (, int256 price, , , ) = PriceFeedInterface(depositData.priceFeed)
            .latestRoundData();
        return price / depositData.pricePrecision;
    }
}

// File: contracts/LockerPass.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.17;

interface IERC721 {
    event Transfer(
        address indexed from,
        address indexed to,
        uint256 indexed tokenId
    );
    event Approval(
        address indexed owner,
        address indexed approved,
        uint256 indexed tokenId
    );
    event ApprovalForAll(
        address indexed owner,
        address indexed operator,
        bool approved
    );

    function balanceOf(address owner) external view returns (uint256 balance);

    function ownerOf(uint256 tokenId) external view returns (address owner);

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    function transferFrom(address from, address to, uint256 tokenId) external;

    function approve(address to, uint256 tokenId) external;

    function setApprovalForAll(address operator, bool approved) external;

    function getApproved(
        uint256 tokenId
    ) external view returns (address operator);

    function isApprovedForAll(
        address owner,
        address operator
    ) external view returns (bool);
}

interface IERC165 {
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

interface IERC721Receiver {
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

contract LockerPass is IERC721 {
    address public admin;
    uint256 public nextId = 0;
    string public name;
    string public symbol;
    mapping(uint256 => bool) public isFrozen;
    mapping(uint256 => address) private _owners;
    mapping(address => uint256) private _balances;
    mapping(uint256 => address) private _tokenApprovals;
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    error ERC721NonexistentToken(uint256 tokenId);
    error ERC721InvalidReceiver(address receiver);
    error ERC721InvalidAddress(address target);

    modifier requireMinted(uint256 _tokenId) {
        if (_ownerOf(_tokenId) == address(0)) {
            revert ERC721NonexistentToken(_tokenId);
        }
        _;
    }

    modifier onlyAdmin() {
        require(msg.sender == admin, "Access denied!");
        _;
    }

    modifier onlyOwner(uint256 _tokenId) {
        require(_ownerOf(_tokenId) == msg.sender, "Access denied!");
        _;
    }

    constructor(address _admin, string memory _name, string memory _symbol) {
        admin = _admin;
        name = _name;
        symbol = _symbol;
    }

    function supportsInterface(bytes4 interfaceId) public view returns (bool) {
        return
            interfaceId == type(IERC721).interfaceId ||
            interfaceId == type(IERC165).interfaceId;
    }

    function balanceOf(address _owner) external view returns (uint256) {
        if (_owner == address(0)) {
            revert ERC721InvalidAddress(_owner);
        }

        return _balances[_owner];
    }

    function ownerOf(uint256 _tokenId) external view returns (address) {
        return _ownerOf(_tokenId);
    }

    function _ownerOf(uint256 _tokenId) internal view returns (address) {
        return _owners[_tokenId];
    }

    function mint(address _to) external onlyAdmin {
        _owners[nextId] = _to;
        unchecked {
            _balances[_to] += 1;
        }

        nextId = nextId + 1;
    }

    function burn(uint256 _tokenId) external requireMinted(_tokenId) onlyAdmin {
        address owner = _ownerOf(_tokenId);
        _balances[owner] -= 1;
        _owners[_tokenId] = address(0);
    }

    function approve(
        address _to,
        uint256 _tokenId
    ) external requireMinted(_tokenId) onlyOwner(_tokenId) {
        emit Approval(msg.sender, _to, _tokenId);
        _tokenApprovals[_tokenId] = _to;
    }

    function getApproved(
        uint256 _tokenId
    ) external view requireMinted(_tokenId) returns (address) {
        return _tokenApprovals[_tokenId];
    }

    function setApprovalForAll(address _operator, bool _approved) public {
        emit ApprovalForAll(msg.sender, _operator, _approved);
        _operatorApprovals[msg.sender][_operator] = _approved;
    }

    function isApprovedForAll(
        address _owner,
        address _operator
    ) external view returns (bool) {
        return _operatorApprovals[_owner][_operator];
    }

    function freeze(
        uint256 _tokenId
    ) public requireMinted(_tokenId) onlyOwner(_tokenId) {
        require(isFrozen[_tokenId] == false, "Token is already frozen!");
        isFrozen[_tokenId] = true;
    }

    function transferFrom(address _from, address _to, uint256 _tokenId) public {
        require(!isFrozen[_tokenId], "Token is frozen!");

        if (_to == address(0)) {
            revert ERC721InvalidReceiver(_to);
        }
        _checkAuthorized(msg.sender, _tokenId);

        if (_from != address(0)) {
            unchecked {
                _balances[_from] -= 1;
            }
        }

        if (_to != address(0)) {
            unchecked {
                _balances[_to] += 1;
            }
        }

        emit Transfer(_from, _to, _tokenId);
        _owners[_tokenId] = _to;
    }

    function transferFromAndFreeze(
        address _from,
        address _to,
        uint256 _tokenId
    ) public onlyOwner(_tokenId) {
        transferFrom(_from, _to, _tokenId);
        isFrozen[_tokenId] = true;
    }

    function safeTransferFrom(
        address _from,
        address _to,
        uint256 _tokenId
    ) public {
        _checkOnERC721Received(_from, _to, _tokenId, "");
        transferFrom(_from, _to, _tokenId);
    }

    function safeTransferFrom(
        address _from,
        address _to,
        uint256 _tokenId,
        bytes calldata _data
    ) public {
        _checkOnERC721Received(_from, _to, _tokenId, _data);
        transferFrom(_from, _to, _tokenId);
    }

    function safeTransferFromAndFreeze(
        address _from,
        address _to,
        uint256 _tokenId
    ) external onlyOwner(_tokenId) {
        safeTransferFrom(_from, _to, _tokenId);
        isFrozen[_tokenId] = true;
    }

    function safeTransferFromAndFreeze(
        address _from,
        address _to,
        uint256 _tokenId,
        bytes calldata _data
    ) external onlyOwner(_tokenId) {
        safeTransferFrom(_from, _to, _tokenId, _data);
        isFrozen[_tokenId] = true;
    }

    function _checkAuthorized(
        address _operator,
        uint256 _tokenId
    ) internal view {
        address tokenOwner = _owners[_tokenId];
        bool isOwner = tokenOwner == _operator;
        bool isApproved = _tokenApprovals[_tokenId] == _operator;
        bool approvedForAll = _operatorApprovals[tokenOwner][_operator];
        require(isOwner || isApproved || approvedForAll, "Access denied");
    }

    function _checkOnERC721Received(
        address _from,
        address _to,
        uint256 _tokenId,
        bytes memory _data
    ) private {
        if (_to.code.length > 0) {
            try
                IERC721Receiver(_to).onERC721Received(
                    msg.sender,
                    _from,
                    _tokenId,
                    _data
                )
            returns (bytes4 retval) {
                if (retval != IERC721Receiver.onERC721Received.selector) {
                    revert ERC721InvalidReceiver(_to);
                }
            } catch (bytes memory reason) {
                if (reason.length == 0) {
                    revert ERC721InvalidReceiver(_to);
                } else {
                    /// @solidity memory-safe-assembly
                    assembly {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
        }
    }
}