Building a Decentralised IDO Launchpad on Celo Blockchain: A Comprehensive Tutorial for Solidity Developers

Building a Decentralised IDO Launchpad on Celo Blockchain: A Comprehensive Tutorial for Solidity Developers https://celo.academy/uploads/default/optimized/2X/b/b37b6d088bec87968f8d088f97ab67fa042806ab_2_1024x576.png
none 4.5 2

Introduction

Welcome to this tutorial where we will delve into the exciting world of Initial DEX Offerings (IDOs) and explore the possibilities of launching projects on a decentralized platform. In this tutorial, we will specifically focus on building a decentralized launchpad on the Celo blockchain. Whether you are a developer with some experience in Solidity or someone looking to expand their skills, this tutorial will equip you with the knowledge and tools to create a decentralized launchpad application on Celo.

Table of Contents

Building a decentralised IDO launchpad contract on Celo Blockchain

Objective

The objective of this tutorial is to provide developers with a comprehensive understanding of IDOs and the benefits of launching on decentralized platforms. By the end of this tutorial, you will have a solid grasp of the technical aspects involved in building a launchpad smart contract on the Celo blockchain. This tutorial will guide you through the process of creating the smart contract, integrating it with the Celo network, and testing its functionality.

Let’s dive in and unlock the world of decentralized finance and project launchpads on the Celo blockchain!

Prerequisites

Before diving into building a decentralized launchpad on the Celo blockchain for Initial DEX Offerings (IDOs), it is important to ensure you have a strong foundation in the following areas:

  • Solidity: Solidity is the primary programming language used for writing smart contracts on the Celo blockchain. Make sure you have a good understanding of Solidity and its syntax.

  • Command Line Proficiency: Familiarize yourself with using the command line interface (CLI), such as Terminal or Command Prompt, as you will need to run commands and scripts throughout the tutorial.

  • Proficiency in Hardhat: Hardhat is one of development environments specifically designed for building, testing, and deploying smart contracts on the Celo blockchain.

Requirements

To successfully follow along with this tutorial, make sure you have the following requirements fulfilled:

  • Text Editor: We recommend using Visual Studio Code (VS Code) as your text editor for this tutorial. VS Code is a widely-used integrated development environment (IDE) that offers powerful features for writing and editing code.

  • Node.js: Install Node.js on your system, preferably version 10 or higher. Node.js provides a runtime environment for executing JavaScript code outside of a web browser.

  • npm (Node Package Manager): npm is a package manager for JavaScript and comes bundled with Node.js. You will need npm to install and manage dependencies required for the development process.

Initial DEX Offering(IDO)

An Initial DEX Offering (IDO) is a fundraising method for new cryptocurrency projects where tokens are sold directly on a decentralized exchange (DEX). It is an alternative to traditional Initial Coin Offerings (ICOs) and Initial Exchange Offerings (IEOs), which are often centralized and conducted on centralized exchanges.

The concept of IDO

Imagine you have an amazing idea for a new cryptocurrency project, such as building a decentralized gaming platform. However, you need funds to turn your idea into reality. In the traditional world, raising money for your project can be quite challenging. Investors might have specific demands or requirements, and the process itself can be time-consuming.

But now, there’s a new way to raise funds called an Initial DEX Offering or IDO. It’s like a virtual marketplace where people from all over the world can purchase tokens that represent their ownership in your project. The unique thing about IDOs is that they take place on decentralized platforms, meaning there’s no central authority in control. It’s similar to a digital marketplace where people can directly buy tokens using their preferred cryptocurrencies like Celo.

By launching your project through an IDO, you can reach a global audience of potential investors and supporters. The decentralized nature of IDOs offers more freedom and flexibility compared to traditional fundraising methods. You won’t have to rely solely on the decisions and demands of specific investors or companies. Instead, you can connect directly with a wide range of individuals who believe in your project and want to be part of its success.

How IDO works

Let’s walk through the typical steps involved in an IDO:

  • Project Preparation: The project team develops their cryptocurrency project, such as a new token, decentralized application, or platform. They define the project’s goals, roadmap, and tokenomics.

  • Smart Contract Creation: The project team creates a smart contract, this smart contract defines the rules and parameters of the IDO, such as the token sale price, token distribution, and fundraising cap.

  • Platform Selection: The project team selects a decentralized exchange (DEX) launchpad platform to host their IDO, which is what we will be building in this tutorial.

  • Token Allocation: The project team determines the allocation of tokens for the IDO. They typically reserve a portion of the tokens for the IDO participants, team members, advisors, and community incentives.

  • Whitelisting: Depending on the platform and regulatory requirements, the project team may implement a whitelisting process for participants. This ensures that only approved participants can invest in the IDO.

  • Investment Period: The IDO opens for a specific period during which participants can invest. Participants send their desired cryptocurrency (such CELO) to the smart contract address associated with the IDO. In return, they receive the project’s tokens based on the predefined token price.

  • Token Distribution: After investing, the project’s tokens are automatically sent to the participants’ wallets through the smart contract. The timing of the distribution, whether immediate or at a designated future time, depends on the specific logic programmed into the smart contract. This is why it is essential for developers to thoroughly review the smart contract code before making any investment in a project. By carefully examining the smart contract, developers can understand how the token distribution process is designed, including any potential delays, conditions, or restrictions that may apply. This diligence ensures that developers have a clear understanding of how their investments will be handled and empowers them to make well-informed decisions.

  • Liquidity Provision: After the IDO, the project team and liquidity providers may add liquidity to the token pair on the DEX platform. This ensures that the project’s tokens have sufficient trading volume and liquidity for further trading.

Note: Each IDO Launchpad may have its own specific variations and processes depending on the platform and project’s requirements. However, the general idea is to provide a decentralized and transparent fundraising method where individuals can directly participate in supporting and investing in new cryptocurrency projects.

Some of the most popular IDO platforms include:

  • Polkastarter
  • Seedify
  • GameFi
  • TrustPad

Launchpad

A launchpad is a platform that facilitates the launching of new cryptocurrency projects, typically through Initial DEX Offerings (IDOs) or Initial Coin Offerings (ICOs). It acts as a launching pad for these projects, providing the infrastructure and tools necessary for their successful introduction to the market. A launchpad serves as an intermediary between project teams and it’s potential investors. It also provides a space for investors to discover and participate in these projects.

Steps involved in building an IDO Launchpad

Building an IDO launchpad involves several key steps to create a platform that enables the launching of projects and facilitates token sales. Here is an overview of the typical steps involved:

  • Determine the specific features and functionalities you want to incorporate into your IDO launchpad. Consider aspects such as user registration, project submission, token sale mechanics, KYC procedures, token distribution, and security measures.

  • Select the blockchain platform on which your IDO launchpad will be built like Celo blockchain.

  • Create the smart contracts to handle the core functionalities of the IDO launchpad. This includes contracts for project submissions, token sales, user whitelisting, KYC verification, and token distribution. Ensure that the smart contracts are secure, audited, and properly tested.

  • Design an intuitive and user-friendly interface for the IDO launchpad. Consider the user experience, project browsing, token purchase process, and account management features. You can build a web-based interface or develop a mobile application based on your target audience and platform requirements.

  • Integrate the smart contracts and blockchain functionality into the user interface. This involves connecting the user interface with the deployed smart contracts on the chosen blockchain platform.

  • Implement robust security measures to protect user data and funds. Conduct thorough security audits and penetration testing to identify vulnerabilities.

  • Test the functionality of the IDO launchpad thoroughly. Conduct both unit tests and end-to-end tests to ensure all features work as intended. Once testing is complete, deploy the smart contracts to the celo blockchain.

  • Develop a strategy to build and engage a community of users and projects on your IDO launchpad. Implement features to promote project visibility, user feedback, and community-driven decision-making. Create a marketing plan to attract projects, investors, and users to your platform.

  • Regularly monitor and maintain the IDO launchpad platform. Address any bugs, security vulnerabilities, or user feedback promptly. Continuously update and improve the platform with new features and enhancements to stay competitive in the evolving market.

Tutorial

STEP 1 - Set up Hardhat Environment

To begin setting up the Hardhat environment for your smart contract implementation, you will first need to create a new folder on your system. You can do this by using the ‘mkdir’ command in your terminal followed by the desired name of your folder. For example:

mkdir launchpad

Next, navigate to your project folder using the ‘cd’ command, like so:

cd launchpad

Once you have cd into the folder, you can initialize a new npm project inside it by running the following command:

npm init -y

This will create a “package.json” file in your project folder with default settings. Run the following command to initialize the Hardhat environment and create some default configuration files and folders required for building and testing smart contracts.

npm install hardhat --save-dev
npx hardhat

We will be using a typescript project for this tutorial, so click on “Create a typescript project” and enter this and other prompt options.

Finally, open your project folder in VScode by running this command in your terminal:

code .

This will open up your project folder in Visual Studio Code, where you can start setting up your Hardhat environment and writing your smart contract code.

STEP 2 - Create your IDO Launchpad Smart Contract

To begin, locate the “contracts” folder in the root directory of your project. This folder will serve as the destination for your new TypeScript file. By navigating to the “contracts” folder, you can effortlessly add your desired files.

In the context of this tutorial, we will focus on creating an IDO Launchpad Contract and a token contract . To accomplish this, you will only need to generate two files.

IDO Launchpad Smart Contract Code

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";

contract Launchpad is Pausable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    using SafeMath for uint256;
    ///////////////EVENTS//////////////////
    /**
     * @dev Emits when a new project is listed on the launchpad with its details
     */
    event ProjectListed(
        uint256 indexed projectId,
        address indexed projectOwner,
        address indexed token,
        uint256 tokenPrice,
        uint256 minInvestment,
        uint256 maxInvestment,
        uint256 maxCap, //IDO totalsupply
        uint256 endTimeInMinutes
    );

    /**
     * @dev Emits when an investor/project participant makes an investment/contribution in a particular IDO project
     */
    event InvestmentMade(
        uint256 indexed projectId,
        address indexed investor,
        uint256 amountInvested
    );

    event Swept(address to, uint256 value);

    /////////////////STATE VARIABLES///////////////////
    address public launchPadadmin;

    uint256 projectsCurrentId;

    struct IDOProject {
        address projectOwner;
        IERC20 token;
        uint256 tokenPrice;
        uint256 minInvestment;
        uint256 maxInvestment;
        uint256 maxCap; //IDO totalSupply
        uint256 IDOduration;
        bool isActive;
        uint256 totalAmountRaised;
        uint256 totalTokenIDOClaimed;
        address[] whiteListedAddresses;
        address[] projectInvestors;
        bool withdrawn;
    }

    //Tracks the investment amount of each participant for a specific project
    mapping(uint256 => mapping(address => uint256)) projectInvestments;
    //Keeps track of whitelisted tokens for the launchpad
    mapping(address => bool) tokenListed;
    //tracks whether a participant has already claimed their allocated tokens
    mapping(address => bool) claimed;

    // The allocation of a particular IDO for each participant
    mapping(uint256 => mapping(address => uint256)) allocation;

    mapping(uint256 => mapping(address => bool)) private whitelistedAddresses;

    mapping(uint256 => IDOProject) projects;

    ///////////////ERRORS//////////////////
    error NotLaunchPadAdmin();
    error TokenPriceMustBeGreaterThanZero();
    error MinimumInvestmentMustBeGreaterThanZero();
    error MaxInvestmentMustBeGreaterOrEqualToMinInvestment();
    error MaxCapMustBeGreaterOrEqualToMaxInvestment();
    error EndTimeMustBeInTheFuture();
    error InvalidProjectID();
    error ProjectNotActive();
    error InvestmentAmtBelowMinimum();
    error InvestmentAmtExceedsMaximum();
    error ProjectEnded();
    error NotProjectOwner();
    error AlreadyWithdrawn();
    error ProjectStillInProgress();
    error AddressZero();
    error TxnFailed();
    error TokenAlreadyWhitelisted();
    error ContractNotFullyFunded();
    error EmptyAddress();
    error NotWhiteListed();
    error MaxCapExceeded();
    error TokenAllocationMustBeGreaterThanZero();
    error UserAlreadyWhitelisted();
    error OldAdmin();

    constructor() {
        launchPadadmin = msg.sender;
    }

    /**
     * @dev function to list a new project with its details
     */

    function listProject(
        IERC20 _token,
        uint256 _tokenPrice,
        uint256 _minInvestment,
        uint256 _maxInvestment,
        uint256 _maxCap,
        uint256 _endTime,
        address[] memory _whiteListedUsers
    ) external whenNotPaused {
        if (_tokenPrice == 0) revert TokenPriceMustBeGreaterThanZero();
        if (_minInvestment == 0)
            revert MinimumInvestmentMustBeGreaterThanZero();
        if (_maxInvestment < _minInvestment)
            revert MaxInvestmentMustBeGreaterOrEqualToMinInvestment();
        if (_maxInvestment > _maxCap)
            revert MaxCapMustBeGreaterOrEqualToMaxInvestment();
       if(_endTime == 0) revert EndTimeMustBeInTheFuture();

        if (_whiteListedUsers.length == 0) revert EmptyAddress();

        projectsCurrentId = projectsCurrentId + 1;

        for (uint256 i; i < _whiteListedUsers.length; i++) {
            address user = _whiteListedUsers[i];
            if (user == address(0)) revert AddressZero();
            whitelistedAddresses[projectsCurrentId][user] = true;
        }

        IDOProject storage project = projects[projectsCurrentId];

        if (tokenListed[address(_token)] == true)
            revert TokenAlreadyWhitelisted();

        project.projectOwner = msg.sender;
        project.token = _token;
        project.tokenPrice = _tokenPrice;
        project.minInvestment = _minInvestment;
        project.maxInvestment = _maxInvestment;
        project.maxCap = _maxCap;
        project.IDOduration = (_endTime * 1 minutes).add(block.timestamp);
        project.whiteListedAddresses = _whiteListedUsers;
        project.isActive = true;

        tokenListed[address(_token)] = true;

        emit ProjectListed(
            projectsCurrentId,
            msg.sender,
            address(_token),
            _tokenPrice,
            _minInvestment,
            _maxInvestment,
            _maxCap,
            _endTime
        );
    }

    function isWhitelisted(
        uint256 _projectId,
        address _address
    ) private view returns (bool) {
        return whitelistedAddresses[_projectId][_address];
    }

    function invest(uint256 _projectId) external payable whenNotPaused {
        if (_projectId > projectsCurrentId || _projectId == 0)
            revert InvalidProjectID();

        IDOProject storage project = projects[_projectId];

        if (isWhitelisted(_projectId, msg.sender) == false)
            revert NotWhiteListed();
        if (project.isActive == false) revert ProjectNotActive();

        if (IERC20(project.token).balanceOf(address(this)) < project.maxCap)
            revert ContractNotFullyFunded();

        if (block.timestamp > project.IDOduration) revert ProjectEnded();

        if (msg.value < project.minInvestment)
            revert InvestmentAmtBelowMinimum();
        if (
            (projectInvestments[_projectId][msg.sender].add(msg.value)) >
            project.maxInvestment
        ) revert InvestmentAmtExceedsMaximum();

        uint256 investmentAmount = msg.value;

        // Calculate token allocation
        uint256 tokenAllocation = (investmentAmount / project.tokenPrice).mul(
            1e18
        );
        if (tokenAllocation == 0) revert TokenAllocationMustBeGreaterThanZero();

        // Ensure token allocation doesn't exceed the maximum cap
        if (tokenAllocation > project.maxCap) revert MaxCapExceeded();

        // Deduct the token allocation from the total token supply
        project.maxCap = project.maxCap.sub(tokenAllocation);

        projectInvestments[_projectId][msg.sender] = projectInvestments[
            _projectId
        ][msg.sender].add(investmentAmount);

        allocation[_projectId][msg.sender] = allocation[_projectId][msg.sender]
            .add(tokenAllocation);

        project.totalTokenIDOClaimed = project.totalTokenIDOClaimed.add(
            tokenAllocation
        );

        // Transfer the allocated tokens to the participant.
        IERC20(project.token).safeTransfer(_msgSender(), tokenAllocation);
        project.totalAmountRaised = project.totalAmountRaised.add(
            investmentAmount
        );

        bool alreadyInvestor = false;
        for (uint256 i; i < project.projectInvestors.length; i++) {
            if (project.projectInvestors[i] == msg.sender) {
                alreadyInvestor = true;
                break;
            }
        }

        if (!alreadyInvestor) {
            project.projectInvestors.push(msg.sender);
        }
        emit InvestmentMade(
            _projectId,
            msg.sender,
            projectInvestments[_projectId][msg.sender]
        );
    }

    /**
     * @dev Pause the sale
     */
    function pause() external {
        if (msg.sender != launchPadadmin) revert NotLaunchPadAdmin();
        super._pause();
    }

    /**
     * @dev Unpause the sale
     */
    function unpause() external {
        if (msg.sender != launchPadadmin) revert NotLaunchPadAdmin();
        super._unpause();
    }

    function AddUserForAParticularProject(
        uint256 _projectId,
        address _user
    ) external whenNotPaused {
        if (_projectId > projectsCurrentId || _projectId == 0)
            revert InvalidProjectID();

        IDOProject storage project = projects[_projectId];
        if (msg.sender != project.projectOwner) revert NotProjectOwner();
        if (_user == address(0)) revert AddressZero();
        if (whitelistedAddresses[_projectId][_user] == true)
            revert UserAlreadyWhitelisted();

        whitelistedAddresses[_projectId][_user] = true;
        project.whiteListedAddresses.push(_user);
    }


    /**
     * @dev alternative to Deposit IDO token for investment
     */
    function depositIDOTokens(
        uint256 _projectId,
        uint256 amount
    ) external whenNotPaused {
        if (_projectId > projectsCurrentId || _projectId == 0)
            revert InvalidProjectID();

        IDOProject storage project = projects[_projectId];
        if (msg.sender != project.projectOwner) revert NotProjectOwner();

        IERC20(project.token).safeTransferFrom(
            _msgSender(),
            address(this),
            amount
        );
    }

    /**
     * @dev function allows the IDO project owner to withdraw the raised funds after the listing project period ends
     */
    function withdrawAmountRaised(
        uint256 _projectID
    ) external payable whenNotPaused nonReentrant {
        if (_projectID > projectsCurrentId || _projectID == 0)
            revert InvalidProjectID();
        IDOProject storage project = projects[_projectID];

        if (msg.sender != project.projectOwner) revert NotProjectOwner();
        if (project.withdrawn == true) revert AlreadyWithdrawn();

        if (block.timestamp < project.IDOduration)
            revert ProjectStillInProgress();
        uint256 amountRaised = project.totalAmountRaised;

        project.totalAmountRaised = 0;
        project.withdrawn = true;
        (bool success, ) = payable(msg.sender).call{value: amountRaised}("");
        if (!success) revert TxnFailed();
    }

    function changeLaunchPadAdmin(address _newAdmin) external whenNotPaused {
        if (msg.sender != launchPadadmin) revert NotLaunchPadAdmin();
        if (_newAdmin == address(0)) revert AddressZero();
        if (_newAdmin == launchPadadmin) revert OldAdmin();
        launchPadadmin = _newAdmin;
    }

    function getIDOTokenBalanceInLaunchPad(
        uint256 projectId
    ) public view returns (uint256) {
        if (projectId > projectsCurrentId || projectId == 0)
            revert InvalidProjectID();

        IDOProject memory project = projects[projectId];
        return IERC20(project.token).balanceOf(address(this));
    }

    function getTokenLeftForAParticularIDO(
        uint256 projectId
    ) public view returns (uint256) {
        if (projectId > projectsCurrentId || projectId == 0)
            revert InvalidProjectID();

        IDOProject memory project = projects[projectId];
        uint256 tokenLeft = project.maxCap.sub(project.totalTokenIDOClaimed);

        return tokenLeft;
    }

    function sweep(uint256 _projectID, address to) external whenNotPaused {
        if (_projectID > projectsCurrentId || _projectID == 0)
            revert InvalidProjectID();
        IDOProject storage project = projects[_projectID];

        if (msg.sender != project.projectOwner) revert NotProjectOwner();
        if (to == address(0)) revert AddressZero();

        if (block.timestamp < project.IDOduration)
            revert ProjectStillInProgress();

        uint256 balance = getIDOTokenBalanceInLaunchPad(_projectID);
        IERC20(project.token).safeTransfer(to, balance);

        emit Swept(to, balance);
    }

    function getInvestorsForAParticularProject(
        uint256 _projectID
    ) external view returns (address[] memory) {
        if (_projectID > projectsCurrentId || _projectID == 0)
            revert InvalidProjectID();

        IDOProject memory project = projects[_projectID];
        return project.projectInvestors;
    }

    function getUserInvestmentForAnIDOInCELO(
        uint256 _projectID,
        address _i
    ) external view returns (uint256) {
        if (_projectID > projectsCurrentId || _projectID == 0)
            revert InvalidProjectID();
        return projectInvestments[_projectID][_i];
    }

    function getAUserAllocationForAProject(
        uint256 _projectID,
        address userAddr
    ) external view returns (uint256) {
        if (_projectID > projectsCurrentId || _projectID == 0)
            revert InvalidProjectID();

        return allocation[_projectID][userAddr];
    }

    function cancelIDOProject(uint256 _projectId) external {
        if (msg.sender != launchPadadmin) revert NotLaunchPadAdmin();
        if (_projectId > projectsCurrentId || _projectId == 0)
            revert InvalidProjectID();

        IDOProject storage project = projects[_projectId];
        project.isActive = false;
        project.IDOduration = 0;
        address to = project.projectOwner;

        uint256 balance = getIDOTokenBalanceInLaunchPad(_projectId);
        IERC20(project.token).safeTransfer(to, balance);
    }

    function getProjectDetails(
        uint256 _projectID
    ) external view returns (IDOProject memory) {
        if (_projectID > projectsCurrentId || _projectID == 0)
            revert InvalidProjectID();
        return projects[_projectID];
    }

    function getProjectPrice(
        uint256 _projectID
    ) external view returns (uint256) {
        if (_projectID > projectsCurrentId || _projectID == 0)
            revert InvalidProjectID();

        IDOProject memory project = projects[_projectID];
        return project.tokenPrice;
    }

    function getProjectMaxCap(
        uint256 _projectID
    ) external view returns (uint256) {
        if (_projectID > projectsCurrentId || _projectID == 0)
            revert InvalidProjectID();

        IDOProject memory project = projects[_projectID];
        return project.maxCap;
    }

    function getProjectTotalAmtRaised(
        uint256 _projectID
    ) external view returns (uint256) {
        if (_projectID > projectsCurrentId || _projectID == 0)
            revert InvalidProjectID();

        IDOProject memory project = projects[_projectID];
        return project.totalAmountRaised;
    }

    function getProjectTotalTokenIDOClaimed(
        uint256 _projectID
    ) external view returns (uint256) {
        if (_projectID > projectsCurrentId || _projectID == 0)
            revert InvalidProjectID();

        IDOProject memory project = projects[_projectID];
        return project.totalTokenIDOClaimed;
    }

    function getTimeLeftForAParticularProject(
        uint256 projectId
    ) public view returns (uint256) {
        if (projectId > projectsCurrentId || projectId == 0)
            revert InvalidProjectID();

        IDOProject storage project = projects[projectId];

        if (block.timestamp >= project.IDOduration) {
            return 0; // IDOProject has ended
        } else {
            uint256 timeLeftInSeconds = project.IDOduration - block.timestamp;
            uint256 timeLeftInMinutes = timeLeftInSeconds / 60; // Convert seconds to minutes
            return timeLeftInMinutes;
        }
    }

    function getCurrentProjectID() external view returns (uint256) {
        return projectsCurrentId;
    }

    ///@dev function to get contract balance
    function getContractBal() external view returns (uint256) {
        return address(this).balance;
    }

    function getTotalInvestorsForAParticularProject(
        uint256 projectId
    ) external view returns (uint256) {
        if (projectId > projectsCurrentId || projectId == 0)
            revert InvalidProjectID();

        IDOProject memory project = projects[projectId];
        return project.projectInvestors.length;
    }
}

IDO Launchpad Smart Contract Explained

Let’s go through each functionality in the code:

Import Statement

These import external Solidity contracts from the OpenZeppelin library, which provide various utilities and security features used in the Launchpad contract.

Events

event ProjectListed(
        uint256 indexed projectId,
        address indexed projectOwner,
        address indexed token,
        uint256 tokenPrice,
        uint256 minInvestment,
        uint256 maxInvestment,
        uint256 maxCap, //IDO totalsupply
        uint256 endTimeInMinutes
    );

This event is emitted when a project is listed on the IDO launchpad. It includes information such as the project ID, the address of the IDO project owner, the token address, the token price, the minimum and maximum investment amounts, the maximum cap (total supply of tokens for the IDO), and the end time of the IDO in minutes. This event allows users to track and be notified when a project is listed on the launchpad.

event InvestmentMade(
        uint256 indexed projectId,
        address indexed investor,
        uint256 amountInvested
    );

This event is emitted when an investor or project participant makes an investment or contribution in a specific IDO project. It includes information such as the project ID, the address of the investor, and the amount invested. This event allows project owners to track and monitor investments made in the IDO project.

event Swept(address to, uint256 value);

This event is emitted when the remaining amount of tokens or value for a particular IDO is swept or transferred to a specific address. It includes the address to which the tokens or value are swept and the amount being swept. This event can be used to track and verify token transfers or value movements within the IDO launchpad contract.

State Variables

projects: This state variable is a mapping that stores information about each project listed on the IDO launchpad. The key is the project ID, which is an unsigned integer, and the value is a struct that contains various details about the project, such as the project owner, token address, token price, minimum and maximum investment amounts, maximum cap (total supply of tokens for the IDO), and end time of the IDO.

investments: This state variable is a mapping that keeps track of the investments made by each participant in a specific project. The key is a composite of the project ID and the investor’s address, and the value is the amount invested by that participant in the project.

allocations: This state variable is a mapping that stores the token allocations for each participant in a specific project. The key is the participant’s address, and the value is the amount of tokens allocated to that participant.

claimedTokens: This state variable is a mapping that keeps track of the tokens claimed by each participant. The key is the participant’s address, and the value is the amount of tokens claimed by that participant.

token: This state variable represents the ERC20 token contract that will be used for the IDO. It stores the address of the deployed token contract, allowing the launchpad contract to interact with the token contract for various operations such as transferring tokens and checking balances.

projectIdCounter: This state variable is an unsigned integer that serves as a counter for assigning unique project IDs to each project listed on the launchpad. It increments by one each time a new project is listed, ensuring that each project has a unique identifier.

admin: This state variable represents the address of the admin or owner of the IDO launchpad contract.

Custom Errors

Custom error definitions are provided to handle different error conditions that can occur during contract execution. Custom errors save gas in contract deployments or production. This is because custom errors are smaller than string error messages. The amount of gas required to deploy or call a contract is proportional to the size of the code. So, by using custom errors, you can reduce the amount of gas required to deploy or call your contract. You are welcome🤗.

To define a custom error in Solidity, you use the error keyword. To throw a custom error, you use the revert keyword.

  • NotLaunchPadAdmin(): This error is thrown when a function is called by an address that is not the admin or owner of the launchpad contract. Certain privileged functions can only be executed by the admin, so if a non-admin address attempts to call such a function, this error is raised.

  • TokenPriceMustBeGreaterThanZero(): This error indicates that the token price provided is not greater than zero. When listing a project on the launchpad, the token price must be a positive value to ensure that the token has a non-zero value for investment calculations and allocations.

  • MinimumInvestmentMustBeGreaterThanZero(): This error is triggered when the specified minimum investment amount is not greater than zero. The minimum investment amount determines the smallest permissible investment that a participant can make in the IDO project. It must be a positive value to ensure meaningful participation.

  • MaxInvestmentMustBeGreaterOrEqualToMinInvestment(): This error occurs when the maximum investment amount specified for a project is less than the minimum investment amount. The maximum investment amount represents the upper limit on the investment that a participant can make in the project. It should be greater than or equal to the minimum investment amount for consistency.

  • MaxCapMustBeGreaterOrEqualToMaxInvestment(): This error is raised when the maximum cap or total supply of tokens for the IDO project is less than the maximum investment amount. The maximum cap determines the total number of tokens available for the IDO, and the maximum investment amount restricts the investment limit per participant. To ensure that the maximum investment is meaningful, the maximum cap should be greater than or equal to the maximum investment.

  • EndTimeMustBeInFuture(): This error indicates that the specified end time for the IDO project has already passed. The end time represents the deadline or closing time of the IDO, and it should be set in the future to allow participants to invest within the specified period.

  • InvalidProjectID(): This error is thrown when an invalid or nonexistent project ID is provided. Project IDs are used to uniquely identify projects listed on the launchpad, and this error is raised if an incorrect or non-existent ID is used in a function call.

  • ProjectNotActive(): This error occurs when a function related to a specific project is called, but that project is not currently active. The project may have already ended or not yet started, and certain functions can only be executed when the project is active.

  • InvestmentAmtBelowMinimum(): This error is triggered when an investment amount is below the specified minimum investment for a project. Participants must meet the minimum investment requirement to participate in the project, and this error is raised if the investment amount falls below that threshold.

  • InvestmentAmtExceedsMaximum(): This error is raised when an investment amount exceeds the specified maximum investment for a project. Participants are restricted from investing more than the maximum investment amount, and this error is thrown if the investment amount surpasses that limit.

  • ProjectEnded(): This error indicates that the project has already ended.

  • NotProjectOwner(): This error occurs when a function is called by an address that is not the owner of the specified project. Only the project owner has certain privileges and can execute specific actions related to their project.

  • AlreadyWithdrawn(): This error is thrown when an IDO project owner attempts to withdraw total funds gotten for the project but has already done so previously.

  • ProjectStillInProgress(): This error indicates that a particular IDO project is still in progress.

WRITE FUNCTIONS

Constructor

A constructor function is defined to set the deployer of the IDO Launchpad contract as the launchPadadmin.

listProject Function

The listProject function is used to list a new project on the launchpad. It takes several parameters including the token to be listed, token price, minimum and maximum investment amounts, maximum token cap, end time of the project, and a list of whitelisted users.

Here’s an explanation of the function:

  • The function is marked as external, meaning it can be called from outside the contract, and is subject to the whenNotPaused modifier, which ensures that the contract is not paused before executing the function.

  • The function begins by performing several input validations using if statements and the revert keyword to revert the transaction if any condition fails.

  • It checks that the token price is greater than zero. If the condition fails, it reverts the transaction with the TokenPriceMustBeGreaterThanZero() error.

  • It checks that the minimum investment amount is greater than zero. If the condition fails, it reverts the transaction with the MinimumInvestmentMustBeGreaterThanZero() error.

  • It verifies that the maximum investment amount is greater than or equal to the minimum investment amount. If the condition fails, it reverts the transaction with the MaxInvestmentMustBeGreaterOrEqualToMinInvestment() error.

  • It checks that the maximum investment amount is not greater than the maximum token cap. If the condition fails, it reverts the transaction with the MaxCapMustBeGreaterOrEqualToMaxInvestment() error.

  • It checks that the list of whitelisted users is not empty. If the condition fails, it reverts the transaction with the EmptyAddress() error.

  • The projectsCurrentId is incremented to assign a new ID to the project.

  • It iterates through the list of whitelisted users and adds them to the whitelistedAddresses mapping for the current project. It reverts the transaction if any whitelisted address is set to the zero address (address(0)) with the AddressZero() error.

  • It creates a new IDOProject struct and assigns it to the project variable.

  • It checks if the token has already been listed by verifying the tokenListed mapping. If the token has already been listed, it reverts the transaction with the TokenAlreadyWhitelisted() error.

  • It sets various properties of the project struct including the project owner, token, token price, minimum and maximum investment amounts, maximum token cap, end time, list of whitelisted addresses, and sets the project as active.

  • It updates the tokenListed mapping to mark the token as listed.

  • Finally, it emits the ProjectListed event to signal that the project has been successfully listed, providing relevant information such as the project ID, project owner, token address, token price, investment amounts, maximum token cap, and end time.

This function allows project owners to list their projects on the launchpad, specifying various parameters and whitelisted users. Once listed, participants can make investments in the project during the specified duration.

isWhitelisted Function

The isWhitelisted function is a private view function that checks whether a specific address is whitelisted for a particular project. It takes two parameters: _projectId, which represents the ID of the project, and _address, which represents the address to be checked.

The function uses the _projectId and _address parameters to access the whitelistedAddresses mapping.

The function returns the value stored in the whitelistedAddresses mapping for the given _projectId and _address. If the value is true, it means the address is whitelisted. If the value is false or if no value is found, it means the address is not whitelisted.

Since the function is marked as private, it can only be called from within the contract itself. It is used internally to check whether an address is whitelisted before allowing certain operations or interactions with the project.

invest Function

The invest function allows an investor to make an investment/contribution to a specific project in the IDO launchpad. Here’s an explanation of the function:

  • The function takes one parameter _projectId, which represents the ID of the project in which the investor wants to invest.

  • The function first performs several validation checks to ensure the investment can be made:

  • It checks if the _projectId is valid (not greater than the current project ID or equal to 0). If it’s invalid, the function reverts with the InvalidProjectID error.

  • It retrieves the project details from the projects mapping using the _projectId.

  • It checks if the investor’s address is whitelisted for the project by calling the isWhitelisted function. If the address is not whitelisted, the function reverts with the NotWhiteListed error.

  • It checks if the project is active. If it’s not active, the function reverts with the ProjectNotActive error.

  • It checks if the contract’s balance of the project’s token is less than the maximum cap of the project. If the contract is not fully funded for that particular IDO project, the function reverts with the ContractNotFullyFunded error. This is to ensure that if investors invest, they get the IDO tokens as promised.

  • It checks if the current timestamp is greater than the end time of the project. If the project has ended, the function reverts with the ProjectEnded error.

  • It checks if the investment amount sent by the investor is less than the minimum investment required for the project. If it’s below the minimum, the function reverts with the InvestmentAmtBelowMinimum error.

  • It checks if the sum of the investor’s current investment and the investment amount exceeds the maximum investment allowed for the project. If it exceeds the maximum, the function reverts with the InvestmentAmtExceedsMaximum error.

After the validation checks pass, the function proceeds with the investment process:

  • It calculates the token allocation based on the investment amount and the token price. The token allocation is the amount of tokens the investor will receive in exchange for their investment.

  • It checks if the token allocation is greater than 0. If it’s 0, it reverts with the TokenAllocationMustBeGreaterThanZero error.

  • It checks if the token allocation exceeds the maximum cap of the project. If it exceeds, the function reverts with the MaxCapExceeded error, which means that there are no enough tokens avaiable for purchase.

  • It subtracts the token allocation from the project’s maximum cap, reducing the available token supply for future investments.

  • It adds the investment amount to the investor’s total investment for the project.

  • It adds the token allocation to the investor’s allocation for the project.

  • It updates the total token amount claimed for the project by adding the token allocation.

  • It transfers the allocated tokens from the contract to the investor’s address using the safeTransfer function.

  • It updates the total amount raised for the project by adding the investment amount.

  • It checks if the investor is already in the list of project investors. If not, it adds the investor’s address to the list.

  • It emits the InvestmentMade event to notify listeners about the investment.

pause and unpause Functions

These functions allow the launchPadadmin to pause or unpause the sale of projects on the launchpad.

AddUserForAParticularProject Function

The AddUserForAParticularProject function allows the project owner to add a user to the whitelist of a specific project. Here’s an explanation of the function:

The function takes two parameters: _projectId represents the ID of the project for which the user is being added, and _user represents the address of the user being added to the whitelist.

The function first performs several validation checks:

  • It checks if the _projectId is valid (not greater than the current project ID or equal to 0). If it’s invalid, the function reverts with the InvalidProjectID error.

  • It retrieves the project details from the projects mapping using the _projectId.

  • It checks if the caller of the function (msg.sender) is the project owner. If the caller is not the owner, the function reverts with the NotProjectOwner error.

  • It checks if the _user address is a non-zero address. If it’s a zero address, the function reverts with the AddressZero error.

  • It checks if the _user address is already whitelisted for the project. If it’s already whitelisted, the function reverts with the UserAlreadyWhitelisted error.

After the validation checks pass, the function proceeds with adding the user to the whitelist:

  • It sets the value of whitelistedAddresses[_projectId][_user] to true, indicating that the user is now whitelisted for the project.

  • It appends the _user address to the whiteListedAddresses array of the project.

By calling this function, the project owner can add additional users to the whitelist of a specific project, allowing them to participate in the IDO launchpad and make investments in that project.

depositIDOTokens Function

This function enables an IDO project owner to deposit IDO tokens for investment.

withdrawAmountRaised Function

The withdrawAmountRaised function allows the project owner to withdraw the total amount raised in the IDO project. Here is an explanation of the function:

The function takes an input parameter _projectID, which represents the ID of the project from which the funds will be withdrawn.

First, the function performs several checks:

  • It verifies that the _projectID is valid and corresponds to an existing project.

  • It checks if the message sender (caller of the function) is the project owner. If not, it reverts the transaction with the error message NotProjectOwner().

  • It checks if the funds have already been withdrawn from the project. If yes, it reverts the transaction with the error message AlreadyWithdrawn().

  • The function then checks if the project’s IDO duration has already passed. If the current block’s timestamp is less than the IDO duration, it means the project is still in progress, and the function reverts the transaction with the error message ProjectStillInProgress().

If all the checks pass, the function retrieves the total amount raised in the project and assigns it to the variable amountRaised.

The totalAmountRaised variable in the project is not explicitly set to zero in this function, but it is assumed that the project owner will withdraw the entire amount, effectively setting it to zero.

The withdrawn flag in the project is set to true, indicating that the funds have been withdrawn.

Finally, the function attempts to transfer the amountRaised to the project owner’s address using the call function. If the transfer is successful, the function completes. Otherwise, it reverts the transaction with the error message TxnFailed().

This function ensures that only the project owner can withdraw the funds raised in the project after the IDO duration has passed.

changeLaunchPadAdmin Function

This function allows the current launchPadadmin to transfer the admin role to a new address.

cancelIDOProject Function

The cancelIDOProject function allows the launchpad admin to cancel an IDO project and return the remaining IDO tokens to the project owner. It takes an input parameter _projectId, which represents the ID of the project to be canceled.

  • First, the function checks if the message sender (caller of the function) is the launchpad admin. If not, it reverts the transaction with the error message NotLaunchPadAdmin().

  • The function then performs checks on the validity of the _projectId. It verifies if the _projectId is within the range of valid project IDs (from 1 to projectsCurrentId) and if it is not zero. If the checks fail, the function reverts the transaction with the error message InvalidProjectID().

Once the checks pass, the function retrieves the IDO project information using the _projectId and assigns it to the variable project.

  • The isActive flag of the project is set to false, indicating that the project is no longer active.

  • The IDOduration of the project is set to 0, effectively canceling the duration.

  • The address of the project owner is stored in the variable “to”.

  • The function retrieves the balance of the IDO tokens associated with the project in the launchpad using the getIDOTokenBalanceInLaunchPad function (not shown in the provided code).

  • Finally, the function transfers the IDO tokens back to the project owner by calling the safeTransfer function of the ERC20 token contract. The tokens are transferred from the launchpad contract (where the tokens are held) to the project owner’s address (to).

Sweep Function

The sweep function allows the IDO project owner to sweep (transfer) the remaining IDO tokens associated with a specific project to a specified address. The function takes two input parameters: _projectID, which represents the ID of the project from which the tokens will be swept, and to, which represents the address to which the tokens will be transferred.

  • First, the function checks the validity of the _projectID. It verifies if the _projectID is within the range of valid project IDs (from 1 to projectsCurrentId) and if it is not zero. If the check fails, the function reverts the transaction with the error message InvalidProjectID().

  • The function retrieves the IDO project information using the _projectID and assigns it to the variable project.

  • The function checks if the message sender (caller of the function) is the project owner. If not, it reverts the transaction with the error message NotProjectOwner().

  • It also checks if the to address is not the zero address. If it is the zero address, the function reverts the transaction with the error message AddressZero().

  • Next, the function checks if the project’s IDO duration has ended by comparing the current block timestamp with the stored IDOduration value. If the duration is still in progress, the function reverts the transaction with the error message ProjectStillInProgress().

  • The function retrieves the balance of the IDO tokens associated with the project in the launchpad using the getIDOTokenBalanceInLaunchPad function (not shown in the provided code) and assigns it to the variable balance.

  • Finally, the function transfers the IDO tokens from the launchpad contract to the specified to address using the safeTransfer function of the ERC20 token contract. The amount transferred is equal to the balance value. Additionally, it emits the Swept event, indicating the successful transfer of tokens to the specified address.

This function allows the project owner to sweep the remaining IDO tokens from a project and transfer them to a designated address once the IDO duration has ended.

VIEW FUNCTIONS

getProjectDetails(uint256 _projectID) external view returns (IDOProject memory): This function returns the complete details of an IDO project specified by _projectID. It retrieves the project details from the projects mapping and returns them as an IDOProject struct.

getProjectPrice(uint256 _projectID) external view returns (uint256): This function returns the token price of an IDO project specified by _projectID. It retrieves the tokenPrice value from the corresponding IDOProject struct.

getProjectMaxCap(uint256 _projectID) external view returns (uint256): This function returns the maximum token cap of an IDO project specified by _projectID. It retrieves the maxCap value from the corresponding IDOProject struct.

getProjectTotalAmtRaised(uint256 _projectID) external view returns (uint256): This function returns the total amount raised in the specified IDO project. It retrieves the totalAmountRaised value from the corresponding IDOProject struct.

getProjectTotalTokenIDOClaimed(uint256 _projectID) external view returns (uint256): This function returns the total token amount claimed in the specified IDO project. It retrieves the totalTokenIDOClaimed value from the corresponding IDOProject struct.

getTimeLeftForAParticularProject(uint256 projectId) public view returns (uint256): This function returns the time left (in minutes) for a particular IDO project specified by projectId. It calculates the remaining time by subtracting the current block timestamp from the IDOduration value stored in the project’s IDOProject struct.

getCurrentProjectID() external view returns (uint256): This function returns the current project ID (projectsCurrentId).

getContractBal() external view returns (uint256): This function returns the balance of the contract (launchpad contract) in the native token.

getTotalInvestorsForAParticularProject(uint256 projectId) external view returns (uint256): This function returns the total number of investors in a particular IDO project specified by projectId. It retrieves the length of the projectInvestors array from the corresponding IDOProject struct.

getInvestorsForAParticularProject(uint256 _projectID) external view returns (address[] memory): This function returns an array of addresses representing the investors in a particular IDO project specified by _projectID. It retrieves the projectInvestors array from the corresponding IDOProject struct.

getUserInvestmentForAnIDOInCELO(uint256 _projectID, address _i) external view returns (uint256): This function returns the investment amount made by a specific user (_i) in an IDO project specified by _projectID. It retrieves the investment amount from the projectInvestments mapping using the _projectID and _i parameters.

getAUserAllocationForAProject(uint256 _projectID, address userAddr) external view returns (uint256): This function returns the token allocation of a specific user (userAddr) in an IDO project specified by _projectID. It retrieves the allocation amount from the allocation mapping using the _projectID and userAddr parameters.

getIDOTokenBalanceInLaunchPad(uint256 projectId) public view returns (uint256): This function returns the balance of IDO tokens held by the launch

Token Contract

In order to launch an IDO project on the launchpad for testing purposes, we need a token contract associated with the address. Let’s consider the example of a basic ERC20 token named “YamToken.”

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

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

contract YamToken is ERC20, Ownable {
    constructor() ERC20("YamToken", "YAM") {
        _mint(address(this), 9000000 * 10 ** decimals());
    }

    function mint(address _launchPadAddr, uint256 amount) public onlyOwner {
        _mint(_launchPadAddr, amount);
    }
}


Brief Breakdown of the YamToken Contract

Inside the YamToken contract, is a constructor function which is executed only once during the contract deployment. It initializes the token by giving it a name (“YamToken”) and a symbol (“YAM”).

The _mint function is then called to create and allocate an initial supply of tokens to the YamToken contract itself (address address(this)). In this example, 9,000,000 tokens are minted and multiplied by 10 raised to the power of decimals(). The decimals() function is a built-in function in the ERC20 contract that returns the number of decimal places for the token.

The mint function is defined to allow the contract owner/deployer to mint additional tokens. It takes two parameters: _launchPadAddr, the address to which the minted tokens will be sent, and amount, the number of tokens to mint. The function can only be called by the contract owner (as specified by the onlyOwner modifier), providing control over the token supply.

STEP 3 - Testing the IDO launchpad smart contract

The purpose of this unit test is to ensure that the functionality of the IDO launchpad smart contract behaves as expected and handles different scenarios correctly. It verifies the behavior of the contract by executing specific test cases and checking if the expected outcomes are met.

In the root directory of your project, navigate to the “test” folder. Inside this folder, create a new JavaScript or TypeScript file for the unit test. For example, you can name it something like Launchpad.js or Launchpad.ts . We would be using typescript for this tutorial.

import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { expect } from "chai";
import { ethers } from "hardhat";
const helpers = require("@nomicfoundation/hardhat-network-helpers")


describe("Launchpad", function () {
  // We define a fixture to reuse the same setup in every test.
  // We use loadFixture to run this setup once, snapshot that state,
  // and reset Hardhat Network to that snapshot in every test.
  async function deployLaunchpad() {

    // Contracts are deployed using the first signer/account by default
    const [launchpadadmin, projectOwner, address2, address3, address4, address5, otherAccount] = await ethers.getSigners();

    const YamToken = await ethers.getContractFactory("YamToken");
    const yamtoken = await YamToken.deploy();

    const Launchpad = await ethers.getContractFactory("Launchpad");
    const launchpad = await Launchpad.deploy();



    return { yamtoken, launchpad, projectOwner, launchpadadmin, address2, address3, address4, address5, otherAccount };
  }

  describe("Launch Project Test Cases", function () {
    it("Should revert if token price is set to zero", async function () {
      const { yamtoken, launchpad, projectOwner, address2, address3, address4, address5 } = await loadFixture(deployLaunchpad);

      const TokenPrice = 0;
      const minInvestment = ethers.utils.parseEther("1");
      const maxInvestmentt = ethers.utils.parseEther("5");
      const maxCap = ethers.utils.parseEther("10000");

      const currentTimestampInSeconds = Math.floor(Date.now() / 1000);
      const endTime = currentTimestampInSeconds + 60;

      const whiteListedAddress = [address2.address, address3.address, address4.address, address5.address]

      await expect(launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestmentt, maxCap, endTime, whiteListedAddress)).to.be.revertedWithCustomError(launchpad, "TokenPriceMustBeGreaterThanZero");

    });

    it("Should revert if minimum investment is zero", async function () {
      const { yamtoken, launchpad, projectOwner, address2, address3, address4, address5 } = await loadFixture(deployLaunchpad);
      const TokenPrice = ethers.utils.parseEther("1");
      const minInvestment = 0;
      const maxInvestment = ethers.utils.parseEther("5");
      const maxCap = ethers.utils.parseEther("10000");

      const currentTimestampInSeconds = Math.floor(Date.now() / 1000);
      const endTime = currentTimestampInSeconds + 60;

      const whiteListedAddress = [address2.address, address3.address, address4.address, address5.address]

      await expect(launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress)).to.be.revertedWithCustomError(launchpad, "MinimumInvestmentMustBeGreaterThanZero");

    })

    it("Should revert if maximum investment is lesser than minimum investment", async function () {
      const { yamtoken, launchpad, projectOwner, address2, address3, address4, address5 } = await loadFixture(deployLaunchpad);
      const TokenPrice = ethers.utils.parseUnits("10");
      const minInvestment = ethers.utils.parseEther("50");
      const maxInvestment = ethers.utils.parseEther("40");
      const maxCap = ethers.utils.parseEther("1000");

      const currentTimestampInSeconds = Math.floor(Date.now() / 1000);
      const endTime = currentTimestampInSeconds + 60;

      const whiteListedAddress = [address2.address, address3.address, address4.address, address5.address]

      await expect(launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress)).to.be.revertedWithCustomError(launchpad, "MaxInvestmentMustBeGreaterOrEqualToMinInvestment");

    })

    it("Should revert if maximum cap is lesser than the maximum investment", async function () {
      const { yamtoken, launchpad, address2, address3, address4, address5, projectOwner } = await loadFixture(deployLaunchpad);
      const TokenPrice = ethers.utils.parseUnits("10");
      const minInvestment = ethers.utils.parseEther("1");
      const maxInvestment = ethers.utils.parseEther("5000");
      const maxCap = ethers.utils.parseEther("1000");

      const currentTimestampInSeconds = Math.floor(Date.now() / 1000);
      const endTime = currentTimestampInSeconds + 60;

      const whiteListedAddress = [address2.address, address3.address, address4.address, address5.address]

      await expect(launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress)).to.be.revertedWithCustomError(launchpad, "MaxCapMustBeGreaterOrEqualToMaxInvestment");

    })


    it("Should revert if the whitelist addresses array is empty", async function () {
      const { yamtoken, launchpad, projectOwner } = await loadFixture(deployLaunchpad);
      const TokenPrice = ethers.utils.parseUnits("10");
      const minInvestment = ethers.utils.parseEther("1");
      const maxInvestment = ethers.utils.parseEther("500");
      const maxCap = ethers.utils.parseEther("10000");

      const currentTimestampInSeconds = Math.floor(Date.now() / 1000);
      const endTime = currentTimestampInSeconds + 60;

      const whiteListedAddress = new Array();

      await expect(launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress)).to.be.revertedWithCustomError(launchpad, "EmptyAddress");

    })

    it("Should revert if the whitelist addresses array contains a zero address", async function () {
      const { yamtoken, launchpad, address2, address3, address4, address5, projectOwner } = await loadFixture(deployLaunchpad);
      const TokenPrice = ethers.utils.parseUnits("10");
      const minInvestment = ethers.utils.parseEther("1");
      const maxInvestment = ethers.utils.parseEther("500");
      const maxCap = ethers.utils.parseEther("10000");
      const endTime = Math.floor(Date.now() / 1000) + 3600;
      const whiteListedAddress = [address2.address, ethers.constants.AddressZero, address3.address, address4.address, address5.address]

      await expect(launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress)).to.be.revertedWithCustomError(launchpad, "AddressZero");

    })

    it("Should revert if token address has already been whitelisted", async function () {
      const { yamtoken, launchpad, address2, address3, address4, address5, projectOwner } = await loadFixture(deployLaunchpad);
      const TokenPrice = ethers.utils.parseUnits("10");
      const minInvestment = ethers.utils.parseEther("1");
      const maxInvestment = ethers.utils.parseEther("500");
      const maxCap = ethers.utils.parseEther("10000");

      const endTime = Math.floor(Date.now() / 1000) + 3600;
      const whiteListedAddress = [address2.address, address3.address, address4.address, address5.address]

      const LaunchProject = await (launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
      LaunchProject.wait()

      await expect(launchpad.connect(address2).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress)).to.be.revertedWithCustomError(launchpad, "TokenAlreadyWhitelisted");

    })
  })

  describe("Invest Project Test Case", function () {
    it("Should revert if caller is not a whitelisted user", async function () {
      const { launchpad, yamtoken, address2, projectOwner, address3, address4, address5, otherAccount } = await loadFixture(deployLaunchpad);

      const TokenPrice = ethers.utils.parseUnits("10");
      const minInvestment = ethers.utils.parseEther("1");
      const maxInvestment = ethers.utils.parseEther("500");
      const maxCap = ethers.utils.parseEther("10000");

      const endTime = Math.floor(Date.now() / 1000) + 3600;
      const whiteListedAddress = [address2.address, address3.address, address4.address, address5.address]

      const LaunchProject = await (launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
      LaunchProject.wait()

      await expect(launchpad.connect(otherAccount).invest(1)).to.be.revertedWithCustomError(launchpad, "NotWhiteListed")
    })

    it("Should revert if project ID doesn't exist", async function () {
      const { yamtoken, projectOwner, address2, launchpad, otherAccount } = await loadFixture(deployLaunchpad);

      const TokenPrice = ethers.utils.parseUnits("10");
      const minInvestment = ethers.utils.parseEther("1");
      const maxInvestment = ethers.utils.parseEther("500");
      const maxCap = ethers.utils.parseEther("10000");

      const endTime = Math.floor(Date.now() / 1000) + 3600;
      const whiteListedAddress = [address2.address, otherAccount.address]

      const LaunchProject = await (launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
      LaunchProject.wait()

      await expect(launchpad.connect(address2).invest(12)).to.be.revertedWithCustomError(launchpad, "InvalidProjectID");
    })

    it("Should revert if contract has not been fully funded with the tokens for sale", async function () {
      const { launchpad, projectOwner, yamtoken, otherAccount, address2 } = await loadFixture(deployLaunchpad);

      const TokenPrice = ethers.utils.parseUnits("10");
      const minInvestment = ethers.utils.parseEther("1");
      const maxInvestment = ethers.utils.parseEther("500");
      const maxCap = ethers.utils.parseEther("10000");

      const endTime = Math.floor(Date.now() / 1000) + 3600;
      const whiteListedAddress = [address2.address, otherAccount.address]

      const LaunchProject = await (launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
      LaunchProject.wait()

      await expect(launchpad.connect(address2).invest(1)).to.be.revertedWithCustomError(launchpad, "ContractNotFullyFunded");

    })

    it("Should revert if project has ended", async function () {
      const { launchpad, launchpadadmin, yamtoken, otherAccount, address2 } = await loadFixture(deployLaunchpad);

      const TokenPrice = ethers.utils.parseUnits("10");
      const minInvestment = ethers.utils.parseEther("1");
      const maxInvestment = ethers.utils.parseEther("500");
      const maxCap = ethers.utils.parseEther("10000");
      const amount = ethers.utils.parseEther("1")

      const endTime = Math.floor(Date.now() / 1000) + 1;
      const whiteListedAddress = [address2.address, otherAccount.address]

      //fund contract
      const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
      TransferTokens.wait()

      const LaunchProject = await (launchpad.connect(launchpadadmin).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, 0, whiteListedAddress));
      LaunchProject.wait()

      await expect(launchpad.connect(address2).invest(1, { value: amount })).to.be.revertedWithCustomError(launchpad, "ProjectEnded");

    })

    it("Should revert if investment amount is below the minimum investment amount for IDO project", async function () {
      const { launchpad, launchpadadmin, projectOwner, yamtoken, otherAccount, address2 } = await loadFixture(deployLaunchpad);

      const TokenPrice = ethers.utils.parseUnits("10");
      const minInvestment = ethers.utils.parseEther("5");
      const maxInvestment = ethers.utils.parseEther("500");
      const maxCap = ethers.utils.parseEther("10000");
      const amount = ethers.utils.parseEther("1")

      const endTime = Math.floor(Date.now() / 1000) + 100;
      const whiteListedAddress = [address2.address, otherAccount.address]

      //fund contract
      const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
      TransferTokens.wait()

      const LaunchProject = await (launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
      LaunchProject.wait()

      await expect(launchpad.connect(address2).invest(1, { value: amount })).to.be.revertedWithCustomError(launchpad, "InvestmentAmtBelowMinimum");

    })

    it("Should revert if total investment amount of a user exceeds maximum set investment amount per account", async function () {
      const { launchpad, launchpadadmin, projectOwner, yamtoken, otherAccount, address2 } = await loadFixture(deployLaunchpad);

      const TokenPrice = ethers.utils.parseUnits("10");
      const minInvestment = ethers.utils.parseEther("2");
      const maxInvestment = ethers.utils.parseEther("10");
      const maxCap = ethers.utils.parseEther("10000");
      const amount = ethers.utils.parseEther("12")

      const endTime = Math.floor(Date.now() / 1000) + 100;
      const whiteListedAddress = [address2.address, otherAccount.address]

      //fund contract
      const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
      TransferTokens.wait()

      const LaunchProject = await (launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
      LaunchProject.wait()

      await expect(launchpad.connect(address2).invest(1, { value: amount })).to.be.revertedWithCustomError(launchpad, "InvestmentAmtExceedsMaximum");
    })


    it("Should revert if total allocation is greater than project Maximum Cap", async function () {
      const { launchpad, launchpadadmin, projectOwner, yamtoken, otherAccount, address2 } = await loadFixture(deployLaunchpad);

      const TokenPrice = ethers.utils.parseEther("0.5");
      const minInvestment = ethers.utils.parseEther("1");
      const maxInvestment = ethers.utils.parseEther("30");
      const maxCap = ethers.utils.parseEther("50");
      const amount = ethers.utils.parseEther("15")

      const endTime = Math.floor(Date.now() / 1000) + 100;
      const whiteListedAddress = [address2.address, otherAccount.address]

      //fund contract
      const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
      TransferTokens.wait()

      const LaunchProject = await (launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
      LaunchProject.wait()

      const Invest = await launchpad.connect(address2).invest(1, { value: amount })
      Invest.wait()

      const getUserInvestmentHere = await launchpad.getUserInvestmentForAnIDOInCELO(1, address2.address)
      console.log("Address 2 Investment is", getUserInvestmentHere)

      await expect(launchpad.connect(address2).invest(1, { value: amount })).to.be.revertedWithCustomError(launchpad, "MaxCapExceeded");

    })

    it("Should invest successfully", async function () {
      const { launchpad, launchpadadmin, yamtoken, otherAccount, address2 } = await loadFixture(deployLaunchpad);

      const TokenPrice = ethers.utils.parseEther("1");
      const minInvestment = ethers.utils.parseEther("2");
      const maxInvestment = ethers.utils.parseEther("10");
      const maxCap = ethers.utils.parseEther("10000");
      const amount = ethers.utils.parseEther("2")

      const endTime = Math.floor(Date.now() / 1000) + 100;
      const whiteListedAddress = [address2.address, otherAccount.address]

      //fund contract
      const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
      TransferTokens.wait()

      const LaunchProject = await (launchpad.connect(launchpadadmin).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
      LaunchProject.wait()

      const Invest = await launchpad.connect(address2).invest(1, { value: amount })
      Invest.wait()

      const userAllocation = await launchpad.getAUserAllocationForAProject(1, address2.address);
      console.log("Address2 token Investment allocation is", userAllocation)

      expect(userAllocation).greaterThanOrEqual(amount)
    })

    it("Should revert if project is not active", async function () {
      const { launchpad, launchpadadmin, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

      const TokenPrice = ethers.utils.parseEther("0.2");
      const minInvestment = ethers.utils.parseEther("1");
      const maxInvestment = ethers.utils.parseEther("10");
      const maxCap = ethers.utils.parseEther("5000");
      const amount = ethers.utils.parseEther("5")

      const endTime = Math.floor(Date.now() / 1000) + 100;
      const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

      //fund contract
      const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
      TransferTokens.wait()

      const LaunchProject = await (launchpad.connect(launchpadadmin).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
      LaunchProject.wait()

      const Invest = await launchpad.connect(address2).invest(1, { value: amount })
      Invest.wait()

      const cancelProject = await launchpad.connect(launchpadadmin).cancelIDOProject(1);
      cancelProject.wait()

      await expect(launchpad.connect(address2).invest(1, { value: amount })).to.be.revertedWithCustomError(launchpad, "ProjectNotActive");

    })

    describe("Add User For A Particular IDO Project Functionality", function () {
      it("Should revert if caller is not project owner", async function () {
        const { launchpad, launchpadadmin, projectOwner, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");
        const amount = ethers.utils.parseEther("5")

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        //fund contract
        const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
        TransferTokens.wait()

        const LaunchProject = await (launchpad.connect(launchpadadmin).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(address2).AddUserForAParticularProject(1, projectOwner.address)).to.be.revertedWithCustomError(launchpad, "NotProjectOwner");
      })

      it("Should revert if the inputed address is an zero address", async function () {
        const { launchpad, launchpadadmin, projectOwner, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        //fund contract
        const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
        TransferTokens.wait()

        const LaunchProject = await (launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(projectOwner).AddUserForAParticularProject(1, ethers.constants.AddressZero)).to.be.revertedWithCustomError(launchpad, "AddressZero");
      })

      it("Should revert if the inputed address has already been whitelisted", async function () {
        const { launchpad, launchpadadmin, projectOwner, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        //fund contract
        const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
        TransferTokens.wait()

        const LaunchProject = await (launchpad.connect(projectOwner).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(projectOwner).AddUserForAParticularProject(1, otherAccount.address)).to.be.revertedWithCustomError(launchpad, "UserAlreadyWhitelisted");
      })

      it("Should revert if the inputed projectID doesn't exist", async function () {
        const { launchpad, launchpadadmin, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        //fund contract
        const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
        TransferTokens.wait()

        const LaunchProject = await (launchpad.connect(launchpadadmin).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(launchpadadmin).AddUserForAParticularProject(12, otherAccount.address)).to.be.revertedWithCustomError(launchpad, "InvalidProjectID");
      })
    })

    describe("Withdraw Amount Raised For An IDO project Test Case", function () {
      it("Should revert if caller is not IDO project owner", async function () {
        const { launchpad, launchpadadmin, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");
        const amount = ethers.utils.parseEther("5");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        //fund contract
        const TransferTokens = await yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap)
        TransferTokens.wait()

        const LaunchProject = await launchpad.connect(launchpadadmin).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress);
        LaunchProject.wait()

        await expect(launchpad.connect(address2).withdrawAmountRaised(1)).to.be.revertedWithCustomError(launchpad, "NotProjectOwner")
      })
      it("Should revert if project ID is invalid", async function () {
        const { launchpad, launchpadadmin, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        //fund contract
        const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
        TransferTokens.wait();

        const LaunchProject = await (launchpad.connect(address2).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(address2).withdrawAmountRaised(10)).to.be.revertedWithCustomError(launchpad, "InvalidProjectID")
      })

      it("Should revert if project is still in progress", async function () {
        const { launchpad, launchpadadmin, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        //fund contract
        const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
        TransferTokens.wait();

        const LaunchProject = await (launchpad.connect(address2).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(address2).withdrawAmountRaised(1)).to.be.revertedWithCustomError(launchpad, "ProjectStillInProgress")
      })

    })

    describe("Project Cancellation Test Cases", function () {
      it("Should revert if caller is not launchpad admin", async function () {
        const { launchpad, launchpadadmin, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        //fund contract
        const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
        TransferTokens.wait()

        const LaunchProject = await (launchpad.connect(launchpadadmin).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(otherAccount).cancelIDOProject(1)).to.be.revertedWithCustomError(launchpad, "NotLaunchPadAdmin");
      })

      it("Should cancel and send the IDO tokens to the project owner successfully", async function () {
        const { launchpad, launchpadadmin, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");
        const amount = ethers.utils.parseEther("5")

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        //fund contract
        const TransferTokens = await (yamtoken.connect(launchpadadmin).mint(launchpad.address, maxCap))
        TransferTokens.wait()

        const LaunchProject = await (launchpad.connect(address2).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        const Invest = await launchpad.connect(address2).invest(1, { value: amount });
        Invest.wait()

        const OwnerBalanceBeforeCancellation = await yamtoken.balanceOf(address2.address);
        console.log("Owner's IDO token balance before cancellation is", OwnerBalanceBeforeCancellation)

        const Cancel = await launchpad.connect(launchpadadmin).cancelIDOProject(1);
        Cancel.wait();

        const OwnerBalanceAfterCancellation = await yamtoken.balanceOf(address2.address);
        console.log("Owner's IDO token balance after cancellation is", OwnerBalanceAfterCancellation)

        expect(OwnerBalanceAfterCancellation).to.be.equal(maxCap)

        const ProjectTotalRaisedFunds = await launchpad.getProjectTotalAmtRaised(1);
        console.log("Project total amount raised is", ProjectTotalRaisedFunds)

        const WithdrawAmountGottenSofar = await launchpad.connect(address2).withdrawAmountRaised(1)
        WithdrawAmountGottenSofar.wait()
      })
    })

    describe("Launchpad Admin Change Test Case", function () {
      it("Should revert when contract is paused", async function () {
        const { launchpad, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        const LaunchProject = await (launchpad.connect(address2).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(address2).changeLaunchPadAdmin(address2.address)).to.be.reverted;
      })
      it("Should revert if caller is not launchpad admin", async function () {
        const { launchpad, address3 } = await loadFixture(deployLaunchpad);

        await expect(launchpad.connect(address3).changeLaunchPadAdmin(address3.address)).to.be.revertedWithCustomError(launchpad, "NotLaunchPadAdmin");
      })

      it("Should revert if the inputed address is a zero address", async function () {
        const { launchpadadmin, launchpad } = await loadFixture(deployLaunchpad);

        await expect(launchpad.connect(launchpadadmin).changeLaunchPadAdmin(ethers.constants.AddressZero)).to.be.revertedWithCustomError(launchpad, "AddressZero")
      })


      it("Should revert if the inputed address is the same as the current launchpad admin address", async function () {
        const { launchpadadmin, launchpad } = await loadFixture(deployLaunchpad);

        await expect(launchpad.connect(launchpadadmin).changeLaunchPadAdmin(launchpadadmin.address)).to.be.revertedWithCustomError(launchpad, "OldAdmin")
      })

      it("Should set the new launchpad admin successfully", async function () {
        const { launchpadadmin, launchpad, address3 } = await loadFixture(deployLaunchpad);

        const OldAdmin = await launchpad.launchPadadmin()
        console.log("Old Launchpad Admin", OldAdmin)

        const SetNewAdmin = await launchpad.connect(launchpadadmin).changeLaunchPadAdmin(address3.address);
        SetNewAdmin.wait();

        expect(await launchpad.launchPadadmin()).to.be.equal(address3.address)
      })
    })

    describe("Sweep Function Test Case", function () {
      it("Should revert if caller is not project owner", async function () {
        const { launchpad, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = 10;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        const LaunchProject = await (launchpad.connect(address2).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(address3).sweep(1, address3.address)).to.be.revertedWithCustomError(launchpad, "NotProjectOwner");
      })

      it("Should revert when contract is paused", async function () {
        const { launchpad, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        const LaunchProject = await (launchpad.connect(address2).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(address2).sweep(1, address2.address)).to.be.reverted;
      })

      it("Should revert if project ID is not valid", async function () {
        const { launchpad, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        const LaunchProject = await (launchpad.connect(address2).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait();

        await expect(launchpad.connect(address2).sweep(10, address2.address)).to.be.revertedWithCustomError(launchpad, "InvalidProjectID")
      })

      it("Should revert if inputed address is an address zero", async function () {
        const { launchpad, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        const LaunchProject = await (launchpad.connect(address2).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(address2).sweep(1, ethers.constants.AddressZero)).to.be.revertedWithCustomError(launchpad, "AddressZero");

      })

      it("Should revert if project is still ongoing", async function () {
        const { launchpad, yamtoken, otherAccount, address2, address3 } = await loadFixture(deployLaunchpad);

        const TokenPrice = ethers.utils.parseEther("0.2");
        const minInvestment = ethers.utils.parseEther("1");
        const maxInvestment = ethers.utils.parseEther("10");
        const maxCap = ethers.utils.parseEther("5000");

        const endTime = Math.floor(Date.now() / 1000) + 100;
        const whiteListedAddress = [address2.address, address3.address, otherAccount.address]

        const LaunchProject = await (launchpad.connect(address2).listProject(yamtoken.address, TokenPrice, minInvestment, maxInvestment, maxCap, endTime, whiteListedAddress));
        LaunchProject.wait()

        await expect(launchpad.connect(address2).sweep(1, address2.address)).to.be.revertedWithCustomError(launchpad, "ProjectStillInProgress");
      })

    })

    describe("When launchpad is paused", function () {
      it("Should revert if caller is not launchpad admin", async function () {
        const { launchpad, address2 } = await loadFixture(deployLaunchpad);

        await expect(launchpad.connect(address2).pause()).to.be.revertedWithCustomError(launchpad, "NotLaunchPadAdmin")
      })
    })
  })



})

First, let’s compile our smart contract using this command line in our VSCode terminal:


npx hardhat compile

Then, run


npx hardhat test

From the image provided above, it is evident that all the test cases have successfully passed.

These test cases help verify that the IDO Launchpad smart contract functions correctly, handles invalid inputs, and enforces the necessary conditions and restrictions during project listing on the launchpad. It helps catch potential bugs, ensures the contract’s desired behavior, and improves the overall reliability and security of the Launchpad platform.

STEP 4 - Deploying the IDO launchpad smart contract

Make sure to follow these steps before deploying your contract to the Celo testnet:

Add the Celo testnet RPC to your Metamask wallet. If you haven’t done so, refer to this guide for instructions on how to add it. Also, visit this site to obtain a faucet.

Update the hardhat.config.ts file in the root directory of your project to include the Celo network configuration. This will enable the usage of your private key for your Celo account during contract deployment.

To securely store your private key, create a “.env” file and store your private key there. You can use the dotenv package to load the private key into your Hardhat configuration. Here’s an example of how to set it up:

  • Install the dotenv package:

npm install dotenv

  • Create a .env file in the root directory of your project, paste your private key, and CELOSCAN_API_KEY into it:

PRIVATE_KEY=<your-private-key>

CELOSCAN_API_KEY = <CELOSCAN_API_KEY>

Here’s an example of how to add the Celo network configuration to your hardhat.config.ts file:

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
require("dotenv").config();


type HttpNetworkAccountsUserConfig = any;
const config: HardhatUserConfig = {
  solidity: "0.8.4",
  networks: {
    alfajores: {
      url: "https://alfajores-forno.celo-testnet.org",
      accounts: [process.env.PRIVATE_KEY] as HttpNetworkAccountsUserConfig | undefined,
      chainId: 44787,
    }
  },
  etherscan: {
    apiKey: process.env.CELOSCAN_API_KEY
  }
};



export default config;

Next thing is to write our deploy scripts like so;

import { ethers } from "hardhat";

async function main() {

  ///DEPLOYING A TOKEN SAMPLE FOR INTERACTION
  const YamToken = await ethers.getContractFactory("YamToken");
  const yam = await YamToken.deploy();

  await yam.deployed();

  console.log("Yamtoken Contract Address is", yam.address);

  ///DEPLOYING LAUNCHPAD CONTRACT
  const LaunchPad = await ethers.getContractFactory("Launchpad");
  const IDOPad = await LaunchPad.deploy();

  await IDOPad.deployed();

  console.log("LaunchPad Contract Address is", IDOPad.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Let’s deploy our contract using this command line in our VSCode terminal:

npx hardhat run scripts/deploy.ts --network alfajores

IDO Launchpad Contract on Celo testnet explorer

Conclusion

Congratulations :tada:! You have successfully completed the tutorial on building a decentralized IDO launchpad contract on the Celo blockchain. Throughout this tutorial, we explored the concept of Initial DEX Offerings (IDOs) and learned how to create a launchpad application that enables the launching of projects on a decentralized platform.

Now that you have gained a solid understanding of building a decentralized launchpad on Celo, feel free to explore further and enhance the functionality of your launchpad contract. You can add additional features like token locking mechanisms, voting systems, or integrate with other protocols to expand the capabilities of your launchpad.

I hope this tutorial has provided you with valuable insights and empowered you to create your own decentralized launchpad on the Celo blockchain. Happy building, and best of luck with your future projects!

The link to the project repository can be found here.

14 Likes

I previously submitted this proposal for building a decentralized IDO launchpad platform on Celo prior to the migration to Celo Academy. I am interested in becoming the new owner of this project and managing it.

Thank you.

6 Likes

Congratulations @Oluwatosin-Serah on your first post on the academy.

Please exercise patience while you are being made the owner of the proposal by @Celo_Academy . For the meantime, you can commence working on your proposal.

6 Likes

Hi @Oluwatosin-Serah - you are now the owner and can get started whenever you’d like. Thank you and welcome to Celo Academy! :mortar_board: :seedling:

8 Likes

Thank you

6 Likes

I will be reviewing this @Oluwatosin-Serah

5 Likes

Okay, I will be awaiting your review @ishan.pathak2711 .

5 Likes

Beautiful piece. Very well explained. I learned something new.

7 Likes

I am pleased that the objective was accomplished, allowing any reader to peruse the tutorial and acquire valuable insights @OtakIsreal

6 Likes

Thank you

6 Likes

Hey @Oluwatosin-Serah , You rock it here. Keep up the good work. You can always improve better.

10 Likes

Thanks @bobelr

5 Likes

You’re welcome @Oluwatosin-Serah

10 Likes

Congrats on your first post and being part of this community. Well done :+1:

6 Likes

This is a well detailed tutorial , and it shows that a lot of work went into this , well done @Oluwatosin-Serah

4 Likes

Thank you @Clue

4 Likes

I appreciate your time in reading and thank you for the feedback, @Encrypted.

7 Likes

you are welcome , keep it up.

5 Likes

You are welcome

5 Likes

Very comprehensive, keep it up @Oluwatosin-Serah

6 Likes