Skip to content

fix(launkr): pin contract addresses, fix create-pool decimals, wait for swap confirmations - #416

Open
biwasxyz wants to merge 1 commit into
mainfrom
fix/launkr-review-followups
Open

biwasxyz wants to merge 1 commit into
mainfrom
fix/launkr-review-followups

Conversation

@biwasxyz

@biwasxyz biwasxyz commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Follow-up review of #414 (launkr skill). Six fixes; one is a blocker that would permanently misconfigure a token.

Blocker — create-pool would initialize a recovered token with 0 decimals

#414 replaced a hardcoded uintCV(6) with a get-decimals read off the already-deployed token, reasoning it is "not a parameter to get right, it's a fact to look up." That is inverted for this command.

From the deployed template source:

(define-data-var token-decimals uint u0)

The only thing that ever sets it is the token's one-shot initialize() — and initialize() is called by the singleton, during pool creation, from this very argument ((try! (contract-call? token initialize name symbol decimals supply uri)) in lp-singleton-v6).

So on the exact state create-pool exists to recover from — token deployed, no pool yet — get-decimals returns u0, because the token has no decimals yet. That 0 was fed straight back into create-pool-*, permanently initializing the recovered token with 0 decimals. initialize() runs once and answers ERR_ALREADY_INITIALIZED forever after. Strictly worse than the hardcoded 6 it replaced.

Now: explicit --decimals (default 6, the tokenDecimals constant /api/protocol reports), plus an up-front refusal if the token reports a non-zero total supply — initialize() mints the whole supply, so that means it has already run and pool creation could only abort.

Security — the live-config fetch had quietly disabled the deploy verification

fetchProtocolConfig made /api/protocol authoritative for both singleton and template, with NET_CONFIG demoted to a silent fallback. But verifyDeploySourceMatchesTemplate compares the API's clarityCode against the on-chain source at the template address — and once that address also came from the API, both sides of the comparison shared an origin. The check could only catch launkr.io disagreeing with itself, not the compromised or hijacked launkr.io it was written for.

Same for singleton: a substituted address collects a --mode direct STX seed under an eq post-condition that authorizes the transfer perfectly happily. (Swaps are largely self-protecting — an imposter can't satisfy gte min-tokens-out on the real token's FT.)

Still fetches live, because that is how a redeploy gets noticed — but NET_CONFIG is the trust anchor again. Drift is refused and both addresses printed; --allow-config-change is the deliberate per-invocation override.

Verified against the live API by temporarily mis-pinning the singleton:

{ "error": "Refusing to proceed — https://launkr.io/api/protocol returned contract
   addresses that differ from the ones pinned in this skill:
     - singleton: pinned \"...lp-singleton-v5\", API returned \"...lp-singleton-v6\"" }

and --allow-config-change proceeding with a stderr warning.

Also fixed

  • Broadcast reported as success. launch/create-pool were changed to wait for confirmation precisely because broadcast ≠ confirmed, but the swaps and fee-receiver rotation still printed success: true the instant a txid came back — leaving "trade happened", "trade pending" and "trade aborted on a post-condition" indistinguishable. Waiting is now the default everywhere; --no-wait keeps fire-and-forget but labels it broadcast: true, confirmed: false.
  • Two unchecked pool args. validatePoolStepMatchesRequest now covers all nine. uri was caller-supplied and never compared; decimals had nothing to compare against until --decimals existed. Both are written permanently by the one-shot initialize().
  • Unverified principal in output. launch validated against the locally derived ${address}.${contractName} but printed the API's intent.tokenPrincipal (and built the launkr.io link from it). Now reports the derived one; a mismatch warns on stderr.
  • Documented but unreachable commands. SKILL.md describes swap-and-burn, quote-swap-and-burn and is-paused; none were callable. Added, with signatures and post-conditions read off the deployed singleton's interface and source rather than inferred from the prose — swap-and-burn has no recipient, is fee-free, and burns from the singleton's own balance under (with-ft token-principal "strategy-token" dy), so Deny mode needs that outflow covered. Write paths now pre-flight is-paused (one free read) instead of paying a fee to discover a guaranteed abort.

Verification

  • bun run typecheck, bun run validate (202/202), bun test launkr/launkr.test.ts (38/38, up from 25)
  • New tests cover config-drift detection, --decimals bounds, and the uri/decimals arg checks
  • is-paused, get-pool, quote-buy, quote-swap-and-burn all run against live mainnet; quote-swap-and-burn correctly returns none on a bonding pool
  • Config-drift refusal and --allow-config-change override both exercised end-to-end

Separate finding, not fixed here

bun run typecheck only covers src/**/*tsconfig.json has "include": ["src/**/*"], so no skill CLI is type-checked, including this one. CLAUDE.md and CONTRIBUTING both present it as the pre-PR gate. It is not theoretical: two of the edits in this PR produced type errors that the repo typecheck reported clean and a direct tsc on the file caught immediately. A repo-wide run over the skill directories currently reports ~144 errors, so fixing it is its own piece of work rather than something to fold in here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01G7a33ETgZSZrwKbfQbZ6xJ

…or swap confirmations

Follow-up review of #414. Six changes, one of them a blocker.

**create-pool would permanently initialize a recovered token with 0 decimals.**
#414 replaced a hardcoded `uintCV(6)` with a `get-decimals` read off the
already-deployed token, on the reasoning that decimals is "not a parameter to
get right, it's a fact to look up." That is inverted for this command: the
byte-frozen template ships `(define-data-var token-decimals uint u0)`, and the
only thing that ever sets it is the token's one-shot `initialize()`, which the
singleton calls *during pool creation* from that very argument. So on the exact
state `create-pool` exists to recover from — token deployed, no pool — the read
returns u0 and feeds 0 straight back in, unfixable afterwards
(ERR_ALREADY_INITIALIZED). Now an explicit `--decimals` (default 6), plus an
up-front refusal if the token reports a non-zero total supply, which means
initialize() has already run.

**The live-config fetch had disabled the deploy verification.** Making
/api/protocol authoritative for `template` meant `verifyDeploySourceMatchesTemplate`
compared an API-supplied contract body against an API-supplied template address —
same origin on both sides, so it could only catch launkr.io disagreeing with
itself, not the compromised launkr.io it was written for. Same for `singleton`:
a substituted address collects a `--mode direct` STX seed under an `eq`
post-condition that authorizes the transfer happily. Still fetches live (that is
how a redeploy gets noticed) but NET_CONFIG is the trust anchor again: drift is
refused and printed, with `--allow-config-change` as the deliberate override.

Also:
- swaps and fee-receiver rotation now wait for a terminal on-chain status
  instead of printing `success: true` on broadcast; `--no-wait` keeps the old
  behaviour but labels it `broadcast: true, confirmed: false`
- `validatePoolStepMatchesRequest` covers all nine args — `uri` and `decimals`
  were unchecked, and both are written permanently by initialize()
- `launch` reports the locally derived token principal rather than the API's
  unverified `intent.tokenPrincipal`
- adds `swap-and-burn`, `quote-swap-and-burn` and `is-paused`, all documented in
  SKILL.md but previously unreachable; write paths now pre-flight the pause

Signatures and post-conditions for the new commands were read off the deployed
singleton's interface and source, not inferred. Config-drift refusal and
override both verified against the live API; new read commands verified against
a real mainnet pool.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7a33ETgZSZrwKbfQbZ6xJ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant