Lending Pool Contract

Contract Details
This is the verified source code for the main Zakaas Lending Pool.
Contract Address0x4e8c63cc36ab2258b395193a67085a16844c07d4
View on Polygonscan
Solidity Source Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title LendingPool
 * @author Zakaas Protocol
 * @notice A smart contract for a lending pool where users can deposit ETH or ERC20 tokens
 * to earn yield based on selected lending opportunities and lock-in periods.
 */
contract LendingPool is Ownable, ReentrancyGuard {
    // =================================================================
    // State Variables
    // =================================================================

    // Struct to store the details of a user's deposit.
    struct Deposit {
        address user;           // The address of the depositor.
        address assetAddress;   // The address of the deposited token (address(0) for ETH).
        uint256 amount;         // The amount of the asset deposited.
        uint256 opportunityId;  // The ID representing the chosen lending opportunity.
        uint256 lockInPeriod;   // The duration of the lock-in in months.
        uint256 depositTime;    // The timestamp when the deposit was made.
        uint256 unlockTime;     // The timestamp when the funds can be withdrawn.
    }

    // A mapping from a user's address to an array of their deposits.
    mapping(address => Deposit[]) public userDeposits;

    // A counter to track the total number of lending opportunities.
    // This can be used to validate opportunityId on deposit.
    uint256 public totalOpportunities;

    // =================================================================
    // Events
    // =================================================================

    /**
     * @notice Emitted when a user successfully deposits funds into the pool.
     * @param user The address of the user who made the deposit.
     * @param assetAddress The address of the token deposited (address(0) for ETH).
     * @param amount The amount deposited.
     * @param opportunityId The ID of the chosen lending opportunity.
     * @param unlockTime The timestamp when the deposit will be unlocked.
     */
    event NewDeposit(
        address indexed user,
        address indexed assetAddress,
        uint256 amount,
        uint256 indexed opportunityId,
        uint256 unlockTime
    );

    /**
     * @notice Emitted when a user successfully withdraws their funds.
     * @param user The address of the user who withdrew.
     * @param depositIndex The index of the deposit in the user's deposit array.
     * @param amount The amount of the asset withdrawn.
     */
    event Withdrawal(
        address indexed user,
        uint256 indexed depositIndex,
        uint256 amount
    );

    // =================================================================
    // Constructor
    // =================================================================

    /**
     * @notice Initializes the contract.
     * @param _initialOwner The address that will own the contract.
     * @param _totalOpportunities The initial number of available lending opportunities.
     */
    constructor(address _initialOwner, uint256 _totalOpportunities) Ownable(_initialOwner) {
        totalOpportunities = _totalOpportunities;
    }

    // =================================================================
    // Core Functions
    // =================================================================

    /**
     * @notice Allows a user to deposit either ETH or an ERC20 token into the pool.
     * @dev This function handles both asset types to provide a unified deposit experience.
     * For ETH deposits, `_assetAddress` must be `address(0)` and ETH must be sent with the transaction (`msg.value`).
     * For ERC20 deposits, `_assetAddress` is the token's address and `msg.value` must be 0.
     * @param _assetAddress The address of the ERC20 token, or address(0) for native ETH.
     * @param _amount The amount of the asset to deposit (for ERC20s only, ignored for ETH).
     * @param _opportunityId The ID for the selected lending opportunity.
     * @param _lockInPeriod The lock-in duration in months (e.g., 3, 6, 12).
     */
    function deposit(
        address _assetAddress,
        uint256 _amount,
        uint256 _opportunityId,
        uint256 _lockInPeriod
    ) external payable nonReentrant {
        // --- Input Validation ---
        require(_opportunityId < totalOpportunities, "LendingPool: Invalid opportunity ID.");
        require(_lockInPeriod == 3 || _lockInPeriod == 6 || _lockInPeriod == 12, "LendingPool: Invalid lock-in period.");

        uint256 depositAmount;

        // --- Asset Handling ---
        if (_assetAddress == address(0)) {
            // This is a native ETH deposit.
            require(msg.value > 0, "LendingPool: ETH amount must be greater than zero.");
            require(_amount == 0, "LendingPool: Amount must be 0 for ETH deposits.");
            depositAmount = msg.value;
        } else {
            // This is an ERC20 token deposit.
            require(msg.value == 0, "LendingPool: msg.value must be 0 for ERC20 deposits.");
            require(_amount > 0, "LendingPool: ERC20 amount must be greater than zero.");
            depositAmount = _amount;

            // Transfer the ERC20 tokens from the user to this contract.
            // The user must have approved this contract to spend the tokens beforehand.
            bool success = IERC20(_assetAddress).transferFrom(msg.sender, address(this), depositAmount);
            require(success, "LendingPool: ERC20 transfer failed.");
        }

        // --- Create and Store Deposit ---
        uint256 unlockTime = block.timestamp + (_lockInPeriod * 30 days); // Approximation: 30 days per month

        Deposit memory newDeposit = Deposit({
            user: msg.sender,
            assetAddress: _assetAddress,
            amount: depositAmount,
            opportunityId: _opportunityId,
            lockInPeriod: _lockInPeriod,
            depositTime: block.timestamp,
            unlockTime: unlockTime
        });

        userDeposits[msg.sender].push(newDeposit);

        // --- Emit Event ---
        emit NewDeposit(msg.sender, _assetAddress, depositAmount, _opportunityId, unlockTime);
    }

    /**
     * @notice Allows a user to withdraw a specific deposit after its lock-in period has ended.
     * @param _depositIndex The index of the deposit in the user's `userDeposits` array.
     */
    function withdraw(uint256 _depositIndex) external nonReentrant {
        // --- Validation ---
        require(_depositIndex < userDeposits[msg.sender].length, "LendingPool: Invalid deposit index.");
        
        Deposit storage depositToWithdraw = userDeposits[msg.sender][_depositIndex];

        require(block.timestamp >= depositToWithdraw.unlockTime, "LendingPool: Deposit is still locked.");
        require(depositToWithdraw.amount > 0, "LendingPool: Deposit has already been withdrawn.");

        uint256 amountToWithdraw = depositToWithdraw.amount;
        
        // Mark the deposit as withdrawn by setting its amount to 0 to prevent re-withdrawal.
        depositToWithdraw.amount = 0;

        // --- Asset Transfer ---
        if (depositToWithdraw.assetAddress == address(0)) {
            // Withdraw native ETH.
            (bool success, ) = msg.sender.call{value: amountToWithdraw}("");
            require(success, "LendingPool: ETH withdrawal failed.");
        } else {
            // Withdraw ERC20 token.
            bool success = IERC20(depositToWithdraw.assetAddress).transfer(msg.sender, amountToWithdraw);
            require(success, "LendingPool: ERC20 withdrawal failed.");
        }

        // --- Emit Event ---
        emit Withdrawal(msg.sender, _depositIndex, amountToWithdraw);
    }

    // =================================================================
    // Owner-Only Functions
    // =================================================================

    /**
     * @notice Allows the owner to update the total number of lending opportunities.
     * @param _newTotal The new total number of opportunities.
     */
    function setTotalOpportunities(uint256 _newTotal) external onlyOwner {
        totalOpportunities = _newTotal;
    }

    /**
     * @notice In case funds get stuck, allows the owner to withdraw any ETH from the contract.
     */
    function emergencyWithdrawETH() external onlyOwner {
        (bool success, ) = owner().call{value: address(this).balance}("");
        require(success, "LendingPool: Emergency ETH withdrawal failed.");
    }

    /**
     * @notice In case funds get stuck, allows the owner to withdraw any specified ERC20 token.
     * @param _tokenAddress The address of the ERC20 token to withdraw.
     */
    function emergencyWithdrawERC20(address _tokenAddress) external onlyOwner {
        IERC20 token = IERC20(_tokenAddress);
        uint256 balance = token.balanceOf(address(this));
        require(balance > 0, "LendingPool: No tokens to withdraw.");
        bool success = token.transfer(owner(), balance);
        require(success, "LendingPool: Emergency ERC20 withdrawal failed.");
    }
}