03-init-sale.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. import "dotenv/config"
  2. import { ethers } from "ethers"
  3. import * as fs from "fs"
  4. import * as path from "path"
  5. import { StandardMerkleTree } from "@openzeppelin/merkle-tree"
  6. import { MockUSD1__factory, BasicERC20__factory, Launchpad__factory } from "../typechain-types/factories/contracts"
  7. import type { MockUSD1, BasicERC20, Launchpad } from "../typechain-types/contracts"
  8. async function main() {
  9. // 获取环境配置
  10. const environment = process.env.ENVIRONMENT || "bsctest"
  11. const rpcUrl = process.env.RPC_URL || "https://data-seed-prebsc-1-s1.binance.org:8545/"
  12. const outputDir = process.env.OUTPUT_DIR || "./output/bsctest"
  13. console.log(`🚀 第3步:初始化销售并铸造代币...`)
  14. console.log(`🌍 环境: ${environment}`)
  15. console.log(`🔗 RPC: ${rpcUrl}`)
  16. console.log(`📁 输出目录: ${outputDir}`)
  17. // 检查环境变量
  18. if (!process.env.PRIVATE_KEY) {
  19. console.error("❌ 需要设置 PRIVATE_KEY 环境变量")
  20. process.exit(1)
  21. }
  22. // 从之前的步骤加载合约地址
  23. const contractAddressesPath = path.join(outputDir, "contract-addresses.json")
  24. if (!fs.existsSync(contractAddressesPath)) {
  25. console.error(`❌ 未找到 ${contractAddressesPath} 文件。请先运行第1步。`)
  26. process.exit(1)
  27. }
  28. const contractAddresses = JSON.parse(fs.readFileSync(contractAddressesPath, "utf8"))
  29. console.log("📋 已从之前的步骤加载合约地址")
  30. // 设置提供者和部署者
  31. const provider = new ethers.JsonRpcProvider(rpcUrl)
  32. const deployer = new ethers.Wallet(process.env.PRIVATE_KEY, provider)
  33. // 获取当前nonce
  34. const currentNonce = await provider.getTransactionCount(deployer.address)
  35. console.log("📋 当前nonce:", currentNonce)
  36. // 连接到已部署的合约
  37. const mockUSD1 = MockUSD1__factory.connect(contractAddresses.mockUSD1, deployer)
  38. const saleToken = BasicERC20__factory.connect(contractAddresses.saleToken, deployer)
  39. const launchpad = Launchpad__factory.connect(contractAddresses.launchpad, deployer)
  40. // 检查并确保有足够的销售代币(幂等操作)
  41. console.log("\n💰 检查销售代币余额...")
  42. const totalTokensForSale = ethers.parseEther("50000") // 5万个代币(对应50K USDT目标)
  43. // 检查部署者余额
  44. const deployerBalance = await saleToken.balanceOf(deployer.address)
  45. console.log("📊 部署者当前余额:", ethers.formatEther(deployerBalance), "代币")
  46. // 检查Launchpad合约余额
  47. const launchpadBalance = await saleToken.balanceOf(contractAddresses.launchpad)
  48. console.log("📊 Launchpad合约当前余额:", ethers.formatEther(launchpadBalance), "代币")
  49. // 检查部署者对Launchpad的授权
  50. const allowance = await saleToken.allowance(deployer.address, contractAddresses.launchpad)
  51. console.log("📊 当前授权额度:", ethers.formatEther(allowance), "代币")
  52. // 计算需要的代币数量
  53. const totalNeeded = totalTokensForSale
  54. const currentTotal = deployerBalance + launchpadBalance
  55. console.log("📊 需要总代币数:", ethers.formatEther(totalNeeded), "代币")
  56. console.log("📊 当前总代币数:", ethers.formatEther(currentTotal), "代币")
  57. // 如果部署者余额不足,需要铸造
  58. let nonce = currentNonce
  59. if (deployerBalance < totalNeeded) {
  60. const needToMint = totalNeeded - deployerBalance
  61. console.log("🔄 需要向部署者铸造", ethers.formatEther(needToMint), "个代币")
  62. await saleToken.connect(deployer).mint(deployer.address, needToMint, { nonce: nonce++ })
  63. console.log("✅ 已向部署者铸造", ethers.formatEther(needToMint), "个销售代币")
  64. } else {
  65. console.log("✅ 部署者已有足够的代币余额")
  66. }
  67. // 如果授权不足,需要批准
  68. if (allowance < totalNeeded) {
  69. const needToApprove = totalNeeded - allowance
  70. console.log("🔄 需要批准", ethers.formatEther(needToApprove), "个代币给Launchpad")
  71. await saleToken.connect(deployer).approve(contractAddresses.launchpad, needToApprove, { nonce: nonce++ })
  72. console.log("✅ 已批准", ethers.formatEther(needToApprove), "个代币给Launchpad")
  73. } else {
  74. console.log("✅ Launchpad已有足够的代币授权")
  75. }
  76. // 从JSON文件加载测试账户
  77. const testAccountsPath = "./scripts/testAccount/BLaunchpadTest.json"
  78. if (!fs.existsSync(testAccountsPath)) {
  79. console.error("❌ 未找到测试账户文件:", testAccountsPath)
  80. process.exit(1)
  81. }
  82. const testAccounts = JSON.parse(fs.readFileSync(testAccountsPath, "utf8"))
  83. console.log("📋 已从", testAccountsPath, "加载", testAccounts.length, "个测试账户")
  84. // 从加载的账户创建测试用户(所有20个账户)
  85. const testUsers = testAccounts.map((account: any) => new ethers.Wallet(account.privateKey, provider))
  86. // 基于第2步的USD1分发情况创建销售参数
  87. const now = Math.floor(Date.now() / 1000)
  88. const startTime = now + 60 // 1分钟后开始
  89. const endTime = now + 3600 // 1小时后结束(更短的时间用于测试)
  90. const paymentTokenPrice = ethers.parseEther("1") // 1 USDT 每代币
  91. const feeBps = 500 // 5% 手续费
  92. // 设置一个固定目标,如果有5个以上用户参与就会超过
  93. // 每个用户可以贡献最多10,000 USD1(他们的全部USD1余额)
  94. // 5个用户 × 10,000 USD1 = 50,000 USD1 = 50,000 USD1 目标(刚好达到)
  95. // 6个用户 × 10,000 USD1 = 60,000 USD1 > 50,000 USD1 目标(超募)
  96. // 直接设置目标为 50,000 USD1(带 18 位小数)
  97. const contributionTarget = ethers.parseEther("50000") // 50K USDT 目标
  98. console.log("\n🎯 目标设置为 50K USDT")
  99. console.log("📊 如果有5个以上用户参与 (5 × 10K = 50K),销售将达到目标")
  100. console.log("📊 如果有6个以上用户参与 (6 × 10K = 60K),销售将超额认购")
  101. console.log("📊 如果少于5个用户参与,销售将认购不足")
  102. console.log("\n📋 销售参数:")
  103. console.log("- 开始时间:", new Date(startTime * 1000).toLocaleString())
  104. console.log("- 结束时间:", new Date(endTime * 1000).toLocaleString())
  105. console.log("- 持续时间:", Math.floor((endTime - startTime) / 3600), "小时")
  106. console.log("- 目标:", ethers.formatEther(contributionTarget), "USDT")
  107. console.log("- 价格:", ethers.formatEther(paymentTokenPrice), "USDT 每代币")
  108. console.log("- 手续费:", feeBps / 100, "%")
  109. console.log("- 销售代币总数:", ethers.formatEther(totalTokensForSale))
  110. // 验证销售参数
  111. if (startTime >= endTime) {
  112. console.error("❌ 错误:开始时间必须在结束时间之前")
  113. process.exit(1)
  114. }
  115. if (contributionTarget <= 0) {
  116. console.error("❌ 错误:贡献目标必须大于0")
  117. process.exit(1)
  118. }
  119. if (paymentTokenPrice <= 0) {
  120. console.error("❌ 错误:支付代币价格必须大于0")
  121. process.exit(1)
  122. }
  123. // 创建销售(幂等操作)
  124. console.log("\n🎯 检查并创建销售...")
  125. try {
  126. // 尝试获取销售详情,如果成功说明销售已存在
  127. const saleDetails = await launchpad.getSaleDetails()
  128. const currentSaleToken = saleDetails[0]
  129. const currentTotalTokens = saleDetails[1]
  130. const currentContribution = saleDetails[2] // totalContributionAmount
  131. // 检查是否已经调用过createSale(销售代币地址不为0)
  132. if (currentSaleToken === ethers.ZeroAddress || currentTotalTokens === BigInt(0)) {
  133. console.log("📋 检测到未初始化的销售,直接创建...")
  134. console.log("📦 创建新销售...")
  135. console.log("📋 参数详情:")
  136. console.log(` - 销售代币: ${contractAddresses.saleToken}`)
  137. console.log(` - 总代币数: ${ethers.formatEther(totalTokensForSale)}`)
  138. console.log(` - 贡献目标: ${ethers.formatEther(contributionTarget)}`)
  139. console.log(` - 开始时间: ${startTime} (${new Date(startTime * 1000).toLocaleString()})`)
  140. console.log(` - 结束时间: ${endTime} (${new Date(endTime * 1000).toLocaleString()})`)
  141. console.log(` - 支付代币: ${contractAddresses.mockUSD1}`)
  142. console.log(` - 价格: ${ethers.formatEther(paymentTokenPrice)}`)
  143. console.log(` - 手续费: ${feeBps} bps`)
  144. try {
  145. const tx = await launchpad.connect(deployer).createSale(
  146. contractAddresses.saleToken, // _token
  147. totalTokensForSale, // _totalTokensForSale
  148. contributionTarget, // _contributionTarget
  149. startTime, // _startTime
  150. endTime, // _endTime
  151. contractAddresses.mockUSD1, // _paymentToken (USD1)
  152. paymentTokenPrice, // _price
  153. feeBps, // _feeBps
  154. { nonce: nonce++ }
  155. )
  156. console.log("📋 交易哈希:", tx.hash)
  157. const receipt = await tx.wait()
  158. console.log("📋 交易状态:", receipt?.status === 1 ? "✅ 成功" : "❌ 失败")
  159. console.log("✅ 销售创建成功")
  160. } catch (error) {
  161. console.error("❌ 创建销售失败:", error)
  162. throw error
  163. }
  164. } else {
  165. // 销售已存在,检查参数一致性
  166. console.log("📋 销售已存在,检查参数...")
  167. console.log("- 销售代币地址:", currentSaleToken)
  168. console.log("- 总代币数量:", ethers.formatEther(currentTotalTokens))
  169. console.log("- 当前贡献:", ethers.formatEther(currentContribution))
  170. // 检查销售参数是否与预期一致
  171. const expectedSaleToken = contractAddresses.saleToken
  172. const expectedTotalTokens = totalTokensForSale
  173. const expectedTarget = contributionTarget
  174. const expectedStartTime = BigInt(startTime)
  175. const expectedEndTime = BigInt(endTime)
  176. const expectedPaymentToken = contractAddresses.mockUSD1
  177. const expectedPrice = paymentTokenPrice
  178. const expectedFeeBps = BigInt(feeBps)
  179. // 获取当前销售参数
  180. const currentTarget = BigInt(saleDetails[3]) // contributionTarget
  181. const currentStartTime = BigInt(saleDetails[4]) // startTime
  182. const currentEndTime = BigInt(saleDetails[5]) // endTime
  183. const currentMerkleRoot = saleDetails[6] // merkleRoot
  184. const currentPaymentToken = saleDetails[7] // paymentTokenAddress
  185. const currentPrice = BigInt(saleDetails[8]) // paymentTokenPrice
  186. const currentFeeBps = BigInt(saleDetails[9]) // feeBps
  187. console.log("\n📊 参数对比:")
  188. console.log("- 销售代币地址:", currentSaleToken === expectedSaleToken ? "✅ 一致" : "❌ 不一致")
  189. console.log("- 总代币数量:", currentTotalTokens === expectedTotalTokens ? "✅ 一致" : "❌ 不一致")
  190. console.log("- 贡献目标:", currentTarget === expectedTarget ? "✅ 一致" : "❌ 不一致")
  191. // 时间容差检查(1小时 = 3600秒)
  192. const timeTolerance = BigInt(3600)
  193. const startTimeDiff =
  194. currentStartTime > expectedStartTime
  195. ? currentStartTime - expectedStartTime
  196. : expectedStartTime - currentStartTime
  197. const endTimeDiff =
  198. currentEndTime > expectedEndTime ? currentEndTime - expectedEndTime : expectedEndTime - currentEndTime
  199. const startTimeConsistent = startTimeDiff <= timeTolerance
  200. const endTimeConsistent = endTimeDiff <= timeTolerance
  201. console.log("- 开始时间:", startTimeConsistent ? "✅ 一致" : "❌ 不一致")
  202. if (!startTimeConsistent) {
  203. console.log(` 当前: ${new Date(Number(currentStartTime) * 1000).toLocaleString()}`)
  204. console.log(` 预期: ${new Date(Number(expectedStartTime) * 1000).toLocaleString()}`)
  205. console.log(` 差异: ${Number(startTimeDiff)} 秒`)
  206. }
  207. console.log("- 结束时间:", endTimeConsistent ? "✅ 一致" : "❌ 不一致")
  208. if (!endTimeConsistent) {
  209. console.log(` 当前: ${new Date(Number(currentEndTime) * 1000).toLocaleString()}`)
  210. console.log(` 预期: ${new Date(Number(expectedEndTime) * 1000).toLocaleString()}`)
  211. console.log(` 差异: ${Number(endTimeDiff)} 秒`)
  212. }
  213. console.log("- 支付代币:", currentPaymentToken === expectedPaymentToken ? "✅ 一致" : "❌ 不一致")
  214. console.log("- 价格:", currentPrice === expectedPrice ? "✅ 一致" : "❌ 不一致")
  215. console.log("- 手续费:", currentFeeBps === expectedFeeBps ? "✅ 一致" : "❌ 不一致")
  216. // 检查是否有任何参数不一致
  217. const isConsistent =
  218. currentSaleToken === expectedSaleToken &&
  219. currentTotalTokens === expectedTotalTokens &&
  220. currentTarget === expectedTarget &&
  221. startTimeConsistent &&
  222. endTimeConsistent &&
  223. currentPaymentToken === expectedPaymentToken &&
  224. currentPrice === expectedPrice &&
  225. currentFeeBps === expectedFeeBps
  226. if (isConsistent) {
  227. console.log("\n✅ 销售参数与预期一致,无需重置")
  228. } else {
  229. console.log("\n⚠️ 销售参数与预期不一致")
  230. // 检查是否有贡献,如果有贡献则不能重置
  231. if (currentContribution > 0) {
  232. console.error("❌ 错误:销售已有贡献,无法重置。请手动处理或等待销售结束")
  233. console.error("当前贡献:", ethers.formatEther(currentContribution), "USD1")
  234. process.exit(1)
  235. }
  236. // 检查是否是严重不一致(销售代币地址为0)
  237. const isSeverelyInconsistent =
  238. currentSaleToken === ethers.ZeroAddress ||
  239. currentTotalTokens === BigInt(0) ||
  240. currentPaymentToken === ethers.ZeroAddress
  241. if (isSeverelyInconsistent) {
  242. console.log("🔄 检测到严重不一致,尝试重新创建销售...")
  243. // 尝试重新创建销售(这可能会失败,但值得尝试)
  244. try {
  245. console.log("📦 重新创建销售...")
  246. await launchpad.connect(deployer).createSale(
  247. contractAddresses.saleToken, // _token
  248. totalTokensForSale, // _totalTokensForSale
  249. contributionTarget, // _contributionTarget
  250. startTime, // _startTime
  251. endTime, // _endTime
  252. contractAddresses.mockUSD1, // _paymentToken (USD1)
  253. paymentTokenPrice, // _price
  254. feeBps, // _feeBps
  255. {
  256. gasLimit: 10000000,
  257. gasPrice: ethers.parseUnits("0.1", "gwei"),
  258. nonce: nonce++
  259. }
  260. )
  261. console.log("✅ 销售重新创建成功")
  262. } catch (createError) {
  263. console.error("❌ 重新创建销售失败:", createError)
  264. console.log("💡 提示:请手动处理不一致的参数,或重新部署合约")
  265. console.log("💡 当前销售将继续使用现有参数")
  266. }
  267. } else {
  268. console.log("💡 提示:参数轻微不一致,合约不支持自动重置销售")
  269. console.log("💡 请手动处理不一致的参数,或等待销售结束后重新部署")
  270. console.log("💡 当前销售将继续使用现有参数")
  271. }
  272. }
  273. }
  274. } catch (error) {
  275. // 销售不存在,创建新销售
  276. console.log("📦 创建新销售...")
  277. await launchpad.connect(deployer).createSale(
  278. contractAddresses.saleToken, // _token
  279. totalTokensForSale, // _totalTokensForSale
  280. contributionTarget, // _contributionTarget
  281. startTime, // _startTime
  282. endTime, // _endTime
  283. contractAddresses.mockUSD1, // _paymentToken (USD1)
  284. paymentTokenPrice, // _price
  285. feeBps, // _feeBps
  286. { nonce: nonce++ }
  287. )
  288. console.log("✅ 销售创建成功")
  289. }
  290. console.log("\n📋 测试用户已加载:")
  291. console.log(`- 总用户数: ${testUsers.length}`)
  292. for (let i = 0; i < Math.min(5, testUsers.length); i++) {
  293. console.log(`- 用户${i + 1}: ${testUsers[i].address}`)
  294. }
  295. if (testUsers.length > 5) {
  296. console.log(`- ... 还有 ${testUsers.length - 5} 个用户`)
  297. }
  298. // 检查第2步分发的USD1余额(前5个用户)
  299. console.log("\n💰 检查第2步分发的USD1余额...")
  300. let totalUsd1Balance = ethers.parseEther("0")
  301. for (let i = 0; i < Math.min(5, testUsers.length); i++) {
  302. const balance = await mockUSD1.balanceOf(testUsers[i].address)
  303. totalUsd1Balance += balance
  304. console.log(`✅ 用户${i + 1} USD1余额: ${ethers.formatEther(balance)}`)
  305. }
  306. if (testUsers.length > 5) {
  307. console.log(`- ... 还有 ${testUsers.length - 5} 个用户有类似余额`)
  308. }
  309. // 验证用户是否有足够的USD1进行销售
  310. const expectedBalancePerUser = ethers.parseEther("10000")
  311. const avgBalance = totalUsd1Balance / BigInt(Math.min(5, testUsers.length))
  312. if (avgBalance < expectedBalancePerUser) {
  313. console.warn(
  314. `⚠️ 警告:平均USD1余额 (${ethers.formatEther(avgBalance)}) 少于预期 (${ethers.formatEther(expectedBalancePerUser)})`
  315. )
  316. console.warn(" 请确保第2步(分发代币)已成功运行")
  317. }
  318. // 为白名单设置Merkle根(幂等操作)
  319. console.log("\n🌳 检查并设置白名单...")
  320. let whitelistData: [string, bigint][]
  321. // 每个用户可以贡献最多10,000 USD1(使用他们的全部USD1余额)
  322. const maxContributionPerUser = ethers.parseEther("10000")
  323. // 将所有测试用户添加到白名单,设置大的贡献限额
  324. whitelistData = testUsers.map((user: ethers.Wallet) => [user.address, maxContributionPerUser])
  325. const totalMaxContribution = maxContributionPerUser * BigInt(testUsers.length)
  326. console.log(
  327. `📋 白名单:所有 ${testUsers.length} 个用户每人最多可贡献 ${ethers.formatEther(maxContributionPerUser)} USDT`
  328. )
  329. console.log(`📋 最大总贡献:${ethers.formatEther(totalMaxContribution)} USDT`)
  330. console.log(`📋 目标:${ethers.formatEther(contributionTarget)} USDT`)
  331. console.log(`📋 超额认购阈值:5个用户 (5 × 10K = 50K = 50K 目标)`)
  332. const tree = StandardMerkleTree.of(whitelistData, ["address", "uint256"])
  333. // 检查当前Merkle根
  334. console.log("🔍 检查当前Merkle根...")
  335. try {
  336. const currentMerkleRoot = await launchpad.merkleRoot()
  337. console.log("📊 当前Merkle根:", currentMerkleRoot)
  338. console.log("📊 新计算的Merkle根:", tree.root)
  339. if (currentMerkleRoot === tree.root) {
  340. console.log("✅ Merkle根未变化,无需更新")
  341. } else if (currentMerkleRoot === ethers.ZeroHash) {
  342. console.log("📝 当前Merkle根为空,设置新的Merkle根...")
  343. await launchpad.connect(deployer).setMerkleRoot(tree.root, { nonce: nonce++ })
  344. console.log("✅ Merkle根已设置:", tree.root)
  345. } else {
  346. console.log("📝 Merkle根已变化,更新Merkle根...")
  347. console.log(" - 旧Merkle根:", currentMerkleRoot)
  348. console.log(" - 新Merkle根:", tree.root)
  349. await launchpad.connect(deployer).setMerkleRoot(tree.root, { nonce: nonce++ })
  350. console.log("✅ Merkle根已更新:", tree.root)
  351. }
  352. } catch (error) {
  353. // 如果获取Merkle根失败,说明还没有设置
  354. console.log("📝 无法获取当前Merkle根,设置新的Merkle根...")
  355. await launchpad.connect(deployer).setMerkleRoot(tree.root, { nonce: nonce++ })
  356. console.log("✅ Merkle根已设置:", tree.root)
  357. }
  358. // 为后续步骤保存额外数据
  359. const step2Data = {
  360. ...contractAddresses,
  361. testUsers: testUsers.map((user: ethers.Wallet) => user.address),
  362. merkleRoot: tree.root,
  363. whitelistData: whitelistData,
  364. saleParams: {
  365. startTime,
  366. endTime,
  367. contributionTarget: contributionTarget.toString(),
  368. paymentTokenPrice: paymentTokenPrice.toString(),
  369. feeBps,
  370. },
  371. // 第4步的控制参数
  372. oversubscriptionThreshold: 5, // 5个以上用户 = 超额认购
  373. maxContributionPerUser: maxContributionPerUser.toString(),
  374. totalMaxContribution: totalMaxContribution.toString(),
  375. }
  376. const step2DataPath = path.join(outputDir, "step2-data.json")
  377. fs.writeFileSync(
  378. step2DataPath,
  379. JSON.stringify(step2Data, (key, value) => (typeof value === "bigint" ? value.toString() : value), 2)
  380. )
  381. console.log("\n🎉 销售初始化成功完成!")
  382. console.log("\n📋 总结:")
  383. console.log("- 创建了50K USDT目标的销售")
  384. console.log("- 所有20个测试用户已添加到白名单")
  385. console.log("- 每个用户最多可贡献10K USDT")
  386. console.log("- 超额认购阈值:5个以上用户")
  387. console.log("- 销售持续时间:24小时")
  388. console.log(`- 数据已保存到 ${step2DataPath}`)
  389. console.log("\n📋 下一步:运行 'npm run 04:contribute' 测试贡献")
  390. console.log(" - 使用少于5个用户进行认购不足场景")
  391. console.log(" - 使用5个以上用户进行超额认购场景")
  392. console.log("\n📋 控制场景:")
  393. console.log(" - 认购不足:npm run 04:contribute (4个用户)")
  394. console.log(" - 超额认购:SCENARIO=1 npm run 04:contribute (6个用户)")
  395. }
  396. // 导出main函数供多环境脚本调用
  397. export { main }
  398. // 如果直接运行此脚本
  399. if (require.main === module) {
  400. main()
  401. .then(() => process.exit(0))
  402. .catch((error) => {
  403. console.error("❌ 错误:", error)
  404. process.exit(1)
  405. })
  406. }