security(community-tax): ratify InstantiateMsg.launcher so GetLauncherOrigin cannot spoof EnableFeature #1229

Open
opened 2026-09-11 07:47:57 +00:00 by PlasticDigits · 0 comments

Summary

InstantiateMsg.launcher is an unauthenticated caller field. GetLauncherOrigin returns that stored address with no instantiate-sender check, so a token that was not created by the official launcher can claim it. After #606 (T606-3 / T606-4), the official launcher treats GetLauncherOrigin.launcher == self as proof it may forward EnableFeature (and AutoLP bind) for that token.

This is auth / confused-deputy today on live community-tax-token + community-token-launcher. It is not a pair LP drain. Protocol invoice integrity and launcher-only privileges are in scope.

Do not reopen #606 (Enable Feature path was Unauthorized; that is fixed). Do not fold into #592 (template + catalog-filter design). Rogue --admin instantiate was assumed to yield GetLauncherOrigin.launcher == null (O601-4 / #620). That assumption does not hold if the instantiator sets launcher.

Impact (Immunefi-style)

Today (auth): Anyone who can instantiate the listed tax wasm (code_id is factory-whitelisted) can set launcher to the official launcher. The official launcher then:

  1. Accepts manager-paid EnableFeature for that instance (enable_feature queries GetLauncherOrigin and compares to env.contract.address).
  2. Forwards the UST1 invoice into that token. The token treats origin.launcher as an EnableFeature payer and Transfers the invoice to its cmm_treasury (also caller-chosen at instantiate). Protocol CMM need not receive the 50 UST1.
  3. On AutoV2Lp, instantiates an AutoLP sister with CMM as wasm admin and BindAutolp (gated only by stored CONFIG.launcher).

Catalog: If attestation is GetLauncherOrigin == env launcher without a launcher instantiate tx and CMM wasm admin, a rogue instance can look official. #626 already asked for code_id + CMM admin + launcher-tx + origin; origin alone must not be enough.

Not claimed: draining unrelated pair LP, stealing another manager’s UST1 in a different tx, or wasm-admin takeover of launcher-created tokens. Direct EnableFeature as manager (no launcher) is out of this ticket except where the same unratified field is the gate.

Current codebase

Token instantiate copies msg.launcher into CONFIG after addr_validate only. info.sender is not required to equal that address.

smartcontracts/contracts/community-tax-token/src/contract.rs  instantiate
smartcontracts/contracts/community-tax-token/src/msg.rs       InstantiateMsg.launcher

QueryMsg::GetLauncherOrigin returns cfg.launcher with no origin proof.

Official create does stamp launcher: Some(env.contract.address) from the launcher (community-token-launcher create_token). Direct wasm instantiate can stamp the same string without going through that crate.

Launcher enable_feature (post-#606):

  • payer == GetConfig.manager
  • GetLauncherOrigin.launcher == this launcher
  • then UST1 Send hook EnableFeature to the token (no second send to CMM)
  • AutoV2Lp → instantiate AutoLP (CMM admin) + later BindAutolp

Token invoice::assert_invoice_payer: EnableFeature allows payer == manager or payer == config.launcher. BindAutolp is info.sender == config.launcher.

There is no launcher-side map of tokens created in REPLY_TOKEN. Origin is entirely the token’s self-report.

Why this is needed

#606’s long-term path made the official launcher a privileged EnableFeature deputy because origin said so. Origin is not an attestation of WasmMsg::Instantiate from that deputy. Retail Create Token, catalog attested_cmm / created_by_launcher, and AutoLP bind all inherit that lie.

Invoice 50 UST1 is meant to land at protocol CMM (T592-4). A self-reported launcher plus a self-reported cmm_treasury breaks that for the official forward path.

Constraints / guardrails

  1. Honest launcher create must still stamp origin and still unlock SKUs via manager → launcher → token (T606, C593-4).
  2. Direct instantiate (no launcher) remains possible for the listed code_id; it must not obtain official origin, launcher EnableFeature, or BindAutolp from the official launcher. Catalog must still refuse unverified admin (O601-4, F6).
  3. Do not loosen payer == manager for arbitrary contracts. Do not accept a second launcher address after instantiate (no manager UpdateSettings of origin).
  4. AdoptMigrateMsg.official_launcher stays CMM-only same-crate/adopt (#626). This ticket does not add a public adopt stamp.
  5. Exact 50 UST1; wrong token / amount / no-op revert; fee not kept. MintControl instantiate-only. Do not mix EnableFeature into a settings batch.
  6. No public exploit recipe in the MR. Tests assert reject / invoice destination, not a drain script.
  7. Columbus-5 token + launcher need CMM / DEX 2-of-3 migrate after the wasm fix. dApp-only origin checks are not sufficient.
  8. Do not whitelist launcher or AutoLP code_ids. Do not treat indexer events as on-chain origin.

Relevant files

Path Why
smartcontracts/contracts/community-tax-token/src/contract.rs Instantiate launcher stamp; GetLauncherOrigin; BindAutolp
smartcontracts/contracts/community-tax-token/src/msg.rs InstantiateMsg.launcher; LauncherOriginResponse; adopt official_launcher
smartcontracts/contracts/community-tax-token/src/invoice.rs T606 launcher as EnableFeature payer; invoice cmm_treasury
smartcontracts/contracts/community-token-launcher/src/contract.rs create_token stamp; enable_feature origin query; AutoLP reply
smartcontracts/contracts/community-token-launcher/src/state.rs No created-token set today — add if using allowlist
smartcontracts/contracts/community-tax-token/src/state.rs Config.launcher
Indexer community-token catalog (I594 / O601-4) Must not attest on GetLauncherOrigin alone
skills/AGENTS_COMMUNITY_TAX_CW20.md / Create Token skill Origin = ratified instantiate sender, not a free field
Launcher / token multitest (audit_poc or sibling) Reject spoofed origin; honest create still works

Defense in depth; ship both on-chain (one migrate):

  1. Ratify at instantiate. If InstantiateMsg.launcher is Some(addr), require info.sender == addr (after validate). Otherwise ignore / store None (prefer reject if Some and sender mismatch — fail closed). Honest launcher instantiate already has sender == launcher. Direct instantiate cannot claim the official launcher.
  2. Launcher allowlist. On REPLY_TOKEN, persist created token addresses. enable_feature / AutoLP bind only if the token is in that set. Do not treat GetLauncherOrigin as sufficient. Query mismatch → Unauthorized; UST1 not forwarded.
  3. Keep catalog belt. Attest only with LCD code_id + CMM admin + instantiate tx from env launcher (or ratified origin and those LCD checks). Document that GetLauncherOrigin is not a listing proof.
  4. Adopt remains #626: CMM migrate writes official launcher; rogue wasm admin cannot.

Prefer (1)+(2) in one wasm pair so a future query bug cannot re-open the deputy.

Acceptance criteria

  • AC1. Token instantiate with launcher = official launcher and info.sender ≠ that launcher does not persist that origin (GetLauncherOrigin.launcher is None or instantiate reverts).
  • AC2. Official launcher CreateToken still stores GetLauncherOrigin.launcher == launcher and CMM wasm admin.
  • AC3. Official launcher EnableFeature for a token it did not instantiate (including AC1 spoof attempt) → Unauthorized; SKU unchanged; launcher does not forward UST1 to that token; protocol CMM is not used as a pass-through into attacker cmm_treasury.
  • AC4. Honest path: launcher-created token, manager pays 50 UST1 EnableFeature via launcher → SKU on, protocol CMM receives 50 UST1.
  • AC5. BindAutolp / AutoLP instantiate from official launcher does not run for a token missing from the launcher’s created set.
  • AC6. Manager direct-to-token EnableFeature (no launcher) unchanged for a token with launcher == None.
  • AC7. Catalog / I594: instance with spoofed or null origin and/or non-CMM admin is not attested_cmm.
  • AC8. Docs/skills: origin is instantiate-sender ratification; #606 deputy language updated. make verify-issue-606 stays green and new origin tests pass.
  • AC9. MR states columbus-5 migrate for token + launcher (same 2-of-3 as prior tax wasm).

Test plan (functional)

# Path Expect
T1 Launcher CreateToken (0 SKU) Origin = launcher; admin = CMM
T2 T1 then manager EnableFeature via launcher SKU on; CMM +50 UST1
T3 Direct instantiate, omit launcher Origin None; launcher EnableFeature Unauthorized
T4 Direct instantiate, launcher = official, sender = EOA AC1: reject or origin None
T5 T4 then manager pays launcher EnableFeature AC3: no forward
T6 Direct instantiate, launcher = self (a dummy contract) Only that dummy is origin; official launcher still Unauthorized
T7 Settings batch still manager → token Unchanged
T8 AutoV2Lp on honest create/enable Sister bind only for created token
T9 MintControl via EnableFeature Still instantiate-only

Test plan (attack / abuse)

Non-exploitative. Assert reverts and balances; do not publish a mainnet recipe.

# Vector Expect
A1 Instantiate listed tax wasm with launcher = official launcher, cmm_treasury = attacker, then launcher EnableFeature Instantiate and/or enable fails; attacker does not receive the invoice; official CMM unchanged if enable never forwards
A2 Same as A1 with manager = attacker (deputy check would pass if origin were trusted) Still Unauthorized at launcher
A3 Random contract calls launcher enable_feature for a launcher-created token Unauthorized (existing T606)
A4 EnableFeature AutoV2Lp on spoofed origin No AutoLP instantiate, no BindAutolp
A5 Catalog query of A1 instance Not attested
A6 AdoptMigrateMsg.official_launcher from non-CMM admin Unchanged reject (#626)
A7 Re-entrancy / double-forward of UST1 Invoice taken at most once; not split to attacker treasury

Verification criteria

  • New crate tests: instantiate sender mismatch; launcher allowlist miss; honest create+enable still green.
  • Invert or add audit_poc (or sibling) for spoofed GetLauncherOrigin — the case must fail closed, not document a working deputy.
  • cargo test -p cl8y-community-tax-token and -p cl8y-community-token-launcher (including audit_poc).
  • make verify-issue-606 / catalog greps for O601-4 remain green.
  • Manual LocalTerra: free create via launcher still Enable Feature; a hand instantiate with copied launcher field cannot unlock via official launcher.

Out of scope

  • Pair AMM / limit-book drains.
  • Changing SKU prices or mixing Enable Feature into settings batches.
  • Factory delist of the tax code_id (mitigation is origin, not unlist).
  • Ops Coolify / host work (migrate is on-chain 2-of-3 only, named in AC9).
  • Reopening #606 / #592 / #626 for this origin bug.

First-pass model recommendation

Recommendation: grok-high

Rationale: CosmWasm token + launcher auth (instantiate origin, invoice deputy, AutoLP bind). Founder-required surface (contracts / wasm / wallet 2-of-3 migrate). Crosses community-tax-token instantiate/query/invoice and community-token-launcher enable/reply state, plus catalog attestation. Not a local three-file edit; Composer criteria fail on security, wasm, and cross-crate protocol. Verify with crate tests + inverted origin case + #606 regression, then 2-of-3 migrate — not UI-only.

## Summary `InstantiateMsg.launcher` is an unauthenticated caller field. `GetLauncherOrigin` returns that stored address with no instantiate-sender check, so a token that was **not** created by the official launcher can claim it. After #606 (**T606-3** / **T606-4**), the official launcher treats `GetLauncherOrigin.launcher == self` as proof it may forward `EnableFeature` (and AutoLP bind) for that token. This is **auth / confused-deputy today** on live `community-tax-token` + `community-token-launcher`. It is **not** a pair LP drain. Protocol invoice integrity and launcher-only privileges are in scope. Do **not** reopen #606 (Enable Feature path was Unauthorized; that is fixed). Do **not** fold into #592 (template + catalog-filter design). Rogue `--admin` instantiate was assumed to yield `GetLauncherOrigin.launcher == null` (**O601-4** / #620). That assumption does not hold if the instantiator sets `launcher`. ## Impact (Immunefi-style) **Today (auth):** Anyone who can instantiate the listed tax wasm (code_id is factory-whitelisted) can set `launcher` to the official launcher. The official launcher then: 1. Accepts manager-paid `EnableFeature` for that instance (`enable_feature` queries `GetLauncherOrigin` and compares to `env.contract.address`). 2. Forwards the UST1 invoice into **that** token. The token treats `origin.launcher` as an `EnableFeature` payer and `Transfer`s the invoice to **its** `cmm_treasury` (also caller-chosen at instantiate). Protocol CMM need not receive the 50 UST1. 3. On `AutoV2Lp`, instantiates an AutoLP sister with CMM as wasm admin and `BindAutolp` (gated only by stored `CONFIG.launcher`). **Catalog:** If attestation is `GetLauncherOrigin == env launcher` without a launcher instantiate tx **and** CMM wasm admin, a rogue instance can look official. #626 already asked for code_id + CMM admin + launcher-tx + origin; origin alone must not be enough. **Not claimed:** draining unrelated pair LP, stealing another manager’s UST1 in a different tx, or wasm-admin takeover of launcher-created tokens. Direct `EnableFeature` as manager (no launcher) is out of this ticket except where the same unratified field is the gate. ## Current codebase Token instantiate copies `msg.launcher` into `CONFIG` after `addr_validate` only. `info.sender` is not required to equal that address. ```text smartcontracts/contracts/community-tax-token/src/contract.rs instantiate smartcontracts/contracts/community-tax-token/src/msg.rs InstantiateMsg.launcher ``` `QueryMsg::GetLauncherOrigin` returns `cfg.launcher` with no origin proof. Official create **does** stamp `launcher: Some(env.contract.address)` from the launcher (`community-token-launcher` `create_token`). Direct wasm instantiate can stamp the same string without going through that crate. Launcher `enable_feature` (post-#606): - `payer == GetConfig.manager` - `GetLauncherOrigin.launcher == this launcher` - then UST1 `Send` hook `EnableFeature` to the token (no second send to CMM) - `AutoV2Lp` → instantiate AutoLP (CMM admin) + later `BindAutolp` Token `invoice::assert_invoice_payer`: `EnableFeature` allows `payer == manager` **or** `payer == config.launcher`. `BindAutolp` is `info.sender == config.launcher`. There is **no** launcher-side map of tokens created in `REPLY_TOKEN`. Origin is entirely the token’s self-report. ## Why this is needed #606’s long-term path made the official launcher a privileged `EnableFeature` deputy **because** origin said so. Origin is not an attestation of `WasmMsg::Instantiate` from that deputy. Retail Create Token, catalog `attested_cmm` / `created_by_launcher`, and AutoLP bind all inherit that lie. Invoice 50 UST1 is meant to land at protocol CMM (**T592-4**). A self-reported launcher plus a self-reported `cmm_treasury` breaks that for the official forward path. ## Constraints / guardrails 1. Honest launcher create must still stamp origin and still unlock SKUs via manager → launcher → token (**T606**, **C593-4**). 2. Direct instantiate (no launcher) remains possible for the listed code_id; it must **not** obtain official origin, launcher `EnableFeature`, or `BindAutolp` from the official launcher. Catalog must still refuse unverified admin (**O601-4**, F6). 3. Do **not** loosen `payer == manager` for arbitrary contracts. Do **not** accept a second launcher address after instantiate (no manager `UpdateSettings` of origin). 4. `AdoptMigrateMsg.official_launcher` stays CMM-only same-crate/adopt (#626). This ticket does not add a public adopt stamp. 5. Exact 50 UST1; wrong token / amount / no-op revert; fee not kept. MintControl instantiate-only. Do not mix `EnableFeature` into a settings batch. 6. No public exploit recipe in the MR. Tests assert **reject** / **invoice destination**, not a drain script. 7. Columbus-5 token + launcher need CMM / DEX 2-of-3 **migrate** after the wasm fix. dApp-only origin checks are not sufficient. 8. Do not whitelist launcher or AutoLP code_ids. Do not treat indexer events as on-chain origin. ## Relevant files | Path | Why | | --- | --- | | `smartcontracts/contracts/community-tax-token/src/contract.rs` | Instantiate `launcher` stamp; `GetLauncherOrigin`; `BindAutolp` | | `smartcontracts/contracts/community-tax-token/src/msg.rs` | `InstantiateMsg.launcher`; `LauncherOriginResponse`; adopt `official_launcher` | | `smartcontracts/contracts/community-tax-token/src/invoice.rs` | T606 launcher as `EnableFeature` payer; invoice `cmm_treasury` | | `smartcontracts/contracts/community-token-launcher/src/contract.rs` | `create_token` stamp; `enable_feature` origin query; AutoLP reply | | `smartcontracts/contracts/community-token-launcher/src/state.rs` | No created-token set today — add if using allowlist | | `smartcontracts/contracts/community-tax-token/src/state.rs` | `Config.launcher` | | Indexer community-token catalog (I594 / O601-4) | Must not attest on `GetLauncherOrigin` alone | | `skills/AGENTS_COMMUNITY_TAX_CW20.md` / Create Token skill | Origin = ratified instantiate sender, not a free field | | Launcher / token multitest (`audit_poc` or sibling) | Reject spoofed origin; honest create still works | ## Recommended direction Defense in depth; ship **both** on-chain (one migrate): 1. **Ratify at instantiate.** If `InstantiateMsg.launcher` is `Some(addr)`, require `info.sender == addr` (after validate). Otherwise ignore / store `None` (prefer **reject** if `Some` and sender mismatch — fail closed). Honest launcher instantiate already has `sender == launcher`. Direct instantiate cannot claim the official launcher. 2. **Launcher allowlist.** On `REPLY_TOKEN`, persist created token addresses. `enable_feature` / AutoLP bind only if the token is in that set. Do not treat `GetLauncherOrigin` as sufficient. Query mismatch → `Unauthorized`; UST1 not forwarded. 3. **Keep catalog belt.** Attest only with LCD `code_id` + CMM admin + instantiate tx from env launcher (or ratified origin **and** those LCD checks). Document that `GetLauncherOrigin` is not a listing proof. 4. Adopt remains #626: CMM migrate writes official launcher; rogue wasm admin cannot. Prefer (1)+(2) in one wasm pair so a future query bug cannot re-open the deputy. ## Acceptance criteria - [ ] **AC1.** Token instantiate with `launcher = official launcher` and `info.sender ≠ that launcher` does **not** persist that origin (`GetLauncherOrigin.launcher` is `None` or instantiate reverts). - [ ] **AC2.** Official launcher `CreateToken` still stores `GetLauncherOrigin.launcher == launcher` and CMM wasm admin. - [ ] **AC3.** Official launcher `EnableFeature` for a token it did **not** instantiate (including AC1 spoof attempt) → `Unauthorized`; SKU unchanged; launcher does not forward UST1 to that token; protocol CMM is not used as a pass-through into attacker `cmm_treasury`. - [ ] **AC4.** Honest path: launcher-created token, manager pays 50 UST1 `EnableFeature` via launcher → SKU on, **protocol** CMM receives 50 UST1. - [ ] **AC5.** `BindAutolp` / AutoLP instantiate from official launcher does not run for a token missing from the launcher’s created set. - [ ] **AC6.** Manager direct-to-token `EnableFeature` (no launcher) unchanged for a token with `launcher == None`. - [ ] **AC7.** Catalog / I594: instance with spoofed or null origin and/or non-CMM admin is not `attested_cmm`. - [ ] **AC8.** Docs/skills: origin is instantiate-sender ratification; #606 deputy language updated. `make verify-issue-606` stays green **and** new origin tests pass. - [ ] **AC9.** MR states columbus-5 migrate for token + launcher (same 2-of-3 as prior tax wasm). ## Test plan (functional) | # | Path | Expect | | --- | --- | --- | | T1 | Launcher `CreateToken` (0 SKU) | Origin = launcher; admin = CMM | | T2 | T1 then manager `EnableFeature` via launcher | SKU on; CMM +50 UST1 | | T3 | Direct instantiate, omit `launcher` | Origin `None`; launcher `EnableFeature` Unauthorized | | T4 | Direct instantiate, `launcher` = official, sender = EOA | AC1: reject or origin `None` | | T5 | T4 then manager pays launcher `EnableFeature` | AC3: no forward | | T6 | Direct instantiate, `launcher` = self (a dummy contract) | Only that dummy is origin; official launcher still Unauthorized | | T7 | Settings batch still manager → token | Unchanged | | T8 | AutoV2Lp on honest create/enable | Sister bind only for created token | | T9 | MintControl via `EnableFeature` | Still instantiate-only | ## Test plan (attack / abuse) Non-exploitative. Assert reverts and balances; do not publish a mainnet recipe. | # | Vector | Expect | | --- | --- | --- | | A1 | Instantiate listed tax wasm with `launcher` = official launcher, `cmm_treasury` = attacker, then launcher `EnableFeature` | Instantiate and/or enable **fails**; attacker does not receive the invoice; official CMM unchanged if enable never forwards | | A2 | Same as A1 with `manager` = attacker (deputy check would pass if origin were trusted) | Still Unauthorized at launcher | | A3 | Random contract calls launcher `enable_feature` for a launcher-created token | Unauthorized (existing T606) | | A4 | `EnableFeature` AutoV2Lp on spoofed origin | No AutoLP instantiate, no `BindAutolp` | | A5 | Catalog query of A1 instance | Not attested | | A6 | `AdoptMigrateMsg.official_launcher` from non-CMM admin | Unchanged reject (#626) | | A7 | Re-entrancy / double-forward of UST1 | Invoice taken at most once; not split to attacker treasury | ## Verification criteria - New crate tests: instantiate sender mismatch; launcher allowlist miss; honest create+enable still green. - Invert or add `audit_poc` (or sibling) for spoofed `GetLauncherOrigin` — the case must **fail closed**, not document a working deputy. - `cargo test -p cl8y-community-tax-token` and `-p cl8y-community-token-launcher` (including `audit_poc`). - `make verify-issue-606` / catalog greps for O601-4 remain green. - Manual LocalTerra: free create via launcher still Enable Feature; a hand instantiate with copied launcher field cannot unlock via official launcher. ## Out of scope - Pair AMM / limit-book drains. - Changing SKU prices or mixing Enable Feature into settings batches. - Factory delist of the tax code_id (mitigation is origin, not unlist). - Ops Coolify / host work (migrate is on-chain 2-of-3 only, named in AC9). - Reopening #606 / #592 / #626 for this origin bug. ## First-pass model recommendation Recommendation: grok-high Rationale: CosmWasm token + launcher auth (instantiate origin, invoice deputy, AutoLP bind). Founder-required surface (contracts / wasm / wallet 2-of-3 migrate). Crosses `community-tax-token` instantiate/query/invoice and `community-token-launcher` enable/reply state, plus catalog attestation. Not a local three-file edit; Composer criteria fail on security, wasm, and cross-crate protocol. Verify with crate tests + inverted origin case + #606 regression, then 2-of-3 migrate — not UI-only.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#1229
No description provided.