// File: contracts/Factory.sol




// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import './Pool.sol';

/**
 * @notice This contract is responsible for deploying new pools and providing them with the current fee and fee recipient
 */
contract Factory {

    address public operator;
    address public pendingOperator;
    address public feeRecipient;
    uint public feePoleis;
    uint public constant MAX_FEE_POLEIS = 0.1e18;
    bytes16 private constant _SYMBOLS = "0123456789abcdef";
    bytes32 public immutable POOL_SYMBOL_PREFIX;
    mapping (Pool => bool) public isPool;
    Pool[] public pools;

    constructor(address _operator, string memory _poolSymbolPrefix) {
        operator = _operator;
        POOL_SYMBOL_PREFIX = pack(_poolSymbolPrefix);
    }

    function pack(string memory unpacked) internal pure returns (bytes32 packed) {
        require (bytes(unpacked).length < 32);
        assembly {
            packed := mload (add (unpacked, 31))
        }
    }

    function unpack(bytes32 packed) internal pure returns (string memory unpacked) {
        uint l = uint (packed >> 248);
        require (l < 32);
        unpacked = string (new bytes (l));
        assembly {
            mstore (add (unpacked, 31), packed) // Potentially writes into unallocated memory, which is fine
        }
    }

    /**
     * @notice Get the number of deployed pools
     * @return uint number of deployed pools
     * @dev Useful for iterating on all pools
     */
    function getPoolsLength() external view returns (uint) {
        return pools.length;
    }

    /**
     * @notice Set the fee recipient for all pools
     * @param _feeRecipient address of the new fee recipient
     * @dev Only callable by the operator
     */
    function setFeeRecipient(address _feeRecipient) external {
        require(msg.sender == operator, "Factory: not operator");
        require(_feeRecipient != address(0), "Factory: feeRecipient zero");
        feeRecipient = _feeRecipient;
    }

    /**
     * @notice Transfers pool tokens to the recipient
     * @param _feePoleis Transfers pool tokens to the recipient
     * @dev 
     */
    function setFeePoleis(uint _feePoleis) external {
        require(msg.sender == operator, "Factory: not operator");
        require(_feePoleis <= MAX_FEE_POLEIS, "Factory: fee too high");
        if(_feePoleis > 0) require(feeRecipient != address(0), "Factory: fee recipient is zero address");
        feePoleis = _feePoleis;
    }

    /**
     * @notice Set a pending operator for the factory
     * @param _pendingOperator address of the new pending operator
     * @dev Only callable by the operator
     */
    function setPendingOperator(address _pendingOperator) external {
        require(msg.sender == operator, "Factory: not operator");
        pendingOperator = _pendingOperator;
    }

    /**
     * @notice Accept the pending operator
     * @dev Only callable by the pending operator
     */
    function acceptOperator() external {
        require(msg.sender == pendingOperator, "Factory: not pending operator");
        operator = pendingOperator;
        pendingOperator = address(0);
    }

    /**
     * @notice 
     * @return address 
     * @dev
     */
    function getFee() external view returns (address, uint) {
        uint _feePoleis = feePoleis;
        if(_feePoleis == 0) return (address(0), 0);
        return (feeRecipient, _feePoleis);
    }

    /**
     * @dev Return the log in base 10, rounded down, of a positive value. Returns 0 if given 0.
     */
    function log10(uint256 value) internal pure returns (uint256) {
        uint256 result = 0;
        unchecked {
            if (value >= 10 ** 64) {
                value /= 10 ** 64;
                result += 64;
            }
            if (value >= 10 ** 32) {
                value /= 10 ** 32;
                result += 32;
            }
            if (value >= 10 ** 16) {
                value /= 10 ** 16;
                result += 16;
            }
            if (value >= 10 ** 8) {
                value /= 10 ** 8;
                result += 8;
            }
            if (value >= 10 ** 4) {
                value /= 10 ** 4;
                result += 4;
            }
            if (value >= 10 ** 2) {
                value /= 10 ** 2;
                result += 2;
            }
            if (value >= 10 ** 1) {
                result += 1;
            }
        }
        return result;
    }    

    /**
     * @dev Converts a `uint256` to its ASCII `string` decimal representation.
     */
    function toString(uint256 value) internal pure returns (string memory) {
        unchecked {
            uint256 length = log10(value) + 1;
            string memory buffer = new string(length);
            uint256 ptr;
            /// @solidity memory-safe-assembly
            assembly {
                ptr := add(buffer, add(32, length))
            }
            while (true) {
                ptr--;
                /// @solidity memory-safe-assembly
                assembly {
                    mstore8(ptr, byte(mod(value, 10), _SYMBOLS))
                }
                value /= 10;
                if (value == 0) break;
            }
            return buffer;
        }
    }

    /**
     * @notice Transfers pool tokens to the recipient
     * @param _collateralToken Transfers pool tokens to the recipient
     * @param _loanToken Transfers pool tokens to the recipient
     * @param _maxCollateralRatioPoleis Transfers pool tokens to the recipient
     * @param _financePoleis Transfers pool tokens to the recipient
     * @param _collateralRatioFallDuration Transfers pool tokens to the recipient
     * @param _collateralRatioRecoveryDuration Transfers pool tokens to the recipient
     * @param _minRatePoleis Transfers pool tokens to the recipient
     * @param _financeRatePoleis Transfers pool tokens to the recipient
     * @param _maxRatePoleis Transfers pool tokens to the recipient
     * @return Pool Transfers pool tokens to the recipient
     */
    function deployFinancePool(
        IERC20 _collateralToken,
        IERC20 _loanToken,
        uint _maxCollateralRatioPoleis,
        uint _financePoleis,
        uint _collateralRatioFallDuration,
        uint _collateralRatioRecoveryDuration,
        uint _minRatePoleis,
        uint _financeRatePoleis,
        uint _maxRatePoleis
    ) external returns (Pool) {
        string memory poolNumberString = toString(pools.length);
        string memory prefix = unpack(POOL_SYMBOL_PREFIX);
        Pool pool = new Pool(
            string(abi.encodePacked(prefix, poolNumberString)),
            string(abi.encodePacked("Poleis ", prefix, poolNumberString, " Pool")),
            _collateralToken,
            _loanToken,
            _maxCollateralRatioPoleis,
            _financePoleis,
            _collateralRatioFallDuration,
            _collateralRatioRecoveryDuration,
            _minRatePoleis,
            _financeRatePoleis,
            _maxRatePoleis
        );
        isPool[pool] = true;
        emit PoolDeployed(pools.length, address(pool), address(_collateralToken), address(_loanToken), _maxCollateralRatioPoleis, _financePoleis, _collateralRatioFallDuration, _collateralRatioRecoveryDuration, _minRatePoleis, _financeRatePoleis, _maxRatePoleis);
        pools.push(pool);
        return pool;
    }
    event PoolDeployed(uint poolId, address pool, address indexed collateralToken, address indexed loanToken, uint indexed maxCollateralRatioPoleis, uint financePoleis, uint collateralRatioFallDuration, uint collateralRatioRecoveryDuration, uint minRatePoleis, uint financeRatePoleis, uint maxRatePoleis);
}
// File: contracts/Pool.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

interface IFactory {
    function getFee() external view returns (address to, uint feePoleis);
}

interface IERC20 {
    function balanceOf(address) external view returns(uint);
    function transferFrom(address sender, address recipient, uint256 amount) external returns(bool);
    function transfer(address, uint) external returns (bool);
    function decimals() external view returns (uint8);
}

contract Pool {

    IFactory public immutable FACTORY;
    IERC20 public immutable COLLATERAL_TOKEN;
    IERC20 public immutable LOAN_TOKEN;
    string public symbol;
    string public name;
    uint8 public constant decimals = 18;
    uint private constant RATE_CEILING = 100e18; 
    uint public constant MINIMUM_LIQUIDITY = 10 ** 3;
    uint public constant MIN_LOCKUP_DURATION = 30 minutes;
    uint public immutable MIN_RATE;
    uint public immutable FINANCE_RATE;
    uint public immutable MAX_RATE;
    uint public immutable MAX_COLLATERAL_RATIO_POLEIS;
    uint public immutable FINANCE_POLEIS;
    uint public immutable COLLATERAL_RATIO_FALL_DURATION;
    uint public immutable COLLATERAL_RATIO_RECOVERY_DURATION;
    bytes4 private constant TRANSFER_SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
    bytes4 private constant TRANSFER_FROM_SELECTOR = bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
    uint public lastCollateralRatioPoleis;
    uint public debtSharesSupply;
    mapping (address => uint) public debtSharesBalanceOf;
    uint public lastTotalDebt;
    uint public lastAccrueInterestTime;
    uint public totalSupply;
    mapping (address => mapping (address => uint)) public allowance;
    mapping (address => uint) public balanceOf;
    mapping (address => uint) public collateralBalanceOf;
    uint public lastLoanTokenBalance;
    mapping (address => uint) public lastDepositTimestamp;

    constructor(
        string memory _symbol,
        string memory _name,
        IERC20 _collateralToken,
        IERC20 _loanToken,
        uint _maxCollateralRatioPoleis,
        uint _financePoleis,
        uint _collateralRatioFallDuration,
        uint _collateralRatioRecoveryDuration,
        uint _minRatePoleis,
        uint _financeRatePoleis,
        uint _maxRatePoleis
    ) {
        require(_collateralToken != _loanToken, "Pool: collateral and loan tokens are the same");
        require(_collateralRatioFallDuration > 0, "Pool: _collateralRatioFallDuration too low");
        require(_collateralRatioFallDuration <= _maxCollateralRatioPoleis, "Pool: _collateralRatioFallDuration too high");
        require(_collateralRatioRecoveryDuration > 0, "Pool: _collateralRatioRecoveryDuration too low");
        require(_collateralRatioRecoveryDuration <= _maxCollateralRatioPoleis, "Pool: _collateralRatioRecoveryDuration too high");
        require(_maxCollateralRatioPoleis> 0, "Pool: _maxCollateralRatioPoleis too low");
        require(_financePoleis < 1e18, "Pool: _financePoleis too high");
        require(_minRatePoleis <= _financeRatePoleis, "Pool: _minRatePoleis too high");
        require(_financeRatePoleis <= _maxRatePoleis, "Pool: _financeRatePoleis too high");
        require(_maxRatePoleis <= RATE_CEILING, "Pool: _maxRatePoleis too high");
        symbol = _symbol;
        name = _name;
        FACTORY = IFactory(msg.sender);
        COLLATERAL_TOKEN = _collateralToken;
        LOAN_TOKEN = _loanToken;
        MAX_COLLATERAL_RATIO_POLEIS = _maxCollateralRatioPoleis;
        FINANCE_POLEIS = _financePoleis;
        COLLATERAL_RATIO_FALL_DURATION = _collateralRatioFallDuration;
        COLLATERAL_RATIO_RECOVERY_DURATION = _collateralRatioRecoveryDuration;
        lastCollateralRatioPoleis = _maxCollateralRatioPoleis;
        MIN_RATE = _minRatePoleis;
        FINANCE_RATE = _financeRatePoleis;
        MAX_RATE = _maxRatePoleis;
    }

    function safeTransfer(IERC20 token, address to, uint value) internal {
        (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(TRANSFER_SELECTOR, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'Pool: TRANSFER_FAILED');
    }

    function safeTransferFrom(IERC20 token, address from, address to, uint value) internal {
        (bool success, bytes memory data) = address(token).call(abi.encodeWithSelector(TRANSFER_FROM_SELECTOR, from, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'Pool: TRANSFER_FROM_FAILED');
    }

    /**
     * @notice Gets the current state of pool variables based on the current time
     * @param _loanTokenBalance The current balance of the loan token in the pool
     * @param _feePoleis The fee to be charged on interest accrual
     * @param _lastCollateralRatioPoleis The collateral ratio at the last interest accrual
     * @param _totalSupply The total supply of pool tokens at the last interest accrual
     * @param _lastAccrueInterestTime The last time interest was accrued
     * @param _totalDebt The total debt of the pool at the last interest accrual
     * @return _currentTotalSupply The current total supply of pool tokens
     * @return _accruedFeeShares The accrued fee shares to be transferred to the fee recipient
     * @return _currentCollateralRatioPoleis The current collateral ratio
     * @return _currentTotalDebt The current total debt of the pool
     * @dev This view function behaves as a pure function with the exception of immutable variables (which are constant)
     */
    function getCurrentState(
        uint _loanTokenBalance,
        uint _feePoleis,
        uint _lastCollateralRatioPoleis,
        uint _totalSupply,
        uint _lastAccrueInterestTime,
        uint _totalDebt
        ) internal view returns (
            uint _currentTotalSupply,
            uint _accruedFeeShares,
            uint _currentCollateralRatioPoleis,
            uint _currentTotalDebt
        ) {
        
        /**
         * @dev 1. Set default return values
         */
        _currentTotalSupply = _totalSupply;
        _currentTotalDebt = _totalDebt;
        _currentCollateralRatioPoleis = _lastCollateralRatioPoleis;
        // _accruedFeeShares = 0;

        /**
         * @dev 2. Get the time passed since the last interest accrual
         */
        uint _timeDelta = block.timestamp - _lastAccrueInterestTime;
        
        /**
         * @dev 3. If the time passed is 0, return the current values
         */
        if(_timeDelta == 0) return (_currentTotalSupply, _accruedFeeShares, _currentCollateralRatioPoleis, _currentTotalDebt);
        
        /**
         * @dev 4. Calculate the supplied value
         */
        uint _supplied = _totalDebt + _loanTokenBalance;

        /**
         * @dev 5. Calculate the utilization
         */
        uint _util = getUtilizationPoleis(_totalDebt, _supplied);

        /**
         * @dev 6. Calculate the collateral ratio
         */
        _currentCollateralRatioPoleis = getCollateralRatioPoleis(
            _util,
            _lastAccrueInterestTime,
            block.timestamp,
            _lastCollateralRatioPoleis,
            COLLATERAL_RATIO_FALL_DURATION,
            COLLATERAL_RATIO_RECOVERY_DURATION,
            MAX_COLLATERAL_RATIO_POLEIS,
            FINANCE_POLEIS
        );

        /**
         * @dev 7. If there is no debt, return the current values
         */
        if(_totalDebt == 0) return (_currentTotalSupply, _accruedFeeShares, _currentCollateralRatioPoleis, _currentTotalDebt);

        /**
         * @dev 8. Calculate the borrow rate
         */
        uint _borrowRate = getBorrowRatePoleis(_util, FINANCE_POLEIS, MIN_RATE, FINANCE_RATE, MAX_RATE);
        
        /**
         * @dev 9. Calculate the interest
         */
        uint _interest = _totalDebt * _borrowRate * _timeDelta / (365 days * 1e18); // does the optimizer optimize this? or should it be a constant?
        
        /**
         * @dev 10. Update the total debt
         */
        _currentTotalDebt += _interest;
        
        /**
         * @dev 11. If there is no fee, return the current values
         */
        if(_feePoleis == 0) return (_currentTotalSupply, _accruedFeeShares, _currentCollateralRatioPoleis, _currentTotalDebt);
        
        /**
         * @dev 12. Calculate the fee
         */
        uint fee = _interest * _feePoleis / 1e18;
        
        /**
         * @dev 13. Calculate the accrued fee shares
         */
        _accruedFeeShares = fee * _totalSupply / (_supplied + _interest - fee); 
        
        /**
         * @dev 14. Update the total supply
         */
        _currentTotalSupply += _accruedFeeShares;
    }

    /**
     * @notice Gets the current borrow rate in Poleis (scaled by 1e18)
     * @param _util The utilization in Poleis (scaled by 1e18)
     * @param _financePoleis The utilization at which the borrow rate will be at the finance rate in Poleis (scaled by 1e18)
     * @param _minRatePoleis The minimum borrow rate at 0% utilization in Poleis (scaled by 1e18)
     * @param _financeRatePoleis The borrow rate at the finance utilization in Poleis (scaled by 1e18)
     * @param _maxRatePoleis The maximum borrow rate at 100% utilization in Poleis (scaled by 1e18)
     * @return uint The borrow rate in Poleis (scaled by 1e18)
     */
    function getBorrowRatePoleis(uint _util, uint _financePoleis, uint _minRatePoleis, uint _financeRatePoleis, uint _maxRatePoleis) internal pure returns (uint) {
        if(_util <= _financePoleis) {
            return (_financeRatePoleis - _minRatePoleis) * 1e18 * _util / _financePoleis / 1e18 + _minRatePoleis; 
        } else {
            uint excessUtil = _util - _financePoleis;
            return (_maxRatePoleis - _financeRatePoleis) * 1e18 * excessUtil / (1e18 - _financePoleis) / 1e18 + _financeRatePoleis; 
        }
    }

    /**
     * @notice Gets the current pool utilization rate in Poleis (scaled by 1e18)
     * @param _totalDebt The total debt of the pool
     * @param _supplied The total supplied loan tokens of the pool
     * @return uint The pool utilization rate in Poleis (scaled by 1e18)
     */
    function getUtilizationPoleis(uint _totalDebt, uint _supplied) internal pure returns (uint) {
        if(_supplied == 0) return 0;
        return _totalDebt * 1e18 / _supplied;
    }

    /**
     * @notice Converts a loan token amount to shares
     * @param _tokenAmount The loan token amount to convert
     * @param _supplied The total supplied loan tokens of the pool
     * @param _sharesTotalSupply The total supply of shares of the pool
     * @param roundUpCheck Whether to check and round up the shares amount
     * @return uint The shares amount
     */
    function tokenToShares(uint _tokenAmount, uint _supplied, uint _sharesTotalSupply, bool roundUpCheck) internal pure returns (uint) {
        if(_supplied == 0) return _tokenAmount;
        uint shares = _tokenAmount * _sharesTotalSupply / _supplied;
        if(roundUpCheck && shares * _supplied < _tokenAmount * _sharesTotalSupply) shares++;
        return shares;
    }

    /**
     * @notice Transfers pool tokens to the recipient
     * @param _util Transfers pool tokens to the recipient
     * @param _lastAccrueInterestTime Transfers pool tokens to the recipient
     * @param _now Transfers pool tokens to the recipient
     * @param _lastCollateralRatioPoleis Transfers pool tokens to the recipient
     * @param _collateralRatioFallDuration Transfers pool tokens to the recipient
     * @param _collateralRatioRecoveryDuration Transfers pool tokens to the recipient
     * @param _maxCollateralRatioPoleis Transfers pool tokens to the recipient
     * @param _financePoleis Transfers pool tokens to the recipient
     * @return uint 
     */
    function getCollateralRatioPoleis(
        uint _util,
        uint _lastAccrueInterestTime,
        uint _now,
        uint _lastCollateralRatioPoleis,
        uint _collateralRatioFallDuration,
        uint _collateralRatioRecoveryDuration,
        uint _maxCollateralRatioPoleis,
        uint _financePoleis
        ) internal pure returns (uint) {
        unchecked {
            if(_lastAccrueInterestTime == _now) return _lastCollateralRatioPoleis;
            
            // If utilization is less than or equal to finance, we are increasing collateral ratio
            if(_util <= _financePoleis) {
                // The collateral ratio can only increase if it is less than the max collateral ratio
                if(_lastCollateralRatioPoleis == _maxCollateralRatioPoleis) return _lastCollateralRatioPoleis;

                // If the collateral ratio can increase, we calculate the increase
                uint timeDelta = _now - _lastAccrueInterestTime;
                uint change = timeDelta * _maxCollateralRatioPoleis / _collateralRatioRecoveryDuration;

                // If the change in collateral ratio is greater than the max collateral ratio, we set the collateral ratio to the max collateral ratio
                if(_lastCollateralRatioPoleis + change >= _maxCollateralRatioPoleis) {
                    return _maxCollateralRatioPoleis;
                } else {
                    // Otherwise we increase the collateral ratio by the change
                    return _lastCollateralRatioPoleis + change;
                }
            } else {
                // If utilization is greater than the finance, we are decreasing collateral ratio
                // The collateral ratio can only decrease if it is greater than 0
                if(_lastCollateralRatioPoleis == 0) return 0;

                // If the collateral ratio can decrease, we calculate the decrease
                uint timeDelta = _now - _lastAccrueInterestTime;
                uint change = timeDelta * _maxCollateralRatioPoleis / _collateralRatioFallDuration;

                // If the change in collateral ratio is greater than the collateral ratio, we set the collateral ratio to 0
                if(_lastCollateralRatioPoleis <= change) {
                    return 0;
                } else {
                    // Otherwise we decrease the collateral ratio by the change
                    return _lastCollateralRatioPoleis - change;
                }
            }
        }
    }

    /// @notice Transfers pool tokens to the recipient
    /// @param to The address of the recipient
    /// @param amount The amount of pool tokens to transfer
    /// @return bool that indicates if the operation was successful
    function transfer(address to, uint amount) external returns (bool) {
        require(to != address(0), "Pool: to cannot be address 0");
        require(lastDepositTimestamp[msg.sender] + MIN_LOCKUP_DURATION <= block.timestamp, "Pool: cannot transfer within lockup duration");
        balanceOf[msg.sender] -= amount;
        unchecked {
            balanceOf[to] += amount;
        }
        emit Transfer(msg.sender, to, amount);
        return true;
    }

    /**
     * @notice Transfers pool tokens on behalf of one address to another
     * @param from The address of the sender
     * @param to The address of the recipient
     * @param amount The amount of pool tokens to transfer
     * @return bool that indicates if the operation was successful
     */
    function transferFrom(address from, address to, uint amount) external returns (bool) {
        require(to != address(0), "Pool: to cannot be address 0");
        require(lastDepositTimestamp[from] + MIN_LOCKUP_DURATION <= block.timestamp, "Pool: cannot transfer within lockup duration");
        if(from != msg.sender) {
            allowance[from][msg.sender] -= amount;
        }
        balanceOf[from] -= amount;
        unchecked {
            balanceOf[to] += amount;
        }
        emit Transfer(from, to, amount);
        return true;
    }

    /**
     * @notice Approves an address to spend pool tokens on behalf of the sender
     * @param spender The address of the spender
     * @param amount The amount of pool tokens to approve
     * @return bool that indicates if the operation was successful
     */
    function approve(address spender, uint amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    /**
     * @notice Increases the allowance of an address to spend tokens on behalf of the sender
     * @param spender The address of the spender
     * @param addedValue The amount of tokens to increase the allowance by 
     */
    function increaseAllowance(address spender, uint addedValue) external returns (bool) {
        allowance[msg.sender][spender] += addedValue;
        emit Approval(msg.sender, spender, allowance[msg.sender][spender]);
        return true;
    }

    /**
     * @notice Decreases the allowance of an address to spend tokens on behalf of the sender
     * @param spender The address of the spender
     * @param subtractedValue The amount of tokens to decrease the allowance by
     */
    function decreaseAllowance(address spender, uint subtractedValue) external returns (bool) {
        allowance[msg.sender][spender] -= subtractedValue;
        emit Approval(msg.sender, spender, allowance[msg.sender][spender]);
        return true;
    }

    /**
     * @notice Deposit loan tokens in exchange for pool tokens
     * @param amount The amount of loan tokens to deposit 
     */
    function deposit(uint amount) external {
        (address _feeRecipient, uint _feePoleis) = FACTORY.getFee();
        (  
            uint _currentTotalSupply,
            uint _accruedFeeShares,
            uint _currentCollateralRatioPoleis,
            uint _currentTotalDebt
        ) = getCurrentState(
            lastLoanTokenBalance,
            _feePoleis,
            lastCollateralRatioPoleis,
            totalSupply,
            lastAccrueInterestTime,
            lastTotalDebt
        );

        if(_currentTotalSupply == 0) {
            _currentTotalSupply = MINIMUM_LIQUIDITY;
            balanceOf[address(0)] = MINIMUM_LIQUIDITY;
            emit Transfer(address(0), address(0), MINIMUM_LIQUIDITY);
        }
        
        uint _shares = tokenToShares(amount, (_currentTotalDebt + lastLoanTokenBalance), _currentTotalSupply, false);
        require(_shares > 0, "Pool: 0 shares");
        _currentTotalSupply += _shares;

        // commit current state
        balanceOf[msg.sender] += _shares;
        totalSupply = _currentTotalSupply;
        // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest
        if (lastTotalDebt != _currentTotalDebt) {
            lastTotalDebt = _currentTotalDebt;
            lastAccrueInterestTime = block.timestamp;
        }
        lastCollateralRatioPoleis = _currentCollateralRatioPoleis;
        emit Deposit(msg.sender, amount);
        emit Transfer(address(0), msg.sender, _shares);
        if(_accruedFeeShares > 0) {
            balanceOf[_feeRecipient] += _accruedFeeShares;
            emit Transfer(address(0), _feeRecipient, _accruedFeeShares);
        }
        lastDepositTimestamp[msg.sender] = block.timestamp; // commiting the last commit time for the user to prevent transfers within the lockup duration

        // interactions
        safeTransferFrom(LOAN_TOKEN, msg.sender, address(this), amount);

        // sync balance
        lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this));
    }

    /**
     * @notice Withdraw loan tokens in exchange for pool tokens
     * @param amount The amount of loan tokens to withdraw
     * @dev If amount is type(uint).max, withdraws all loan tokens
     */
    function withdraw(uint amount) external {
        require(lastDepositTimestamp[msg.sender] + MIN_LOCKUP_DURATION <= block.timestamp, "Pool: cannot transfer within lockup duration");
        (address _feeRecipient, uint _feePoleis) = FACTORY.getFee();
        (  
            uint _currentTotalSupply,
            uint _accruedFeeShares,
            uint _currentCollateralRatioPoleis,
            uint _currentTotalDebt
        ) = getCurrentState(
            lastLoanTokenBalance,
            _feePoleis,
            lastCollateralRatioPoleis,
            totalSupply,
            lastAccrueInterestTime,
            lastTotalDebt
        );

        uint _shares;
        if (amount == type(uint).max) {
            amount = balanceOf[msg.sender] * (_currentTotalDebt + lastLoanTokenBalance) / _currentTotalSupply;
            _shares = balanceOf[msg.sender];
        } else {
            _shares = tokenToShares(amount, (_currentTotalDebt + lastLoanTokenBalance), _currentTotalSupply, true);
        }
        _currentTotalSupply -= _shares;

        // commit current state
        balanceOf[msg.sender] -= _shares;
        totalSupply = _currentTotalSupply;
        // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest
        if (lastTotalDebt != _currentTotalDebt) {
            lastTotalDebt = _currentTotalDebt;
            lastAccrueInterestTime = block.timestamp;
        }
        lastCollateralRatioPoleis = _currentCollateralRatioPoleis;
        emit Withdraw(msg.sender, amount);
        emit Transfer(msg.sender, address(0), _shares);
        if(_accruedFeeShares > 0) {
            balanceOf[_feeRecipient] += _accruedFeeShares;
            emit Transfer(address(0), _feeRecipient, _accruedFeeShares);
        }

        // interactions
        safeTransfer(LOAN_TOKEN, msg.sender, amount);

        // sync balance
        lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this));
    }

    /**
     * @notice Deposit collateral tokens
     * @param to The address to receive the collateral deposit
     * @param amount The amount of collateral tokens to deposit 
     */
    function addCollateral(address to, uint amount) external {
        collateralBalanceOf[to] += amount;
        safeTransferFrom(COLLATERAL_TOKEN, msg.sender, address(this), amount);
        emit AddCollateral(to, msg.sender, amount);
    }

    /**
     * @notice Gets the debt of a user
     * @param _userDebtShares The amount of debt shares of the user
     * @param _debtSharesSupply The total amount of debt shares
     * @param _totalDebt The total amount of debt
     * @return uint The debt of the user
     */
    function getDebtOf(uint _userDebtShares, uint _debtSharesSupply, uint _totalDebt) internal pure returns (uint) {
        if (_debtSharesSupply == 0) return 0;
        uint debt = _userDebtShares * _totalDebt / _debtSharesSupply;
        if(debt * _debtSharesSupply < _userDebtShares * _totalDebt) debt++;
        return debt;
    }
    
    /**
     * @notice Withdraw collateral tokens
     * @param amount The amount of collateral tokens to withdraw
     */
    function removeCollateral(uint amount) external {
        (address _feeRecipient, uint _feePoleis) = FACTORY.getFee();
        (  
            uint _currentTotalSupply,
            uint _accruedFeeShares,
            uint _currentCollateralRatioPoleis,
            uint _currentTotalDebt
        ) = getCurrentState(
            lastLoanTokenBalance,
            _feePoleis,
            lastCollateralRatioPoleis,
            totalSupply,
            lastAccrueInterestTime,
            lastTotalDebt
        );

        uint userDebt = getDebtOf(debtSharesBalanceOf[msg.sender], debtSharesSupply, _currentTotalDebt);
        if(userDebt > 0) {
            uint userCollateralRatioPoleis = userDebt * 1e36 / (collateralBalanceOf[msg.sender] - amount);
            require(userCollateralRatioPoleis <= (_currentCollateralRatioPoleis * 1e18), "Pool: user collateral ratio too high");
        }

        // commit current state
        totalSupply = _currentTotalSupply;
        // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest
        if (lastTotalDebt != _currentTotalDebt) {
            lastTotalDebt = _currentTotalDebt;
            lastAccrueInterestTime = block.timestamp;
        }
        lastCollateralRatioPoleis = _currentCollateralRatioPoleis;
        collateralBalanceOf[msg.sender] -= amount;
        emit RemoveCollateral(msg.sender, amount);
        if(_accruedFeeShares > 0) {
            balanceOf[_feeRecipient] += _accruedFeeShares;
            emit Transfer(address(0), _feeRecipient, _accruedFeeShares);
        }

        // interactions
        safeTransfer(COLLATERAL_TOKEN, msg.sender, amount);

        // sync balance
        lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this));
    }

    /**
     * @notice Borrow loan tokens
     * @param amount The amount of loan tokens to borrow
     */
    function borrow(uint amount) external {
        (address _feeRecipient, uint _feePoleis) = FACTORY.getFee();
        (  
            uint _currentTotalSupply,
            uint _accruedFeeShares,
            uint _currentCollateralRatioPoleis,
            uint _currentTotalDebt
        ) = getCurrentState(
            lastLoanTokenBalance,
            _feePoleis,
            lastCollateralRatioPoleis,
            totalSupply,
            lastAccrueInterestTime,
            lastTotalDebt
        );

        uint _debtSharesSupply = debtSharesSupply;
        uint userDebt = getDebtOf(debtSharesBalanceOf[msg.sender], _debtSharesSupply, _currentTotalDebt) + amount;
        uint userCollateralRatioPoleis = userDebt * 1e36 / collateralBalanceOf[msg.sender];
        require(userCollateralRatioPoleis <= (_currentCollateralRatioPoleis * 1e18), "Pool: user collateral ratio too high");

        uint _newUtil = getUtilizationPoleis(_currentTotalDebt + amount, (_currentTotalDebt + lastLoanTokenBalance));
        require(_newUtil <= FINANCE_POLEIS, "Pool: utilization too high");

        uint _shares = tokenToShares(amount, _currentTotalDebt, _debtSharesSupply, true);
        _currentTotalDebt += amount;

        // commit current state
        debtSharesBalanceOf[msg.sender] += _shares;
        debtSharesSupply = _debtSharesSupply + _shares;
        totalSupply = _currentTotalSupply;
        // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest
        if (lastTotalDebt != _currentTotalDebt) {
            lastTotalDebt = _currentTotalDebt;
            lastAccrueInterestTime = block.timestamp;
        }
        lastCollateralRatioPoleis = _currentCollateralRatioPoleis;
        emit Borrow(msg.sender, amount);
        if(_accruedFeeShares > 0) {
            balanceOf[_feeRecipient] += _accruedFeeShares;
            emit Transfer(address(0), _feeRecipient, _accruedFeeShares);
        }

        // interactions
        safeTransfer(LOAN_TOKEN, msg.sender, amount);

        // sync balance
        lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this));
    }

    /**
     * @notice Repay loan tokens debt
     * @param borrower The address of the borrower to repay on their behalf
     * @param amount The amount of loan tokens to repay
     * @dev If amount is max uint, all debt will be repaid
     */
    function repay(address borrower, uint amount) external {
        (address _feeRecipient, uint _feePoleis) = FACTORY.getFee();
        (  
            uint _currentTotalSupply,
            uint _accruedFeeShares,
            uint _currentCollateralRatioPoleis,
            uint _currentTotalDebt
        ) = getCurrentState(
            lastLoanTokenBalance,
            _feePoleis,
            lastCollateralRatioPoleis,
            totalSupply,
            lastAccrueInterestTime,
            lastTotalDebt
        );

        uint _debtSharesSupply = debtSharesSupply;

        uint _shares;
        if(amount == type(uint).max) {
            amount = getDebtOf(debtSharesBalanceOf[borrower], _debtSharesSupply, _currentTotalDebt);
            _shares = debtSharesBalanceOf[borrower];
        } else {
            _shares = tokenToShares(amount, _currentTotalDebt, _debtSharesSupply, false);
        }
        _currentTotalDebt -= amount;

        // commit current state
        debtSharesBalanceOf[borrower] -= _shares;
        debtSharesSupply = _debtSharesSupply - _shares;
        totalSupply = _currentTotalSupply;
        // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest
        if (lastTotalDebt != _currentTotalDebt) {
            lastTotalDebt = _currentTotalDebt;
            lastAccrueInterestTime = block.timestamp;
        }
        lastCollateralRatioPoleis = _currentCollateralRatioPoleis;
        emit Repay(borrower, msg.sender, amount);
        if(_accruedFeeShares > 0) {
            balanceOf[_feeRecipient] += _accruedFeeShares;
            emit Transfer(address(0), _feeRecipient, _accruedFeeShares);
        }

        // interactions
        safeTransferFrom(LOAN_TOKEN, msg.sender, address(this), amount);
        
        // sync balance
        lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this));
    }

    /**
     * @notice Seize collateral from an underwater borrower in exchange for repaying their debt
     * @param borrower The address of the borrower to liquidate
     * @param amount The amount of debt to repay
     * @dev If amount is max uint, all debt will be liquidated
     */
    function liquidate(address borrower, uint amount) external {
        (address _feeRecipient, uint _feePoleis) = FACTORY.getFee();
        (  
            uint _currentTotalSupply,
            uint _accruedFeeShares,
            uint _currentCollateralRatioPoleis,
            uint _currentTotalDebt
        ) = getCurrentState(
            lastLoanTokenBalance,
            _feePoleis,
            lastCollateralRatioPoleis,
            totalSupply,
            lastAccrueInterestTime,
            lastTotalDebt
        );

        uint collateralBalance = collateralBalanceOf[borrower];
        uint _debtSharesSupply = debtSharesSupply;
        uint userDebt = getDebtOf(debtSharesBalanceOf[borrower], _debtSharesSupply, _currentTotalDebt);
        uint userCollateralRatioPoleis = userDebt * 1e36 / collateralBalance;
        require(userCollateralRatioPoleis > (_currentCollateralRatioPoleis * 1e18), "Pool: borrower not liquidatable");

        address _borrower = borrower; // avoid stack too deep
        uint _amount = amount; // avoid stack too deep
        uint _shares;
        uint collateralReward;
        if(_amount == type(uint).max || _amount == userDebt) {
            collateralReward = collateralBalance;
            _shares = debtSharesBalanceOf[_borrower];
            _amount = userDebt;
        } else {
            collateralReward = _amount * collateralBalance / userDebt;
            _shares = tokenToShares(_amount, _currentTotalDebt, _debtSharesSupply, false);
        }

        require(_shares != 0, "Pool: zero shares to liquidate");
        _currentTotalDebt -= _amount;

        // commit current state
        debtSharesBalanceOf[_borrower] -= _shares;
        debtSharesSupply = _debtSharesSupply - _shares;
        collateralBalanceOf[_borrower] = collateralBalance - collateralReward;
        totalSupply = _currentTotalSupply;
        // avoid recording accrue interest time if there is no change in total debt i.e. 0 interest
        if (lastTotalDebt != _currentTotalDebt) {
            lastTotalDebt = _currentTotalDebt;
            lastAccrueInterestTime = block.timestamp;
        }
        lastCollateralRatioPoleis = _currentCollateralRatioPoleis;
        emit Liquidate(_borrower, msg.sender, _amount, collateralReward);
        if(_accruedFeeShares > 0) {
            address __feeRecipient = _feeRecipient; // avoid stack too deep
            balanceOf[__feeRecipient] += _accruedFeeShares;
            emit Transfer(address(0), __feeRecipient, _accruedFeeShares);
        }

        // interactions
        safeTransferFrom(LOAN_TOKEN, msg.sender, address(this), _amount);
        safeTransfer(COLLATERAL_TOKEN, msg.sender, collateralReward);


        // sync balance
        lastLoanTokenBalance = LOAN_TOKEN.balanceOf(address(this));
    }

    event Transfer(address indexed from, address indexed to, uint value);
    event Approval(address indexed owner, address indexed spender, uint value);
    event Deposit(address indexed user, uint amount);
    event Withdraw(address indexed user, uint amount);
    event Borrow(address indexed user, uint amount);
    event Repay(address indexed user, address indexed caller, uint amount);
    event Liquidate(address indexed user, address indexed liquidator, uint amount, uint collateralReward);
    event AddCollateral(address indexed user, address indexed caller, uint amount);
    event RemoveCollateral(address indexed user, uint amount);
}