안녕하세요, 오늘은 제가 BNB 테스트넷에서 작동하는 DApp 프로젝트 BNB.Fun을 개발하며 겪은 과정을 공유하려고 합니다. 이 프로젝트는 토큰 발행부터 프리세일(구매/환불) 기능까지 구현하는 것을 목표로 했고, 여러 시행착오 끝에 성공적으로 완성했습니다. 그 여정을 정리해 보겠습니다.
https://ititit1.tistory.com/159
BNB Chain 토큰생성 Dapp 생성 (Solidity Project Day2) + React
잡담최종 프로젝트 형태는 Pump.fun과 비슷한 Dapp을 배포하는것입니다! 저만의 차별성이 있기도하고 아이디어가 번뜩 떠올라서 구현해보려고합니다. 100개 프로젝트 만들다보면 한개쯤 대박 터지
ititit1.tistory.com
Step 1: 컨트랙트 코드(Solidity)
-
TokenFactory:
-
createToken(name, symbol): 0.01 BNB를 받고 CustomToken과 Presale 인스턴스를 생성.
-
withdraw(): 컨트랙트에 쌓인 BNB를 소유자에게 전송가능하도록 구현
-
배포 주소: 0x7E5b012c635d9bB9c19aAD3E37F059C4410E3AA1. (bsc testnet)
-
-
CustomToken:
-
ERC-20 기반 토큰, 초기 공급량은 Presale에 할당.
-
예: 0x173ACc12fd867f2bee4bECB1e05a2e2fB106f1Ee.
-
-
Presale:
-
buyTokens(): BNB로 토큰 구매.
-
refund(): 투자한 BNB 환불 및 토큰 반환.
-
예: 0x790fc6bB52B34488D9b6a9630e7DcC516d47c8a6.
-
Step 2: 프론트엔드 구현
-
App.js:
-
MetaMask 연결 및 tokens 상태 관리.
-
로컬 스토리지로 tokens 유지 (새로고침 시 초기화 방지).
-
-
CreateTokenModal.js:
-
토큰 이름, 심볼 입력 후 TokenFactory.createToken 호출.
-
발행된 tokenAddress와 presaleAddress를 tokens에 추가.
-
-
PresalePage.js:
-
URL 파라미터(tokenAddress)로 토큰 식별.
-
tokens에서 presaleAddress 찾아 프리세일 데이터 로드.
-
"Buy Now"와 "Refund" 버튼으로 구매/환불 기능 제공.
-


Step 3: 디버깅과 해결 과정
-
"Transaction execution reverted":
-
원인1 : 가스 한도 부족 또는 잔액 문제 /
-
해결: gasLimit: 3000000으로 증가, 계정에 0.1 BNB 충전.
- 원인2. : CustomToken이 Presale 주소를 필요로 하는데, Presale은 CustomToken 주소를 필요로 함 → 순서 문제로 인해 CustomToken의 초기 공급량이 잘못된 주소(address(0))로 민팅되거나, 이후 Presale에서 토큰을 다룰 때 오류 발생.
-
해결: Presale을 먼저 만들어 presaleAddress를 확보한 뒤, 이를 CustomToken에 전달 → CustomToken이 올바른 Presale 주소에 민팅하고, Presale은 이후 업데이트로 토큰을 인식.
*이 문제로 진짜 몇일동안 뭐가 문제인지 고민함...(백업하고 구현하고 문제발생되고 다시 백업하고 구현하고 문제 생기고.. 반복)
-
-
"Presale address not found in local token list":
-
원인: PresalePage가 tokens에서 데이터를 찾지 못함.
-
해결: URL을 최신 토큰 주소로 수정, tokens 상태를 로컬 스토리지로 유지.
-
-
"limit exceeded" (eth_getLogs):
-
원인: TokenFactory 이벤트 조회 시 블록 범위 초과.
-
해결: eth_getLogs 대신 tokens 상태만 사용.
-
Step 4: 최종 테스트
-
URL: http://localhost:3000/presale/0x173ACc12fd867f2bee4bECB1e05a2e2fB106f1Ee.
-
결과:
-
"Presale Details"에 현재 비율, 모금액 표시.
-
"Buy Now"로 0.01 BNB 구매 성공.
-
"Refund"로 환불 성공.
-



주요코드(Solidity)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./customToken.sol";
import "./Presale.sol";
contract TokenFactory is Ownable {
uint256 public constant CREATE_FEE = 0.01 ether;
event TokenCreated(address tokenAddress, address presaleAddress);
constructor() Ownable(msg.sender) {}
function createToken(string memory name, string memory symbol) external payable returns (address, address) {
require(msg.value == CREATE_FEE, "Must send exactly 0.01 BNB");
// Presale 먼저 생성
Presale newPresale = new Presale(IERC20(address(0)));
address presaleAddress = address(newPresale);
// CustomToken 생성 및 Presale로 토큰 전송
CustomToken newToken = new CustomToken(name, symbol, presaleAddress);
address tokenAddress = address(newToken);
// Presale에 토큰 설정 및 소유권 이전
newPresale.updateToken(IERC20(tokenAddress));
newPresale.transferOwnership(msg.sender);
emit TokenCreated(tokenAddress, presaleAddress);
return (tokenAddress, presaleAddress);
}
function withdraw() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
interface IPancakeSwapRouter {
function addLiquidityETH(
address token,
uint256 amountTokenDesired,
uint256 amountTokenMin,
uint256 amountETHMin,
address to,
uint256 deadline
) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity);
}
contract Presale is Ownable, ReentrancyGuard {
IERC20 public token;
uint256 public constant INITIAL_RATE = 10_000_000 * 10**18; // 1 BNB = 1,000만 토큰
uint256 public constant PRICE_INCREASE = 10; // 10% 증가
uint256 public constant INCREASE_INTERVAL = 1 hours; // 1시간마다
uint256 public constant TARGET_BNB = 5 ether; // 5 BNB 목표
uint256 public constant LIQUIDITY_TOKENS = 50_000_000 * 10**18; // 5천만 토큰
uint256 public startTime;
uint256 public totalBNB;
bool public liquidityAdded;
mapping(address => uint256) public contributions; // 투자 BNB
mapping(address => uint256) public tokenBalances; // 구매 토큰
IPancakeSwapRouter public pancakeSwapRouter;
address public constant PANCAKESWAP_ROUTER = 0xD99D1c33F9fC3444f8101754aBC46c52416550D1; // BSC Testnet 라우터
event TokensPurchased(address buyer, uint256 bnbAmount, uint256 tokenAmount);
event Refunded(address user, uint256 bnbAmount);
event LiquidityAdded(uint256 tokenAmount, uint256 bnbAmount);
constructor(IERC20 _token) Ownable(msg.sender) {
token = _token;
pancakeSwapRouter = IPancakeSwapRouter(PANCAKESWAP_ROUTER);
startTime = block.timestamp;
}
function getCurrentRate() public view returns (uint256) {
uint256 intervals = (block.timestamp - startTime) / INCREASE_INTERVAL;
return INITIAL_RATE - (INITIAL_RATE * PRICE_INCREASE * intervals) / 100; // 토큰 수 감소
}
function buyTokens() external payable nonReentrant {
require(!liquidityAdded, "Presale ended");
require(msg.value > 0, "Must send BNB");
uint256 rate = getCurrentRate();
uint256 tokenAmount = (msg.value * rate) / 1 ether;
require(token.balanceOf(address(this)) >= tokenAmount, "Not enough tokens");
contributions[msg.sender] += msg.value;
tokenBalances[msg.sender] += tokenAmount;
totalBNB += msg.value;
token.transfer(msg.sender, tokenAmount);
emit TokensPurchased(msg.sender, msg.value, tokenAmount);
if (totalBNB >= TARGET_BNB) {
addLiquidity();
}
}
function refund() external nonReentrant {
require(!liquidityAdded, "Presale ended");
uint256 bnbAmount = contributions[msg.sender];
require(bnbAmount > 0, "No contribution");
uint256 refundAmount = (bnbAmount * 90) / 100;
uint256 tokenAmount = tokenBalances[msg.sender];
contributions[msg.sender] = 0;
tokenBalances[msg.sender] = 0;
totalBNB -= bnbAmount;
require(token.transferFrom(msg.sender, address(this), tokenAmount), "Token transfer failed");
payable(msg.sender).transfer(refundAmount);
emit Refunded(msg.sender, refundAmount);
}
function addLiquidity() internal {
require(!liquidityAdded, "Liquidity already added");
liquidityAdded = true;
token.approve(address(pancakeSwapRouter), LIQUIDITY_TOKENS);
pancakeSwapRouter.addLiquidityETH{value: TARGET_BNB}(
address(token),
LIQUIDITY_TOKENS,
LIQUIDITY_TOKENS * 90 / 100, // 10% 슬리피지 허용
TARGET_BNB * 90 / 100,
address(this),
block.timestamp + 15 minutes
);
emit LiquidityAdded(LIQUIDITY_TOKENS, TARGET_BNB);
uint256 remainingTokens = token.balanceOf(address(this));
if (remainingTokens > 0) {
token.transfer(owner(), remainingTokens);
}
}
function withdrawBNB() external onlyOwner {
require(liquidityAdded, "Presale not ended");
payable(owner()).transfer(address(this).balance);
}
function updateToken(IERC20 _token) external onlyOwner {
require(address(token) == address(0), "Token already set");
token = _token;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract CustomToken is ERC20, Ownable {
constructor(
string memory name,
string memory symbol,
address presaleAddress
) ERC20(name, symbol) Ownable(msg.sender) {
uint256 initialSupply = 100_000_000 * 10**18; // 1억 토큰
_mint(address(this), initialSupply);
if (presaleAddress != address(0)) {
_transfer(address(this), presaleAddress, initialSupply);
}
}
}
'Solidity' 카테고리의 다른 글
BNB Chain 토큰생성 Dapp 생성 (Solidity Project Day2) + React (0) | 2025.03.25 |
---|---|
Truffle require('chai') Error [ERR_REQUIRE_ESM 해결방법 (1) | 2024.02.24 |
Solidity Tutorial lec.2 / 함수제어자, 예외처리, 전역변수msg (0) | 2024.02.18 |
Solidity Tutorial lec.1 / Solidity란, 스마트계약, Solidity 문법 (0) | 2024.02.16 |