fix(community-tax): AutoLP pair must be factory-listed with the tax token + skim floor (M-2, M-3) #610

Closed
opened 2026-08-23 11:49:58 +00:00 by PlasticDigits · 20 comments
PlasticDigits commented 2026-08-23 11:49:58 +00:00 (Migrated from gitlab.com)

Audit INTERNAL_KIMIK3_1787468843 M-2 / M-3. Template #592 T592-10. Bind / SKU init #605 (H-1 — AutoLP is paid and never bound; do not re-do bind here). PoC: poc_autolp_manager_can_skim_to_fake_pair. M-2 has no PoC yet.

Gated on #605: no factory check or skim floor matters until a token has cfg.autolp set. Implement against the AutoLP crate now; ship migrate of 11613 when bind exists.

Product decision (2026-08-23): pair must be factory-listed and have the tax token as one side.


Current codebase

community-tax-autolp:

  • UpdateConfig { pair } only addr_validates. No factory Pair lookup. No check that asset_infos contains cfg.token.
  • SkimToLp is permissionless (T592-10). Sends half the tax-token balance to cfg.pair with Cw20HookMsg::Swap { max_spread: None, min_return: None, hybrid: pool_only }.
  • UpdateConfig merges omitted Option fields (does not wipe). Manager-only.
  • Token apply_autolp_settings errors if cfg.autolp is None. Launcher create_token builds AutolpInit then let _ = (code_id, autolp_init) — bind is #605.

Anyone can sandwich a permissionless no-floor skim. A manager (or stolen manager key) can point pair at a contract that accepts the hook and keeps the tokens.


Why this is needed

AutoLP is sold as “optional skim into a CL8Y pair.” Without a factory+token-side check, skim is an arbitrary Send to a manager-chosen address. Without a floor, permissionless skim is a sandwich. Both waste tax that was supposed to deepen the book.


Constraints / guardrails

  1. T592-10: SkimToLp stays permissionless and is never called from token Transfer / Send or pair AfterSwap.
  2. Do not add pair/router FoT math (H-01).
  3. pair set (instantiate or UpdateConfig) must:
    • succeed factory.Pair { asset_infos } (or equivalent factory query the pair itself reports) for a CL8Y factory pair, and
    • include cfg.token as one of the two asset_infos.
  4. Reject native-only / wrong-token / non-factory contracts.
  5. Skim must pass a floor (min_return and/or max_spread). Do not leave both None on the swap hook.
  6. Manager can still change treasury on the token; this ticket is only AutoLP pair + skim execution.
  7. Reentrancy lock SKIMMING stays. Do not call skim from the pair hook.

Relevant files

  • smartcontracts/contracts/community-tax-autolp/src/contract.rs (execute_update, execute_skim, reply provide)
  • smartcontracts/contracts/community-tax-autolp/src/msg.rs
  • smartcontracts/contracts/community-tax-token/src/invoice.rs (apply_autolp_settings)
  • smartcontracts/contracts/community-token-launcher/src/contract.rs (when #605 instantiates AutoLP)
  • smartcontracts/contracts/community-token-launcher/tests/audit_poc.rs
  • skills/AGENTS_COMMUNITY_TAX_CW20.md (T592-10)

Factory / pair types: dex_common::factory::PairResponse, PairInfo.asset_infos.


  1. AutoLP instantiate stores factory (from token/launcher config) if not already present — needed for the lookup. If the crate has no factory field today, add it on instantiate / UpdateConfig (manager cannot point factory at a fake registry without also passing a real Pair query; prefer immutable factory from launcher).
  2. On pair set: query factory Pair for the two asset infos (token + quote). Quote may be CW20 or native; one side must be cfg.token. Store only the returned contract_addr.
  3. On skim: require configured pair; set min_return from a manager-set basis (new config field, default conservative) or max_spread with a documented cap (e.g. 50–200 bps). Permissionless caller cannot override the floor downward.
  4. Invert poc_autolp_manager_can_skim_to_fake_pair. Add a skim-below-floor revert test.

Acceptance criteria

  • UpdateConfig { pair: fake } (mock that is not factory-listed, or listed but neither asset is the tax token) reverts.
  • UpdateConfig { pair } of a factory pair whose assets are [tax_token, quote] succeeds.
  • Skim to that pair executes with a non-None min_return and/or max_spread.
  • Skim that would violate the floor reverts; tax balance stays on AutoLP.
  • Omitted UpdateConfig fields still merge (existing sanity).
  • poc_autolp_manager_can_skim_to_fake_pair fails or is inverted.
  • T592-10 note: pair must be factory-listed with this token; skim has a floor.

Test plan — all paths

  1. Instantiate AutoLP with pair = None — skim skips / errors as today until pair set.
  2. Set valid factory pair (token as asset 0 or asset 1).
  3. Set pair with token missing → revert.
  4. Set random contract → revert.
  5. Skim below threshold → skip (existing).
  6. Skim above threshold, floor satisfied → swap+provide path (once #605 bind + real pair).
  7. Skim above threshold, floor violated → revert, SKIMMING cleared.
  8. Non-manager UpdateConfig → Unauthorized.
  9. Permissionless SkimToLp still allowed for a valid pair.

Test plan — attack / abuse

  1. Fake pair drain (M-3 PoC): manager points at a hook sink — must revert on set (not only on skim).
  2. Wrong listed pair: factory pair of two other tokens — revert (token must be one side).
  3. Sandwich (M-2): no-floor skim — after fix, attacker moving the pool through the floor reverts the skim; tax remains.
  4. Re-entrancy: pair hook calling SkimToLp again — SKIMMING / lock still holds; do not unlock before messages are queued in a way that allows a second skim in the same tx against a malicious pair (document residual if reply unlocks before provide).
  5. Factory spoof: if factory is manager-settable, manager could deploy a fake factory that returns any pair — prefer immutable factory from launcher/token.
  6. Do not call skim from taxed Transfer (T592-10).

Verification

  • cargo test -p cl8y-community-tax-autolp + inverted audit PoC.
  • After #605 bind: LocalTerra skim against a real CL8Y pair; reject a second CW20 as pair.
  • Docs T592-10 + Manage Token copy (pair must be this token’s CL8Y pool).
## Parent / related Audit [`INTERNAL_KIMIK3_1787468843`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/audits/INTERNAL_KIMIK3_1787468843.md) **M-2 / M-3**. Template [#592](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/592) **T592-10**. Bind / SKU init [#605](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/605) (**H-1** — AutoLP is paid and never bound; **do not re-do bind here**). PoC: `poc_autolp_manager_can_skim_to_fake_pair`. M-2 has no PoC yet. **Gated on #605:** no factory check or skim floor matters until a token has `cfg.autolp` set. Implement against the AutoLP crate now; ship migrate of **11613** when bind exists. **Product decision (2026-08-23):** `pair` must be factory-listed **and** have the tax token as one side. --- ## Current codebase [`community-tax-autolp`](smartcontracts/contracts/community-tax-autolp/src/contract.rs): - `UpdateConfig { pair }` only `addr_validate`s. No factory `Pair` lookup. No check that `asset_infos` contains `cfg.token`. - `SkimToLp` is permissionless (**T592-10**). Sends half the tax-token balance to `cfg.pair` with `Cw20HookMsg::Swap { max_spread: None, min_return: None, hybrid: pool_only }`. - `UpdateConfig` **merges** omitted `Option` fields (does not wipe). Manager-only. - Token `apply_autolp_settings` errors if `cfg.autolp` is None. Launcher `create_token` builds `AutolpInit` then `let _ = (code_id, autolp_init)` — bind is **#605**. Anyone can sandwich a permissionless no-floor skim. A manager (or stolen manager key) can point `pair` at a contract that accepts the hook and keeps the tokens. --- ## Why this is needed AutoLP is sold as “optional skim into a CL8Y pair.” Without a factory+token-side check, skim is an arbitrary Send to a manager-chosen address. Without a floor, permissionless skim is a sandwich. Both waste tax that was supposed to deepen the book. --- ## Constraints / guardrails 1. **T592-10:** `SkimToLp` stays permissionless and is **never** called from token `Transfer` / `Send` or pair `AfterSwap`. 2. Do not add pair/router FoT math (**H-01**). 3. `pair` set (instantiate or `UpdateConfig`) must: - succeed `factory.Pair { asset_infos }` (or equivalent factory query the pair itself reports) for a **CL8Y factory** pair, and - include **`cfg.token` as one of the two `asset_infos`**. 4. Reject native-only / wrong-token / non-factory contracts. 5. Skim must pass a **floor** (`min_return` and/or `max_spread`). Do not leave both `None` on the swap hook. 6. Manager can still change treasury on the **token**; this ticket is only AutoLP pair + skim execution. 7. Reentrancy lock `SKIMMING` stays. Do not call skim from the pair hook. --- ## Relevant files - `smartcontracts/contracts/community-tax-autolp/src/contract.rs` (`execute_update`, `execute_skim`, reply provide) - `smartcontracts/contracts/community-tax-autolp/src/msg.rs` - `smartcontracts/contracts/community-tax-token/src/invoice.rs` (`apply_autolp_settings`) - `smartcontracts/contracts/community-token-launcher/src/contract.rs` (when #605 instantiates AutoLP) - `smartcontracts/contracts/community-token-launcher/tests/audit_poc.rs` - `skills/AGENTS_COMMUNITY_TAX_CW20.md` (T592-10) Factory / pair types: `dex_common::factory::PairResponse`, `PairInfo.asset_infos`. --- ## Recommended direction 1. AutoLP instantiate stores `factory` (from token/launcher config) if not already present — needed for the lookup. If the crate has no factory field today, add it on instantiate / `UpdateConfig` (manager cannot point factory at a fake registry without also passing a real Pair query; prefer **immutable factory** from launcher). 2. On pair set: query factory `Pair` for the two asset infos (token + quote). Quote may be CW20 or native; **one side must be `cfg.token`**. Store only the returned `contract_addr`. 3. On skim: require configured pair; set `min_return` from a manager-set basis (new config field, default conservative) **or** `max_spread` with a documented cap (e.g. 50–200 bps). Permissionless caller cannot override the floor downward. 4. Invert `poc_autolp_manager_can_skim_to_fake_pair`. Add a skim-below-floor revert test. --- ## Acceptance criteria - [ ] `UpdateConfig { pair: fake }` (mock that is not factory-listed, or listed but neither asset is the tax token) **reverts**. - [ ] `UpdateConfig { pair }` of a factory pair whose assets are `[tax_token, quote]` **succeeds**. - [ ] Skim to that pair executes with a non-`None` `min_return` and/or `max_spread`. - [ ] Skim that would violate the floor **reverts**; tax balance stays on AutoLP. - [ ] Omitted `UpdateConfig` fields still merge (existing sanity). - [ ] `poc_autolp_manager_can_skim_to_fake_pair` **fails or is inverted**. - [ ] T592-10 note: pair must be factory-listed with this token; skim has a floor. --- ## Test plan — all paths 1. Instantiate AutoLP with `pair = None` — skim skips / errors as today until pair set. 2. Set valid factory pair (token as asset 0 or asset 1). 3. Set pair with token missing → revert. 4. Set random contract → revert. 5. Skim below threshold → skip (existing). 6. Skim above threshold, floor satisfied → swap+provide path (once #605 bind + real pair). 7. Skim above threshold, floor violated → revert, `SKIMMING` cleared. 8. Non-manager `UpdateConfig` → Unauthorized. 9. Permissionless `SkimToLp` still allowed for a valid pair. --- ## Test plan — attack / abuse 1. **Fake pair drain (M-3 PoC):** manager points at a hook sink — must revert on set (not only on skim). 2. **Wrong listed pair:** factory pair of two other tokens — revert (token must be one side). 3. **Sandwich (M-2):** no-floor skim — after fix, attacker moving the pool through the floor reverts the skim; tax remains. 4. **Re-entrancy:** pair hook calling `SkimToLp` again — `SKIMMING` / lock still holds; do not unlock before messages are queued in a way that allows a second skim in the same tx against a malicious pair (document residual if reply unlocks before provide). 5. **Factory spoof:** if factory is manager-settable, manager could deploy a fake factory that returns any pair — **prefer immutable factory** from launcher/token. 6. **Do not** call skim from taxed `Transfer` (T592-10). --- ## Verification - `cargo test -p cl8y-community-tax-autolp` + inverted audit PoC. - After #605 bind: LocalTerra skim against a real CL8Y pair; reject a second CW20 as `pair`. - Docs T592-10 + Manage Token copy (pair must be this token’s CL8Y pool).
PlasticDigits commented 2026-08-23 11:49:59 +00:00 (Migrated from gitlab.com)

marked as related to #592

marked as related to #592
PlasticDigits commented 2026-08-23 11:50:00 +00:00 (Migrated from gitlab.com)

marked as related to #605

marked as related to #605
PlasticDigits commented 2026-08-23 12:20:39 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1122

mentioned in merge request !1122
PlasticDigits commented 2026-08-23 12:20:41 +00:00 (Migrated from gitlab.com)

mentioned in issue #605

mentioned in issue #605
PlasticDigits commented 2026-08-23 12:41:00 +00:00 (Migrated from gitlab.com)

mentioned in issue #611

mentioned in issue #611
PlasticDigits commented 2026-08-24 00:53:22 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1130

mentioned in merge request !1130
PlasticDigits commented 2026-08-24 00:53:30 +00:00 (Migrated from gitlab.com)

Implemented on issue/610-autolp-factory-pair-skim-floor — MR !1130.

Completed:

  • UpdateConfig { pair: fake } / wrong-token listed pair reverts
  • Factory pair [tax_token, quote] succeeds (asset 0 or 1)
  • Skim always sets max_spread (default 100 bps, cap 200); optional min_return
  • Floor violation reverts; tax stays on AutoLP; SKIMMING rolls back
  • Omitted UpdateConfig fields still merge
  • poc_autolp_manager_can_skim_to_fake_pair inverted
  • T592-10 + M610-1–M610-8 in skills/AGENTS_COMMUNITY_TAX_AUTOLP.md; Manage Token copy

Not in this MR:

  • LocalTerra skim against a live CL8Y pair (multitest covers the paths)
  • CMM migrate of AutoLP 11613 (no instances yet; ops after merge)
Implemented on `issue/610-autolp-factory-pair-skim-floor` — MR !1130. Completed: - [x] `UpdateConfig { pair: fake }` / wrong-token listed pair reverts - [x] Factory pair `[tax_token, quote]` succeeds (asset 0 or 1) - [x] Skim always sets `max_spread` (default 100 bps, cap 200); optional `min_return` - [x] Floor violation reverts; tax stays on AutoLP; `SKIMMING` rolls back - [x] Omitted `UpdateConfig` fields still merge - [x] `poc_autolp_manager_can_skim_to_fake_pair` inverted - [x] T592-10 + **M610-1–M610-8** in `skills/AGENTS_COMMUNITY_TAX_AUTOLP.md`; Manage Token copy Not in this MR: - LocalTerra skim against a live CL8Y pair (multitest covers the paths) - CMM migrate of AutoLP **11613** (no instances yet; ops after merge)
PlasticDigits commented 2026-08-24 02:44:30 +00:00 (Migrated from gitlab.com)

mentioned in commit 77d57d20ea

mentioned in commit 77d57d20eaada15f08db819ad6715c0df6e416de
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-24 02:44:31 +00:00
PlasticDigits commented 2026-08-24 02:45:16 +00:00 (Migrated from gitlab.com)

mentioned in commit 574793b4ef

mentioned in commit 574793b4efcce6069c85b07e63a93d7b0a0c647b
PlasticDigits commented 2026-08-24 03:01:15 +00:00 (Migrated from gitlab.com)

mentioned in issue #616

mentioned in issue #616
PlasticDigits commented 2026-08-24 03:01:16 +00:00 (Migrated from gitlab.com)

marked as related to #616

marked as related to #616
PlasticDigits commented 2026-08-24 03:01:54 +00:00 (Migrated from gitlab.com)

Merge sanity (!1130 landed on main; CI not waited).

Conflicts with !1127 were docs/Makefile only (kept M610 + option-2 wording + #613 verify target).

Automated: make verify-issue-610 8/8 (AutoLP crate, inverted poc_autolp_manager_can_skim_to_fake_pair, launcher factory pin, Manage Token / SKU copy).

Acceptance leftover (ops, not reopen)

  • LocalTerra skim against a live CL8Y pair was not run (multitest covers the paths).
  • CMM migrate of AutoLP 11613 still required when bind exists. No instances at audit; do not whitelist AutoLP.

Post-merge store/skim: #616. Bind/store remains on #611.

**Merge sanity (!1130 landed on `main`; CI not waited).** Conflicts with !1127 were docs/Makefile only (kept M610 + option-2 wording + #613 verify target). Automated: `make verify-issue-610` **8/8** (AutoLP crate, inverted `poc_autolp_manager_can_skim_to_fake_pair`, launcher factory pin, Manage Token / SKU copy). **Acceptance leftover (ops, not reopen)** - LocalTerra skim against a live CL8Y pair was not run (multitest covers the paths). - CMM migrate of AutoLP **11613** still required when bind exists. No instances at audit; do not whitelist AutoLP. Post-merge store/skim: [#616](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/616). Bind/store remains on [#611](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/611).
PlasticDigits commented 2026-08-24 05:53:59 +00:00 (Migrated from gitlab.com)

mentioned in issue #620

mentioned in issue #620
PlasticDigits commented 2026-08-24 05:54:01 +00:00 (Migrated from gitlab.com)

marked as related to #620

marked as related to #620
PlasticDigits commented 2026-08-24 05:54:24 +00:00 (Migrated from gitlab.com)

marked as related to #623

marked as related to #623
PlasticDigits commented 2026-08-24 06:19:37 +00:00 (Migrated from gitlab.com)

mentioned in issue #624

mentioned in issue #624
PlasticDigits commented 2026-08-25 02:04:15 +00:00 (Migrated from gitlab.com)

mentioned in issue #633

mentioned in issue #633
PlasticDigits commented 2026-08-25 02:23:34 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1137

mentioned in merge request !1137
PlasticDigits commented 2026-08-25 02:29:31 +00:00 (Migrated from gitlab.com)

mentioned in issue #635

mentioned in issue #635
PlasticDigits commented 2026-08-25 02:29:32 +00:00 (Migrated from gitlab.com)

marked as related to #635

marked as related to #635
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#610
No description provided.