helium3@sina.com 1 сар өмнө
parent
commit
7df40688c3

+ 0 - 216
README.md

@@ -1,216 +0,0 @@
-![Img](header.png)
-
-# TypeScript Solidity Boilerplate Starter Kit
-
-A BoilerPlate Template Project To Start Solidity Development With Hardhat and Typescript.
-All you have to do is create a new repository from the template and start coding your smart contracts.
-
-## Hardhat Configuration
-
-- [Typechain](https://github.com/dethcrypto/TypeChain) plugin enabled (typescript type bindings for smart contracts)
-- [Ignition](https://hardhat.org/ignition/docs/getting-started) for contract deployment
-- Testing environment configured and operational, with test coverage
-- Prettier and eslint configured for project files and solidity smart contract
-- [Solhint](https://github.com/protofire/solhint) configured for enforcing best practices
-- Github actions workflows prepared for CI/CD
-- Prepared Smart Contract Examples, Tests, Deployments and Tasks for Common ERC Standards (ERC20, ERC721, ERC1155)
-
-Check the Hardhat documentation for more information.
-
-https://hardhat.org/getting-started/
-
-## Project Structure
-
-```text
-.
-├── contracts
-│   ├── BasicERC1155.sol
-│   ├── BasicERC20.sol
-│   └── BasicERC721.sol
-├── ignition
-│   ├── deployments
-│   ├── modules
-│   │   ├── BasicERC1155Module.ts
-│   │   ├── BasicERC20Module.ts
-│   │   └── BasicERC721Module.ts
-│   └── parameters
-│       └── custom.json
-├── tasks
-│   ├── erc1155
-│   │   ├── base-uri.ts
-│   │   ├── contract-uri.ts
-│   │   └── mint.ts
-│   ├── erc20
-│   │   └── mint.ts
-│   ├── erc721
-│   │   ├── base-uri.ts
-│   │   ├── contract-uri.ts
-│   │   └── mint.ts
-│   └── utils
-│       ├── accounts.ts
-│       ├── balance.ts
-│       ├── block-number.ts
-│       └── send-eth.ts
-├── test
-│   ├── BasicERC1155.ts
-│   ├── BasicERC20.ts
-│   └── BasicERC721.ts
-└── hardhat.config.ts
-```
-
-## Supported Networks
-
-- Hardhat Network (localhost)
-- Ethereum Mainnet
-- Ethereum Sepolia Testnet
-- Polygon Mainnet
-- Polygon Mumbai Testnet
-
-Feel free to add more networks in `hardhat.config.ts` file.
-
-## Hardhat Shorthand
-
-We recommend installing `hh autocomplete` so you can use `hh` shorthand globally.
-
-```shell
-npm i -g hardhat-shorthand
-```
-
-https://hardhat.org/guides/shorthand.html
-
-### Common Shorthand Commands
-
-- `hh compile` - to compile smart contract and generate typechain ts bindings
-- `hh test` - to run tests
-- `hh igntion` - to deploy smart contracts
-- `hh node` - to run a localhost node
-- `hh help` - to see all available commands
-- `hh TABTAB` - to use autocomplete
-
-## Usage
-
-### Setup
-
-#### 1. Install Dependencies
-
-```shell
-npm install
-```
-
-#### 2. Compile Contracts
-
-```shell
-npm run compile
-```
-
-#### 3. Environment Setup
-
-Create `.env` file and add your environment variables. You can use `.env.example` as a template.
-
-If you are going to use public network, make sure you include the right RPC provider for that network.
-
-Make sure you include either `MNEMONIC` or `PRIVATE_KEY` in your `.env` file.
-
-### Example Flow - Deploy ERC721 Token
-
-> This is an example flow to deploy an ERC721 token to a public network and interact with it.
-
-#### 1.1 Deploy BasicERC721 Contract
-
-```shell
-hh ignition deploy ignition/modules/BasicERC721Module.ts --network sepolia
-```
-
-**Verify contract**
-
-```shell
-hh ignition verify chain-11155111
-```
-
-#### 1.2 Deploy and Verify
-
-```shell
-hh ignition deploy ignition/modules/BasicERC721Module.ts --network sepolia --verify
-```
-
-#### 1.3 Deploy and Verify with Custom Parameters
-
-Look at `ignition/parameters/custom.json` to see how to adjust contract parameters
-
-```shell
-hh ignition deploy ignition/modules/BasicERC721Module.ts --network sepolia --verify --parameters ignition/parameters/custom.json
-```
-
-#### 2. Interact With Contract - Mint
-
-```shell
-hh erc721-mint \
- --contract 0x1FEB5675Be6F256c4680BE447D6C353E02e04fb9 \
- --recipient 0x73faDd7E476a9Bc2dA6D1512A528366A3E50c3cF \
- --network sepolia
-```
-
----
-
-### Testing
-
-#### Run Tests
-
-```shell
-npm run test
-```
-
-#### Run Coverage
-
-```shell
-npm run coverage
-```
-
----
-
-### Project Hygiene
-
-#### Prettier - Non Solidity Files
-
-```shell
-npm run format:check
-npm run format:write
-```
-
-#### Lint - Non Solidity Files
-
-```shell
-npm run lint:check
-npm run lint:fix
-```
-
-#### Prettier - Solidity
-
-```shell
-npm run sol:format:check
-npm run sol:format:write
-```
-
-#### Solhint - Enforcing styles and security best practices
-
-```shell
-npm run solhint
-```
-
-<br>
-
-## Contact Protokol
-
-**Need additional help with your solidity project?**
-
-Protokol builds custom blockchain and web3 solutions for organisations of all sizes. We build everything from smart contracts, to dApps, to fully bespoke web3 solutions.
-
-Reach out at [protokol.com/contact](https://www.protokol.com/contact/) to learn how our web3 development services could help bring your project to life.
-
-<br>
-
-## Join Protokol
-
-**Looking for an exciting new role in web3?**
-
-Head over to [protokol.com/careers](https://www.protokol.com/careers/) to discover the roles we have available or to submit your résumé.

+ 0 - 79
contracts/BasicERC1155.sol

@@ -1,79 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity 0.8.28;
-
-import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
-import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
-import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
-import { ERC1155Pausable } from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Pausable.sol";
-import { ERC1155Burnable } from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol";
-import { ERC1155Supply } from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
-
-/**
- * This file was generated with Openzeppelin Wizard and later modified.
- * GO TO: https://wizard.openzeppelin.com/#erc1155
- */
-contract BasicERC1155 is ERC1155, Ownable, ERC1155Pausable, ERC1155Burnable, ERC1155Supply {
-	using Strings for uint256;
-
-	string public name;
-	string public symbol;
-
-	string private _contractURI;
-
-	constructor(
-		string memory _name,
-		string memory _symbol,
-		string memory baseURI,
-		string memory contractURI_,
-		address initialOwner
-	) ERC1155(baseURI) Ownable(initialOwner) {
-		name = _name;
-		symbol = _symbol;
-		_contractURI = contractURI_;
-	}
-
-	function setContractURI(string memory contractURI_) external onlyOwner {
-		_contractURI = contractURI_;
-	}
-
-	function setURI(string memory newuri) external onlyOwner {
-		_setURI(newuri);
-	}
-
-	function pause() external onlyOwner {
-		_pause();
-	}
-
-	function unpause() external onlyOwner {
-		_unpause();
-	}
-
-	function mint(address account, uint256 id, uint256 amount) external onlyOwner {
-		_mint(account, id, amount, "");
-	}
-
-	function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) external onlyOwner {
-		_mintBatch(to, ids, amounts, "");
-	}
-
-	function contractURI() external view returns (string memory) {
-		return _contractURI;
-	}
-
-	/**
-	 * @dev Returns base uri and adds .json suffix
-	 * This is useful for metadata published on ipfs where files have .json suffix
-	 */
-	function uri(uint256 id) public view override returns (string memory) {
-		return bytes(super.uri(id)).length > 0 ? string(abi.encodePacked(super.uri(id), id.toString(), ".json")) : "";
-	}
-
-	function _update(
-		address from,
-		address to,
-		uint256[] memory ids,
-		uint256[] memory values
-	) internal override(ERC1155, ERC1155Pausable, ERC1155Supply) {
-		super._update(from, to, ids, values);
-	}
-}

+ 0 - 89
contracts/BasicERC721.sol

@@ -1,89 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity 0.8.28;
-
-import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
-import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
-import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
-import { ERC721Enumerable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
-import { ERC721Pausable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol";
-import { ERC721Burnable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
-
-/**
- * This file was generated with Openzeppelin Wizard and later modified.
- * GO TO: https://wizard.openzeppelin.com/#erc721
- */
-contract BasicERC721 is ERC721, ERC721Enumerable, ERC721Pausable, Ownable, ERC721Burnable {
-	using Strings for uint256;
-
-	uint256 private _nextTokenId;
-	string private _tokenBaseURI;
-	string private _contractURI;
-
-	constructor(
-		string memory name,
-		string memory symbol,
-		string memory tokenBaseURI,
-		string memory contractURI_,
-		address initialOwner
-	) ERC721(name, symbol) Ownable(initialOwner) {
-		_tokenBaseURI = tokenBaseURI;
-		_contractURI = contractURI_;
-	}
-
-	function safeMint(address to) external onlyOwner {
-		unchecked {
-			_nextTokenId++;
-		}
-		_safeMint(to, _nextTokenId);
-	}
-
-	function pause() external onlyOwner {
-		_pause();
-	}
-
-	function unpause() external onlyOwner {
-		_unpause();
-	}
-
-	function setBaseURI(string memory baseURI) external onlyOwner {
-		_tokenBaseURI = baseURI;
-	}
-
-	function setContractURI(string memory contractURI_) external onlyOwner {
-		_contractURI = contractURI_;
-	}
-
-	function _baseURI() internal view override returns (string memory) {
-		return _tokenBaseURI;
-	}
-
-	function contractURI() external view returns (string memory) {
-		return _contractURI;
-	}
-
-	/**
-	 * @dev Returns base uri and adds .json suffix
-	 * This is useful for metadata published on ipfs where files have .json suffix
-	 */
-	function tokenURI(uint256 tokenId) public view override returns (string memory) {
-		if (ownerOf(tokenId) == address(0)) revert ERC721NonexistentToken(tokenId);
-
-		return bytes(_baseURI()).length > 0 ? string(abi.encodePacked(_baseURI(), tokenId.toString(), ".json")) : "";
-	}
-
-	function _update(
-		address to,
-		uint256 tokenId,
-		address auth
-	) internal override(ERC721, ERC721Enumerable, ERC721Pausable) returns (address) {
-		return super._update(to, tokenId, auth);
-	}
-
-	function _increaseBalance(address account, uint128 value) internal override(ERC721, ERC721Enumerable) {
-		super._increaseBalance(account, value);
-	}
-
-	function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
-		return super.supportsInterface(interfaceId);
-	}
-}

+ 274 - 0
contracts/Launchpad.sol

@@ -0,0 +1,274 @@
+pragma solidity ^0.8.24;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "hardhat/console.sol"; // For debugging purposes, can be removed in production
+
+contract Launchpad is ReentrancyGuard {
+	address public owner;
+	address public token; // sale token address
+	uint256 public totalTokens; // total tokens for sale
+	uint256 public tokensSold; // tokens sold so far
+	uint256 public startTime; // beginning time of the sale
+	uint256 public endTime; // ending time of the sale
+	bytes32 public merkleRoot; // Merkle root for whitelisting
+	bool public enableClaimToken; // enable claim token
+	uint256 public claimStartTime; // start time for claiming tokens
+	address public paymentToken; // payment tokens accepted for the sale
+	uint256 public salePaymentPrice; //  payment token price in terms of sale token
+	mapping(address => uint256) public claimableTokensAmount; // user contributions for claiming tokens
+	uint256 public accountsCount; // number of accounts that have contributed
+	event SaleCreated(address token, uint256 totalTokens);
+	event Contributed(address user, uint256 amount, address paymentToken);
+	event TokensClaimed(address user, uint256 amount);
+	event SaleCancelled(address token, uint256 refundedAmount);
+	event EnableClaimToken(bool enabled, uint256 claimStartTime);
+	event DisableClaimToken(bool disabled);
+	constructor() {
+		owner = msg.sender;
+		accountsCount = 0;
+	}
+
+	// Modifiers
+	modifier onlyOwner() {
+		require(msg.sender == owner, "Only owner can call this function");
+		_;
+	}
+
+	modifier saleActive() {
+		require(merkleRoot != bytes32(0), "MerkleRoot not initialized");
+		console.log("Claim start time:", startTime);
+		console.log("Current time:", block.timestamp);
+		console.log("Claiming tokens enabled:", endTime);
+		require(block.timestamp >= startTime && block.timestamp <= endTime, "Sale is not active");
+		_;
+	}
+	modifier validClaimTime() {
+		require(enableClaimToken, "Claiming tokens is not enabled");
+		require(block.timestamp >= claimStartTime, "Claiming tokens not started yet");
+		_;
+	}
+
+	// Owner Functions
+	function createSale(
+		address _token,
+		uint256 _totalTokens,
+		uint256 _startTime,
+		uint256 _endTime,
+		address _paymentToken,
+		uint256 _price
+	) public onlyOwner {
+		require(_token != address(0), "Invalid token address");
+		require(_totalTokens > 0, "Total tokens must be greater than 0");
+		require(_startTime < _endTime, "Invalid time range");
+
+		paymentToken = _paymentToken;
+		salePaymentPrice = _price;
+		token = _token;
+		totalTokens = _totalTokens;
+		startTime = _startTime;
+		endTime = _endTime;
+		tokensSold = 0;
+		merkleRoot = bytes32(0); // Reset Merkle root
+		IERC20(_token).transferFrom(msg.sender, address(this), _totalTokens); // Transfer tokens to the contracts
+
+		emit SaleCreated(token, totalTokens);
+	}
+
+	function withdrawRemainingTokens() public onlyOwner {
+		require(block.timestamp > endTime, "Sale is still active");
+		uint256 remaining = IERC20(token).balanceOf(address(this));
+		IERC20(token).transfer(owner, remaining);
+	}
+
+	function withdrawPayments() public onlyOwner {
+		require(block.timestamp > endTime, "Sale is still active");
+		if (paymentToken == address(0)) {
+			payable(owner).transfer(address(this).balance);
+		} else {
+			uint256 balance = IERC20(paymentToken).balanceOf(address(this));
+			IERC20(paymentToken).transfer(owner, balance);
+		}
+	}
+
+	function cancelSale() public onlyOwner {
+		require(block.timestamp < startTime, "Sale already started");
+		require(token != address(0), "Sale not initialized");
+		uint256 balance = IERC20(token).balanceOf(address(this));
+		require(balance > 0, "No tokens to refund");
+		IERC20(token).transfer(owner, balance); // Refund tokens to owner
+		token = address(0);
+		totalTokens = 0;
+		tokensSold = 0;
+		startTime = 0;
+		endTime = 0;
+		merkleRoot = bytes32(0); // Reset Merkle root
+		emit SaleCancelled(token, balance);
+	}
+
+	function enableClaimTokens(uint256 _enableClaimTokenTime) public onlyOwner {
+		require(_enableClaimTokenTime >= block.timestamp, "Enable claim time must be in the future");
+		require(_enableClaimTokenTime >= endTime, "Claiming tokens can only be enabled after the sale ends");
+		enableClaimToken = true;
+		claimStartTime = _enableClaimTokenTime; // Set claim start time to now
+		emit EnableClaimToken(enableClaimToken, claimStartTime);
+	}
+
+	function disableClaimTokens() public onlyOwner {
+		require(enableClaimToken, "Claiming tokens is already disabled");
+		enableClaimToken = false;
+		emit DisableClaimToken(enableClaimToken);
+	}
+
+	// Setter
+	function setOwner(address _newOwner) public onlyOwner {
+		require(_newOwner != address(0), "Invalid owner address");
+		owner = _newOwner;
+	}
+
+	function setSalePayment(address _paymentToken, uint256 _price) public onlyOwner {
+		require(_price > 0, "Price must be greater than 0");
+		paymentToken = _paymentToken;
+		salePaymentPrice = _price;
+	}
+
+	function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {
+		merkleRoot = _merkleRoot;
+	}
+
+	//Getter
+
+	function getOwner() public view returns (address) {
+		return owner;
+	}
+	function getSaleToken() public view returns (address) {
+		return token;
+	}
+	function getTotalTokens() public view returns (uint256) {
+		return totalTokens;
+	}
+	function getTokensSold() public view returns (uint256) {
+		return tokensSold;
+	}
+	function getStartTime() public view returns (uint256) {
+		return startTime;
+	}
+	function getEndTime() public view returns (uint256) {
+		return endTime;
+	}
+	function getSaleDetails()
+		public
+		view
+		returns (
+			address token,
+			uint256 totalTokens,
+			uint256 tokensSold,
+			uint256 startTime,
+			uint256 endTime,
+			bytes32 merkleRoot,
+			address paymentToken,
+			uint256 salePaymentPrice
+		)
+	{
+		return (token, totalTokens, tokensSold, startTime, endTime, merkleRoot, paymentToken, salePaymentPrice);
+	}
+
+	function getClaimableTokens(address _user) public view returns (uint256) {
+		return claimableTokensAmount[_user];
+	}
+
+	function getPaymentTokenPrice() public view returns (uint256) {
+		return salePaymentPrice;
+	}
+	function getPaymentTokens() public view returns (address) {
+		return paymentToken;
+	}
+	function getClaimStartTime() public view returns (uint256) {
+		return claimStartTime;
+	}
+	function isClaimEnabled() public view returns (bool) {
+		return enableClaimToken;
+	}
+
+	//Main Function
+
+	//MUST TEST !
+	function contributeETH(
+		uint256 _buyAmount,
+		uint256 _maxBuyAmount,
+		bytes32[] memory _proof
+	) public payable saleActive nonReentrant {
+		require(paymentToken == address(0), "Payment token must be ETH for this sale");
+		require(msg.value > 0, "Must send ETH");
+		require(_buyAmount > 0, "Must buy a positive amount");
+		require(_buyAmount <= _maxBuyAmount, "Buy amount exceeds max buy amount");
+		require(salePaymentPrice > 0, "Payment token not accepted for this sale");
+
+		// 验证 Merkle 证明
+		bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxBuyAmount))));
+		require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
+
+		uint256 cost = (salePaymentPrice / 10 ** 18) * _buyAmount; // Assuming salePaymentPrice is in wei
+		require(msg.value >= cost, "Not enough ETH sent");
+		tokensSold += _buyAmount;
+		if (claimableTokensAmount[msg.sender] == 0) {
+			accountsCount++; // Increment accounts count only if this is the first contribution
+		}
+		claimableTokensAmount[msg.sender] += _buyAmount; // update user's claimable tokens
+		require(claimableTokensAmount[msg.sender] <= _maxBuyAmount, "Buy amount exceeds max buy amount");
+		payable(msg.sender).transfer(msg.value - cost); //refund excess ETH
+		emit Contributed(msg.sender, _buyAmount, address(0));
+	}
+
+	function contributeERC20(
+		uint256 _buyAmount,
+		uint256 _maxBuyAmount,
+		bytes32[] memory _proof
+	) public saleActive nonReentrant {
+		require(_buyAmount > 0, "Must send a positive amount");
+		require(_buyAmount <= _maxBuyAmount, "Buy amount exceeds max buy amount");
+
+		bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(msg.sender, _maxBuyAmount))));
+		require(MerkleProof.verify(_proof, merkleRoot, leaf), "Invalid proof");
+
+		uint256 price = salePaymentPrice;
+		require(price > 0, "Payment token not accepted for this sale");
+		uint256 cost = (price / 10 ** 18) * _buyAmount; // Assuming salePaymentPrice is in wei
+		require(
+			IERC20(paymentToken).allowance(msg.sender, address(this)) >= cost,
+			"Not enough allowance for payment token"
+		);
+		tokensSold += _buyAmount;
+		if (claimableTokensAmount[msg.sender] == 0) {
+			accountsCount++; // Increment accounts count only if this is the first contribution
+		}
+		claimableTokensAmount[msg.sender] += _buyAmount; // update user's claimable tokens
+		console.log("Claimable tokens amount for user:", claimableTokensAmount[msg.sender]);
+		require(claimableTokensAmount[msg.sender] <= _maxBuyAmount, "Buy amount exceeds max buy amount");
+		IERC20(paymentToken).transferFrom(msg.sender, address(this), cost); // 从用户账户转移支付代币
+
+		emit Contributed(msg.sender, _buyAmount, paymentToken);
+	}
+
+	function claimTokens() public validClaimTime {
+		require(block.timestamp > endTime, "Sale is still active");
+		uint256 amount = claimableTokensAmount[msg.sender];
+		require(amount > 0, "No tokens to claim");
+
+		if (tokensSold <= totalTokens) {
+			IERC20(token).transfer(msg.sender, amount); // 分发销售代币
+			emit TokensClaimed(msg.sender, amount);
+		} else {
+			uint256 boughtAmountAvg = totalTokens / accountsCount; // 平均每个账户购买的代币数量
+			uint256 refundTokenAmount = amount - boughtAmountAvg; // 计算退款金额
+			uint256 refundAmount = (refundAmount / 10 ** 18) * salePaymentPrice;
+			if (paymentToken == address(0)) {
+				payable(msg.sender).transfer(refundAmount); // 退款ETH
+			} else {
+				IERC20(paymentToken).transfer(msg.sender, refundAmount); // 退款ERC20代币
+			}
+			IERC20(token).transfer(msg.sender, boughtAmountAvg); // 分发销售代币
+		}
+	}
+}

+ 0 - 22
ignition/modules/BasicERC1155Module.ts

@@ -1,22 +0,0 @@
-import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"
-
-/**
- * BasicERC1155Module for deploying the BasicERC1155 token contract
- */
-const BasicERC1155Module = buildModule("BasicERC1155Module", (m) => {
-	// Contract parameters
-	const tokenName = m.getParameter("name", "Default Token Name")
-	const tokenSymbol = m.getParameter("symbol", "DTN")
-	const baseUri = m.getParameter("baseUri", "ipfs://base-uri/")
-	const contractUri = m.getParameter("contractUri", "ipfs://contract-uri")
-	// Account index 0 is the owner and deployer
-	const owner = m.getAccount(0)
-
-	const basicERC1155 = m.contract("BasicERC1155", [tokenName, tokenSymbol, baseUri, contractUri, owner], {
-		from: owner,
-	})
-
-	return { basicERC1155 }
-})
-
-export default BasicERC1155Module

+ 0 - 22
ignition/modules/BasicERC721Module.ts

@@ -1,22 +0,0 @@
-import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"
-
-/**
- * BasicERC721Module for deploying the BasicERC721 token contract
- */
-const BasicERC721Module = buildModule("BasicERC721Module", (m) => {
-	// Contract parameters
-	const tokenName = m.getParameter("name", "Default Token Name")
-	const tokenSymbol = m.getParameter("symbol", "DTN")
-	const baseUri = m.getParameter("baseUri", "ipfs://base-uri/")
-	const contractUri = m.getParameter("contractUri", "ipfs://contract-uri")
-	// Account index 0 is the owner and deployer
-	const owner = m.getAccount(0)
-
-	const basicERC721 = m.contract("BasicERC721", [tokenName, tokenSymbol, baseUri, contractUri, owner], {
-		from: owner,
-	})
-
-	return { basicERC721 }
-})
-
-export default BasicERC721Module

+ 16 - 0
ignition/modules/LaunchpadModule.ts

@@ -0,0 +1,16 @@
+import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"
+
+/**
+ * LaunchpadModule for deploying the Launchpad contract
+ */
+const LaunchpadModule = buildModule("LaunchpadModule", (m) => {
+	const owner = m.getAccount(0)
+
+	const launchpad = m.contract("Launchpad", [], {
+		from: owner,
+	})
+
+	return { launchpad }
+})
+
+export default LaunchpadModule

+ 0 - 12
ignition/parameters/custom.json

@@ -2,17 +2,5 @@
 	"BasicERC20Module": {
 		"name": "ProtoToken",
 		"symbol": "PT"
-	},
-	"BasicERC721Module": {
-		"name": "ProtoToken",
-		"symbol": "PT",
-		"baseUri": "ipfs://base-uri/",
-		"contractUri": "ipfs://contract-uri"
-	},
-	"BasicERC1155Module": {
-		"name": "ProtoToken",
-		"symbol": "PT",
-		"baseUri": "ipfs://base-uri/",
-		"contractUri": "ipfs://contract-uri"
 	}
 }

+ 273 - 20
package-lock.json

@@ -9,7 +9,8 @@
 			"version": "1.0.0",
 			"license": "MIT",
 			"dependencies": {
-				"@openzeppelin/contracts": "5.3.0"
+				"@openzeppelin/contracts": "^5.3.0",
+				"@openzeppelin/merkle-tree": "^1.0.8"
 			},
 			"devDependencies": {
 				"@eslint/eslintrc": "^3.3.1",
@@ -605,7 +606,6 @@
 			"version": "4.0.1",
 			"resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz",
 			"integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==",
-			"dev": true,
 			"bin": {
 				"rlp": "bin/rlp"
 			},
@@ -627,7 +627,6 @@
 			"version": "8.1.0",
 			"resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz",
 			"integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==",
-			"dev": true,
 			"dependencies": {
 				"@ethereumjs/rlp": "^4.0.1",
 				"ethereum-cryptography": "^2.0.0",
@@ -641,7 +640,6 @@
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
 			"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
-			"dev": true,
 			"dependencies": {
 				"@noble/hashes": "1.3.1"
 			},
@@ -653,7 +651,6 @@
 			"version": "1.3.1",
 			"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
 			"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
-			"dev": true,
 			"engines": {
 				"node": ">= 16"
 			},
@@ -665,7 +662,6 @@
 			"version": "2.1.2",
 			"resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz",
 			"integrity": "sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==",
-			"dev": true,
 			"dependencies": {
 				"@noble/curves": "1.1.0",
 				"@noble/hashes": "1.3.1",
@@ -1623,13 +1619,166 @@
 				"@jridgewell/sourcemap-codec": "^1.4.10"
 			}
 		},
+		"node_modules/@metamask/abi-utils": {
+			"version": "2.0.4",
+			"resolved": "https://registry.npmjs.org/@metamask/abi-utils/-/abi-utils-2.0.4.tgz",
+			"integrity": "sha512-StnIgUB75x7a7AgUhiaUZDpCsqGp7VkNnZh2XivXkJ6mPkE83U8ARGQj5MbRis7VJY8BC5V1AbB1fjdh0hupPQ==",
+			"license": "(Apache-2.0 AND MIT)",
+			"dependencies": {
+				"@metamask/superstruct": "^3.1.0",
+				"@metamask/utils": "^9.0.0"
+			},
+			"engines": {
+				"node": ">=16.0.0"
+			}
+		},
+		"node_modules/@metamask/superstruct": {
+			"version": "3.2.1",
+			"resolved": "https://registry.npmjs.org/@metamask/superstruct/-/superstruct-3.2.1.tgz",
+			"integrity": "sha512-fLgJnDOXFmuVlB38rUN5SmU7hAFQcCjrg3Vrxz67KTY7YHFnSNEKvX4avmEBdOI0yTCxZjwMCFEqsC8k2+Wd3g==",
+			"license": "MIT",
+			"engines": {
+				"node": ">=16.0.0"
+			}
+		},
+		"node_modules/@metamask/utils": {
+			"version": "9.3.0",
+			"resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-9.3.0.tgz",
+			"integrity": "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==",
+			"license": "ISC",
+			"dependencies": {
+				"@ethereumjs/tx": "^4.2.0",
+				"@metamask/superstruct": "^3.1.0",
+				"@noble/hashes": "^1.3.1",
+				"@scure/base": "^1.1.3",
+				"@types/debug": "^4.1.7",
+				"debug": "^4.3.4",
+				"pony-cause": "^2.1.10",
+				"semver": "^7.5.4",
+				"uuid": "^9.0.1"
+			},
+			"engines": {
+				"node": ">=16.0.0"
+			}
+		},
+		"node_modules/@metamask/utils/node_modules/@ethereumjs/common": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz",
+			"integrity": "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==",
+			"license": "MIT",
+			"dependencies": {
+				"@ethereumjs/util": "^8.1.0",
+				"crc-32": "^1.2.0"
+			}
+		},
+		"node_modules/@metamask/utils/node_modules/@ethereumjs/tx": {
+			"version": "4.2.0",
+			"resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-4.2.0.tgz",
+			"integrity": "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==",
+			"license": "MPL-2.0",
+			"dependencies": {
+				"@ethereumjs/common": "^3.2.0",
+				"@ethereumjs/rlp": "^4.0.1",
+				"@ethereumjs/util": "^8.1.0",
+				"ethereum-cryptography": "^2.0.0"
+			},
+			"engines": {
+				"node": ">=14"
+			}
+		},
+		"node_modules/@metamask/utils/node_modules/@noble/curves": {
+			"version": "1.4.2",
+			"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz",
+			"integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==",
+			"license": "MIT",
+			"dependencies": {
+				"@noble/hashes": "1.4.0"
+			},
+			"funding": {
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/@metamask/utils/node_modules/@noble/hashes": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
+			"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
+			"license": "MIT",
+			"engines": {
+				"node": ">= 16"
+			},
+			"funding": {
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/@metamask/utils/node_modules/@scure/bip32": {
+			"version": "1.4.0",
+			"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz",
+			"integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==",
+			"license": "MIT",
+			"dependencies": {
+				"@noble/curves": "~1.4.0",
+				"@noble/hashes": "~1.4.0",
+				"@scure/base": "~1.1.6"
+			},
+			"funding": {
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/@metamask/utils/node_modules/@scure/bip39": {
+			"version": "1.3.0",
+			"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz",
+			"integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@noble/hashes": "~1.4.0",
+				"@scure/base": "~1.1.6"
+			},
+			"funding": {
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/@metamask/utils/node_modules/ethereum-cryptography": {
+			"version": "2.2.1",
+			"resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz",
+			"integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==",
+			"license": "MIT",
+			"dependencies": {
+				"@noble/curves": "1.4.2",
+				"@noble/hashes": "1.4.0",
+				"@scure/bip32": "1.4.0",
+				"@scure/bip39": "1.3.0"
+			}
+		},
+		"node_modules/@metamask/utils/node_modules/semver": {
+			"version": "7.7.2",
+			"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+			"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+			"license": "ISC",
+			"bin": {
+				"semver": "bin/semver.js"
+			},
+			"engines": {
+				"node": ">=10"
+			}
+		},
+		"node_modules/@metamask/utils/node_modules/uuid": {
+			"version": "9.0.1",
+			"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+			"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+			"funding": [
+				"https://github.com/sponsors/broofa",
+				"https://github.com/sponsors/ctavan"
+			],
+			"license": "MIT",
+			"bin": {
+				"uuid": "dist/bin/uuid"
+			}
+		},
 		"node_modules/@noble/ciphers": {
 			"version": "1.3.0",
 			"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
 			"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
-			"dev": true,
 			"license": "MIT",
-			"peer": true,
 			"engines": {
 				"node": "^14.21.3 || >=16"
 			},
@@ -1653,7 +1802,6 @@
 			"version": "1.3.2",
 			"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
 			"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
-			"dev": true,
 			"engines": {
 				"node": ">= 16"
 			},
@@ -2605,6 +2753,96 @@
 			"integrity": "sha512-zj/KGoW7zxWUE8qOI++rUM18v+VeLTTzKs/DJFkSzHpQFPD/jKKF0TrMxBfGLl3kpdELCNccvB3zmofSzm4nlA==",
 			"license": "MIT"
 		},
+		"node_modules/@openzeppelin/merkle-tree": {
+			"version": "1.0.8",
+			"resolved": "https://registry.npmjs.org/@openzeppelin/merkle-tree/-/merkle-tree-1.0.8.tgz",
+			"integrity": "sha512-E2c9/Y3vjZXwVvPZKqCKUn7upnvam1P1ZhowJyZVQSkzZm5WhumtaRr+wkUXrZVfkIc7Gfrl7xzabElqDL09ow==",
+			"license": "MIT",
+			"dependencies": {
+				"@metamask/abi-utils": "^2.0.4",
+				"ethereum-cryptography": "^3.0.0"
+			}
+		},
+		"node_modules/@openzeppelin/merkle-tree/node_modules/@noble/curves": {
+			"version": "1.9.0",
+			"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz",
+			"integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==",
+			"license": "MIT",
+			"dependencies": {
+				"@noble/hashes": "1.8.0"
+			},
+			"engines": {
+				"node": "^14.21.3 || >=16"
+			},
+			"funding": {
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/@openzeppelin/merkle-tree/node_modules/@noble/hashes": {
+			"version": "1.8.0",
+			"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+			"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+			"license": "MIT",
+			"engines": {
+				"node": "^14.21.3 || >=16"
+			},
+			"funding": {
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/@openzeppelin/merkle-tree/node_modules/@scure/base": {
+			"version": "1.2.6",
+			"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
+			"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
+			"license": "MIT",
+			"funding": {
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/@openzeppelin/merkle-tree/node_modules/@scure/bip32": {
+			"version": "1.7.0",
+			"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
+			"integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
+			"license": "MIT",
+			"dependencies": {
+				"@noble/curves": "~1.9.0",
+				"@noble/hashes": "~1.8.0",
+				"@scure/base": "~1.2.5"
+			},
+			"funding": {
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/@openzeppelin/merkle-tree/node_modules/@scure/bip39": {
+			"version": "1.6.0",
+			"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
+			"integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
+			"license": "MIT",
+			"dependencies": {
+				"@noble/hashes": "~1.8.0",
+				"@scure/base": "~1.2.5"
+			},
+			"funding": {
+				"url": "https://paulmillr.com/funding/"
+			}
+		},
+		"node_modules/@openzeppelin/merkle-tree/node_modules/ethereum-cryptography": {
+			"version": "3.2.0",
+			"resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-3.2.0.tgz",
+			"integrity": "sha512-Urr5YVsalH+Jo0sYkTkv1MyI9bLYZwW8BENZCeE1QYaTHETEYx0Nv/SVsWkSqpYrzweg6d8KMY1wTjH/1m/BIg==",
+			"license": "MIT",
+			"dependencies": {
+				"@noble/ciphers": "1.3.0",
+				"@noble/curves": "1.9.0",
+				"@noble/hashes": "1.8.0",
+				"@scure/bip32": "1.7.0",
+				"@scure/bip39": "1.6.0"
+			},
+			"engines": {
+				"node": "^14.21.3 || >=16",
+				"npm": ">=9"
+			}
+		},
 		"node_modules/@openzeppelin/test-helpers": {
 			"version": "0.5.16",
 			"resolved": "https://registry.npmjs.org/@openzeppelin/test-helpers/-/test-helpers-0.5.16.tgz",
@@ -2728,7 +2966,6 @@
 			"version": "1.1.9",
 			"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz",
 			"integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==",
-			"dev": true,
 			"license": "MIT",
 			"funding": {
 				"url": "https://paulmillr.com/funding/"
@@ -2738,7 +2975,6 @@
 			"version": "1.3.1",
 			"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
 			"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
-			"dev": true,
 			"dependencies": {
 				"@noble/curves": "~1.1.0",
 				"@noble/hashes": "~1.3.1",
@@ -2752,7 +2988,6 @@
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
 			"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
-			"dev": true,
 			"dependencies": {
 				"@noble/hashes": "1.3.1"
 			},
@@ -2764,7 +2999,6 @@
 			"version": "1.3.1",
 			"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
 			"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
-			"dev": true,
 			"engines": {
 				"node": ">= 16"
 			},
@@ -2776,7 +3010,6 @@
 			"version": "1.2.1",
 			"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
 			"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
-			"dev": true,
 			"dependencies": {
 				"@noble/hashes": "~1.3.0",
 				"@scure/base": "~1.1.0"
@@ -4451,6 +4684,15 @@
 				"@types/chai": "*"
 			}
 		},
+		"node_modules/@types/debug": {
+			"version": "4.1.12",
+			"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+			"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@types/ms": "*"
+			}
+		},
 		"node_modules/@types/estree": {
 			"version": "1.0.8",
 			"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -4509,6 +4751,12 @@
 			"dev": true,
 			"peer": true
 		},
+		"node_modules/@types/ms": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+			"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+			"license": "MIT"
+		},
 		"node_modules/@types/node": {
 			"version": "22.15.34",
 			"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.34.tgz",
@@ -6350,7 +6598,6 @@
 			"version": "1.2.2",
 			"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
 			"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
-			"dev": true,
 			"bin": {
 				"crc32": "bin/crc32.njs"
 			},
@@ -6527,7 +6774,6 @@
 			"version": "4.3.4",
 			"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
 			"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
-			"dev": true,
 			"dependencies": {
 				"ms": "2.1.2"
 			},
@@ -10193,8 +10439,7 @@
 		"node_modules/micro-ftch": {
 			"version": "0.3.1",
 			"resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz",
-			"integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==",
-			"dev": true
+			"integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg=="
 		},
 		"node_modules/micro-packed": {
 			"version": "0.7.3",
@@ -10493,8 +10738,7 @@
 		"node_modules/ms": {
 			"version": "2.1.2",
 			"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-			"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-			"dev": true
+			"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
 		},
 		"node_modules/multibase": {
 			"version": "0.6.1",
@@ -11429,6 +11673,15 @@
 				"node": ">=4"
 			}
 		},
+		"node_modules/pony-cause": {
+			"version": "2.1.11",
+			"resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz",
+			"integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==",
+			"license": "0BSD",
+			"engines": {
+				"node": ">=12.0.0"
+			}
+		},
 		"node_modules/prelude-ls": {
 			"version": "1.1.2",
 			"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",

+ 5 - 5
package.json

@@ -8,6 +8,9 @@
 		"coverage": "hardhat coverage",
 		"deploy": "hardhat ignition deploy",
 		"node": "hardhat node",
+		"deployLaunchpad:local": "npx hardhat ignition deploy ignition/modules/LaunchpadModule.ts --network localhost --reset",
+		"visualize": "npx hardhat ignition visualize ignition/modules/LaunchpadModule.ts",
+		"deployLaunchpad:sep": "npx hardhat ignition deploy ignition/modules/LaunchpadModule.ts --network sepolia",
 		"sol:format:check": "prettier --check --plugin=prettier-plugin-solidity 'contracts/**/*.sol'",
 		"sol:format:write": "prettier --write --plugin=prettier-plugin-solidity 'contracts/**/*.sol'",
 		"solhint": "solhint -f table 'contracts/**/*.sol'",
@@ -16,13 +19,10 @@
 		"lint:check": "eslint .",
 		"lint:fix": "eslint --fix ."
 	},
-	"contributors": [
-		"Kristjan Kosic <kristjan@protokol.com>",
-		"Žan Kovač <zan@protokol.com>"
-	],
 	"license": "MIT",
 	"dependencies": {
-		"@openzeppelin/contracts": "5.3.0"
+		"@openzeppelin/contracts": "^5.3.0",
+		"@openzeppelin/merkle-tree": "^1.0.8"
 	},
 	"devDependencies": {
 		"@eslint/eslintrc": "^3.3.1",

+ 0 - 21
tasks/erc1155/base-uri.ts

@@ -1,21 +0,0 @@
-import { task } from "hardhat/config"
-
-/**
- Example:
- npx hardhat erc1155-base-uri \
- --contract 0x320bd6de80d3D5361e1c9bB4CF1D7D9Ef24F3Ac7 \
- --uri https://ipfs.io/ipfs/new-base-uri-ipfs-hash/ \
- --network sepolia
- */
-task("erc1155-base-uri", "Set new base URI for BasicERC1155 Smart Contract")
-	.addParam<string>("contract", "BasicERC1155 Smart Contract Address")
-	.addParam<string>("uri", "New Base URI")
-	.setAction(async (taskArgs, { ethers }) => {
-		const contract = await ethers.getContractAt("BasicERC1155", taskArgs.contract)
-
-		const trx = await contract.setURI(taskArgs.uri)
-
-		console.log(`Transaction Hash: ${trx.hash}`)
-		await trx.wait(2)
-		console.log("Transaction confirmed")
-	})

+ 0 - 25
tasks/erc1155/contract-uri.ts

@@ -1,25 +0,0 @@
-import { task } from "hardhat/config"
-
-/**
- Example:
- npx hardhat erc1155-contract-uri \
- --contract 0x320bd6de80d3D5361e1c9bB4CF1D7D9Ef24F3Ac7 \
- --uri https://ipfs.io/ipfs/new-contract-uri-ipfs-hash \
- --network sepolia
- */
-task("erc1155-contract-uri", "Set new Contract URI for BasicERC1155 Smart Contract")
-	.addParam<string>("contract", "BasicERC1155 Smart Contract Address")
-	.addParam<string>("uri", "New Contract URI")
-	.setAction(async (taskArgs, { ethers }) => {
-		const contract = await ethers.getContractAt("BasicERC1155", taskArgs.contract)
-
-		console.log(`Current Contract URI: ${await contract.contractURI()}`)
-
-		const trx = await contract.setContractURI(taskArgs.uri)
-
-		console.log(`Transaction Hash: ${trx.hash}`)
-		await trx.wait(2)
-		console.log("Transaction confirmed")
-
-		console.log(`New Contract URI: ${await contract.contractURI()}`)
-	})

+ 0 - 3
tasks/erc1155/index.ts

@@ -1,3 +0,0 @@
-import "./base-uri"
-import "./contract-uri"
-import "./mint"

+ 0 - 25
tasks/erc1155/mint.ts

@@ -1,25 +0,0 @@
-import { task } from "hardhat/config"
-
-/**
- Example:
- npx hardhat erc1155-mint \
- --contract 0x320bd6de80d3D5361e1c9bB4CF1D7D9Ef24F3Ac7 \
- --recipient 0x73faDd7E476a9Bc2dA6D1512A528366A3E50c3cF \
- --id 1 \
- --amount 10 \
- --network sepolia
- */
-task("erc1155-mint", "Mint tokens for BasicERC1155 Smart Contract")
-	.addParam<string>("contract", "BasicERC1155 Smart Contract Address")
-	.addParam<string>("recipient", "Token Recipient")
-	.addParam<string>("id", "Token ID")
-	.addParam<string>("amount", "Token Amount")
-	.setAction(async (taskArgs, { ethers }) => {
-		const contract = await ethers.getContractAt("BasicERC1155", taskArgs.contract)
-
-		const mintTrx = await contract.mint(taskArgs.recipient, taskArgs.id, taskArgs.amount)
-
-		console.log(`Transaction Hash: ${mintTrx.hash}`)
-		await mintTrx.wait(2)
-		console.log("Transaction confirmed")
-	})

+ 0 - 21
tasks/erc721/base-uri.ts

@@ -1,21 +0,0 @@
-import { task } from "hardhat/config"
-
-/**
- Example:
- npx hardhat erc721-base-uri \
- --contract 0x77337983A7D1699FaF51a5f43b9907fB7B614097 \
- --uri https://ipfs.io/ipfs/new-base-uri-ipfs-hash/ \
- --network sepolia
- */
-task("erc721-base-uri", "Set new base URI for BasicERC721 Smart Contract")
-	.addParam<string>("contract", "BasicERC721 Smart Contract Address")
-	.addParam<string>("uri", "New Base URI")
-	.setAction(async (taskArgs, { ethers }) => {
-		const contract = await ethers.getContractAt("BasicERC721", taskArgs.contract)
-
-		const trx = await contract.setBaseURI(taskArgs.uri)
-
-		console.log(`Transaction Hash: ${trx.hash}`)
-		await trx.wait(2)
-		console.log("Transaction confirmed")
-	})

+ 0 - 25
tasks/erc721/contract-uri.ts

@@ -1,25 +0,0 @@
-import { task } from "hardhat/config"
-
-/**
- Example:
- npx hardhat erc721-contract-uri \
- --contract 0x77337983A7D1699FaF51a5f43b9907fB7B614097 \
- --uri https://ipfs.io/ipfs/new-contract-uri-ipfs-hash \
- --network sepolia
- */
-task("erc721-contract-uri", "Set new Contract URI for BasicERC721 Smart Contract")
-	.addParam<string>("contract", "BasicERC721 Smart Contract Address")
-	.addParam<string>("uri", "New Contract URI")
-	.setAction(async (taskArgs, { ethers }) => {
-		const contract = await ethers.getContractAt("BasicERC721", taskArgs.contract)
-
-		console.log(`Current Contract URI: ${await contract.contractURI()}`)
-
-		const trx = await contract.setContractURI(taskArgs.uri)
-
-		console.log(`Transaction Hash: ${trx.hash}`)
-		await trx.wait(2)
-		console.log("Transaction confirmed")
-
-		console.log(`New Contract URI: ${await contract.contractURI()}`)
-	})

+ 0 - 3
tasks/erc721/index.ts

@@ -1,3 +0,0 @@
-import "./base-uri"
-import "./contract-uri"
-import "./mint"

+ 0 - 21
tasks/erc721/mint.ts

@@ -1,21 +0,0 @@
-import { task } from "hardhat/config"
-
-/**
- Example:
- npx hardhat erc721-mint \
- --contract 0x77337983A7D1699FaF51a5f43b9907fB7B614097 \
- --recipient 0x73faDd7E476a9Bc2dA6D1512A528366A3E50c3cF \
- --network sepolia
- */
-task("erc721-mint", "Mint token for BasicERC721 Smart Contract")
-	.addParam<string>("contract", "BasicERC721 Smart Contract Address")
-	.addParam<string>("recipient", "NFT Token Recipient")
-	.setAction(async (taskArgs, { ethers }) => {
-		const contract = await ethers.getContractAt("BasicERC721", taskArgs.contract)
-
-		const mintTrx = await contract.safeMint(taskArgs.recipient)
-
-		console.log(`Transaction Hash: ${mintTrx.hash}`)
-		await mintTrx.wait(2)
-		console.log("Transaction confirmed")
-	})

+ 0 - 2
tasks/index.ts

@@ -1,4 +1,2 @@
 import "./erc20"
-import "./erc721"
-import "./erc1155"
 import "./utils"

+ 0 - 90
test/BasicERC1155.ts

@@ -1,90 +0,0 @@
-import { expect } from "chai"
-import { ethers } from "hardhat"
-import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"
-
-describe("BasicERC1155", () => {
-	const setupFixture = async () => {
-		const signers = await ethers.getSigners()
-
-		const name = "ProtoToken"
-		const symbol = "PT"
-		const baseURI = "ipfs://base-uri/"
-		const contractURI = "ipfs://contract-uri"
-		const owner = signers[0].address
-
-		const BasicERC1155 = await ethers.getContractFactory("BasicERC1155")
-		const contract = await BasicERC1155.deploy(name, symbol, baseURI, contractURI, owner, {
-			from: owner,
-		})
-
-		return {
-			contract,
-			contractAddress: await contract.getAddress(),
-			deployer: owner,
-			accounts: await ethers.getSigners(),
-			contractConstructor: {
-				name,
-				symbol,
-				baseURI,
-				contractURI,
-				owner,
-			},
-		}
-	}
-
-	it("Should Return Valid Contract Configurations Passed In Constructor", async () => {
-		const { contractConstructor, contract } = await loadFixture(setupFixture)
-
-		expect(await contract.name()).to.equal(contractConstructor.name)
-		expect(await contract.symbol()).to.equal(contractConstructor.symbol)
-		expect(await contract.contractURI()).to.equal(contractConstructor.contractURI)
-		expect(await contract.owner()).to.equal(contractConstructor.owner)
-	})
-
-	describe("Minting Functionality", () => {
-		it("Should Increase Supply When Minting", async () => {
-			const { contract, deployer } = await loadFixture(setupFixture)
-
-			expect(await contract["totalSupply(uint256)"](1)).to.equal(0)
-			expect(await contract["totalSupply(uint256)"](2)).to.equal(0)
-
-			await contract.mintBatch(deployer, [1, 2], [2000, 4000])
-
-			expect(await contract["totalSupply(uint256)"](1)).to.equal(2000)
-			expect(await contract["totalSupply(uint256)"](2)).to.equal(4000)
-
-			expect(await contract.balanceOf(deployer, 1)).to.equal(2000)
-			expect(await contract.balanceOf(deployer, 2)).to.equal(4000)
-		})
-
-		it("Should Allow Minting Only to Contract Owner - mint", async () => {
-			const { contract, accounts } = await loadFixture(setupFixture)
-
-			await expect(contract.connect(accounts[1]).mint(accounts[1].address, 1, 1000))
-				.to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount")
-				.withArgs(await accounts[1].getAddress())
-		})
-
-		it("Should Allow Minting Only to Contract Owner - mintBatch", async () => {
-			const { contract, accounts } = await loadFixture(setupFixture)
-
-			await expect(contract.connect(accounts[1]).mintBatch(accounts[1].address, [1], [1000]))
-				.to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount")
-				.withArgs(await accounts[1].getAddress())
-		})
-	})
-
-	describe("Contract And Token Metadata", () => {
-		it("Should Return Correct Token URI", async () => {
-			const { contract, contractConstructor } = await loadFixture(setupFixture)
-
-			expect(await contract.uri(1)).to.equal(`${contractConstructor.baseURI}1.json`)
-		})
-
-		it("Should Return Correct Contract URI", async () => {
-			const { contract, contractConstructor } = await loadFixture(setupFixture)
-
-			expect(await contract.contractURI()).to.equal(contractConstructor.contractURI)
-		})
-	})
-})

+ 0 - 138
test/BasicERC721.ts

@@ -1,138 +0,0 @@
-import { expect } from "chai"
-import { ethers } from "hardhat"
-import { makeInterfaceId } from "@openzeppelin/test-helpers"
-import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"
-
-describe("BasicERC721", () => {
-	const setupFixture = async () => {
-		const signers = await ethers.getSigners()
-
-		const name = "ProtoToken"
-		const symbol = "PT"
-		const baseURI = "ipfs://base-uri/"
-		const contractURI = "ipfs://contract-uri"
-		const owner = signers[0].address
-
-		const BasicERC721 = await ethers.getContractFactory("BasicERC721")
-		const contract = await BasicERC721.deploy(name, symbol, baseURI, contractURI, owner, {
-			from: owner,
-		})
-
-		return {
-			contract,
-			contractAddress: await contract.getAddress(),
-			deployer: owner,
-			accounts: await ethers.getSigners(),
-			contractConstructor: {
-				name,
-				symbol,
-				baseURI,
-				contractURI,
-				owner,
-			},
-		}
-	}
-
-	it("Should Return Valid Contract Configurations Passed In Constructor", async () => {
-		const { contractConstructor, contract } = await loadFixture(setupFixture)
-
-		expect(await contract.name()).to.equal(contractConstructor.name)
-		expect(await contract.symbol()).to.equal(contractConstructor.symbol)
-		expect(await contract.contractURI()).to.equal(contractConstructor.contractURI)
-		expect(await contract.owner()).to.equal(contractConstructor.owner)
-	})
-
-	describe("Minting Functionality", () => {
-		it("Should Increase Total Supply When Minting", async () => {
-			const { contract, deployer } = await loadFixture(setupFixture)
-
-			expect(await contract.totalSupply()).to.equal(0)
-
-			await contract.safeMint(deployer)
-
-			await contract.safeMint(deployer)
-
-			await contract.safeMint(deployer)
-
-			expect(await contract.totalSupply()).to.equal(3)
-		})
-
-		it("Should Mint Tokens With Correct Token IDs", async () => {
-			const { contract, accounts } = await loadFixture(setupFixture)
-
-			await contract.safeMint(await accounts[0].getAddress())
-
-			await contract.safeMint(await accounts[1].getAddress())
-
-			await contract.safeMint(await accounts[2].getAddress())
-
-			expect(await contract.ownerOf(1)).to.equal(await accounts[0].getAddress())
-			expect(await contract.ownerOf(2)).to.equal(await accounts[1].getAddress())
-			expect(await contract.ownerOf(3)).to.equal(await accounts[2].getAddress())
-		})
-
-		it("Should Allow Minting Only to Contract Owner", async () => {
-			const { contract, accounts } = await loadFixture(setupFixture)
-
-			await expect(contract.connect(accounts[1]).safeMint(await accounts[1].getAddress()))
-				.to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount")
-				.withArgs(await accounts[1].getAddress())
-		})
-	})
-
-	describe("Contract And Token Metadata", () => {
-		it("Should Return Correct Token URI", async () => {
-			const { contract, accounts, contractConstructor } = await loadFixture(setupFixture)
-
-			await contract.safeMint(await accounts[0].getAddress())
-
-			expect(await contract.tokenURI(1)).to.equal(`${contractConstructor.baseURI}1.json`)
-		})
-
-		it("Should Return Correct Token URI", async () => {
-			const { contract } = await loadFixture(setupFixture)
-
-			await expect(contract.tokenURI(1))
-				.to.be.revertedWithCustomError(contract, "ERC721NonexistentToken")
-				.withArgs(1)
-		})
-
-		it("Should Return Correct Contract URI", async () => {
-			const { contract, contractConstructor } = await loadFixture(setupFixture)
-
-			expect(await contract.contractURI()).to.equal(contractConstructor.contractURI)
-		})
-	})
-
-	describe("InterfaceId", () => {
-		it("Should Validate IERC721", async () => {
-			const { contract } = await loadFixture(setupFixture)
-
-			const erc721InterfaceId = makeInterfaceId.ERC165([
-				"balanceOf(address)",
-				"ownerOf(uint256)",
-				"safeTransferFrom(address,address,uint256)",
-				"transferFrom(address,address,uint256)",
-				"approve(address,uint256)",
-				"getApproved(uint256)",
-				"setApprovalForAll(address,bool)",
-				"isApprovedForAll(address,address)",
-				"safeTransferFrom(address,address,uint256,bytes)",
-			])
-
-			expect(await contract.supportsInterface(erc721InterfaceId)).to.equal(true)
-		})
-
-		it("Should Validate IERC721Enumerable", async () => {
-			const { contract } = await loadFixture(setupFixture)
-
-			const erc721EnumerableInterfaceId = makeInterfaceId.ERC165([
-				"totalSupply()",
-				"tokenOfOwnerByIndex(address,uint256)",
-				"tokenByIndex(uint256)",
-			])
-
-			expect(await contract.supportsInterface(erc721EnumerableInterfaceId)).to.equal(true)
-		})
-	})
-})

+ 317 - 0
test/LaunchpadOwner.ts

@@ -0,0 +1,317 @@
+import { expect } from "chai"
+import hre, { ethers } from "hardhat"
+import { StandardMerkleTree } from "@openzeppelin/merkle-tree"
+import { time } from "@nomicfoundation/hardhat-toolbox/network-helpers"
+
+describe("Launchpad", () => {
+	async function deployLaunchpadFixture() {
+		const signers = await hre.ethers.getSigners()
+		const owner = signers[0].address
+		const Launchpad = await hre.ethers.getContractFactory("Launchpad")
+		const Erc20Token = await hre.ethers.getContractFactory("BasicERC20")
+		const launchpadContract = await Launchpad.deploy({
+			from: owner,
+		})
+		const saleTokenContract = await Erc20Token.deploy("TestToken", "TT", owner, {
+			from: owner,
+		})
+		const buyTokenContract = await Erc20Token.deploy("Usdt", "USDT", owner, {
+			from: owner,
+		})
+
+		const launchpadAddress = await launchpadContract.getAddress()
+
+		await saleTokenContract.mint(signers[0].address, ethers.parseEther("1000000"))
+		await saleTokenContract.approve(launchpadAddress, ethers.MaxUint256)
+		await buyTokenContract.mint(signers[1].address, ethers.parseEther("10000"))
+
+		return {
+			launchpadContract,
+			saleTokenContract,
+			buyTokenContract,
+			owner,
+		}
+	}
+
+	async function createNativeSaleFixture() {
+		const { launchpadContract, saleTokenContract, owner } = await deployLaunchpadFixture()
+
+		const balanceBefore = await saleTokenContract.balanceOf(owner)
+		const saleTokenAddress = await saleTokenContract.getAddress()
+		const startTime = Math.floor(Date.now() / 1000) + 60 // 1 minute from now
+		const endTime = Math.floor(Date.now() / 1000) + 60 * 60 // 1 hour from now
+		const paymentTokens = ethers.ZeroAddress
+		const paymentTokenPrice = ethers.parseEther("1")
+
+		await launchpadContract.createSale(
+			saleTokenAddress,
+			balanceBefore,
+			startTime,
+			endTime,
+			paymentTokens,
+			paymentTokenPrice
+		)
+
+		return {
+			launchpadContract,
+			saleTokenContract,
+			owner,
+		}
+	}
+
+	describe("init function and setters", () => {
+		it("Should Allow Owner to create a sale use native token", async () => {
+			const { launchpadContract, saleTokenContract, owner } = await deployLaunchpadFixture()
+
+			const balanceBefore = await saleTokenContract.balanceOf(owner)
+			const saleTokenAddress = await saleTokenContract.getAddress()
+			const startTime = Math.floor(Date.now() / 1000) + 60 // 1 minute from now
+			const endTime = Math.floor(Date.now() / 1000) + 60 * 60 // 1 hour from now
+			const paymentTokens = ethers.ZeroAddress
+			const paymentTokenPrice = ethers.parseEther("1") / 2500n
+			await launchpadContract.createSale(
+				saleTokenAddress,
+				balanceBefore,
+				startTime,
+				endTime,
+				paymentTokens,
+				paymentTokenPrice
+			)
+			expect(await saleTokenContract.balanceOf(owner)).to.equal(0)
+			expect(await launchpadContract.getSaleToken()).to.equal(saleTokenAddress)
+			expect(await launchpadContract.getTotalTokens()).to.equal(balanceBefore)
+			expect(await launchpadContract.getStartTime()).to.equal(startTime)
+			expect(await launchpadContract.getEndTime()).to.equal(endTime)
+			expect(await launchpadContract.getPaymentTokens()).to.equal(ethers.ZeroAddress)
+			expect(await launchpadContract.getPaymentTokenPrice()).to.equal(paymentTokenPrice)
+			expect(await launchpadContract.getTokensSold()).to.equal(0n)
+		})
+
+		it("should Allow Owner reset payment info", async () => {
+			const { launchpadContract, saleTokenContract, owner } = await createNativeSaleFixture()
+			const paymentTokenBefore = await launchpadContract.getPaymentTokens()
+			const paymentTokenPriceBefore = await launchpadContract.getPaymentTokenPrice()
+			expect(await launchpadContract.getPaymentTokens()).to.equal(paymentTokenBefore)
+			expect(await launchpadContract.getPaymentTokenPrice()).to.equal(paymentTokenPriceBefore)
+			const newPaymentToken = await saleTokenContract.getAddress()
+			const newPaymentTokenPrice = ethers.parseEther("1")
+			await launchpadContract.setSalePayment(newPaymentToken, newPaymentTokenPrice, {
+				from: owner,
+			})
+			expect(await launchpadContract.getPaymentTokens()).to.equal(newPaymentToken)
+			expect(await launchpadContract.getPaymentTokenPrice()).to.equal(newPaymentTokenPrice)
+		})
+
+		it("should Allow Owner set MerkleRoot", async () => {
+			const { launchpadContract, owner } = await createNativeSaleFixture()
+			const signers = await hre.ethers.getSigners()
+
+			const values = [
+				[signers[0].address, ethers.parseEther("50")],
+				[signers[1].address, ethers.parseEther("150")],
+				[signers[2].address, ethers.parseEther("100")],
+			]
+			const tree = StandardMerkleTree.of(values, ["address", "uint256"])
+			const root = tree.root
+			await launchpadContract.setMerkleRoot(root, { from: owner })
+			expect(await launchpadContract.merkleRoot()).to.equal(tree.root)
+		})
+
+		it("should Allow Owner transfer Owner", async () => {
+			const { launchpadContract, owner } = await createNativeSaleFixture()
+			const signers = await hre.ethers.getSigners()
+			const newOwner = signers[1].address
+			await launchpadContract.setOwner(newOwner, { from: owner })
+			expect(await launchpadContract.owner()).to.equal(newOwner)
+		})
+	})
+
+	describe("after init buy function", () => {
+		it("should Allow user use native token buy token", async () => {
+			const { launchpadContract, owner } = await createNativeSaleFixture()
+
+			const signers = await hre.ethers.getSigners()
+			const values = [
+				[signers[0].address, ethers.parseEther("50")],
+				[signers[1].address, ethers.parseEther("10")],
+				[signers[2].address, ethers.parseEther("100")],
+				[signers[3].address, ethers.parseEther("200")],
+				[signers[4].address, ethers.parseEther("300")],
+			]
+			const tree = StandardMerkleTree.of(values, ["address", "uint256"])
+			const root = tree.root
+			const buyAmount = ethers.parseEther("1")
+			const buyer = signers[1]
+			let maxBuyAmount = 0n
+			let proof = [] as string[]
+			let wrongProof = [] as string[]
+			for (const [i, v] of tree.entries()) {
+				if (v[0] === buyer.address) {
+					proof = tree.getProof(i)
+					if (typeof v[1] === "bigint") {
+						maxBuyAmount = v[1]
+					}
+				}
+				if (v[0] === signers[2].address) {
+					wrongProof = tree.getProof(i)
+				}
+			}
+			await expect(
+				launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof)
+			).to.be.revertedWith("MerkleRoot not initialized")
+			await launchpadContract.setMerkleRoot(root, { from: owner })
+
+			await expect(
+				launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof)
+			).to.be.revertedWith("Sale is not active")
+			await time.increase(100)
+			await expect(
+				launchpadContract.connect(buyer).contributeETH(buyAmount + maxBuyAmount, maxBuyAmount, proof, {
+					value: ethers.parseEther("0.5"),
+				})
+			).to.be.revertedWith("Buy amount exceeds max buy amount")
+			await expect(
+				launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, wrongProof, {
+					value: ethers.parseEther("0.5"),
+				})
+			).to.be.revertedWith("Invalid proof")
+			await expect(
+				launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof, {
+					value: ethers.parseEther("0.5"),
+				})
+			).to.be.revertedWith("Not enough ETH sent")
+			await expect(
+				launchpadContract.connect(buyer).contributeETH(ethers.parseEther("50"), maxBuyAmount, proof, {
+					value: ethers.parseEther("50"),
+				})
+			).to.revertedWith("Buy amount exceeds max buy amount")
+			//success once
+			await expect(
+				launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof, {
+					value: ethers.parseEther("1"),
+				})
+			).to.emit(launchpadContract, "Contributed")
+			//make it success twice
+			await expect(
+				launchpadContract.connect(buyer).contributeETH(ethers.parseEther("4"), maxBuyAmount, proof, {
+					value: ethers.parseEther("4"),
+				})
+			).to.emit(launchpadContract, "Contributed")
+			expect(await launchpadContract.getClaimableTokens(buyer.address)).to.equal(
+				buyAmount + ethers.parseEther("4")
+			)
+			//failed by exceeding max buy amount
+			await expect(
+				launchpadContract.connect(buyer).contributeETH(ethers.parseEther("10"), maxBuyAmount, proof, {
+					value: ethers.parseEther("10"),
+				})
+			).to.be.revertedWith("Buy amount exceeds max buy amount")
+		})
+		it("should Allow user claim token after finished sale", async () => {
+			const { launchpadContract, saleTokenContract, owner } = await createNativeSaleFixture()
+
+			const signers = await hre.ethers.getSigners()
+			const values = [
+				[signers[0].address, ethers.parseEther("50")],
+				[signers[1].address, ethers.parseEther("10")],
+				[signers[2].address, ethers.parseEther("100")],
+				[signers[3].address, ethers.parseEther("200")],
+				[signers[4].address, ethers.parseEther("300")],
+			]
+			const tree = StandardMerkleTree.of(values, ["address", "uint256"])
+			const root = tree.root
+			const buyAmount = ethers.parseEther("1")
+			const buyer = signers[1]
+			let maxBuyAmount = 0n
+			let proof = [] as string[]
+			for (const [i, v] of tree.entries()) {
+				if (v[0] === buyer.address) {
+					proof = tree.getProof(i)
+					if (typeof v[1] === "bigint") {
+						maxBuyAmount = v[1]
+					}
+				}
+			}
+			await launchpadContract.setMerkleRoot(root, { from: owner })
+			await time.increase(100)
+			await expect(
+				launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof, {
+					value: ethers.parseEther("1"),
+				})
+			).to.emit(launchpadContract, "Contributed")
+			expect(await launchpadContract.getClaimableTokens(buyer.address)).to.equal(buyAmount)
+			await expect(launchpadContract.connect(buyer).claimTokens()).to.be.revertedWith(
+				"Claiming tokens is not enabled"
+			)
+			expect(await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60)).to.emit(
+				launchpadContract,
+				"EnableClaimToken"
+			)
+			await expect(launchpadContract.connect(buyer).claimTokens()).to.be.revertedWith(
+				"Claiming tokens not started yet"
+			)
+			await time.increase(60 * 60 + 100) // increase time to finish sale
+			await expect(launchpadContract.connect(signers[2]).claimTokens()).to.be.revertedWith("No tokens to claim")
+			//Transfer token
+			await expect(launchpadContract.connect(buyer).claimTokens()).to.emit(saleTokenContract, "Transfer")
+			//Balance check
+			expect(await saleTokenContract.balanceOf(buyer.address)).to.equal(buyAmount)
+		})
+		it("should Allow owner claim contribution token after finished sale", async () => {
+			const { launchpadContract, saleTokenContract, owner } = await createNativeSaleFixture()
+
+			const signers = await hre.ethers.getSigners()
+			const values = [
+				[signers[0].address, ethers.parseEther("50")],
+				[signers[1].address, ethers.parseEther("10")],
+				[signers[2].address, ethers.parseEther("100")],
+				[signers[3].address, ethers.parseEther("200")],
+				[signers[4].address, ethers.parseEther("300")],
+			]
+			const tree = StandardMerkleTree.of(values, ["address", "uint256"])
+			const root = tree.root
+			const buyAmount = ethers.parseEther("1")
+			const buyer = signers[1]
+			let maxBuyAmount = 0n
+			let proof = [] as string[]
+			for (const [i, v] of tree.entries()) {
+				if (v[0] === buyer.address) {
+					proof = tree.getProof(i)
+					if (typeof v[1] === "bigint") {
+						maxBuyAmount = v[1]
+					}
+				}
+			}
+			await launchpadContract.setMerkleRoot(root, { from: owner })
+			await time.increase(100)
+
+			const launchpadAddress = await launchpadContract.getAddress()
+			await expect(
+				launchpadContract.connect(buyer).contributeETH(buyAmount, maxBuyAmount, proof, {
+					value: ethers.parseEther("1"),
+				})
+			).to.emit(launchpadContract, "Contributed")
+
+			await expect(launchpadContract.withdrawPayments()).to.be.revertedWith("Sale is still active")
+
+			await launchpadContract.enableClaimTokens(Math.floor(Date.now() / 1000) + 60 * 60)
+
+			await time.increase(60 * 60 + 100) // increase time to finish sale
+			await launchpadContract.connect(buyer).claimTokens()
+			//todo 写不动了 接下来测试 非 owner withdraw token/钱 owner 的 withdraw token/钱
+			await expect(launchpadContract.connect(signers[2]).withdrawPayments()).to.revertedWith(
+				"Only owner can call this function"
+			)
+			await expect(launchpadContract.withdrawPayments()).to.emit(launchpadContract, "WithdrawPayments")
+			expect(await ethers.provider.getBalance(launchpadAddress)).to.equal(0n)
+			await expect(launchpadContract.connect(signers[2]).withdrawRemainingTokens()).to.revertedWith(
+				"Only owner can call this function"
+			)
+			await expect(launchpadContract.withdrawRemainingTokens()).to.emit(
+				launchpadContract,
+				"WithdrawRemainingTokens"
+			)
+			expect(await saleTokenContract.balanceOf(launchpadAddress)).to.equal(0)
+		})
+	})
+})