@gangfun/cli 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,8 +87,8 @@ gang bond burn --token <addr> | gang bond tx --hash <0x..> # push one buy-
87
87
  # A creator's gang sell (curve or pool) / gang withdraw below X is refused unless --break-bond (past T: --settle-first)
88
88
  # Launch in one command: one tx per run — execute it, run the very same command again, until launch.stage = done
89
89
  gang create … --dev-buy <quote> [--bond <quote> [--bond-hold <tokens|N%|all>] --bond-days <7-90>] # V3: create → dev buy → bond
90
- gang create … --buy-tax 3 --sell-tax 3 --buyback 50 [--dev-buy …] # V3: the recipient's part of the tax 50% bought back + burned, the rest to you
91
- gang buyback status|set|apply --token 0x… [--pct 0-100] # the buy-back splitter: ratio (raise now, cut after 3 days), what waits
90
+ gang create … --buy-tax 3 --sell-tax 3 --buyback 50 [--buyback-to 0x…] [--dev-buy …] # V3: the recipient's part of the tax 50% bought back + burned, the rest to you (or --buyback-to)
91
+ gang buyback status|set|set-payee|apply --token 0x… [--pct 0-100] [--to 0x…] # the buy-back splitter: ratio (raise now, cut after 3 days), payee (at once), what waits
92
92
  gang create --fair … [--shares <n>] --bond <quote> [--bond-hold …] --bond-days <7-90> # 打新: create (dev shares) → bond
93
93
  gang execute --request-id <id>
94
94
  gang tx --hash <0x..> [--testnet] # V2 / Soft and GangLaunchpad events (GangLaunchpad: units "raw", amounts in wei)
@@ -227,4 +227,4 @@ gang agents --window 30d --sort follow
227
227
 
228
228
  ## Version
229
229
 
230
- `0.13.0` · repo [gangfun/cli](https://github.com/gangfun/cli) · published to [npm](https://www.npmjs.com/package/@gangfun/cli) on push to `main`
230
+ `0.15.0` · repo [gangfun/cli](https://github.com/gangfun/cli) · published to [npm](https://www.npmjs.com/package/@gangfun/cli) on push to `main`
package/README.zh-CN.md CHANGED
@@ -77,8 +77,8 @@ gang bond burn --token <addr> | gang bond tx --hash <0x..> # 推进一块
77
77
  # 创建者用 gang sell(内盘或外盘)/ gang withdraw 卖到低于 X 会被拒,除非加 --break-bond(过了 T:--settle-first 先取回)
78
78
  # 一条命令发射:每次运行产出一笔交易,执行后再运行同一条命令,直到 launch.stage = done
79
79
  gang create … --dev-buy <报价币数量> [--bond <数量> [--bond-hold <枚数|N%|all>] --bond-days <7-90>] # V3:发币 → dev 内盘买入 → 押金
80
- gang create … --buy-tax 3 --sell-tax 3 --buyback 50 [--dev-buy …] # V3:接收地址那份税 50% 回购销毁,其余给你
81
- gang buyback status|set|apply --token 0x… [--pct 0-100] # 回购合约:比例(提高立即生效,下调 3 天后)、待回购金额
80
+ gang create … --buy-tax 3 --sell-tax 3 --buyback 50 [--buyback-to 0x…] [--dev-buy …] # V3:接收地址那份税 50% 回购销毁,其余给你(或 --buyback-to 指定的地址)
81
+ gang buyback status|set|set-payee|apply --token 0x… [--pct 0-100] [--to 0x…] # 回购合约:比例(提高立即生效,下调 3 天后)、收款地址(立即生效)、待回购金额
82
82
  gang create --fair … [--shares <n>] --bond <数量> [--bond-hold …] --bond-days <7-90> # 打新:发币(含 dev 份额)→ 押金
83
83
  gang execute --request-id <id>
84
84
  gang tx --hash <0x..> [--testnet] # 解析 V2 / Soft 和 GangLaunchpad 的事件(GangLaunchpad 的带 units "raw",金额是 wei)
@@ -200,4 +200,4 @@ gang agents --window 30d --sort follow
200
200
 
201
201
  ## 版本
202
202
 
203
- `0.13.0` · [gangfun/cli](https://github.com/gangfun/cli) · push 到 `main` 自动发布到 [npm](https://www.npmjs.com/package/@gangfun/cli)
203
+ `0.15.0` · [gangfun/cli](https://github.com/gangfun/cli) · push 到 `main` 自动发布到 [npm](https://www.npmjs.com/package/@gangfun/cli)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gangfun/cli",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Gang launchpad CLI for Binance Agentic Wallet agents: create on GangLaunchpad (untaxed or tax tokens), 打新 fair mint, PancakeSwap pool trades, LaunchpadV2 and legacy Soft/U/S trade.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/bond.js CHANGED
@@ -15,11 +15,12 @@ import { bawAddress } from './baw.js'
15
15
  * Env: GANG_BOND (mainnet), GANG_TESTNET_BOND (testnet) override the addresses below.
16
16
  */
17
17
 
18
- // script/DeployGangBond.s.sol. Testnet: 2026-10-28, block 139374309, over the set-10 pad — bonds from create on, refunds
19
- // asked for by the creator with a 3-day notice (earlier testnet versions: 0x587f…5d7E after the pair only, 0x5a99…d716
20
- // without the notice; docs/TESTNET-GANG-BOND-2026-10-28.md). Mainnet: not deployed yet (GANG_BOND overrides either)
18
+ // script/DeployGangBond.s.sol. Testnet: 2026-10-29, block 139463214, over the set-10 pad — the audited version (two-step
19
+ // settle, one buy-back chunk per 4 minutes; earlier testnet versions: 0x587f…5d7E after the pair only, 0x5a99…d716
20
+ // without the refund notice, 0xB034…EBE3 before the audit; docs/TESTNET-GANG-BOND-2026-10-28.md). Mainnet: not deployed
21
+ // yet (GANG_BOND overrides either)
21
22
  export const MAINNET_BOND = null
22
- export const TESTNET_BOND = '0xB034cFeb81Cbd29EE3d0CD6a0885766B6F10EBE3'
23
+ export const TESTNET_BOND = '0xd6a8dd5d1E6641a34A255f25Db2B3ED397CB1086'
23
24
 
24
25
  export const BOND_ABI = parseAbi([
25
26
  'function post(address token, uint256 amount, uint256 holdAmount, uint64 duration) payable',
@@ -28,11 +29,12 @@ export const BOND_ABI = parseAbi([
28
29
  'function extend(address token, uint64 by)',
29
30
  'function start(address token)',
30
31
  'function report(address token)',
32
+ 'function requestSettle(address token)',
31
33
  'function settle(address token)',
32
34
  'function requestRefund(address token)',
33
35
  'function refund(address token)',
34
36
  'function burnChunk(address token, uint256 minTokensOut) returns (uint256 quoteIn, uint256 burned)',
35
- 'function bondOf(address token) view returns ((address creator, uint64 postedAt, uint32 round, address quote, uint8 state, uint64 deadline, uint64 duration, uint64 refundAt, uint128 amount, uint128 holdAmount, uint128 burnLeft))',
37
+ 'function bondOf(address token) view returns ((address creator, uint64 postedAt, uint32 round, address quote, uint8 state, uint64 deadline, uint64 duration, uint64 requestAt, uint128 amount, uint128 holdAmount, uint128 burnLeft))',
36
38
  'function isViolated(address token) view returns (bool)',
37
39
  'function isLive(address token) view returns (bool)',
38
40
  'function dumpValue(address token) view returns (uint256)',
@@ -42,12 +44,14 @@ export const BOND_ABI = parseAbi([
42
44
  'function previewBurn(address token) view returns (bool ready, uint256 quoteIn, uint256 tokensOut)',
43
45
  'function MIN_DURATION() view returns (uint64)', 'function MAX_DURATION() view returns (uint64)',
44
46
  'function REPORTER_BPS() view returns (uint256)', 'function MAX_WAIT() view returns (uint64)', 'function REFUND_NOTICE() view returns (uint64)',
47
+ 'function SETTLE_DELAY() view returns (uint64)', 'function CHUNK_GAP() view returns (uint64)',
45
48
  'event BondPosted(address indexed token, address indexed creator, uint32 round, address quote, uint256 amount, uint256 holdAmount, uint64 duration, uint64 deadline)',
46
49
  'event BondStarted(address indexed token, uint32 round, uint64 deadline)',
47
50
  'event BondToppedUp(address indexed token, uint32 round, uint256 added, uint256 amount)',
48
51
  'event HoldRaised(address indexed token, uint32 round, uint256 holdAmount)',
49
52
  'event BondExtended(address indexed token, uint32 round, uint64 duration, uint64 deadline)',
50
53
  'event BondReported(address indexed token, uint32 round, address indexed reporter, uint256 balance, uint256 reward, uint256 burnLeft)',
54
+ 'event SettleRequested(address indexed token, uint32 round, uint64 settleAt)',
51
55
  'event BondSettled(address indexed token, uint32 round, address indexed creator, uint256 amount)',
52
56
  'event RefundRequested(address indexed token, uint32 round, uint64 refundAt)',
53
57
  'event BondRefunded(address indexed token, uint32 round, address indexed creator, uint256 amount)',
@@ -55,7 +59,8 @@ export const BOND_ABI = parseAbi([
55
59
  'error ZeroAddress()', 'error UnknownToken()', 'error NotTradable()', 'error NotCreator()', 'error BondLive()',
56
60
  'error NotActive()', 'error ZeroAmount()', 'error BadDuration()', 'error BadHold()', 'error BadValue()',
57
61
  'error FeeOnTransferQuote()', 'error NotViolated()', 'error Violated()', 'error NotDue()', 'error NothingToBurn()',
58
- 'error BurnedThisBlock()', 'error Slippage()', 'error FlashSwapActive()', 'error AlreadyLive()', 'error AlreadyRequested()', 'error NoRefundRequest()',
62
+ 'error BurnTooSoon()', 'error Slippage()', 'error FlashSwapActive()', 'error AlreadyLive()', 'error AlreadyRequested()', 'error NoRefundRequest()',
63
+ 'error NoSettleRequest()',
59
64
  'error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value)', 'error SafeERC20FailedOperation(address token)',
60
65
  ])
61
66
 
@@ -78,19 +83,21 @@ const HINTS = {
78
83
  FeeOnTransferQuote: 'the quote takes a fee on transfer: it cannot be used for a bond',
79
84
  NotViolated: 'the creator still holds at least X: nothing to report',
80
85
  Violated: 'the creator holds less than X: the bond cannot be settled or refunded, only reported',
86
+ NoSettleRequest: 'ask to settle first (gang bond settle asks; it runs 1 hour later)',
81
87
  AlreadyLive: 'the token reached its pair: the bond runs (gang bond start) instead of being refunded',
82
- AlreadyRequested: 'a refund is already requested: it can run once its 3-day notice is over',
88
+ AlreadyRequested: 'already requested: a refund runs once its 3-day notice is over, a settle 1 hour after its request',
83
89
  NoRefundRequest: 'request the refund first (gang bond refund asks for it; it runs 3 days later)',
84
- NotDue: 'the deadline has not passed yet',
90
+ NotDue: 'not due yet: the deadline, the refund notice or the 1-hour settle wait has not passed',
85
91
  NothingToBurn: 'no slashed bond with buy-back left on this token',
86
- BurnedThisBlock: 'a chunk was already bought for this token in this block: try the next block',
92
+ BurnTooSoon: 'a chunk was bought for this token less than 4 minutes ago: one chunk per 4 minutes',
87
93
  Slippage: 'the buy-back would burn fewer tokens than the minimum: the price moved, re-run',
88
- FlashSwapActive: 'the token\'s pair is mid flash swap: settle cannot count tokens borrowed from it; send settle in its own tx',
94
+ FlashSwapActive: 'the token\'s pair is mid flash swap: settle cannot count tokens borrowed from it; send it in its own tx',
89
95
  }
90
96
 
91
97
  const STATES = ['none', 'active', 'slashed', 'settled', 'refunded']
92
98
  const MAX_WAIT = 30 * 86_400 // GangBond.MAX_WAIT
93
99
  const REFUND_NOTICE = 3 * 86_400 // GangBond.REFUND_NOTICE
100
+ const SETTLE_DELAY = 3_600 // GangBond.SETTLE_DELAY
94
101
  const BPS = 10_000n
95
102
  const TOTAL_SUPPLY = 1_000_000_000n * 10n ** 18n
96
103
  const DAY = 86_400
@@ -194,12 +201,12 @@ async function bondView(ctx, token, m) {
194
201
  ...(m.live
195
202
  ? { note: `the token is on its pair: anyone can start the clock (gang bond start --token ${token}); any other bond write starts it too` }
196
203
  : { refundRequestFrom: new Date(refundAt * 1000).toISOString(), coverageNote: 'no coverage before the pair: shown once it trades on PancakeSwap' }),
197
- ...(b.refundAt !== 0n && !m.live && {
198
- refundRequested: true, refundRunsAt: new Date(Number(b.refundAt) * 1000).toISOString(),
204
+ ...(b.requestAt !== 0n && !m.live && {
205
+ refundRequested: true, refundRunsAt: new Date(Number(b.requestAt) * 1000).toISOString(),
199
206
  refundNote: 'the creator asked for the bond back: it can be withdrawn from refundRunsAt if the token still has no pair; the promise binds until then, and graduation / launch cancels the request',
200
207
  }),
201
208
  ...violatedNote,
202
- ...(!violated && !m.live && raw.now >= refundAt && b.refundAt === 0n && { note: `no pair within 30 days of the post and X is held: the creator can ask for the bond back (gang bond refund --token ${token}); it runs 3 days later` }),
209
+ ...(!violated && !m.live && raw.now >= refundAt && b.requestAt === 0n && { note: `no pair within 30 days of the post and X is held: the creator can ask for the bond back (gang bond refund --token ${token}); it runs 3 days later` }),
203
210
  })
204
211
  return { ...v, _raw: raw }
205
212
  }
@@ -208,12 +215,20 @@ async function bondView(ctx, token, m) {
208
215
  ctx.read(ctx.bond, BOND_ABI, 'coverageBps', [token]),
209
216
  ])
210
217
  const left = Number(b.deadline) - now
218
+ const settleAt = Number(b.requestAt)
211
219
  Object.assign(v, {
212
220
  phase: 'running', violated, dumpValue: q(dump), coveragePct: cov === MAX_UINT ? null : pctOfBps(cov), tier: tierOf(cov), tierNote: TIER_NOTE[tierOf(cov)],
213
221
  toFullCover: dump > b.amount ? q(dump - b.amount) : '0',
214
222
  ...(left > 0 ? { secondsLeft: left, daysLeft: Math.round((left / DAY) * 10) / 10 } : { pastDeadline: true }),
215
223
  ...violatedNote,
216
- ...(!violated && left <= 0 && { note: `past the deadline and X is held: anyone can settle it (gang bond settle --token ${token}); the bond goes back to the creator. Until then it can still be reported if the balance drops below X` }),
224
+ ...(settleAt !== 0 && { settleRequested: true, settleRunsAt: new Date(settleAt * 1000).toISOString() }),
225
+ ...(!violated && left <= 0 && {
226
+ note: settleAt === 0
227
+ ? `past the deadline and X is held: anyone can settle it in two steps (gang bond settle --token ${token} asks; run it again 1 hour later and the bond goes back to the creator). X must still be held then; until then it can be reported if the balance drops below X`
228
+ : now < settleAt
229
+ ? `settle asked for: it runs from settleRunsAt if X is still held then; until then it can be reported if the balance drops below X`
230
+ : `settle is due: gang bond settle --token ${token} returns the bond to the creator (X must still be held)`,
231
+ }),
217
232
  })
218
233
  Object.assign(raw, { dump, cov })
219
234
  }
@@ -227,7 +242,7 @@ async function bondView(ctx, token, m) {
227
242
  ? 'slashed before the pair and still no pair 30 days after the post: gang bond burn --token … sends the rest to 0xdead as quote'
228
243
  : `slashed before the pair: the buy-back starts once the token ${m.kind === 'v3' ? 'graduates' : 'launches'}; if it has not by ${new Date((Number(b.postedAt) + MAX_WAIT) * 1000).toISOString()}, the rest goes to 0xdead as quote`,
229
244
  }),
230
- ...(b.burnLeft > 0n && m.live && { nextChunk: ready ? { quoteIn: q(quoteIn), tokensOut: tokens(tokensOut) } : null, nextChunkNote: ready ? 'gang bond burn --token … buys this chunk now' : 'not ready: the first call after a slash records a price observation, then the buy-back waits 10 minutes and skips while the price is more than 2% above its average (after 20 minutes without a call it starts over)' }),
245
+ ...(b.burnLeft > 0n && m.live && { nextChunk: ready ? { quoteIn: q(quoteIn), tokensOut: tokens(tokensOut) } : null, nextChunkNote: ready ? 'gang bond burn --token … buys this chunk now' : 'not ready: the first call after a slash records a price observation, then the buy-back waits 10 minutes and skips while the price is more than 2% above its average (after 20 minutes without a call it starts over); at most one chunk per 4 minutes' }),
231
246
  })
232
247
  }
233
248
  return { ...v, _raw: raw }
@@ -350,7 +365,7 @@ export async function runBond(sub, { opt, flag, out, fail, summarize }) {
350
365
  if (!same(me(), b.creator)) return fail('only the creator can ask for its bond back', { wallet: me(), creator: b.creator })
351
366
  if (!raw.pending || m.live) return fail(HINTS.AlreadyLive, { deadline: view.deadline })
352
367
  if (view.violated) return fail(HINTS.Violated, { creatorBalance: view.creatorBalance, holdTokens: view.holdTokens })
353
- if (b.refundAt === 0n) {
368
+ if (b.requestAt === 0n) {
354
369
  const requestAt = Number(b.postedAt) + MAX_WAIT
355
370
  if (raw.now < requestAt) return fail('a refund can be asked for only 30 days after the post without a pair', { refundRequestFrom: view.refundRequestFrom, secondsLeft: requestAt - raw.now })
356
371
  const sim = await simulate(ctx, me(), 'requestRefund', [token])
@@ -360,7 +375,7 @@ export async function runBond(sub, { opt, flag, out, fail, summarize }) {
360
375
  note: 'step 1 of 2: starts a 3-day notice (shown on gang.fun). Keep holding X: a breach can still be reported, and if the token graduates / launches meanwhile the request is void and the clock starts. Then run gang bond refund again',
361
376
  }))
362
377
  }
363
- if (raw.now < Number(b.refundAt)) return fail('the refund notice is not over yet', { refundRunsAt: view.refundRunsAt, secondsLeft: Number(b.refundAt) - raw.now })
378
+ if (raw.now < Number(b.requestAt)) return fail('the refund notice is not over yet', { refundRunsAt: view.refundRunsAt, secondsLeft: Number(b.requestAt) - raw.now })
364
379
  const sim = await simulate(ctx, me(), 'refund', [token])
365
380
  if (sim.failed) return out({ success: false, step: 'refund', ...sim.failed })
366
381
  return out(send('refund', [token], { step: 'refund', refund: view.bond, quote: view.quote, note: 'step 2 of 2: no pair after the notice, X held: the bond comes back to you (not counted as honored)' }))
@@ -375,9 +390,19 @@ export async function runBond(sub, { opt, flag, out, fail, summarize }) {
375
390
  }
376
391
  if (raw.now < Number(b.deadline)) return fail(HINTS.NotDue, { deadline: view.deadline, secondsLeft: Number(b.deadline) - raw.now })
377
392
  if (view.violated) return fail(HINTS.Violated, { creatorBalance: view.creatorBalance, holdTokens: view.holdTokens, hint: `gang bond report --token ${token}${net}` })
393
+ // two steps, anyone: ask (X held now), then settle 1 hour later (X held again) — so X can't be met for one tx only
394
+ if (b.requestAt === 0n) {
395
+ const sim = await simulate(ctx, me(), 'requestSettle', [token])
396
+ if (sim.failed) return out({ success: false, step: 'request-settle', ...sim.failed })
397
+ return out(send('requestSettle', [token], {
398
+ step: 'request-settle', creator: b.creator, bond: view.bond, quote: view.quote, settleRunsAbout: new Date((raw.now + SETTLE_DELAY) * 1000).toISOString(),
399
+ note: 'step 1 of 2: the bond can be settled 1 hour after this lands, if the creator still holds X then; until then a drop below X can be reported. Then run gang bond settle again',
400
+ }))
401
+ }
402
+ if (raw.now < Number(b.requestAt)) return fail('the settle wait is not over yet', { settleRunsAt: view.settleRunsAt, secondsLeft: Number(b.requestAt) - raw.now })
378
403
  const sim = await simulate(ctx, me(), 'settle', [token])
379
404
  if (sim.failed) return out({ success: false, step: 'settle', ...sim.failed })
380
- return out(send('settle', [token], { step: 'settle', creator: b.creator, refund: view.bond, quote: view.quote, note: 'the bond goes back to the creator, whoever sends this' }))
405
+ return out(send('settle', [token], { step: 'settle', creator: b.creator, refund: view.bond, quote: view.quote, note: 'step 2 of 2: the bond goes back to the creator, whoever sends this' }))
381
406
  }
382
407
 
383
408
  if (sub === 'burn') {
@@ -394,7 +419,7 @@ export async function runBond(sub, { opt, flag, out, fail, summarize }) {
394
419
  step: 'burn', burnLeft: view.burnLeft, quote: view.quote,
395
420
  ...(toDead ? { quoteToDead: qf(quoteIn), note: 'slashed before the pair and still no pair 30 days after the post: the rest goes to 0xdead as quote' }
396
421
  : ready ? { chunkQuoteIn: qf(quoteIn), expectBurnTokens: tokens(tokensOut), minBurnTokens: tokens(minOut), slippagePct: slippage }
397
- : { note: 'no chunk this time: the call records the price observation (first call after a slash) or waits for the 10-minute average / a price no more than 2% above it. It is cheap; the platform keeper also calls it every 10 minutes. If a chunk becomes buyable before it lands, it reverts (Slippage) rather than buy without a minimum: re-run then' }),
422
+ : { note: 'no chunk this time: the call records the price observation (first call after a slash) or waits for the 10-minute average / a price no more than 2% above it. It is cheap; the platform keeper also calls it every 5 minutes (one chunk per 4 minutes at most). If a chunk becomes buyable before it lands, it reverts (Slippage) rather than buy without a minimum: re-run then' }),
398
423
  }))
399
424
  }
400
425
 
@@ -515,7 +540,8 @@ export async function runBond(sub, { opt, flag, out, fail, summarize }) {
515
540
  * bond, not the creator), else one of:
516
541
  * - { refuse: {…} } — the sell would leave less than X before the deadline (no --break-bond), or after the deadline
517
542
  * before settle (no --settle-first): print it as the failure;
518
- * - { settleStep: {…} } — --settle-first after the deadline: send this settle first, then re-run the sell;
543
+ * - { settleStep: {…} } — --settle-first after the deadline: send this step first (ask to settle, or settle once the
544
+ * 1-hour wait is over), then re-run the sell;
519
545
  * - { info: {…} } — go on, with these fields added to the preview (headroom after the sell, or the --break-bond warning).
520
546
  */
521
547
  export async function bondGuard(ctx, p, rawOnly, summarize) {
@@ -549,10 +575,18 @@ export async function bondGuard(ctx, p, rawOnly, summarize) {
549
575
  const net = p.testnet ? ' --testnet' : ''
550
576
  if (pastDeadline && bal >= b.holdAmount) {
551
577
  if (!p.settleFirst) {
552
- return { refuse: { error: `the bond is past its deadline but not settled: this sell leaves ${tokens(after)} tokens, below X (${tokens(b.holdAmount)}), and anyone could report it before you settle`, ...base, hint: `run gang bond settle --token ${p.token}${net} first, or add --settle-first to this sell` } }
578
+ return { refuse: { error: `the bond is past its deadline but not settled: this sell leaves ${tokens(after)} tokens, below X (${tokens(b.holdAmount)}), and anyone could report it before you settle`, ...base, hint: `run gang bond settle --token ${p.token}${net} first (two steps, 1 hour apart), or add --settle-first to this sell` } }
579
+ }
580
+ const now = Number(block.timestamp)
581
+ if (b.requestAt !== 0n && now < Number(b.requestAt)) {
582
+ return { refuse: { error: 'settle is asked for but its 1-hour wait is not over: keep X until it runs, then settle and sell', ...base, settleRunsAt: new Date(Number(b.requestAt) * 1000).toISOString(), secondsLeft: Number(b.requestAt) - now } }
553
583
  }
554
- const data = encodeFunctionData({ abi: BOND_ABI, functionName: 'settle', args: [p.token] })
555
- return { settleStep: step(ctx, { from: p.me, to: addr, data }, { step: 'settle', chainId: Number(ctx.chainId), token: p.token, refund: base.bond, ...base, note: 'settle first (the bond comes back to you); after executing, re-run the same sell' }, rawOnly, summarize) }
584
+ const fn = b.requestAt === 0n ? 'requestSettle' : 'settle'
585
+ const data = encodeFunctionData({ abi: BOND_ABI, functionName: fn, args: [p.token] })
586
+ const note = fn === 'requestSettle'
587
+ ? 'ask to settle first (step 1 of 2): 1 hour after it lands, settle (the bond comes back to you) and then sell; keep X until then'
588
+ : 'settle first (the bond comes back to you); after executing, re-run the same sell'
589
+ return { settleStep: step(ctx, { from: p.me, to: addr, data }, { step: fn === 'requestSettle' ? 'request-settle' : 'settle', chainId: Number(ctx.chainId), token: p.token, refund: base.bond, ...base, note }, rawOnly, summarize) }
556
590
  }
557
591
  const lost = formatUnits(b.amount, dec)
558
592
  const consequence = `anyone can report it, the reporter takes 1% and the rest of the ${lost} bond buys back and burns the token`
package/src/buyback.js CHANGED
@@ -4,9 +4,10 @@ import { walletFor } from './fair.js'
4
4
  import { parseNum, revertOf as revertIn, step } from './steps.js'
5
5
 
6
6
  /**
7
- * `gang buyback …` and `gang create … --buyback <pct>` — GangBuybackSplitter (dev 税按比例回购销毁): a V3 token's tax
8
- * recipient that splits each payment between a buy-back in the token's PancakeSwap pool, sent to 0xdead, and the dev.
9
- * GangBuybackSplitterFactory clones one per token for the dev (`create(bps, salt)`, at `predict(dev, salt)`) and has
7
+ * `gang buyback …` and `gang create … --buyback <pct> [--buyback-to <addr>]` — GangBuybackSplitter (dev 税按比例回购
8
+ * 销毁): a V3 token's tax recipient that splits each payment between a buy-back in the token's PancakeSwap pool, sent to
9
+ * 0xdead, and the dev's payee (the dev unless it names another address; v2 splitters only, `gang buyback set-payee`).
10
+ * GangBuybackSplitterFactory clones one per token for the dev (`create(bps, salt, payee)`, at `predict(dev, salt)`) and has
10
11
  * GangHandlerAdmin list it, so the dev names it as the recipient at create (launch.js does the three steps: clone →
11
12
  * create → bind). A dev who doesn't
12
13
  * want it leaves --buyback out: the tax goes to their own address as before.
@@ -15,13 +16,15 @@ import { parseNum, revertOf as revertIn, step } from './steps.js'
15
16
  * Env: GANG_SPLITTER_FACTORY / GANG_TESTNET_SPLITTER_FACTORY override the built-in factory addresses.
16
17
  */
17
18
 
18
- // BSC 56, 2026-10-28 (docs/MAINNET-BUYBACK-SPLITTER-2026-10-28.md); lists clones once the Safe ran its batch
19
- export const MAINNET_SPLITTER_FACTORY = '0x6bcd79206da85Aa0643Db2164Ab887b468618546'
20
- export const TESTNET_SPLITTER_FACTORY = '0x0Ff5C8801BdeaDac374fC14fEd6e7B31977c9DA1'
21
- export const BUYBACK_TYPE = 'buyback-burn-v1'
19
+ // BSC 56, factory v2 (payee; docs/MAINNET-BUYBACK-SPLITTER-2026-10-28.md). v1 was 0x6bcd…8546 (2-argument create)
20
+ export const MAINNET_SPLITTER_FACTORY = '0x93467Cb38B9Cb7f6EF3b67d2226Eb8b77ca58F2f'
21
+ export const TESTNET_SPLITTER_FACTORY = '0x08b9E2Cf5F666b55F262239b19e25de25b8ba87D'
22
+ /** The factory's clones; v1 (mainnet's first factory, 0x6bcd…8546) pays the dev, v2 its payee. */
23
+ export const BUYBACK_TYPE = 'buyback-burn-v2'
24
+ const BUYBACK_TYPES = ['buyback-burn-v1', BUYBACK_TYPE]
22
25
 
23
26
  export const FACTORY_ABI = parseAbi([
24
- 'function create(uint16 buybackBps, bytes32 salt) returns (address splitter)',
27
+ 'function create(uint16 buybackBps, bytes32 salt, address payee) returns (address splitter)',
25
28
  'function predict(address dev, bytes32 salt) view returns (address)',
26
29
  'function pad() view returns (address)',
27
30
  'error NotAgent()',
@@ -44,6 +47,8 @@ export const SPLITTER_ABI = parseAbi([
44
47
  'function setBuybackBps(uint16 newBps)',
45
48
  'function applyBuybackBps()',
46
49
  'function releaseDev() returns (uint256)',
50
+ 'function payee() view returns (address)',
51
+ 'function setPayee(address newPayee)',
47
52
  'error NotDev()',
48
53
  'error BadBps()',
49
54
  'error AlreadyBound()',
@@ -93,17 +98,22 @@ export async function splitterOf(read, addr) {
93
98
  if (!addr || addr === zeroAddress) return null
94
99
  let type
95
100
  try { type = await read(addr, SPLITTER_ABI, 'moduleType') } catch { return null }
96
- if (hexToString(type, { size: 32 }) !== BUYBACK_TYPE) return null
101
+ const moduleType = hexToString(type, { size: 32 }).replace(/\0+$/, '')
102
+ if (!BUYBACK_TYPES.includes(moduleType)) return null
97
103
  const [dev, token, bps, cut, left, unsplit, owed, burn] = await Promise.all(
98
104
  ['dev', 'token', 'buybackBps', 'pendingCut', 'buybackLeft', 'unsplit', 'devOwed', 'previewBurn'].map((f) => read(addr, SPLITTER_ABI, f)))
105
+ // v1 has no payee: it pays the dev
106
+ const hasPayee = moduleType !== 'buyback-burn-v1'
107
+ const payee = hasPayee ? await read(addr, SPLITTER_ABI, 'payee') : dev
108
+ const toDev = payee.toLowerCase() === dev.toLowerCase()
99
109
  // the pad takes 18-decimal quotes only
100
110
  const q = (x) => formatUnits(x, 18)
101
111
  return {
102
- splitter: addr, moduleType: BUYBACK_TYPE, dev, token: token === zeroAddress ? null : token, buybackPct: pct(bps), buybackBps: Number(bps),
112
+ splitter: addr, moduleType, dev, payee, payeeChangeable: hasPayee, token: token === zeroAddress ? null : token, buybackPct: pct(bps), buybackBps: Number(bps),
103
113
  pendingCut: cut[0] ? { toPct: pct(cut[1]), appliesFrom: iso(cut[2]), lapsesAfter: iso(Number(cut[2]) + 7 * 86400) } : null,
104
114
  buybackLeft: q(left), unsplit: q(unsplit), devOwed: q(owed),
105
115
  nextChunk: burn[0] ? { quoteIn: q(burn[1]), tokensBurned: formatUnits(burn[2], 18) } : null,
106
- note: `${pct(bps)}% of each tax payment to this recipient is bought back in the pool and burned (0xdead), the rest goes to the dev; a raise applies at once, a cut only 3 days after it is asked for`,
116
+ note: `${pct(bps)}% of each tax payment to this recipient is bought back in the pool and burned (0xdead), the rest goes to ${toDev ? 'the dev' : `the payee ${payee}`}; a raise applies at once, a cut only 3 days after it is asked for${hasPayee ? '; the dev can change the payee at once (gang buyback set-payee)' : ''}`,
107
117
  }
108
118
  }
109
119
 
@@ -125,8 +135,8 @@ export async function simulateCall(client, from, address, abi, functionName, arg
125
135
  }
126
136
  }
127
137
 
128
- const SUBS = ['status', 'set', 'apply', 'bind', 'release']
129
- const USAGE = 'usage: gang buyback <status|set|apply|bind|release> --token <addr> [--pct <0-100>] [--testnet] — see gang help'
138
+ const SUBS = ['status', 'set', 'set-payee', 'apply', 'bind', 'release']
139
+ const USAGE = 'usage: gang buyback <status|set|set-payee|apply|bind|release> --token <addr> [--pct <0-100>] [--to <addr>] [--testnet] — see gang help'
130
140
 
131
141
  /** `gang buyback <sub>`: the splitter of a V3 token (its tax recipient), found from --token. */
132
142
  export async function runBuyback(sub, { opt, flag, out, fail, summarize }) {
@@ -171,6 +181,18 @@ export async function runBuyback(sub, { opt, flag, out, fail, summarize }) {
171
181
  })
172
182
  }
173
183
 
184
+ if (sub === 'set-payee') {
185
+ if (!s.payeeChangeable) return fail('this splitter (buyback-burn-v1) has no payee: it pays the dev; only splitters from the current factory (v2) can name another address')
186
+ if (me().toLowerCase() !== s.dev.toLowerCase()) return fail('only the dev this splitter was made for can change its payee', { wallet: me(), dev: s.dev })
187
+ const to = opt('to')
188
+ if (!isAddress(to ?? '') || to === zeroAddress) fail('--to <0x…> required (checksummed or all lowercase): the address the dev\'s part goes to from now on', { got: to ?? null })
189
+ if (to.toLowerCase() === s.payee.toLowerCase()) return fail('that is the payee already', { payee: s.payee })
190
+ return send('setPayee', [to], {
191
+ step: 'set-payee', fromPayee: s.payee, toPayee: to,
192
+ note: 'applies at once (the dev\'s own share; holders are not affected). Quote that arrived before is split to the current payee first; what is owed goes to the new one',
193
+ })
194
+ }
195
+
174
196
  if (sub === 'apply') {
175
197
  if (!s.pendingCut) return fail(HINTS.NoProposal, { buybackPct: s.buybackPct })
176
198
  if (Date.parse(s.pendingCut.appliesFrom) > Date.now()) return fail(HINTS.NotDue, { appliesFrom: s.pendingCut.appliesFrom })
@@ -179,5 +201,5 @@ export async function runBuyback(sub, { opt, flag, out, fail, summarize }) {
179
201
 
180
202
  // release
181
203
  if (!(Number(s.devOwed) > 0)) return fail(HINTS.NothingOwed)
182
- return send('releaseDev', [], { step: 'release', amount: s.devOwed, note: 'anyone may; the dev gets what a failed push left owed' })
204
+ return send('releaseDev', [], { step: 'release', amount: s.devOwed, payee: s.payee, note: 'anyone may; the payee gets what a failed push left owed' })
183
205
  }
package/src/index.js CHANGED
@@ -284,9 +284,10 @@ graduated tokens only).
284
284
  gang create … --dev-buy <quote> [--bond <quote> [--bond-hold <tokens|N%|all>] --bond-days <7-90>]
285
285
  V3: create → the dev buys on the curve (--dev-buy, as gang buy) → bonds what it
286
286
  holds (--bond-hold, default all; gang bond post). Wrap / approve steps come in between
287
- gang create … --buy-tax <1-5> --sell-tax <1-5> --buyback <0-100> [--dev-buy … [--bond …]]
287
+ gang create … --buy-tax <1-5> --sell-tax <1-5> --buyback <0-100> [--buyback-to <addr>] [--dev-buy … [--bond …]]
288
288
  V3, taxed: the recipient's part of the tax is split, --buyback % bought back in the
289
- pool and burned (0xdead), the rest to you. Steps: clone your buy-back splitter
289
+ pool and burned (0xdead), the rest to you (or --buyback-to <addr>; changeable later
290
+ with gang buyback set-payee). Steps: clone your buy-back splitter
290
291
  (GangBuybackSplitterFactory; listed as a recipient in the same tx) → create with it as
291
292
  the recipient → bind → the rest of the launch. Without --buyback the tax goes to you
292
293
  gang create --fair … [--shares <n>] --bond <quote> [--bond-hold …] --bond-days <7-90>
@@ -314,7 +315,8 @@ graduated tokens only).
314
315
  a reverted simulation with --calldata is doNotSend: true, no tx. V2 / Soft / U / S:
315
316
  --from only on graduated tokens, mainnet only
316
317
  A creator selling below its bonded X (gang bond) is refused unless --break-bond;
317
- past the deadline, --settle-first settles the bond before the sell.
318
+ past the deadline, --settle-first settles the bond before the sell (asks first;
319
+ settles 1 hour later).
318
320
 
319
321
  BOND (押金承诺, GangBond; any GangLaunchpad token, from create on; GANG_BOND / GANG_TESTNET_BOND = its address):
320
322
  gang bond post --token <addr> --hold <tokens|N%> --days <7-90> (--amount <quote> | --cover <coverage %>) [--use-wbnb]
@@ -334,25 +336,31 @@ BOND (押金承诺, GangBond; any GangLaunchpad token, from create on; GANG_BOND
334
336
  gang bond start --token <addr> anyone, once the token graduated / launched: start the clock (deadline = now + days)
335
337
  gang bond report --token <addr> anyone, when the creator holds less than X: 1% of the bond to you, the rest buys
336
338
  back and burns the token (simulated first; nothing is sent when not violated)
337
- gang bond settle --token <addr> anyone, after the deadline while X is held: the bond goes back to the creator
339
+ gang bond settle --token <addr> anyone, after the deadline while X is held, two steps: run it once to ask, again
340
+ 1 hour later (X still held) and the bond goes back to the creator
338
341
  gang bond refund --token <addr> creator, posted before the pair and still no pair 30 days later: run it once to ask
339
342
  (a 3-day notice: X still binds, graduation / launch voids it), again after it to
340
343
  get the bond back
341
344
  gang bond burn --token <addr> [--slippage 1]
342
- anyone: one buy-back chunk of a slashed bond (≤ 0.5% of the pair's quote side; waits
343
- for a 10-minute average and skips when the price is > 2% above it). Slashed before the
345
+ anyone: one buy-back chunk of a slashed bond (≤ 0.5% of the pair's quote side, one
346
+ per 4 minutes; waits for a 10-minute average and skips when the price is > 2% above
347
+ it). Slashed before the
344
348
  pair: waits for it; none 30 days after the post → the rest goes to 0xdead as quote
345
349
  gang bond tx --hash <0x..> decode GangBond events. --testnet, --calldata, --from as in gang v3. A bonded
346
350
  creator's gang v3 sell (curve or pool), gang fair sell and gang fair withdraw are
347
351
  checked against X (--break-bond / --settle-first)
348
352
  BUYBACK (a V3 token whose recipient is a buy-back splitter; GANG_SPLITTER_FACTORY / GANG_TESTNET_SPLITTER_FACTORY):
349
353
  gang buyback status --token <addr> ratio, a cut asked for (applies from / lapses after), quote waiting to be bought
350
- back, owed to the dev, the next chunk (the keeper buys it back every few minutes)
354
+ back, the payee and what is owed to it, the next chunk (the keeper buys it back
355
+ every few minutes)
351
356
  gang buyback set --token <addr> --pct <0-100>
352
357
  dev only: a higher share applies at once; a lower one after a 3-day notice
358
+ gang buyback set-payee --token <addr> --to <addr>
359
+ dev only: the part not bought back goes to --to from now on (at once; splitters
360
+ from the current factory; the first mainnet ones, buyback-burn-v1, pay the dev)
353
361
  gang buyback apply --token <addr> anyone: apply a cut whose notice ran (within 7 days, or it lapses)
354
362
  gang buyback bind --token <addr> | gang buyback release --token <addr>
355
- anyone: bind the splitter to its token (the launch does it) / push the dev what a
363
+ anyone: bind the splitter to its token (the launch does it) / push the payee what a
356
364
  failed payment left owed. --testnet, --calldata, --from as in gang v3
357
365
  gang execute --request-id <id> broadcast a previewed call
358
366
  gang tx --hash <0x..> [--testnet] wait for receipt and decode Gang events: V2 / Soft / U / S, GangLaunchpad (V3 /
package/src/launch.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomBytes } from 'node:crypto'
2
2
  import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
3
3
  import { dirname, join } from 'node:path'
4
- import { createPublicClient, http, parseAbi, formatUnits, encodeFunctionData, zeroAddress } from 'viem'
4
+ import { createPublicClient, http, parseAbi, formatUnits, encodeFunctionData, isAddress, zeroAddress } from 'viem'
5
5
  import { DIR } from './notes.js'
6
6
  import { padNets, NOT_ON_MAINNET } from './gangpad.js'
7
7
  import { bondAddress, BOND_ABI } from './bond.js'
@@ -26,6 +26,7 @@ import { buybackBpsOf, FACTORY_ABI, notDeployed, simulateCall, SPLITTER_ABI, spl
26
26
  * predict(dev, salt), with a fresh salt per plan kept in it (so the address is known before the clone and never another
27
27
  * launch's), the create names it as the recipient, and a bind step follows the create. A launch keeps the mode it began
28
28
  * with: adding --buyback after the create (or dropping it before) is refused rather than starting over.
29
+ * --buyback-to <addr> names where the dev's part goes (default: the dev); the dev can change it later at once.
29
30
  */
30
31
 
31
32
  const LAUNCH_DIR = process.env.GANG_LAUNCH_DIR || join(dirname(DIR), 'launch')
@@ -39,7 +40,7 @@ const PAD_ABI = parseAbi([
39
40
  ])
40
41
  const ERC20_ABI = parseAbi(['function balanceOf(address) view returns (uint256)', 'function symbol() view returns (string)'])
41
42
  /** Flags only the chain reads; everything else goes to the step's own command. */
42
- export const LAUNCH_FLAGS = ['dev-buy', 'bond', 'bond-hold', 'bond-days', 'buyback', 'restart']
43
+ export const LAUNCH_FLAGS = ['dev-buy', 'bond', 'bond-hold', 'bond-days', 'buyback', 'buyback-to', 'restart']
43
44
  const NOTE_FLAGS = ['note', 'conf', 'reveal-after']
44
45
  /** `gang fair create --dev-buy`, with or without --bond (index.js stops the lone flag, runLaunch the chained one). */
45
46
  export const FAIR_DEV_BUY = 'a 打新 dev buys its shares in the create itself: use --shares <n> (not --dev-buy)'
@@ -108,6 +109,7 @@ export async function runLaunch(kind, { io, runV3, runFair, runBond, padNotes })
108
109
 
109
110
  // --buyback: a taxed V3 token whose recipient (the part left after holders) is the dev's splitter
110
111
  let buyback = null
112
+ if (flag('buyback-to') && !flag('buyback')) fail('--buyback-to <addr> goes with --buyback <pct>: it names where the part not bought back goes')
111
113
  if (flag('buyback')) {
112
114
  const zero = (f) => /^0+(\.0+)?$/.test(opt(f) ?? '')
113
115
  if (!flag('buy-tax') || !flag('sell-tax') || (zero('buy-tax') && zero('sell-tax'))) {
@@ -117,7 +119,12 @@ export async function runLaunch(kind, { io, runV3, runFair, runBond, padNotes })
117
119
  if (/^0*100(\.0+)?$/.test(opt('dividend', '0'))) fail('--dividend 100 leaves nothing for the recipient, so nothing to buy back: lower it or drop --buyback')
118
120
  const factory = splitterFactoryAddress(testnet)
119
121
  if (!factory) fail(notDeployed(testnet))
120
- buyback = { bps: buybackBpsOf(fail, opt('buyback')), factory }
122
+ let payee = null
123
+ if (flag('buyback-to')) {
124
+ payee = opt('buyback-to')
125
+ if (!isAddress(payee ?? '') || payee === zeroAddress) fail('--buyback-to must be a 0x address, checksummed or all lowercase (where the part not bought back goes; leave it out for your own wallet)', { got: payee ?? null })
126
+ }
127
+ buyback = { bps: buybackBpsOf(fail, opt('buyback')), factory, payee }
121
128
  }
122
129
 
123
130
  const client = createPublicClient({ chain: net.chain, transport: http(net.rpc) })
@@ -147,15 +154,16 @@ export async function runLaunch(kind, { io, runV3, runFair, runBond, padNotes })
147
154
  if (!plan?.salt) {
148
155
  const salt = `0x${randomBytes(32).toString('hex')}`
149
156
  const at = await read(buyback.factory, FACTORY_ABI, 'predict', [me, salt])
150
- const sim = await simulateCall(client, me, buyback.factory, FACTORY_ABI, 'create', [buyback.bps, salt])
157
+ const args = [buyback.bps, salt, buyback.payee ?? zeroAddress]
158
+ const sim = await simulateCall(client, me, buyback.factory, FACTORY_ABI, 'create', args)
151
159
  if (sim.failed) return out({ success: false, step: 'buyback-splitter', ...tag('buyback-splitter'), ...sim.failed })
152
160
  writePlan(key, { ...base, salt, splitter: at, factory: buyback.factory })
153
- const data = encodeFunctionData({ abi: FACTORY_ABI, functionName: 'create', args: [buyback.bps, salt] })
161
+ const data = encodeFunctionData({ abi: FACTORY_ABI, functionName: 'create', args })
154
162
  return out({
155
163
  ...step({ chainId: net.chainId }, { from: me, to: buyback.factory, data }, {
156
164
  step: 'buyback-splitter', chainId: Number(net.chainId), factory: buyback.factory, buybackPct: buyback.bps / 100,
157
- splitter: at,
158
- note: `clones your buy-back splitter (listed as a tax recipient in the same tx): ${buyback.bps / 100}% of the recipient's part is bought back and burned, the rest goes to you. A raise applies at once later, a cut only after a 3-day notice`,
165
+ splitter: at, payee: buyback.payee ?? me,
166
+ note: `clones your buy-back splitter (listed as a tax recipient in the same tx): ${buyback.bps / 100}% of the recipient's part is bought back and burned, the rest goes to ${buyback.payee ? `${buyback.payee} (--buyback-to)` : 'you'}. Later a raise applies at once, a cut only after a 3-day notice; the payee can be changed at once (gang buyback set-payee)`,
159
167
  }, flag('calldata'), io.summarize),
160
168
  ...tag('buyback-splitter', { splitter: at, next: `${rerun}: it then builds the create with the splitter as the recipient` }),
161
169
  })
@@ -173,6 +181,11 @@ export async function runLaunch(kind, { io, runV3, runFair, runBond, padNotes })
173
181
  if (s.buybackBps !== buyback.bps) {
174
182
  return fail(`this launch's splitter buys back ${s.buybackPct}%, not ${buyback.bps / 100}%: run it again with --buyback ${s.buybackPct}, or add --restart for a new splitter (gang buyback set changes it after the create)`, { splitter: plan.splitter })
175
183
  }
184
+ const wantPayee = (buyback.payee ?? me).toLowerCase()
185
+ if (s.payee.toLowerCase() !== wantPayee) {
186
+ const was = s.payee.toLowerCase() === me.toLowerCase() ? 'leave --buyback-to out' : `--buyback-to ${s.payee}`
187
+ return fail(`this launch's splitter pays ${s.payee}, not ${buyback.payee ?? me}: run it again with ${was}, or add --restart for a new splitter (gang buyback set-payee changes it after the create)`, { splitter: plan.splitter })
188
+ }
176
189
  }
177
190
 
178
191
  // ---- create