├── .gitignore ├── README.md ├── audits └── SuperLauncher Smart Contract Audit Report - QuillAudits.pdf ├── contracts ├── Campaign.sol ├── Factory.sol ├── FeeVault.sol ├── Interfaces.sol ├── Token.sol └── mocks │ ├── MintableERC20.sol │ ├── MockBAT.sol │ ├── MockUniRouter.sol │ └── MockXYZ.sol ├── hardhat.config.js ├── migrations └── 1_initial_migration.js ├── package-lock.json ├── package.json ├── scripts ├── deploy-campaign.js └── deploy-factory.js ├── test ├── 1.token-test.js ├── 2.factory-test.js └── 3.campaign-test.js ├── truffle-config.js ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | cache 2 | artifacts 3 | node_modules 4 | build 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sc 2 | 3 | ## Install dependency 4 | 5 | > yarn 6 | > npm install --save-dev hardhat 7 | 8 | ## Run unit test 9 | 10 | > npx hardhat test 11 | 12 | ## Deploy 13 | 14 | > npx hardhat run --network testnet .\scripts\deploy-campaign.js -------------------------------------------------------------------------------- /audits/SuperLauncher Smart Contract Audit Report - QuillAudits.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperLauncher/SmartContracts/97bba2cad3b4250c88c1525bed2f051df45ce9f4/audits/SuperLauncher Smart Contract Audit Report - QuillAudits.pdf -------------------------------------------------------------------------------- /contracts/Campaign.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | 15 | 16 | pragma solidity ^0.6.0; 17 | pragma experimental ABIEncoderV2; 18 | 19 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 20 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 21 | import "@openzeppelin/contracts/utils/Address.sol"; 22 | import "@openzeppelin/contracts/math/SafeMath.sol"; 23 | import "./Interfaces.sol"; 24 | 25 | contract Campaign { 26 | using Address for address; 27 | using SafeMath for uint256; 28 | using SafeERC20 for ERC20; 29 | 30 | address public factory; 31 | address public campaignOwner; 32 | address public token; 33 | uint256 public softCap; 34 | uint256 public hardCap; 35 | uint256 public tokenSalesQty; 36 | uint256 public feePcnt; 37 | uint256 public qualifyingTokenQty; 38 | uint256 public startDate; 39 | uint256 public endDate; 40 | uint256 public midDate; // Start of public sales for WhitelistedFirstThenEveryone type 41 | uint256 public minBuyLimit; 42 | uint256 public maxBuyLimit; 43 | 44 | // Liquidity 45 | uint256 public lpBnbQty; 46 | uint256 public lpTokenQty; 47 | uint256 public lpLockDuration; 48 | uint256[2] private lpInPool; // This is the actual LP provided in pool. 49 | bool private recoveredUnspentLP; 50 | 51 | // Config 52 | bool public burnUnSold; 53 | 54 | // Misc variables // 55 | uint256 public unlockDate; 56 | uint256 public collectedBNB; 57 | uint256 public lpTokenAmount; 58 | 59 | // States 60 | bool public tokenFunded; 61 | bool public finishUpSuccess; 62 | bool public liquidityCreated; 63 | bool public cancelled; 64 | 65 | // Token claiming by users 66 | mapping(address => bool) public claimedRecords; 67 | bool public tokenReadyToClaim; 68 | 69 | // Map user address to amount invested in BNB // 70 | mapping(address => uint256) public participants; 71 | uint256 public numOfParticipants; 72 | 73 | address public constant BURN_ADDRESS = address(0x000000000000000000000000000000000000dEaD); 74 | 75 | // Whitelisting support 76 | enum Accessibility { 77 | Everyone, 78 | WhitelistedOnly, 79 | WhitelistedFirstThenEveryone 80 | } 81 | Accessibility public accessibility; 82 | uint256 public numOfWhitelisted; 83 | mapping(address => bool) public whitelistedMap; 84 | 85 | 86 | // Vesting Feature Support 87 | uint256 internal constant PERCENT100 = 1e6; 88 | 89 | struct VestingInfo { 90 | uint256[] periods; 91 | uint256[] percents; 92 | uint256 totalVestedBnb; 93 | uint256 startTime; 94 | bool enabled; 95 | bool vestingTimerStarted; 96 | } 97 | VestingInfo public vestInfo; 98 | mapping(address=>mapping(uint256=>bool)) investorsClaimMap; 99 | mapping(uint256=>bool) campaignOwnerClaimMap; 100 | 101 | 102 | // Events 103 | event Purchased( 104 | address indexed user, 105 | uint256 timeStamp, 106 | uint256 amountBnb, 107 | uint256 amountToken 108 | ); 109 | 110 | event LiquidityAdded( 111 | uint256 amountBnb, 112 | uint256 amountToken, 113 | uint256 amountLPToken 114 | ); 115 | 116 | event LiquidityLocked( 117 | uint256 timeStampStart, 118 | uint256 timeStampExpiry 119 | ); 120 | 121 | event LiquidityWithdrawn( 122 | uint256 amount 123 | ); 124 | 125 | event TokenClaimed( 126 | address indexed user, 127 | uint256 timeStamp, 128 | uint256 amountToken 129 | ); 130 | 131 | event Refund( 132 | address indexed user, 133 | uint256 timeStamp, 134 | uint256 amountBnb 135 | ); 136 | 137 | modifier onlyFactory() { 138 | require(msg.sender == factory, "Only factory can call"); 139 | _; 140 | } 141 | 142 | modifier onlyCampaignOwner() { 143 | require(msg.sender == campaignOwner, "Only campaign owner can call"); 144 | _; 145 | } 146 | 147 | modifier onlyFactoryOrCampaignOwner() { 148 | require(msg.sender == factory || msg.sender == campaignOwner, "Only factory or campaign owner can call"); 149 | _; 150 | } 151 | 152 | constructor() public{ 153 | factory = msg.sender; 154 | } 155 | 156 | /** 157 | * @dev Initialize a new campaign. 158 | * @notice - Access control: External. Can only be called by the factory contract. 159 | */ 160 | function initialize 161 | ( 162 | address _token, 163 | address _campaignOwner, 164 | uint256[5] calldata _stats, 165 | uint256[3] calldata _dates, 166 | uint256[2] calldata _buyLimits, 167 | Campaign.Accessibility _access, 168 | uint256[3] calldata _liquidity, 169 | bool _burnUnSold 170 | ) external 171 | { 172 | require(msg.sender == factory,'Only factory allowed to initialize'); 173 | token = _token; 174 | campaignOwner = _campaignOwner; 175 | softCap = _stats[0]; 176 | hardCap = _stats[1]; 177 | tokenSalesQty = _stats[2]; 178 | feePcnt = _stats[3]; 179 | qualifyingTokenQty = _stats[4]; 180 | startDate = _dates[0]; 181 | endDate = _dates[1]; 182 | midDate = _dates[2]; 183 | minBuyLimit = _buyLimits[0]; 184 | maxBuyLimit = _buyLimits[1]; 185 | accessibility = _access; 186 | lpBnbQty = _liquidity[0]; 187 | lpTokenQty = _liquidity[1]; 188 | lpLockDuration = _liquidity[2]; 189 | burnUnSold = _burnUnSold; 190 | } 191 | 192 | /** 193 | * @dev Allows campaign owner to fund in his token. 194 | * @notice - Access control: External, OnlyCampaignOwner 195 | */ 196 | function fundIn() external onlyCampaignOwner { 197 | require(!tokenFunded, "Campaign is already funded"); 198 | uint256 amt = getCampaignFundInTokensRequired(); 199 | require(amt > 0, "Invalid fund in amount"); 200 | 201 | tokenFunded = true; 202 | ERC20(token).safeTransferFrom(msg.sender, address(this), amt); 203 | } 204 | 205 | // In case of a "cancelled" campaign, or softCap not reached, 206 | // the campaign owner can retrieve back his funded tokens. 207 | function fundOut() external onlyCampaignOwner { 208 | require(failedOrCancelled(), "Only failed or cancelled campaign can un-fund"); 209 | 210 | ERC20 ercToken = ERC20(token); 211 | uint256 totalTokens = ercToken.balanceOf(address(this)); 212 | sendTokensTo(campaignOwner, totalTokens); 213 | tokenFunded = false; 214 | } 215 | 216 | /** 217 | * @dev Allows user to buy token. 218 | * @notice - Access control: Public 219 | */ 220 | function buyTokens() public payable { 221 | 222 | require(isLive(), "Campaign is not live"); 223 | require(checkQualifyingTokens(msg.sender), "Insufficient LAUNCH tokens to qualify"); 224 | require(checkWhiteList(msg.sender), "You are not whitelisted"); 225 | 226 | // Check for min purchase amount 227 | require(msg.value >= minBuyLimit, "Less than minimum purchase amount"); 228 | 229 | // Check for over purchase 230 | uint256 invested = participants[msg.sender]; 231 | require(invested.add(msg.value) <= maxBuyLimit, "Exceeded max amount"); 232 | require(msg.value <= getRemaining(),"Insufficent token left"); 233 | 234 | uint256 buyAmt = calculateTokenAmount(msg.value); 235 | 236 | if (invested == 0) { 237 | numOfParticipants = numOfParticipants.add(1); 238 | } 239 | 240 | participants[msg.sender] = participants[msg.sender].add(msg.value); 241 | collectedBNB = collectedBNB.add(msg.value); 242 | 243 | emit Purchased(msg.sender, block.timestamp, msg.value, buyAmt); 244 | } 245 | 246 | /** 247 | * @dev Add liquidity and lock it up. Called after a campaign has ended successfully. 248 | * @notice - Access control: Public. onlyFactoryOrCampaignOwner. This allows the admin or campaignOwner to 249 | * coordinate the adding of LP when all campaigns are completed. This ensure a fairer arrangement, esp 250 | * when multiple campaigns are running in parallel. 251 | */ 252 | function addAndLockLP() external onlyFactoryOrCampaignOwner { 253 | 254 | require(!isLive(), "Presale is still live"); 255 | require(!failedOrCancelled(), "Presale failed or cancelled , can't provide LP"); 256 | require(softCap <= collectedBNB, "Did not reach soft cap"); 257 | 258 | if ((lpBnbQty > 0 && lpTokenQty > 0) && !liquidityCreated) { 259 | 260 | liquidityCreated = true; 261 | 262 | IFactoryGetters fact = IFactoryGetters(factory); 263 | address lpRouterAddress = fact.getLpRouter(); 264 | require(ERC20(address(token)).approve(lpRouterAddress, lpTokenQty)); // Uniswap doc says this is required // 265 | 266 | (uint256 retTokenAmt, uint256 retBNBAmt, uint256 retLpTokenAmt) = IUniswapV2Router02(lpRouterAddress).addLiquidityETH 267 | {value : lpBnbQty} 268 | (address(token), 269 | lpTokenQty, 270 | 0, 271 | 0, 272 | address(this), 273 | block.timestamp + 100000000); 274 | 275 | lpTokenAmount = retLpTokenAmt; 276 | lpInPool[0] = retBNBAmt; 277 | lpInPool[1] = retTokenAmt; 278 | 279 | emit LiquidityAdded(retBNBAmt, retTokenAmt, retLpTokenAmt); 280 | 281 | unlockDate = (block.timestamp).add(lpLockDuration); 282 | emit LiquidityLocked(block.timestamp, unlockDate); 283 | } 284 | } 285 | 286 | /** 287 | * @dev Get the actual liquidity added to LP Pool 288 | * @return - uint256[2] consist of BNB amount, Token amount. 289 | * @notice - Access control: Public, View 290 | */ 291 | function getPoolLP() external view returns (uint256, uint256) { 292 | return (lpInPool[0], lpInPool[1]); 293 | } 294 | 295 | /** 296 | * @dev There are situations that the campaign owner might call this. 297 | * @dev 1: Pancakeswap pool SC failure when we call addAndLockLP(). 298 | * @dev 2: Pancakeswap pool already exist. After we provide LP, thee's some excess bnb/tokens 299 | * @dev 3: Campaign owner decided to change LP arrangement after campaign is successful. 300 | * @dev In that case, campaign owner might recover it and provide LP manually. 301 | * @dev Note: This function can only be called once by factory, as this is not a normal workflow. 302 | * @notice - Access control: External, onlyFactory 303 | */ 304 | function recoverUnspentLp() external onlyFactory { 305 | 306 | require(!recoveredUnspentLP, "You have already recovered unspent LP"); 307 | recoveredUnspentLP = true; 308 | 309 | uint256 bnbAmt; 310 | uint256 tokenAmt; 311 | 312 | if (liquidityCreated) { 313 | // Find out any excess bnb/tokens after LP provision is completed. 314 | bnbAmt = lpBnbQty.sub(lpInPool[0]); 315 | tokenAmt = lpTokenQty.sub(lpInPool[1]); 316 | } else { 317 | // liquidity not created yet. Just returns the full portion of the planned LP 318 | // Only finished success campaign can recover Unspent LP 319 | require(finishUpSuccess, "Campaign not finished successfully yet"); 320 | bnbAmt = lpBnbQty; 321 | tokenAmt = lpTokenQty; 322 | } 323 | 324 | // Return bnb, token if any 325 | if (bnbAmt > 0) { 326 | (bool ok, ) = campaignOwner.call{value: bnbAmt}(""); 327 | require(ok, "Failed to return BNB Lp"); 328 | } 329 | 330 | if (tokenAmt > 0) { 331 | ERC20(token).safeTransfer(campaignOwner, tokenAmt); 332 | } 333 | } 334 | 335 | /** 336 | * @dev When a campaign reached the endDate, this function is called. 337 | * @dev Add liquidity to uniswap and burn the remaining tokens. 338 | * @dev Can be only executed when the campaign completes. 339 | * @dev Anyone can call. Only called once. 340 | * @notice - Access control: Public 341 | */ 342 | function finishUp() external { 343 | 344 | require(!finishUpSuccess, "finishUp is already called"); 345 | require(!isLive(), "Presale is still live"); 346 | require(!failedOrCancelled(), "Presale failed or cancelled , can't call finishUp"); 347 | require(softCap <= collectedBNB, "Did not reach soft cap"); 348 | finishUpSuccess = true; 349 | 350 | uint256 feeAmt = getFeeAmt(collectedBNB); 351 | uint256 unSoldAmtBnb = getRemaining(); 352 | uint256 remainBNB = collectedBNB.sub(feeAmt); 353 | 354 | // If lpBnbQty, lpTokenQty is 0, we won't provide LP. 355 | if ((lpBnbQty > 0 && lpTokenQty > 0)) { 356 | remainBNB = remainBNB.sub(lpBnbQty); 357 | } 358 | 359 | // Send fee to fee address 360 | if (feeAmt > 0) { 361 | (bool sentFee, ) = getFeeAddress().call{value: feeAmt}(""); 362 | require(sentFee, "Failed to send Fee to platform"); 363 | } 364 | 365 | // Send remain bnb to campaign owner if not in vested Mode 366 | if (!vestInfo.enabled) { 367 | (bool sentBnb, ) = campaignOwner.call{value: remainBNB}(""); 368 | require(sentBnb, "Failed to send remain BNB to campaign owner"); 369 | } else { 370 | vestInfo.totalVestedBnb = remainBNB; 371 | } 372 | 373 | // Calculate the unsold amount // 374 | if (unSoldAmtBnb > 0) { 375 | uint256 unsoldAmtToken = calculateTokenAmount(unSoldAmtBnb); 376 | // Burn or return UnSold token to owner 377 | sendTokensTo(burnUnSold ? BURN_ADDRESS : campaignOwner, unsoldAmtToken); 378 | } 379 | } 380 | 381 | 382 | /** 383 | * @dev Allow Factory owner to call this to set the flag to 384 | * @dev enable token claiming. 385 | * @dev This is useful when 1 project has multiple campaigns that need 386 | * @dev to sync up the timing of token claiming After LP provision. 387 | * @notice - Access control: External, onlyFactory 388 | */ 389 | function setTokenClaimable() external onlyFactory { 390 | 391 | require(finishUpSuccess, "Campaign not finished successfully yet"); 392 | 393 | // Token is only claimable in non-vested mode 394 | require(!vestInfo.enabled, "Not applicable to vested mode"); 395 | 396 | tokenReadyToClaim = true; 397 | } 398 | 399 | /** 400 | * @dev Allow users to claim their tokens. 401 | * @notice - Access control: External 402 | */ 403 | function claimTokens() external { 404 | 405 | require(tokenReadyToClaim, "Tokens not ready to claim yet"); 406 | require( claimedRecords[msg.sender] == false, "You have already claimed"); 407 | 408 | uint256 amtBought = getTotalTokenPurchased(msg.sender); 409 | if (amtBought > 0) { 410 | claimedRecords[msg.sender] = true; 411 | ERC20(token).safeTransfer(msg.sender, amtBought); 412 | emit TokenClaimed(msg.sender, block.timestamp, amtBought); 413 | } 414 | } 415 | 416 | /** 417 | * @dev Allows campaign owner to withdraw LP after the lock duration. 418 | * @dev Only able to withdraw LP if lockActivated and lock duration has expired. 419 | * @dev Can call multiple times to withdraw a portion of the total lp. 420 | * @param _lpToken - The LP token address 421 | * @notice - Access control: Internal, OnlyCampaignOwner 422 | */ 423 | function withdrawLP(address _lpToken,uint256 _amount) external onlyCampaignOwner 424 | { 425 | require(liquidityCreated, "liquidity is not yet created"); 426 | require(block.timestamp >= unlockDate ,"Unlock date not reached"); 427 | 428 | ERC20(_lpToken).safeTransfer(msg.sender, _amount); 429 | emit LiquidityWithdrawn( _amount); 430 | } 431 | 432 | /** 433 | * @dev Allows Participants to withdraw/refunds when campaign fails 434 | * @notice - Access control: Public 435 | */ 436 | function refund() external { 437 | require(failedOrCancelled(),"Can refund for failed or cancelled campaign only"); 438 | 439 | uint256 investAmt = participants[msg.sender]; 440 | require(investAmt > 0 ,"You didn't participate in the campaign"); 441 | 442 | participants[msg.sender] = 0; 443 | (bool ok, ) = msg.sender.call{value: investAmt}(""); 444 | require(ok, "Failed to refund BNB to user"); 445 | 446 | if (numOfParticipants > 0) { 447 | numOfParticipants -= 1; 448 | } 449 | 450 | emit Refund(msg.sender, block.timestamp, investAmt); 451 | } 452 | 453 | /** 454 | * @dev To calculate the total token amount based on user's total invested BNB 455 | * @param _user - The user's wallet address 456 | * @return - The total amount of token 457 | * @notice - Access control: Public 458 | */ 459 | function getTotalTokenPurchased(address _user) public view returns (uint256) { 460 | uint256 investAmt = participants[_user]; 461 | return calculateTokenAmount(investAmt); 462 | } 463 | 464 | // Whitelisting Support 465 | /** 466 | * @dev Allows campaign owner to append to the whitelisted addresses. 467 | * @param _addresses - Array of addresses 468 | * @notice - Access control: Public, OnlyCampaignOwner 469 | */ 470 | function appendWhitelisted(address[] memory _addresses) external onlyFactory { 471 | uint256 len = _addresses.length; 472 | for (uint256 n=0; n= midDate); 514 | } 515 | } 516 | 517 | // Helpers // 518 | /** 519 | * @dev To send all XYZ token to either campaign owner or burn address when campaign finishes or cancelled. 520 | * @param _to - The destination address 521 | * @param _amount - The amount to send 522 | * @notice - Access control: Internal 523 | */ 524 | function sendTokensTo(address _to, uint256 _amount) internal { 525 | 526 | // Security: Can only be sent back to campaign owner or burned // 527 | require((_to == campaignOwner)||(_to == BURN_ADDRESS), "Can only be sent to campaign owner or burn address"); 528 | 529 | // Burn or return UnSold token to owner 530 | ERC20 ercToken = ERC20(token); 531 | ercToken.safeTransfer(_to, _amount); 532 | } 533 | 534 | /** 535 | * @dev To calculate the amount of fee in BNB 536 | * @param _amt - The amount in BNB 537 | * @return - The amount of fee in BNB 538 | * @notice - Access control: Internal 539 | */ 540 | function getFeeAmt(uint256 _amt) internal view returns (uint256) { 541 | return _amt.mul(feePcnt).div(1e6); 542 | } 543 | 544 | /** 545 | * @dev To get the fee address 546 | * @return - The fee address 547 | * @notice - Access control: Internal 548 | */ 549 | function getFeeAddress() internal view returns (address) { 550 | IFactoryGetters fact = IFactoryGetters(factory); 551 | return fact.getFeeAddress(); 552 | } 553 | 554 | /** 555 | * @dev To check whether the campaign failed (softcap not met) or cancelled 556 | * @return - Bool value 557 | * @notice - Access control: Public 558 | */ 559 | function failedOrCancelled() public view returns(bool) { 560 | if (cancelled) return true; 561 | 562 | return (block.timestamp >= endDate) && (softCap > collectedBNB) ; 563 | } 564 | 565 | /** 566 | * @dev To check whether the campaign is isLive? isLive means a user can still invest in the project. 567 | * @return - Bool value 568 | * @notice - Access control: Public 569 | */ 570 | function isLive() public view returns(bool) { 571 | if (!tokenFunded || cancelled) return false; 572 | if((block.timestamp < startDate)) return false; 573 | if((block.timestamp >= endDate)) return false; 574 | if((collectedBNB >= hardCap)) return false; 575 | return true; 576 | } 577 | 578 | /** 579 | * @dev Calculate amount of token receivable. 580 | * @param _bnbInvestment - Amount of BNB invested 581 | * @return - The amount of token 582 | * @notice - Access control: Public 583 | */ 584 | function calculateTokenAmount(uint256 _bnbInvestment) public view returns(uint256) { 585 | return _bnbInvestment.mul(tokenSalesQty).div(hardCap); 586 | } 587 | 588 | 589 | /** 590 | * @dev Gets remaining BNB to reach hardCap. 591 | * @return - The amount of BNB. 592 | * @notice - Access control: Public 593 | */ 594 | function getRemaining() public view returns (uint256){ 595 | return (hardCap).sub(collectedBNB); 596 | } 597 | 598 | /** 599 | * @dev Set a campaign as cancelled. 600 | * @dev This can only be set before tokenReadyToClaim, finishUpSuccess, liquidityCreated . 601 | * @dev ie, the users can either claim tokens or get refund, but Not both. 602 | * @notice - Access control: Public, OnlyFactory 603 | */ 604 | function setCancelled() onlyFactory external { 605 | 606 | // If we are in VestingMode, then we should be able to cancel even if finishUp() is called 607 | if (vestInfo.enabled && !vestInfo.vestingTimerStarted) 608 | { 609 | cancelled = true; 610 | return; 611 | } 612 | 613 | require(!tokenReadyToClaim, "Too late, tokens are claimable"); 614 | require(!finishUpSuccess, "Too late, finishUp called"); 615 | require(!liquidityCreated, "Too late, Lp created"); 616 | 617 | cancelled = true; 618 | } 619 | 620 | /** 621 | * @dev Calculate and return the Token amount need to be deposit by the project owner. 622 | * @return - The amount of token required 623 | * @notice - Access control: Public 624 | */ 625 | function getCampaignFundInTokensRequired() public view returns(uint256) { 626 | return tokenSalesQty.add(lpTokenQty); 627 | } 628 | 629 | 630 | /** 631 | * @dev Check whether the user address has enough Launcher Tokens to participate in project. 632 | * @param _user - The address of user 633 | * @return - Bool result 634 | * @notice - Access control: External 635 | */ 636 | function checkQualifyingTokens(address _user) public view returns(bool) { 637 | 638 | if (qualifyingTokenQty == 0) { 639 | return true; 640 | } 641 | 642 | IFactoryGetters fact = IFactoryGetters(factory); 643 | address launchToken = fact.getLauncherToken(); 644 | 645 | IERC20 ercToken = IERC20(launchToken); 646 | uint256 balance = ercToken.balanceOf(_user); 647 | return (balance >= qualifyingTokenQty); 648 | } 649 | 650 | 651 | // Vesting feature support 652 | /** 653 | * @dev Setup and turn on the vesting feature 654 | * @param _periods - Array of period of the vesting. 655 | * @param _percents - Array of percents release of the vesting. 656 | * @notice - Access control: External. onlyFactory. 657 | */ 658 | function setupVestingMode(uint256[] calldata _periods, uint256[] calldata _percents) external onlyFactory { 659 | uint256 len = _periods.length; 660 | require(len>0, "Invalid length"); 661 | require(len == _percents.length, "Wrong ranges"); 662 | 663 | // check that all percentages should add up to 100% // 664 | // 100% is 1e6 665 | uint256 totalPcnt; 666 | for (uint256 n=0; n releaseTime); 712 | uint256 remainTime; 713 | if (!claimable) { 714 | remainTime = releaseTime.sub(now); 715 | } 716 | return (claimable, remainTime); 717 | } 718 | 719 | /** 720 | * @dev Allow users to claim their vested token, according to the index of the vested period. 721 | * @param _index - The index of the vesting period. 722 | * @notice - Access control: External. 723 | */ 724 | function claimVestedTokens(uint256 _index) external { 725 | 726 | (bool claimable, ) = isVestingClaimable(_index); 727 | require(claimable, "Not claimable at this time"); 728 | 729 | uint256 amtTotalToken = getTotalTokenPurchased(msg.sender); 730 | 731 | require(amtTotalToken > 0, "You have not purchased the tokens"); 732 | 733 | bool claimed = investorsClaimMap[msg.sender][_index]; 734 | require(!claimed, "This vest amount is already claimed"); 735 | 736 | investorsClaimMap[msg.sender][_index] = true; 737 | uint256 amtTokens = vestInfo.percents[_index].mul(amtTotalToken).div(PERCENT100); 738 | 739 | ERC20(token).safeTransfer(msg.sender, amtTokens); 740 | emit TokenClaimed(msg.sender, block.timestamp, amtTokens); 741 | } 742 | 743 | /** 744 | * @dev Allow campaign owner to claim their bnb, according to the index of the vested period. 745 | * @param _index - The index of the vesting period. 746 | * @notice - Access control: External. onlyCampaignOwner. 747 | */ 748 | function claimVestedBnb(uint256 _index) external onlyCampaignOwner { 749 | 750 | (bool claimable, ) = isVestingClaimable(_index); 751 | require(claimable, "Not claimable at this time"); 752 | 753 | require(!campaignOwnerClaimMap[_index], "This vest amount is already claimed"); 754 | campaignOwnerClaimMap[_index] = true; 755 | 756 | uint256 amtBnb = vestInfo.percents[_index].mul(vestInfo.totalVestedBnb).div(PERCENT100); 757 | 758 | (bool sentBnb, ) = campaignOwner.call{value: amtBnb}(""); 759 | require(sentBnb, "Failed to send remain BNB to campaign owner"); 760 | } 761 | 762 | /** 763 | * @dev To get the next vesting claim for a user. 764 | * @param _user - The user's address. 765 | * @return - int256 : the next period. -1 to indicate none found. 766 | * @return - uint256 : the amount of token claimable 767 | * @return - uint256 : time left to claim. If 0 (and next claim period is valid), it is currently claimable. 768 | * @notice - Access control: External. View. 769 | */ 770 | function getNextVestingClaim(address _user) external view returns(int256, uint256, uint256) { 771 | 772 | if (!vestInfo.vestingTimerStarted) { 773 | return (-1,0,0); 774 | } 775 | 776 | uint256 amtTotalToken = getTotalTokenPurchased(_user); 777 | if (amtTotalToken==0) { 778 | return (-1,0,0); 779 | } 780 | 781 | uint256 len = vestInfo.periods.length; 782 | for (uint256 n=0; n CampaignInfo) public allCampaigns; 34 | uint256 count; 35 | 36 | address private feeAddress; 37 | address private lpRouter; // Uniswap or PancakeSwap 38 | 39 | constructor( 40 | address _launcherTokenAddress, 41 | address _feeAddress, 42 | address _lpRouter 43 | ) public Ownable() 44 | { 45 | launcherTokenAddress = _launcherTokenAddress; 46 | feeAddress = _feeAddress; 47 | lpRouter = _lpRouter; 48 | } 49 | 50 | /** 51 | * @dev Create a new campaign 52 | * @param _token - The token address 53 | * @param _subIndex - The fund raising round Id 54 | * @param _campaignOwner - Campaign owner address 55 | * @param _stats - Array of 5 uint256 values. 56 | * @notice - [0] Softcap. 1e18 = 1 BNB. 57 | * @notice - [1] Hardcap. 1e18 = 1 BNB. 58 | * @notice - [2] TokenSalesQty. The amount of tokens for sale. Example: 1e8 for 1 token with 8 decimals. 59 | * @notice - [3] feePcnt. 100% is 1e6. 60 | * @notice - [4] QualifyingTokenQty. Number of LAUNCH required to participate. In 1e18 per LAUNCH. 61 | * @param _dates - Array of 3 uint256 dates. 62 | * @notice - [0] Start date. 63 | * @notice - [1] End date. 64 | * @notice - [2] Mid date. For Accessibility.WhitelistedFirstThenEveryone only. 65 | * @param _buyLimits - Array of 2 uint256 values. 66 | * @notice - [0] Min amount in BNB, per purchase. 67 | * @notice - [1] Max accumulated amount in BNB. 68 | * @param _access - Everyone, Whitelisted-only, or hybrid. 69 | * @param _liquidity - Array of 3 uint256 values. 70 | * @notice - [0] BNB amount to use (from token sales) to be used to provide LP. 71 | * @notice - [1] Token amount to be used to provide LP. 72 | * @notice - [2] LockDuration of the LP tokens. 73 | * @param _burnUnSold - Indicate to burn un-sold tokens or not. For successful campaign only. 74 | * @return campaignAddress - The address of the new campaign smart contract created 75 | * @notice - Access control: Public, OnlyOwner 76 | */ 77 | 78 | function createCampaign( 79 | address _token, 80 | uint256 _subIndex, 81 | address _campaignOwner, 82 | uint256[5] calldata _stats, 83 | uint256[3] calldata _dates, 84 | uint256[2] calldata _buyLimits, 85 | Campaign.Accessibility _access, 86 | uint256[3] calldata _liquidity, 87 | bool _burnUnSold 88 | ) external onlyOwner returns (address campaignAddress) 89 | { 90 | require(_stats[0] < _stats[1],"Soft cap can't be higher than hard cap" ); 91 | require(_stats[2] > 0,"Token for sales can't be 0"); 92 | require(_stats[3] <= 10e6, "Invalid fees value"); 93 | require(_dates[0] < _dates[1] ,"Start date can't be higher than end date" ); 94 | require(block.timestamp < _dates[1] ,"End date must be higher than current date "); 95 | require(_buyLimits[1] > 0, "Max allowed can't be 0" ); 96 | require(_buyLimits[0] <= _buyLimits[1],"Min limit can't be greater than max." ); 97 | 98 | if (_liquidity[0] > 0) { // Liquidity provision check // 99 | require(_liquidity[0] <= _stats[0], "BNB for liquidity cannot be greater than softcap"); 100 | require(_liquidity[1] > 0, "Token for liquidity cannot be 0"); 101 | } else { 102 | require(_liquidity[1] == 0, "Both liquidity BNB and token must be 0"); 103 | } 104 | 105 | // Boundary check: After deducting for fee, the Softcap amt left is enough to create the LP 106 | uint256 feeAmt = _stats[0].mul(_stats[3]).div(1e6); 107 | require(_stats[0].sub(feeAmt) >= _liquidity[0], "Liquidity BNB amount is too high"); 108 | 109 | if (_access == Campaign.Accessibility.WhitelistedFirstThenEveryone) { 110 | require((_dates[2] > _dates[0]) && (_dates[2] < _dates[1]) , "Invalid dates setup"); 111 | } 112 | 113 | bytes memory bytecode = type(Campaign).creationCode; 114 | bytes32 salt = keccak256(abi.encodePacked(_token, _subIndex, msg.sender)); 115 | assembly { 116 | campaignAddress := create2(0, add(bytecode, 32), mload(bytecode), salt) 117 | } 118 | 119 | Campaign(campaignAddress).initialize 120 | ( 121 | _token, 122 | _campaignOwner, 123 | _stats, 124 | _dates, 125 | _buyLimits, 126 | _access, 127 | _liquidity, 128 | _burnUnSold 129 | ); 130 | 131 | allCampaigns[count] = CampaignInfo(campaignAddress, _campaignOwner); 132 | count = count.add(1); 133 | 134 | return campaignAddress; 135 | } 136 | 137 | /** 138 | * @dev Cancel a campaign 139 | * @param _campaignID - The campaign ID 140 | * @notice - Access control: External, OnlyOwner 141 | */ 142 | function cancelCampaign(uint256 _campaignID) external onlyOwner { 143 | 144 | require(_campaignID < count, "Invalid ID"); 145 | 146 | CampaignInfo memory info = allCampaigns[_campaignID]; 147 | require(info.contractAddress != address(0), "Invalid Campaign contract"); 148 | 149 | Campaign camp = Campaign(info.contractAddress); 150 | camp.setCancelled(); 151 | } 152 | 153 | 154 | /** 155 | * @dev Append whitelisted addresses to a campaign 156 | * @param _campaignID - The campaign ID 157 | * @param _addresses - Array of addresses 158 | * @notice - Access control: External, OnlyOwner 159 | */ 160 | function appendWhitelisted(uint256 _campaignID, address[] memory _addresses) external onlyOwner { 161 | 162 | require(_campaignID < count, "Invalid ID"); 163 | 164 | CampaignInfo memory info = allCampaigns[_campaignID]; 165 | require(info.contractAddress != address(0), "Invalid Campaign contract"); 166 | 167 | Campaign camp = Campaign(info.contractAddress); 168 | camp.appendWhitelisted(_addresses); 169 | } 170 | 171 | /** 172 | * @dev Remove whitelisted addresses from a campaign 173 | * @param _campaignID - The campaign ID 174 | * @param _addresses - Array of addresses 175 | * @notice - Access control: External, OnlyOwner 176 | */ 177 | function removeWhitelisted(uint256 _campaignID, address[] memory _addresses) external onlyOwner { 178 | 179 | require(_campaignID < count, "Invalid ID"); 180 | 181 | CampaignInfo memory info = allCampaigns[_campaignID]; 182 | require(info.contractAddress != address(0), "Invalid Campaign contract"); 183 | 184 | Campaign camp = Campaign(info.contractAddress); 185 | camp.removeWhitelisted(_addresses); 186 | } 187 | 188 | /** 189 | * @dev Allow Factory owner to call this to set the flag to 190 | * @dev enable token claiming. 191 | * @dev This is useful when 1 project has multiple campaigns that need 192 | * @dev to sync up the timing of token claiming After LP provision. 193 | * @notice - Access control: External, onlyFactory 194 | */ 195 | function setTokenClaimable(uint256 _campaignID) external onlyOwner { 196 | 197 | require(_campaignID < count, "Invalid ID"); 198 | 199 | CampaignInfo memory info = allCampaigns[_campaignID]; 200 | require(info.contractAddress != address(0), "Invalid Campaign contract"); 201 | 202 | Campaign camp = Campaign(info.contractAddress); 203 | camp.setTokenClaimable(); 204 | } 205 | 206 | 207 | /** 208 | * @dev Add liquidity and lock it up. Called after a campaign has ended successfully. 209 | * @notice - Access control: External. OnlyOwner. 210 | */ 211 | function addAndLockLP(uint256 _campaignID) external onlyOwner { 212 | require(_campaignID < count, "Invalid ID"); 213 | 214 | CampaignInfo memory info = allCampaigns[_campaignID]; 215 | require(info.contractAddress != address(0), "Invalid Campaign contract"); 216 | 217 | Campaign camp = Campaign(info.contractAddress); 218 | camp.addAndLockLP(); 219 | } 220 | 221 | /** 222 | * @dev Recover Unspent LP for a campaign 223 | * @param _campaignID - The campaign ID 224 | * @notice - Access control: External, OnlyOwner 225 | */ 226 | function recoverUnspentLp(uint256 _campaignID, address _campaignOwnerForCheck) external onlyOwner { 227 | 228 | require(_campaignID < count, "Invalid ID"); 229 | 230 | CampaignInfo memory info = allCampaigns[_campaignID]; 231 | require(info.contractAddress != address(0), "Invalid Campaign contract"); 232 | require(info.owner == _campaignOwnerForCheck, "Invalid campaign owner"); // additional check 233 | 234 | Campaign camp = Campaign(info.contractAddress); 235 | camp.recoverUnspentLp(); 236 | } 237 | 238 | /** 239 | * @dev Setup and turn on the vesting feature 240 | * @param _campaignID - The campaign ID 241 | * @param _periods - Array of period of the vesting. 242 | * @param _percents - Array of percents release of the vesting. 243 | * @notice - Access control: External. onlyFactory. 244 | */ 245 | function setupVestingMode(uint256 _campaignID, uint256[] calldata _periods, uint256[] calldata _percents) external onlyOwner { 246 | 247 | require(_campaignID < count, "Invalid ID"); 248 | 249 | CampaignInfo memory info = allCampaigns[_campaignID]; 250 | require(info.contractAddress != address(0), "Invalid Campaign contract"); 251 | 252 | Campaign camp = Campaign(info.contractAddress); 253 | camp.setupVestingMode(_periods, _percents); 254 | } 255 | 256 | /** 257 | * @dev Start the vesting counter. This is normally done after public rounds and manual LP is provided. 258 | * @param _campaignID - The campaign ID 259 | * @notice - Access control: External. onlyFactory. 260 | */ 261 | function startVestingMode(uint256 _campaignID) external onlyOwner { 262 | 263 | require(_campaignID < count, "Invalid ID"); 264 | 265 | CampaignInfo memory info = allCampaigns[_campaignID]; 266 | require(info.contractAddress != address(0), "Invalid Campaign contract"); 267 | 268 | Campaign camp = Campaign(info.contractAddress); 269 | camp.startVestingMode(); 270 | } 271 | 272 | 273 | 274 | 275 | 276 | 277 | // IFactoryGetters 278 | /** 279 | * @dev Get the LP router address 280 | * @return - Return the LP router address 281 | * @notice - Access control: External 282 | */ 283 | function getLpRouter() external override view returns(address) { 284 | return lpRouter; 285 | } 286 | 287 | /** 288 | * @dev Get the fee address 289 | * @return - Return the fee address 290 | * @notice - Access control: External 291 | */ 292 | function getFeeAddress() external override view returns(address) { 293 | return feeAddress; 294 | } 295 | 296 | /** 297 | * @dev Get the launcher token address 298 | * @return - Return the address 299 | * @notice - Access control: External 300 | */ 301 | function getLauncherToken() external override view returns(address) { 302 | return launcherTokenAddress; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /contracts/FeeVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | 15 | pragma solidity ^0.6.0; 16 | 17 | import "@openzeppelin/contracts/math/SafeMath.sol"; 18 | 19 | contract FeeVault { 20 | using SafeMath for uint256; 21 | 22 | address public address1; 23 | address public address2; 24 | uint256 public constant RATIO = 30; 25 | 26 | event FeeSent( 27 | address indexed to, 28 | uint amount 29 | ); 30 | 31 | constructor(address _address1, address _address2) public { 32 | address1 = _address1; 33 | address2 = _address2; 34 | } 35 | 36 | /** 37 | * @dev Any BNB sent to this address will be transferred in ratio to the 2 addresses 38 | */ 39 | receive() external payable { 40 | // send 30% to address 1 41 | uint256 amt1 = msg.value.mul(RATIO).div(100); 42 | (bool ok1, ) = address1.call{value: amt1}(""); 43 | require(ok1, "Failed to send BNB to address 1"); 44 | emit FeeSent(address1, amt1); 45 | 46 | // send remaining to address 2 47 | uint256 amt2 = msg.value.sub(amt1); 48 | (bool ok2, ) = address2.call{value: amt2}(""); 49 | require(ok2, "Failed to send BNB to address 2"); 50 | emit FeeSent(address2, amt2); 51 | } 52 | } -------------------------------------------------------------------------------- /contracts/Interfaces.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | 15 | pragma solidity ^0.6.0; 16 | 17 | interface IFactoryGetters { 18 | function getLpRouter() external view returns(address); 19 | function getFeeAddress() external view returns(address); 20 | function getLauncherToken() external view returns(address); 21 | } 22 | 23 | // Uniswap v2 24 | interface IUniswapV2Router02 { 25 | function addLiquidityETH( 26 | address token, 27 | uint amountTokenDesired, 28 | uint amountTokenMin, 29 | uint amountETHMin, 30 | address to, 31 | uint deadline 32 | ) external payable returns (uint amountToken, uint amountETH, uint liquidity); 33 | } 34 | 35 | -------------------------------------------------------------------------------- /contracts/Token.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.6.0; 17 | 18 | import "@openzeppelin/contracts/access/Ownable.sol"; 19 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 20 | import "@openzeppelin/contracts/token/ERC20/ERC20Burnable.sol"; 21 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 22 | import "@openzeppelin/contracts/math/SafeMath.sol"; 23 | 24 | contract SuperLauncherToken is 25 | ERC20, 26 | ERC20Burnable, 27 | Ownable 28 | { 29 | using SafeMath for uint256; 30 | using SafeERC20 for ERC20; 31 | 32 | /** 33 | * @dev - The max supply. 34 | */ 35 | uint256 internal constant INITIAL_SUPPLY = 4_000_000e18; 36 | uint256 internal constant LOCKED_SUPPLY = 2_000_000e18; 37 | uint256 public constant TOTAL_MAX_SUPPLY = 12_000_000e18; 38 | 39 | 40 | 41 | /** 42 | * @dev - The team release schedules 43 | */ 44 | uint256[4] public teamAllocationLocks = [0 days, 30 days, 150 days, 270 days]; 45 | uint256[4] public teamReleaseAmount = [50_000e18, 500_000e18, 700_000e18, 750_000e18]; 46 | bool[4] public teamReleased = [false, false, false, false]; 47 | uint256 public immutable lockStartTime; 48 | 49 | constructor() 50 | public 51 | ERC20("Super Launcher", "LAUNCH") 52 | { 53 | _mint(msg.sender, INITIAL_SUPPLY); 54 | _mint(address(this), LOCKED_SUPPLY); 55 | lockStartTime = now; 56 | } 57 | 58 | /** 59 | * @dev - Mint token 60 | */ 61 | function mint(address _to, uint256 _amount) public onlyOwner { 62 | require(ERC20.totalSupply().add(_amount) <= TOTAL_MAX_SUPPLY, "Max exceeded"); 63 | _mint(_to, _amount); 64 | } 65 | 66 | /** 67 | * @dev - Team allocation - lock and release 68 | */ 69 | function unlockTeamAllocation(uint256 _index) public onlyOwner { 70 | 71 | require(_index < 4, "Index out of range"); 72 | require(teamReleased[_index]==false, "This allocation has been released previously"); 73 | 74 | uint256 duration = teamAllocationLocks[_index]; 75 | require(now >= lockStartTime.add(duration), "Still in time-lock"); 76 | 77 | 78 | teamReleased[_index] = true; 79 | 80 | // transfer to owner address // 81 | ERC20 ercToken = ERC20(address(this)); 82 | ercToken.safeTransfer(msg.sender, teamReleaseAmount[_index]); 83 | } 84 | 85 | function getTeamAllocationUnlockDate(uint256 _index) public view returns (uint256) { 86 | 87 | require(_index < 4, "Index out of range"); 88 | return lockStartTime.add(teamAllocationLocks[_index]); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/mocks/MintableERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 4 | 5 | /** 6 | * @title ERC20Mintable 7 | * @dev ERC20 minting logic 8 | */ 9 | contract MintableERC20 is ERC20 { 10 | 11 | constructor ( 12 | string memory name, 13 | string memory symbol, 14 | uint8 dp 15 | ) public payable ERC20(name, symbol) { 16 | _setupDecimals(dp); 17 | } 18 | 19 | 20 | /** 21 | * @dev Function to mint tokens 22 | * @param value The amount of tokens to mint. 23 | * @return A boolean that indicates if the operation was successful. 24 | */ 25 | function mint(uint256 value) public returns (bool) { 26 | _mint(msg.sender, value); 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contracts/mocks/MockBAT.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | import "./MintableERC20.sol"; 4 | 5 | contract MockBAT is MintableERC20 { 6 | constructor() public MintableERC20("BAT Token", "SMX", 8) {} 7 | } 8 | -------------------------------------------------------------------------------- /contracts/mocks/MockUniRouter.sol: -------------------------------------------------------------------------------- 1 | import "../Interfaces.sol"; 2 | 3 | contract MockUniswapV2Router02 is IUniswapV2Router02 { 4 | function addLiquidityETH( 5 | address token, 6 | uint amountTokenDesired, 7 | uint amountTokenMin, 8 | uint amountETHMin, 9 | address to, 10 | uint deadline 11 | ) override external payable returns (uint amountToken, uint amountETH, uint liquidity){ 12 | 13 | } 14 | } -------------------------------------------------------------------------------- /contracts/mocks/MockXYZ.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | import "./MintableERC20.sol"; 4 | 5 | contract MockXYZ is MintableERC20 { 6 | constructor() public MintableERC20("Symthetix Token", "SMX", 18) {} 7 | } 8 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomiclabs/hardhat-waffle"); 2 | 3 | const fs = require('fs'); 4 | // const privateKey = fs.readFileSync(".secret").toString().trim(); 5 | 6 | // This is a sample Hardhat task. To learn how to create your own go to 7 | // https://hardhat.org/guides/create-task.html 8 | task("accounts", "Prints the list of accounts", async () => { 9 | const accounts = await ethers.getSigners(); 10 | 11 | for (const account of accounts) { 12 | console.log(account.address); 13 | } 14 | }); 15 | 16 | // You need to export an object to set up your config 17 | // Go to https://hardhat.org/config/ to learn more 18 | 19 | /** 20 | * @type import('hardhat/config').HardhatUserConfig 21 | */ 22 | module.exports = { 23 | defaultNetwork: 'hardhat', 24 | networks: { 25 | hardhat: { 26 | gas: 12000000, 27 | blockGasLimit: 0x1fffffffffffff, 28 | allowUnlimitedContractSize: true, 29 | timeout: 1800000 30 | }, 31 | testnet: { 32 | url: `https://data-seed-prebsc-2-s1.binance.org:8545/`, 33 | // accounts: [privateKey] 34 | }, 35 | }, 36 | solidity: { 37 | version: "0.6.12", 38 | settings: { 39 | optimizer: { 40 | enabled: true, 41 | runs: 200 42 | } 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function (deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cms", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://gitlab.com/CardanoX/CMS/sc.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://gitlab.com/CardanoX/CMS/sc/issues" 17 | }, 18 | "homepage": "https://gitlab.com/CardanoX/CMS/sc#readme", 19 | "devDependencies": { 20 | "@eth-optimism/smock": "^0.2.1-alpha.0", 21 | "@nomiclabs/hardhat-ethers": "^2.0.1", 22 | "@nomiclabs/hardhat-waffle": "^2.0.1", 23 | "@openzeppelin/contracts": "3.4.0", 24 | "chai": "^4.3.3", 25 | "ethereum-waffle": "^3.3.0", 26 | "ethers": "^5.0.31", 27 | "hardhat": "^2.1.0", 28 | "bignumber.js": "9.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/deploy-campaign.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("hardhat"); 2 | const BigNumber = require("bignumber.js"); 3 | 4 | async function main() { 5 | // We get the contract to deploy 6 | 7 | const factoryAddress = '0xEAfFeA97CFAED58fb46255160D65d3d0C49eE85c'; 8 | const tokenAddress = '0xfc355A53094bCab803C7B8AcdE05bf6679662e0A'; 9 | const campaignOwner = '0x2f07026A89B1E4E3377e6dA46FD1AB4dD04a255C'; 10 | 11 | //token 12 | // const MockXYZ = await ethers.getContractFactory("MockXYZ"); 13 | // mockXYZ = await MockXYZ.deploy(); 14 | // await mockXYZ.deployed(); 15 | // console.log("mockXYZ deployed to:", mockXYZ.address); 16 | 17 | const Factory = await ethers.getContractFactory("Factory"); 18 | const myFactory = await Factory.attach(factoryAddress); 19 | 20 | const block = await ethers.provider.getBlock("latest"); 21 | //console.log(block); 22 | 23 | const startDate = new BigNumber("1617723600"); 24 | const endDate = startDate.plus(600); 25 | const midDate = startDate.plus(300); 26 | 27 | const campaignAddress = await myFactory.createCampaign( 28 | tokenAddress, //token 29 | "1", //_subIndex 30 | campaignOwner, //campaignOwner 31 | ["10000000000000000", "20000000000000000", "1000000000000000000000", "0", "0"], 32 | [startDate.toString(), endDate.toString(), midDate.toString()], //dates 33 | ["10000000000000000", "10000000000000000"], //_buyLimits 34 | "1", //access 35 | ["8000000000000000", "400000000000000000000", "1800"], //_liquidity 36 | false//burn 37 | ); 38 | 39 | console.log("Campaign deployed to:", campaignAddress); 40 | } 41 | 42 | main() 43 | .then(() => process.exit(0)) 44 | .catch(error => { 45 | console.error(error); 46 | process.exit(1); 47 | }); -------------------------------------------------------------------------------- /scripts/deploy-factory.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("hardhat"); 2 | 3 | async function main() { 4 | // We get the contract to deploy 5 | const BscLauncherToken = await ethers.getContractFactory( 6 | "SuperLauncherToken" 7 | ); 8 | const bscLauncherToken = await BscLauncherToken.deploy(); 9 | await bscLauncherToken.deployed(); 10 | 11 | console.log("SuperLauncherToken deployed to:", bscLauncherToken.address); 12 | 13 | const fee1 = '0x2f07026A89B1E4E3377e6dA46FD1AB4dD04a255C'; 14 | const fee2 = '0x2f07026A89B1E4E3377e6dA46FD1AB4dD04a255C'; 15 | const pcSwapRouter = '0x0000000000000000000000000000000000000000'; 16 | 17 | 18 | const FeeVault = await ethers.getContractFactory("FeeVault"); 19 | const feeVault = await FeeVault.deploy(fee1, fee2); 20 | await feeVault.deployed(); 21 | 22 | const Factory = await ethers.getContractFactory("Factory"); 23 | 24 | const myFactory = await Factory.deploy( 25 | bscLauncherToken.address, 26 | feeVault.address, 27 | pcSwapRouter 28 | ); 29 | await myFactory.deployed(); 30 | 31 | console.log("myFactory deployed to:", myFactory.address); 32 | } 33 | 34 | main() 35 | .then(() => process.exit(0)) 36 | .catch(error => { 37 | console.error(error); 38 | process.exit(1); 39 | }); -------------------------------------------------------------------------------- /test/1.token-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { ethers } = require("hardhat"); 3 | const { smoddit, smockit } = require("@eth-optimism/smock"); 4 | const BigNumber = require("bignumber.js"); 5 | 6 | describe("LAUNCH TOKEN", function () { 7 | let bscLauncherToken; 8 | beforeEach(async () => { }); 9 | 10 | it("Should create token successfully", async function () { 11 | const [owner, addr1] = await ethers.getSigners(); 12 | 13 | const SuperLauncherToken = await ethers.getContractFactory( 14 | "SuperLauncherToken" 15 | ); 16 | 17 | bscLauncherToken = await SuperLauncherToken.deploy(); 18 | await bscLauncherToken.deployed(); 19 | 20 | expect(await bscLauncherToken.owner()).to.equal(owner.address); 21 | 22 | expect(await bscLauncherToken.symbol()).to.equal("LAUNCH"); 23 | 24 | expect(await bscLauncherToken.name()).to.equal("Super Launcher"); 25 | 26 | expect(await bscLauncherToken.totalSupply()).to.equal("6000000000000000000000000"); 27 | }); 28 | 29 | it("Should able to un-lock token", async function () { 30 | const [owner, addr1] = await ethers.getSigners(); 31 | 32 | //un-lock index 0 33 | await bscLauncherToken 34 | .connect(owner) 35 | .unlockTeamAllocation("0"); 36 | 37 | expect(await bscLauncherToken.balanceOf(owner.address)).to.equal( 38 | "4050000000000000000000000" 39 | ); 40 | 41 | //after 30 days 42 | await ethers.provider.send("evm_increaseTime", [2592000]); 43 | //un-lock index 1 44 | await bscLauncherToken 45 | .connect(owner) 46 | .unlockTeamAllocation("1"); 47 | 48 | expect(await bscLauncherToken.balanceOf(owner.address)).to.equal( 49 | "4550000000000000000000000" 50 | ); 51 | 52 | //after 150 days 53 | await ethers.provider.send("evm_increaseTime", [10368000]); 54 | //un-lock index 1 55 | await bscLauncherToken 56 | .connect(owner) 57 | .unlockTeamAllocation("2"); 58 | 59 | expect(await bscLauncherToken.balanceOf(owner.address)).to.equal( 60 | "5250000000000000000000000" 61 | ); 62 | 63 | //after 270 days 64 | await ethers.provider.send("evm_increaseTime", [10368000]); 65 | //un-lock index 1 66 | await bscLauncherToken 67 | .connect(owner) 68 | .unlockTeamAllocation("3"); 69 | 70 | expect(await bscLauncherToken.balanceOf(owner.address)).to.equal( 71 | "6000000000000000000000000" 72 | ); 73 | }); 74 | 75 | it("Can't un-lock token 2 times", async function () { 76 | const [owner, addr1] = await ethers.getSigners(); 77 | 78 | //un-lock index 0 79 | 80 | //after 30 days 81 | await ethers.provider.send("evm_increaseTime", [2592000]); 82 | //un-lock index 1 83 | await expect( 84 | bscLauncherToken 85 | .connect(owner) 86 | .unlockTeamAllocation("0") 87 | ).to.be.revertedWith("This allocation has been released previously"); 88 | 89 | }); 90 | 91 | it("Should able to transfer coin", async function () { 92 | const [owner, addr1] = await ethers.getSigners(); 93 | 94 | await bscLauncherToken 95 | .connect(owner) 96 | .transfer(addr1.address, "6000000000000000000000000"); 97 | 98 | expect(await bscLauncherToken.balanceOf(addr1.address)).to.equal( 99 | "6000000000000000000000000" 100 | ); 101 | }); 102 | 103 | }); 104 | -------------------------------------------------------------------------------- /test/2.factory-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { ethers } = require("hardhat"); 3 | const { smoddit, smockit } = require("@eth-optimism/smock"); 4 | const BigNumber = require("bignumber.js"); 5 | 6 | describe("Factory", function () { 7 | let myFactory; 8 | let mockXYZ; 9 | let bscLauncherToken; 10 | let campaign; 11 | beforeEach(async () => { 12 | const MockXYZ = await ethers.getContractFactory("MockXYZ"); 13 | mockXYZ = await MockXYZ.deploy(); 14 | await mockXYZ.deployed(); 15 | 16 | const BscLauncherToken = await ethers.getContractFactory( 17 | "SuperLauncherToken" 18 | ); 19 | bscLauncherToken = await BscLauncherToken.deploy(); 20 | await bscLauncherToken.deployed(); 21 | }); 22 | 23 | it("Should create factory successfully", async function () { 24 | const [owner, addr1] = await ethers.getSigners(); 25 | 26 | 27 | const FeeVault = await ethers.getContractFactory("FeeVault"); 28 | const feeVault = await FeeVault.deploy(owner.address, addr1.address); 29 | await feeVault.deployed(); 30 | 31 | const MockUniswapV2Router02 = await ethers.getContractFactory( 32 | "MockUniswapV2Router02" 33 | ); 34 | const mockUniswapV2Router02 = await MockUniswapV2Router02.deploy(); 35 | await mockUniswapV2Router02.deployed(); 36 | 37 | const Factory = await ethers.getContractFactory("Factory"); 38 | 39 | myFactory = await Factory.deploy( 40 | bscLauncherToken.address, 41 | feeVault.address, 42 | mockUniswapV2Router02.address 43 | ); 44 | await myFactory.deployed(); 45 | 46 | // getFeeAddress 47 | 48 | expect(await myFactory.getFeeAddress()).to.equal(feeVault.address); 49 | //getLpRouter 50 | 51 | expect(await myFactory.getLpRouter()).to.equal(mockUniswapV2Router02.address); 52 | 53 | //owner 54 | expect(await myFactory.owner()).to.equal(owner.address); 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /test/3.campaign-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { ethers } = require("hardhat"); 3 | const { smoddit, smockit } = require("@eth-optimism/smock"); 4 | const BigNumber = require("bignumber.js"); 5 | 6 | describe("Campaign", function () { 7 | let myFactory; 8 | let mockXYZ; 9 | let mockBAT; 10 | let bscLauncherToken; 11 | let campaign; 12 | let whiteListOnlyCampaign; 13 | 14 | beforeEach(async () => { 15 | const MockXYZ = await ethers.getContractFactory("MockXYZ"); 16 | mockXYZ = await MockXYZ.deploy(); 17 | await mockXYZ.deployed(); 18 | 19 | const MockBAT = await ethers.getContractFactory("MockBAT"); 20 | mockBAT = await MockBAT.deploy(); 21 | await mockBAT.deployed(); 22 | 23 | const BscLauncherToken = await ethers.getContractFactory( 24 | "SuperLauncherToken" 25 | ); 26 | bscLauncherToken = await BscLauncherToken.deploy(); 27 | await bscLauncherToken.deployed(); 28 | 29 | const [owner, addr1] = await ethers.getSigners(); 30 | const FeeVault = await ethers.getContractFactory("FeeVault"); 31 | const feeVault = await FeeVault.deploy(owner.address, addr1.address); 32 | await feeVault.deployed(); 33 | 34 | const MockUniswapV2Router02 = await ethers.getContractFactory( 35 | "MockUniswapV2Router02" 36 | ); 37 | const mockUniswapV2Router02 = await MockUniswapV2Router02.deploy(); 38 | await mockUniswapV2Router02.deployed(); 39 | 40 | const Factory = await ethers.getContractFactory("Factory"); 41 | 42 | myFactory = await Factory.deploy( 43 | bscLauncherToken.address, 44 | feeVault.address, 45 | mockUniswapV2Router02.address 46 | ); 47 | await myFactory.deployed(); 48 | }); 49 | 50 | it("Only admin can create campaign", async function () { 51 | const [owner, addr1] = await ethers.getSigners(); 52 | 53 | const block = await ethers.provider.getBlock("latest"); 54 | //console.log(block); 55 | 56 | const startDate = new BigNumber(block.timestamp); 57 | const endDate = startDate.plus(3600); 58 | const midDate = startDate.plus(1800); 59 | 60 | const campaignAddres = await myFactory.connect(owner).createCampaign( 61 | mockXYZ.address, //token 62 | "0", 63 | addr1.address, //campaignOwner 64 | ["10000000000000000", "20000000000000000", "1000000000000000000000", "0", "0"], //min, max, fee, feePcnt, qualifyingTokenQty 65 | [startDate.toString(), endDate.toString(), midDate.toString()], //dates 66 | ["1000000000000000000", "5000000000000000000"], //_buyLimits 67 | "0", //access 68 | ["8000000000000000", "400000000000000000000", "1800"], //_liquidity 69 | false//burn 70 | ); 71 | 72 | expect(campaignAddres).to.not.equal(null); 73 | 74 | await expect( 75 | myFactory.connect(addr1).createCampaign( 76 | mockXYZ.address, //token 77 | "0", 78 | addr1.address, //campaignOwner 79 | ["10000000000000000", "20000000000000000", "1000000000000000000000", "0", "0"], 80 | [startDate.toString(), endDate.toString(), midDate.toString()], //dates 81 | ["1000000000000000000", "5000000000000000000"], //_buyLimits 82 | "0", //access 83 | ["8000000000000000", "400000000000000000000", "1800"], //_liquidity 84 | false//burn 85 | ) 86 | ).to.be.revertedWith("Ownable: caller is not the owner"); 87 | }); 88 | 89 | it("Should create campaign successfully", async function () { 90 | const [owner, addr1] = await ethers.getSigners(); 91 | 92 | const block = await ethers.provider.getBlock("latest"); 93 | //console.log(block); 94 | 95 | const startDate = new BigNumber(block.timestamp); 96 | const endDate = startDate.plus(3600); 97 | const midDate = startDate.plus(1800); 98 | 99 | const campaignAddres = await myFactory.createCampaign( 100 | mockXYZ.address, //token 101 | "0", 102 | addr1.address, //campaignOwner 103 | ["10000000000000000", "20000000000000000", "1000000000000000000000", "0", "0"], 104 | [startDate.toString(), endDate.toString(), midDate.toString()], //dates 105 | ["1000000000000000", "20000000000000000"], //_buyLimits 106 | "0", //access 107 | ["8000000000000000", "400000000000000000000", "1800"], //_liquidity 108 | false//burn 109 | ); 110 | 111 | const Campaign = await ethers.getContractFactory("Campaign"); 112 | //console.log(campaignAddres.value.toString()); 113 | const camIdxData = await myFactory.allCampaigns( 114 | campaignAddres.value.toString() 115 | ); 116 | const campaignInstance = await Campaign.attach(camIdxData.contractAddress); 117 | 118 | //console.log(await campaignInstance.campaignOwner()); 119 | //check campaign contract address 120 | expect(await campaignInstance.campaignOwner()).to.equal(addr1.address); 121 | //check token address 122 | expect(await campaignInstance.token()).to.equal(mockXYZ.address); 123 | //check feePcnt 124 | expect(await campaignInstance.feePcnt()).to.equal("0"); 125 | // check tokenFunded 126 | expect(await campaignInstance.hardCap()).to.equal("20000000000000000"); 127 | 128 | expect(await campaignInstance.softCap()).to.equal("10000000000000000"); 129 | 130 | expect(await campaignInstance.startDate()).to.equal(startDate.toString()); 131 | 132 | expect(await campaignInstance.endDate()).to.equal(endDate.toString()); 133 | 134 | expect(await campaignInstance.midDate()).to.equal(midDate.toString()); 135 | 136 | expect(await campaignInstance.minBuyLimit()).to.equal( 137 | "1000000000000000" 138 | ); 139 | 140 | expect(await campaignInstance.maxBuyLimit()).to.equal( 141 | "20000000000000000" 142 | ); 143 | 144 | expect(await campaignInstance.lpLockDuration()).to.equal("1800"); 145 | 146 | expect(await campaignInstance.lpBnbQty()).to.equal("8000000000000000"); 147 | 148 | expect(await campaignInstance.lpTokenQty()).to.equal("400000000000000000000"); 149 | 150 | expect(await campaignInstance.lpTokenAmount()).to.equal("0"); 151 | }); 152 | 153 | it("Should create 2 campaign with same token successfully", async function () { 154 | const [owner, addr1] = await ethers.getSigners(); 155 | 156 | const block = await ethers.provider.getBlock("latest"); 157 | //console.log(block); 158 | 159 | const startDate = new BigNumber(block.timestamp); 160 | const endDate = startDate.plus(3600); 161 | const midDate = startDate.plus(1800); 162 | 163 | const campaignAddres = await myFactory.createCampaign( 164 | mockXYZ.address, //token 165 | "0", 166 | addr1.address, //campaignOwner 167 | ["10000000000000000", "20000000000000000", "1000000000000000000000", "0", "0"], 168 | [startDate.toString(), endDate.toString(), midDate.toString()], //dates 169 | ["1000000000000000", "20000000000000000"], //_buyLimits 170 | "0", //access 171 | ["8000000000000000", "400000000000000000000", "1800"], //_liquidity 172 | false//burn 173 | ); 174 | 175 | const Campaign = await ethers.getContractFactory("Campaign"); 176 | //console.log(campaignAddres.value.toString()); 177 | const camIdxData = await myFactory.allCampaigns( 178 | campaignAddres.value.toString() 179 | ); 180 | const campaignInstance = await Campaign.attach(camIdxData.contractAddress); 181 | 182 | //check campaign contract address 183 | expect(await campaignInstance.campaignOwner()).to.equal(addr1.address); 184 | //check token address 185 | expect(await campaignInstance.token()).to.equal(mockXYZ.address); 186 | 187 | const campaignAddres2 = await myFactory.createCampaign( 188 | mockXYZ.address, //token 189 | "1", 190 | addr1.address, //campaignOwner 191 | ["10000000000000000", "20000000000000000", "1000000000000000000000", "0", "0"], 192 | [startDate.toString(), endDate.toString(), midDate.toString()], //dates 193 | ["1000000000000000", "20000000000000000"], //_buyLimits 194 | "0", //access 195 | ["8000000000000000", "400000000000000000000", "1800"], //_liquidity 196 | false//burn 197 | ); 198 | 199 | const Campaign2 = await ethers.getContractFactory("Campaign"); 200 | //console.log(campaignAddres.value.toString()); 201 | const camIdxData2 = await myFactory.allCampaigns( 202 | campaignAddres.value.toString() 203 | ); 204 | const campaignInstance2 = await Campaign.attach(camIdxData2.contractAddress); 205 | 206 | expect(await campaignInstance2.campaignOwner()).to.equal(addr1.address); 207 | //check token address 208 | expect(await campaignInstance2.token()).to.equal(mockXYZ.address); 209 | }); 210 | 211 | it("Should able to fundIn and active campaign", async function () { 212 | //console.log(await ethers.getSigners()); 213 | const [owner, addr1, addr2] = await ethers.getSigners(); 214 | 215 | const block = await ethers.provider.getBlock("latest"); 216 | //console.log(block); 217 | 218 | const startDate = new BigNumber(block.timestamp); 219 | const endDate = startDate.plus(3600); 220 | const midDate = startDate.plus(1800); 221 | 222 | const campaignAddres = await myFactory.createCampaign( 223 | mockXYZ.address, //token 224 | "0", 225 | addr1.address, //campaignOwner 226 | ["1000000000000000000", "4000000000000000000", "18720000000000000000000", "0", "0"], 227 | [startDate.toString(), endDate.toString(), midDate.toString()], //dates 228 | ["1000000000000000000", "2000000000000000000"], //_buyLimits 229 | "0", //access 230 | ["1000000000000000000", "400000000000000000000", "1800"], //_liquidity 231 | false//burn 232 | ); 233 | 234 | const Campaign = await ethers.getContractFactory("Campaign"); 235 | //console.log(campaignAddres.value.toString()); 236 | const camIdxData = await myFactory.allCampaigns( 237 | campaignAddres.value.toString() 238 | ); 239 | const campaignInstance = await Campaign.attach(camIdxData.contractAddress); 240 | 241 | //mint 10mil XYZ to campaign owner 242 | await mockXYZ.connect(addr1).mint("10000000000000000000000000"); 243 | 244 | await mockXYZ 245 | .connect(addr1) 246 | .approve(camIdxData.contractAddress, "10000000000000000000000000000"); 247 | 248 | //not fundIn, user2 can't buy 249 | 250 | await expect( 251 | campaignInstance.connect(addr2).buyTokens() 252 | ).to.be.revertedWith("Campaign is not live"); 253 | 254 | //campaing owner call fundIn() 255 | await campaignInstance.connect(addr1).fundIn(); 256 | 257 | //after fundIn, user2 can buy 258 | expect(await campaignInstance.isLive()).to.equal(true); 259 | 260 | //User2 use 1BNB to buy, earn 4680 XYZ 261 | await campaignInstance 262 | .connect(addr2) 263 | .buyTokens({ value: "1000000000000000000" }); 264 | 265 | //after 3600 seconds 266 | await ethers.provider.send("evm_increaseTime", [3600]); 267 | 268 | //admin call finish 269 | await campaignInstance.connect(addr1).finishUp(); 270 | await campaignInstance.connect(addr1).setTokenClaimable(); 271 | 272 | await campaignInstance 273 | .connect(addr2) 274 | .claimTokens(); 275 | //user should able to claim tokens -> get 4680 XYZ 276 | 277 | expect((await mockXYZ.balanceOf(addr2.address)).toString()).to.equal( 278 | "4680000000000000000000" 279 | ); 280 | }); 281 | 282 | it("Should able to buy token and get correct amount with different token's decimal places", async function () { 283 | //console.log(await ethers.getSigners()); 284 | const [owner, addr1, addr2] = await ethers.getSigners(); 285 | 286 | const block = await ethers.provider.getBlock("latest"); 287 | //console.log(block); 288 | 289 | const startDate = new BigNumber(block.timestamp); 290 | const endDate = startDate.plus(3600); 291 | const midDate = startDate.plus(1800); 292 | 293 | const campaignAddres = await myFactory.createCampaign( 294 | mockBAT.address, //token 295 | "0", 296 | addr1.address, //campaignOwner 297 | ["1000000000000000000", "4000000000000000000", "4000000000", "0", "0"], 298 | [startDate.toString(), endDate.toString(), midDate.toString()], //dates 299 | ["1000000000000000000", "2000000000000000000"], //_buyLimits 300 | "0", //access 301 | ["1000000000000000000", "400000000000000000000", "1800"], //_liquidity 302 | false//burn 303 | ); 304 | 305 | const Campaign = await ethers.getContractFactory("Campaign"); 306 | //console.log(campaignAddres.value.toString()); 307 | const camIdxData = await myFactory.allCampaigns( 308 | campaignAddres.value.toString() 309 | ); 310 | const campaignInstance = await Campaign.attach(camIdxData.contractAddress); 311 | 312 | //mint 10mil XYZ to campaign owner 313 | await mockBAT.connect(addr1).mint("10000000000000000000000000"); 314 | 315 | await mockBAT 316 | .connect(addr1) 317 | .approve(camIdxData.contractAddress, "10000000000000000000000000000"); 318 | 319 | //not fundIn, user2 can't buy 320 | 321 | await expect( 322 | campaignInstance.connect(addr2).buyTokens() 323 | ).to.be.revertedWith("Campaign is not live"); 324 | 325 | //campaing owner call fundIn() 326 | await campaignInstance.connect(addr1).fundIn(); 327 | 328 | //after fundIn, user2 can buy 329 | expect(await campaignInstance.isLive()).to.equal(true); 330 | 331 | //User2 use 1BNB to buy, earn 10 BAT 332 | await campaignInstance 333 | .connect(addr2) 334 | .buyTokens({ value: "1000000000000000000" }); 335 | 336 | //after 3600 seconds 337 | await ethers.provider.send("evm_increaseTime", [3600]); 338 | 339 | //admin call finish 340 | await campaignInstance.connect(addr1).finishUp(); 341 | await campaignInstance.connect(addr1).setTokenClaimable(); 342 | 343 | 344 | //user should get correct token -> get 10 BAT 345 | 346 | await campaignInstance 347 | .connect(addr2) 348 | .claimTokens(); 349 | 350 | expect((await mockBAT.balanceOf(addr2.address)).toString()).to.equal( 351 | "1000000000" 352 | ); 353 | }); 354 | 355 | it("Campaign: WhitelistedOnly, Campaign owner can add whilelist", async function () { 356 | const [owner, addr1, addr2, addr3] = await ethers.getSigners(); 357 | 358 | const block = await ethers.provider.getBlock("latest"); 359 | //console.log(block); 360 | 361 | const startDate = new BigNumber(block.timestamp); 362 | const endDate = startDate.plus(3600); 363 | const midDate = startDate.plus(1800); 364 | 365 | const campaignAddres = await myFactory.createCampaign( 366 | mockXYZ.address, //token 367 | "0", 368 | addr1.address, //campaignOwner 369 | ["2000000000000000000", "4000000000000000000", "18720000000000000000000", "0", "0"], 370 | [startDate.toString(), endDate.toString(), midDate.toString()], //dates 371 | ["1000000000000000000", "2000000000000000000"], //_buyLimits 372 | "1", //access 373 | ["2000000000000000000", "400000000000000000000", "1800"], //_liquidity 374 | false//burn 375 | ); 376 | 377 | const Campaign = await ethers.getContractFactory("Campaign"); 378 | //console.log(campaignAddres.value.toString()); 379 | const camIdxData = await myFactory.allCampaigns( 380 | campaignAddres.value.toString() 381 | ); 382 | whiteListOnlyCampaign = await Campaign.attach(camIdxData.contractAddress); 383 | 384 | //whiteListOnlyCampaign = campaignInstance; 385 | 386 | //add address 2 to white list 387 | 388 | // strange user call func to add him to whitelist 389 | await expect( 390 | whiteListOnlyCampaign.connect(addr2).appendWhitelisted([addr2.address]) 391 | ).to.be.revertedWith("Only campaign owner can call"); 392 | 393 | //add user2 to whilelist 394 | await whiteListOnlyCampaign 395 | .connect(addr1) 396 | .appendWhitelisted([addr2.address, addr3.address]); 397 | 398 | expect( 399 | await whiteListOnlyCampaign.whitelistedMap(addr2.address) 400 | ).to.be.equal(true); 401 | 402 | expect( 403 | await whiteListOnlyCampaign.whitelistedMap(addr3.address) 404 | ).to.be.equal(true); 405 | expect(await whiteListOnlyCampaign.numOfWhitelisted()).to.be.equal("2"); 406 | 407 | //mint 10mil XYZ to campaign owner 408 | await mockXYZ.connect(addr1).mint("10000000000000000000000000"); 409 | 410 | await mockXYZ 411 | .connect(addr1) 412 | .approve(camIdxData.contractAddress, "10000000000000000000000000000"); 413 | 414 | //campaing owner call fundIn() 415 | await whiteListOnlyCampaign.connect(addr1).fundIn(); 416 | //only user2 can buy token b/c he's in whitelist 417 | //User2 use 1BNB to buy, earn 4680 XYZ 418 | await whiteListOnlyCampaign 419 | .connect(addr2) 420 | .buyTokens({ value: "1000000000000000000" }); 421 | 422 | await whiteListOnlyCampaign 423 | .connect(addr3) 424 | .buyTokens({ value: "1000000000000000000" }); 425 | 426 | //only owner can remove an user from whitelist 427 | await whiteListOnlyCampaign 428 | .connect(addr1) 429 | .removeWhitelisted([addr3.address]); 430 | 431 | expect( 432 | await whiteListOnlyCampaign.whitelistedMap(addr3.address) 433 | ).to.be.equal(false); 434 | }); 435 | 436 | it("Exceeded max amount", async function () { 437 | const [owner, addr1, addr2, addr3] = await ethers.getSigners(); 438 | await whiteListOnlyCampaign 439 | .connect(addr1) 440 | .appendWhitelisted([addr3.address]); 441 | 442 | await expect( 443 | whiteListOnlyCampaign 444 | .connect(addr3) 445 | .buyTokens({ value: "2000000000000000000" }) 446 | ).to.be.revertedWith("Exceeded max amount"); 447 | }); 448 | 449 | it("Campaing is done, when reach the hardcap", async function () { 450 | const [owner, addr1, addr2, addr3] = await ethers.getSigners(); 451 | expect(await whiteListOnlyCampaign.isLive()).to.equal(true); 452 | 453 | await whiteListOnlyCampaign 454 | .connect(addr2) 455 | .buyTokens({ value: "1000000000000000000" }); 456 | 457 | await whiteListOnlyCampaign 458 | .connect(addr3) 459 | .buyTokens({ value: "1000000000000000000" }); 460 | 461 | expect(await whiteListOnlyCampaign.isLive()).to.equal(false); 462 | }); 463 | 464 | it("WhitelistedFirstThenEveryone, Should able to buy tokens for white list and everyone", async function () { 465 | const [owner, addr1, addr2, addr3] = await ethers.getSigners(); 466 | 467 | const block = await ethers.provider.getBlock("latest"); 468 | //console.log(block); 469 | 470 | const startDate = new BigNumber(block.timestamp); 471 | const endDate = startDate.plus(3600); 472 | const midDate = startDate.plus(1800); 473 | 474 | const campaignAddres = await myFactory.createCampaign( 475 | mockXYZ.address, //token 476 | "0", 477 | addr1.address, //campaignOwner 478 | ["2000000000000000000", "4000000000000000000", "18720000000000000000000", "0", "0"], 479 | [startDate.toString(), endDate.toString(), midDate.toString()], //dates 480 | ["1000000000000000000", "2000000000000000000"], //_buyLimits 481 | "2", //access 482 | ["2000000000000000000", "400000000000000000000", "1800"], //_liquidity 483 | false//burn 484 | ); 485 | 486 | const Campaign = await ethers.getContractFactory("Campaign"); 487 | //console.log(campaignAddres.value.toString()); 488 | const camIdxData = await myFactory.allCampaigns( 489 | campaignAddres.value.toString() 490 | ); 491 | const campaignInstance = await Campaign.attach(camIdxData.contractAddress); 492 | //add address 2 to white list 493 | 494 | // strange user call func to add him to whitelist 495 | await expect( 496 | campaignInstance.connect(addr2).appendWhitelisted([addr2.address]) 497 | ).to.be.revertedWith("Only campaign owner can call"); 498 | 499 | //campaign owner can add users to whilelist 500 | await campaignInstance.connect(addr1).appendWhitelisted([addr2.address]); 501 | 502 | //mint 10mil XYZ to campaign owner 503 | await mockXYZ.connect(addr1).mint("10000000000000000000000000"); 504 | 505 | await mockXYZ 506 | .connect(addr1) 507 | .approve(camIdxData.contractAddress, "10000000000000000000000000000"); 508 | 509 | //campaing owner call fundIn() 510 | await campaignInstance.connect(addr1).fundIn(); 511 | //only user2 can buy token b/c he's in whitelist 512 | //User2 use 1BNB to buy, earn 4680 XYZ 513 | await campaignInstance 514 | .connect(addr2) 515 | .buyTokens({ value: "1000000000000000000" }); 516 | 517 | await expect( 518 | campaignInstance 519 | .connect(addr3) 520 | .buyTokens({ value: "1000000000000000000" }) 521 | ).to.be.revertedWith("You are not whitelisted"); 522 | 523 | //After midDate, user3 can join to buy 524 | await ethers.provider.send("evm_increaseTime", [1810]); 525 | 526 | await campaignInstance 527 | .connect(addr3) 528 | .buyTokens({ value: "1000000000000000000" }); 529 | }); 530 | 531 | it("Campaing is done, finishUp to withdraw BNB", async function () { 532 | const [owner, addr1, addr2, addr3] = await ethers.getSigners(); 533 | 534 | expect(await whiteListOnlyCampaign.collectedBNB()).to.equal( 535 | "4000000000000000000" 536 | ); 537 | 538 | await whiteListOnlyCampaign.connect(addr1).finishUp(); 539 | 540 | expect(await whiteListOnlyCampaign.finishUpSuccess()).to.equal(true); 541 | }); 542 | 543 | it("Campaing is abort, when can't reach the softcap", async function () { 544 | const [owner,addr1, addr4] = await ethers.getSigners(); 545 | 546 | const block = await ethers.provider.getBlock("latest"); 547 | console.log(block); 548 | 549 | const startDate = new BigNumber(block.timestamp); 550 | const endDate = startDate.plus(3600); 551 | const midDate = startDate.plus(1800); 552 | 553 | const campaignAddres = await myFactory.createCampaign( 554 | mockXYZ.address, //token 555 | "0", 556 | addr1.address, //campaignOwner 557 | ["2000000000000000000", "4000000000000000000", "18720000000000000000000", "0", "0"], 558 | [startDate.toString(), endDate.toString(), midDate.toString()], //dates 559 | ["1000000000000000000", "2000000000000000000"], //_buyLimits 560 | "0", //access 561 | ["2000000000000000000", "400000000000000000000", "1800"], //_liquidity 562 | [false, true] //config 563 | ); 564 | 565 | const Campaign = await ethers.getContractFactory("Campaign"); 566 | 567 | const camIdxData = await myFactory.allCampaigns( 568 | campaignAddres.value.toString() 569 | ); 570 | const campaignInstance = await Campaign.attach(camIdxData.contractAddress); 571 | 572 | //mint 10mil XYZ to campaign owner 573 | await mockXYZ.connect(addr1).mint("10000000000000000000000000"); 574 | 575 | await mockXYZ 576 | .connect(addr1) 577 | .approve(camIdxData.contractAddress, "10000000000000000000000000000"); 578 | 579 | //campaing owner call fundIn() 580 | await campaignInstance.connect(addr1).fundIn(); 581 | 582 | expect((await mockXYZ.balanceOf(addr4.address)).toString()).to.equal( 583 | "0" 584 | ); 585 | 586 | //User2 use 1BNB to buy, earn 4680 XYZ 587 | await campaignInstance 588 | .connect(addr4) 589 | .buyTokens({ value: "1000000000000000000" }); 590 | 591 | await ethers.provider.send("evm_mine", [endDate.plus(1).toNumber()]); 592 | 593 | const block1 = await ethers.provider.getBlock("latest"); 594 | console.log(block1); 595 | 596 | expect(await campaignInstance.isLive()).to.equal(false); 597 | 598 | //user2 can refund 599 | await mockXYZ.connect(addr4).approve(camIdxData.contractAddress, "4680000000000000000000"); 600 | 601 | await campaignInstance.connect(addr4).refund(); 602 | 603 | //get back eth, send token to campaign SC 604 | expect((await mockXYZ.balanceOf(addr4.address)).toString()).to.equal("0"); 605 | 606 | }); 607 | }); 608 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * truffleframework.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWalletProvider = require('@truffle/hdwallet-provider'); 22 | // const infuraKey = "fj4jll3k....."; 23 | // 24 | // const fs = require('fs'); 25 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 26 | 27 | module.exports = { 28 | /** 29 | * Networks define how you connect to your ethereum client and let you set the 30 | * defaults web3 uses to send transactions. If you don't specify one truffle 31 | * will spin up a development blockchain for you on port 9545 when you 32 | * run `develop` or `test`. You can ask a truffle command to use a specific 33 | * network from the command line, e.g 34 | * 35 | * $ truffle test --network 36 | */ 37 | 38 | networks: { 39 | // Useful for testing. The `development` name is special - truffle uses it by default 40 | // if it's defined here and no other network is specified at the command line. 41 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 42 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 43 | // options below to some value. 44 | // 45 | // development: { 46 | // host: "127.0.0.1", // Localhost (default: none) 47 | // port: 8545, // Standard Ethereum port (default: none) 48 | // network_id: "*", // Any network (default: none) 49 | // }, 50 | // Another network with more advanced options... 51 | // advanced: { 52 | // port: 8777, // Custom port 53 | // network_id: 1342, // Custom network 54 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 55 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 56 | // from:
, // Account to send txs from (default: accounts[0]) 57 | // websockets: true // Enable EventEmitter interface for web3 (default: false) 58 | // }, 59 | // Useful for deploying to a public network. 60 | // NB: It's important to wrap the provider as a function. 61 | // ropsten: { 62 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 63 | // network_id: 3, // Ropsten's id 64 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 65 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 66 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 67 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 68 | // }, 69 | // Useful for private networks 70 | // private: { 71 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 72 | // network_id: 2111, // This network is yours, in the cloud. 73 | // production: true // Treats this network as if it was a public net. (default: false) 74 | // } 75 | }, 76 | 77 | // Set default mocha options here, use special reporters etc. 78 | mocha: { 79 | // timeout: 100000 80 | }, 81 | 82 | // Configure your compilers 83 | compilers: { 84 | solc: { 85 | version: "0.6.12", // Fetch exact version from solc-bin (default: truffle's version) 86 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 87 | // settings: { // See the solidity docs for advice about optimization and evmVersion 88 | // optimizer: { 89 | // enabled: false, 90 | // runs: 200 91 | // }, 92 | // evmVersion: "byzantium" 93 | // } 94 | }, 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "noImplicitAny": false, 9 | "resolveJsonModule": true 10 | }, 11 | "include": ["./scripts", "./test", "./tasks"], 12 | "files": [ 13 | "./hardhat.config.js", 14 | "node_modules/@nomiclabs/buidler-ethers/src/type-extensions.d.ts", 15 | "node_modules/buidler-typechain/src/type-extensions.d.ts", 16 | "node_modules/@nomiclabs/buidler-waffle/src/type-extensions.d.ts", 17 | "node_modules/@nomiclabs/buidler-etherscan/src/type-extensions.d.ts" 18 | ] 19 | } 20 | --------------------------------------------------------------------------------