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
- Introduction
- Table of Contents
- Objective
- Prerequisites
- Requirements
- Initial DEX Offering(IDO)
- Launchpad
- Tutorial
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 ! 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.