feat: V3 Grid vault + permissionless Rebalance (from #546) #617

Open
opened 2026-08-24 03:15:15 +00:00 by PlasticDigits · 11 comments
PlasticDigits commented 2026-08-24 03:15:15 +00:00 (Migrated from gitlab.com)

Summary

Implement the V3 Grid vault: a pair-scoped CosmWasm strategy that owns resting limits, flips them buy↔sell with a user-set spread and no drift, and compounds leftover inventory on a permissionless Rebalance. The crank caller may take 5% of accrued grid fees as a tip. This is the protocol half of #546 option E (architecture item 5).

Not this issue: keeper binary (grid-keeper package), /v3 UI, APR chrome. Those are sibling issues linked from #546.

Parent decision: #546.

Current codebase

The DEX already has a one-shot, one-sided limit ladder. It does not keep a maker in a range after fills. Filled makers receive the other token in their wallet; nothing re-escrows it on the opposite side. There is no grid/range strategy object and no vault that owns orders.

Layer Behavior today
Place Cw20HookMsg::PlaceLimitOrderBatch / PlaceLimitOrderLadder. Ladder expands on-chain (equal only). One side per tx — bid escrows token1, ask escrows token0. Mixed buy+sell grids need two placements.
Rung caps Pair max_batch_rungs (factory default / SetPairLimitBatchMax). Hard ceiling MAX_LIMIT_BATCH_RUNGS_HARD_CAP = 100.
Fill Hybrid execute_swap walks the book under max_maker_fills (hard cap 100) and MAX_SCAN_STEPS (500) — L5. Maker payouts are deferred CW20 transfers to the order owner at the end of the swap (#248). No opposite-side place.
Fees Maker pays half of effective pair fee at placement from escrow (I13 can zero the place half at tier 9). Taker half is charged on fill. A flip cycle pays placement fee again on each new order. UpdateLimitOrderPrice does not re-charge place fee, but cannot change side or size.
Discount subject Placement GetDiscount uses the order owner. A vault that Sends CW20 is the owner — the depositor’s CL8Y tier does not apply unless the vault itself is registered / holds CL8Y.
Cancel / claim Owner-only. Batch cancel/claim up to max_batch_rungs (#246). Pause blocks place/cancel/claim (L6).
Custody oracle OrderStatus → Active / ParkedRefund / Unknown (L21). Indexer lifecycle_status=active is not proof the row is still in ORDERS (#530).
Closest cousin Community-tax AutoLP (community-tax-autolp) is a permissionless SkimToLp crank into v2 LP shares. It is not a limit grid and must not be reused as the V3 Grid vault.
Not present No grid vault, no factory CreateGrid, no Rebalance, no crank tip, no “on fill, place opposite” hook, no in-swap flip.

Product confusion to avoid: this is maker inventory on the FIFO limit book, not v2 AMM LP (provide_liquidity) and not /ust1 mint (#531).

Why this is needed

  1. Manual ladders die after the first wave of fills. The actual MM loop is flip + recycle inventory. Retail cannot run that loop from /limits.
  2. A hosted hot-wallet or generic authz MsgExecuteContract grant can cancel and sweep the grid. A vault that is the order owner is the only non-custodial “set and forget” path accepted on #546.
  3. In-swap auto-flip is rejected (#546 option C): it taxes takers and can break L5.
  4. Inventory growth (spread profit) must be compounded by Rebalance, not a user Claim / Reinvest flow.

Constraints / guardrails

  1. Do not put maker re-place on the taker hot path. No extra insert_bid / insert_ask inside execute_swap or the max_maker_fills walk.
  2. Keep L5 / L6 / L14 / L17 / L20 / L21 / L22 / I13. Pause still blocks place/cancel/claim. Hints stay advisory. Crossing stays allowed on-chain; the vault must use post-only flip prices (client-equivalent of #297 / #385). Human vs raw prices unchanged (L20). OrderStatus remains the custody oracle.
  3. One side per existing pair batch. A two-sided re-arm is two pair messages (or two submsgs), not a silent mixed-side batch.
  4. Do not change pool math, wrap fees, or treasury. Pair place-fee still goes to the pair treasury. The 5% tip is taken from vault-accrued grid fees (realized spread surplus), not from pair fee_bps.
  5. No user Claim / Reinvest / harvest execute. Exit is Withdraw (cancel remaining, return both tokens). Surplus is redeployed on Rebalance.
  6. Crank tip is optional and capped. Rebalance { claim_tip: bool }. If true, pay floor(unclaimed_grid_fees * 500 / 10000) (500 bps = 5%) to info.sender in the fee asset(s). Remainder stays in the position. Do not mint unbacked limits to pay the tip (fail closed).
  7. Work cap per Rebalance. Independent of max_maker_fills. Bound inserts, CW20 sends, cancel/claim ids (≤ pair max_batch_rungs, hard cap 100). Cheap no-op when nothing to do (A15).
  8. Blacklist / pause. Vault queries factory BlacklistCheck for the beneficiary on deposit / withdraw / param update / Rebalance. Pair pause → no-op or revert cheaply; do not retry-loop. Blacklisted beneficiary: no withdraw, no new place; do not keep flipping.
  9. No dApp key custody. Vault does not hold a user mnemonic. Official keeper is a separate package (sibling issue).
  10. Do not build Uniswap-v3 ticks or mint AMM LP from this vault.
  11. No farm / incentive program copy or emissions in the contract.
  12. LocalTerra for gas. Do not report SKIP (no LocalTerra) without make setup-cloud-localterra.
  13. Never bind-mount indexer/ into root Docker cargo (see AGENTS.md § Rust / Docker gotchas).

Relevant files

File Role
smartcontracts/packages/dex-common/src/pair.rs PlaceLimitOrderBatch / PlaceLimitOrderLadder, hybrid params
smartcontracts/packages/dex-common/src/limit_placement.rs Ladder expand, hard cap 100, L20 price band
smartcontracts/contracts/pair/src/limit_placement.rs Batch execute; discount vs owner
smartcontracts/contracts/pair/src/orderbook.rs Insert/match; deferred maker sends
smartcontracts/contracts/pair/src/contract.rs execute_swap, OrderStatus, pause
smartcontracts/contracts/pair/src/limit_batch_withdraw.rs Batch cancel/claim
smartcontracts/contracts/community-tax-autolp/ Permissionless crank pattern only (not the product)
smartcontracts/contracts/factory/src/contract.rs Pair listing, blacklist, code ids
docs/limit-orders.md Messages, gas, park reasons
docs/contracts-security-audit.md L5–L22, I13
docs/reference/fee-discount-tiers.md Place-fee vs tier (I13)
frontend-dapp/src/services/terraclassic/terraGas.ts dApp gas envelopes (G1–G6 baseline)
smartcontracts/Cargo.toml Workspace members — add the new crate(s) here
skills/AGENTS_LIMIT_ORDER_BATCH_LADDER.md One side per batch, hints, crossing
  1. New crate smartcontracts/contracts/grid-vault (product name V3 Grid; crate name stays grid-vault). Optional thin grid-factory in the same issue if instantiate-per-(beneficiary, pair) needs an index. Do not reuse AutoLP.
  2. One vault instance per (beneficiary, pair). Vault address is the limit owner. Isolation for cancel blast radius and a path to park via blacklisting that vault if ops ever need L19 on the book. Factory (or grid-factory) records code_id, listed pair, beneficiary; paginated Vaults { beneficiary } / AllVaults for the keeper.
  3. Position params (set on create / update, beneficiary-only): factory-listed pair; human range [P_low, P_high]; spread 1–2500 bps; rung count (≥ 2, ≤ pair max_batch_rungs); equal inventory split unless a later spec says otherwise. Reject spread 0 or > 2500.
  4. E2 no-drift flip: buy fill at human price P → next ask at P * (1 + spread_bps/10000); that ask fill → next bid back at P, not P*(1+s)*(1-s). Prices must stay inside [P_low, P_high] and inside L20. If the flip would cross the live book, skip that rung (post-only); do not take the vault through the spread.
  5. Rebalance (permissionless):
    • Load live ids; LCD-equivalent OrderStatus / pair queries (not indexer).
    • ClaimExpiredLimitOrders for vault-owned parks (L22 — do not treat parked dust as live inventory).
    • For each filled (or Unknown + not in ORDERS) rung, compute the paired opposite and place using received inventory + idle balances, after reserving the optional tip.
    • Compound leftover (realized spread minus tip) into new or existing rungs — “good enough” allocation, not a perfect optimizer.
    • Two one-sided pair batches max per crank if both sides need places.
    • claim_tip = true → pay 5% of unclaimed realized grid fees to caller; write unclaimed -= paid.
    • Emit columnar wasm attrs (action=grid_rebalance, position/vault, flips, tip_paid, fees_remaining) so the indexer can ingest without wasm_attr_last.
  6. Fee accounting: snapshot inventory vs outstanding escrow after each completed ping-pong. unclaimed_grid_fees is only realized surplus (completed buy+sell at the paired prices), never an optimistic mark. Tip cannot exceed unclaimed_grid_fees or spend escrow needed to keep in-range rungs live.
  7. Deposit / Withdraw: beneficiary deposits both CW20s (allowance + send or hook). Withdraw cancels remaining ids (batch), claims parks, returns all free + refunded balances to the beneficiary. No partial “claim profits only.”
  8. I13 honesty: document that place discount is the vault’s GetDiscount. Do not spoof the depositor as trader on the pair. UI/APR sibling may show the user’s tier as an estimate; execution stays vault-address. Optional later: vault holds/registers CL8Y — out of band, not required to ship the vault.
  9. Gas appendix (moves #546 G1–G6 here): measure LocalTerra gas_used for existing ladder G1–G4/G6 and vault deposit + first grid + Rebalance after 1 fill + tip path. Compare to terraGas.ts. Paper-only estimate for in-swap flip (G5) — do not prototype on main.
  10. Playbook + invariant table in docs/contracts-terraclassic.md + docs/contracts-security-audit.md (new G-series). make verify-issue-<iid> greps the note + runs crate tests.

Acceptance criteria

  • AC1 — Vault crate. grid-vault (and factory index if used) is a workspace member; instantiate binds beneficiary + factory-listed pair only.
  • AC2 — Initial grid. Deposit + create places two one-sided batches (bids below / asks above) using existing pair msgs. No mixed-side pair message.
  • AC3 — E2 flip. After a bid fill at P, Rebalance places an ask at P*(1+s) (human, L20). After that ask fills, the next bid is P. No multiplicative drift.
  • AC4 — Compound. Realized surplus remaining after an optional tip is escrowed on subsequent rungs. There is no Claim / Reinvest execute.
  • AC5 — Optional 5% tip. claim_tip=false pays 0. claim_tip=true pays exactly 500 bps of unclaimed_grid_fees (integer floor) to info.sender and reduces the pot. Cannot pay more than the pot; cannot unback live rungs.
  • AC6 — Permissionless + bounded. Anyone may call Rebalance. Work and CW20 sends are capped. Second call with no fills is a cheap no-op.
  • AC7 — Taker-safety. Pair execute_swap is unchanged (no in-swap insert). L5 still holds.
  • AC8 — Pause / blacklist / park. Pause and beneficiary blacklist block place/withdraw/re-arm as specified. Dust parks use reason (L22); flip must not re-place from parked dust.
  • AC9 — Withdraw. Beneficiary-only; cancels + claims; returns both tokens; vault idle = 0 for that position.
  • AC10 — Tests + verify. Multitest coverage below; make verify-issue-<iid>; LocalTerra gas table attached or in docs.
  • AC11 — Copy boundary. Docs say V3 Grid / book escrow, not “LP shares” or an incentive program.

Test plan (all paths)

# Path Assert
T1 Instantiate with non-factory pair Revert
T2 Deposit + create 5+5 grid Two pair txs/submsgs; both sides rest; owner = vault
T3 Mixed-side smuggle into one pair batch Still impossible (pair unchanged)
T4 Taker fills 1 bid Maker payout to vault; OrderStatus not Active
T5 Rebalance after T4 Opposite ask at P*(1+s); bid slot empty at P
T6 Taker fills that ask Next Rebalance restores bid at P (no drift)
T7 Partial fill Only filled notional flips; remainder stays on the same side
T8 claim_tip=true with pot > 0 Caller balance += 5%; pot decreases; rungs still backed
T9 claim_tip=true with pot = 0 No extra CW20 to caller; flip still happens if inventory allows
T10 claim_tip=true pot < 1 unit after floor Pay 0; no revert
T11 Idle Rebalance Cheap no-op; no new order ids
T12 Withdraw mid-grid All vault-owned ids cancelled/claimed; beneficiary receives both tokens
T13 Non-beneficiary withdraw / update Revert
T14 Pair paused Rebalance / deposit place / withdraw-cancel blocked or no-op per L6; no retry burn
T15 Beneficiary blacklisted Deposit / withdraw / re-arm place blocked
T16 Dust park (L22) Rebalance claims; does not place a dust opposite
T17 Flip would cross mid Rung skipped (post-only); no self-trade
T18 UST1/USTR-style decimals Range + flip use L20 human band
T19 Spread 0 / 2501 Reject
T20 Existing place_limit_order_ladder / fill tests Still pass (pair unchanged)

LocalTerra

# Path Assert
M1 G1–G4, G6 baselines on a deployed pair gas_used recorded vs terraGas.ts
M2 Vault deposit + 5+5 open Both sides on limit-book
M3 Hybrid fill ≥1 vault bid + Rebalance Opposite live; tx hash + gas_used
M4 Tip path Caller receives CW20; not LUNC (LUNC swap is the keeper issue)
M5 Pause Vault cannot re-place

Test plan — attack, hack, and abuse vectors

# Vector What to prove
A1 Taker griefing Filling many tiny vault rungs does not increase swap gas. Flip is only on Rebalance.
A2 Cross-side batch Vault never builds a mixed-side PlaceLimitOrderBatch.
A3 Crossing / self-trade Flip prices are post-only vs book head (and ref when a side is empty).
A4 Hint poisoning Vault-supplied hint_after_order_id cannot reorder the book (L14).
A5 Tip drain Caller cannot set tip bps; 5% is hardcoded. Cannot claim twice on the same pot. Cannot tip from escrow.
A6 Unbacked place Rebalance fail-closed if inventory < place-fee + escrow + reserved tip.
A7 Reentrancy CW20 send → pair hook → Rebalance re-entry locked (AutoLP SKIMMING pattern). Owner of orders is the vault, never info.sender of the crank.
A8 Authz / key theft No authz path. Stealing the beneficiary key = withdraw that vault only. Stealing a keeper key = they can only call Rebalance (tip at most 5% of realized fees), not withdraw.
A9 Indexer lie Vault must not trust indexer; on-chain pair queries only.
A10 MEV on re-arm Public mempool; delayed opposite can be picked off (#299). Document; post-only reduces self-take. No private relay.
A11 Blacklist / pause loop Mid-grid blacklist does not burn crank gas in a hot loop.
A12 Fee drain (place-fee) Tight spread vs 90 bps unregistered place can grind inventory. Enforce a min spread vs vault effective place-fee (or document + hard-fail when the next place cannot clear the fee).
A13 Range breakout Price leaves [P_low, P_high]; do not convert leftover to AMM LP. Idle inventory waits; withdraw returns it.
A14 Decimal dust 6 vs 18; leftover below min_remaining_* stays idle, not a ghost rung.
A15 DoS crank Permissionless Rebalance with nothing to do is cheap. Attacker pays their own gas; cannot lock the book.
A16 Factory spoof Instantiate / update pair must be factory-listed (same spirit as M610-1). Immutable factory pointer.
A17 Beneficiary spoof Crank cannot change beneficiary or withdraw.
A18 Share / accounting One beneficiary per vault. No hidden second owner.

Verification criteria

  1. Human review can accept the vault without reading a pair execute_swap diff (there should be none).
  2. Gas appendix: G1–G4, G6 + vault open/Rebalance/withdraw with gas_used and LocalTerra tx hashes.
  3. Invariant checklist: L5, L6, L14, L17, L20, L21, L22, I13 marked unchanged.
  4. make verify-issue-<iid> runs crate tests + doc greps (AC headings, 500 bps tip, no in-swap flip, E2 no-drift).
  5. Sibling keeper / /v3 issues are not blocked on UI, but they are blocked on message shapes and events from this issue.

Out of scope

  • grid-keeper package and tip→LUNC swap.
  • /v3 page, APR estimates, Pool/Limits links.
  • User Claim / Reinvest / Rebalance buttons.
  • Uniswap-v3 ticks, v2 LP mint, incentive programs.
  • Changing max_maker_fills, mixed-side batch, or taker swap envelopes.
  • Hosted authz keeper.
  • In-swap auto-flip.
## Summary Implement the **V3 Grid vault**: a pair-scoped CosmWasm strategy that owns resting limits, flips them **buy↔sell with a user-set spread and no drift**, and compounds leftover inventory on a **permissionless `Rebalance`**. The crank caller **may** take **5% of accrued grid fees** as a tip. This is the protocol half of [#546](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/546) option **E** (architecture item 5). **Not this issue:** keeper binary (`grid-keeper` package), `/v3` UI, APR chrome. Those are sibling issues linked from #546. Parent decision: [#546](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/546). ## Current codebase The DEX already has a **one-shot, one-sided** limit ladder. It does **not** keep a maker in a range after fills. Filled makers receive the other token **in their wallet**; nothing re-escrows it on the opposite side. There is **no** grid/range strategy object and **no** vault that owns orders. | Layer | Behavior today | |-------|----------------| | **Place** | `Cw20HookMsg::PlaceLimitOrderBatch` / `PlaceLimitOrderLadder`. Ladder expands on-chain (`equal` only). **One side per tx** — bid escrows token1, ask escrows token0. Mixed buy+sell grids need **two** placements. | | **Rung caps** | Pair `max_batch_rungs` (factory default / `SetPairLimitBatchMax`). Hard ceiling `MAX_LIMIT_BATCH_RUNGS_HARD_CAP` = **100**. | | **Fill** | Hybrid `execute_swap` walks the book under `max_maker_fills` (hard cap **100**) and `MAX_SCAN_STEPS` (500) — **L5**. Maker payouts are **deferred CW20 transfers to the order owner** at the end of the swap ([#248](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/248)). **No opposite-side place.** | | **Fees** | Maker pays **half** of effective pair fee at **placement** from escrow (**I13** can zero the place half at tier 9). Taker half is charged on fill. A flip cycle pays **placement fee again** on each new order. `UpdateLimitOrderPrice` does **not** re-charge place fee, but **cannot change side or size**. | | **Discount subject** | Placement `GetDiscount` uses the **order owner**. A vault that `Send`s CW20 is the owner — **the depositor’s CL8Y tier does not apply** unless the vault itself is registered / holds CL8Y. | | **Cancel / claim** | Owner-only. Batch cancel/claim up to `max_batch_rungs` ([#246](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/246)). Pause blocks place/cancel/claim (**L6**). | | **Custody oracle** | `OrderStatus` → `Active` / `ParkedRefund` / `Unknown` (**L21**). Indexer `lifecycle_status=active` is **not** proof the row is still in `ORDERS` ([#530](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/530)). | | **Closest cousin** | Community-tax **AutoLP** (`community-tax-autolp`) is a permissionless `SkimToLp` crank into **v2 LP shares**. It is not a limit grid and must not be reused as the V3 Grid vault. | | **Not present** | No grid vault, no factory `CreateGrid`, no `Rebalance`, no crank tip, no “on fill, place opposite” hook, no in-swap flip. | **Product confusion to avoid:** this is **maker inventory on the FIFO limit book**, not v2 AMM LP (`provide_liquidity`) and not `/ust1` mint ([#531](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/531)). ## Why this is needed 1. Manual ladders die after the first wave of fills. The actual MM loop is flip + recycle inventory. Retail cannot run that loop from `/limits`. 2. A hosted hot-wallet or generic `authz` `MsgExecuteContract` grant can **cancel and sweep** the grid. A vault that is the order `owner` is the only non-custodial “set and forget” path accepted on #546. 3. In-swap auto-flip is **rejected** (#546 option C): it taxes takers and can break **L5**. 4. Inventory growth (spread profit) must be **compounded by `Rebalance`**, not a user Claim / Reinvest flow. ## Constraints / guardrails 1. **Do not put maker re-place on the taker hot path.** No extra `insert_bid` / `insert_ask` inside `execute_swap` or the `max_maker_fills` walk. 2. **Keep L5 / L6 / L14 / L17 / L20 / L21 / L22 / I13.** Pause still blocks place/cancel/claim. Hints stay advisory. Crossing stays allowed on-chain; the **vault** must use post-only flip prices (client-equivalent of [#297](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/297) / [#385](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/385)). Human vs raw prices unchanged (**L20**). `OrderStatus` remains the custody oracle. 3. **One side per existing pair batch.** A two-sided re-arm is two pair messages (or two submsgs), not a silent mixed-side batch. 4. **Do not change pool math, wrap fees, or treasury.** Pair place-fee still goes to the pair treasury. The 5% tip is taken from **vault-accrued grid fees** (realized spread surplus), not from pair `fee_bps`. 5. **No user Claim / Reinvest / harvest execute.** Exit is **Withdraw** (cancel remaining, return both tokens). Surplus is redeployed on `Rebalance`. 6. **Crank tip is optional and capped.** `Rebalance { claim_tip: bool }`. If `true`, pay `floor(unclaimed_grid_fees * 500 / 10000)` (**500 bps = 5%**) to `info.sender` in the fee asset(s). Remainder stays in the position. Do not mint unbacked limits to pay the tip (**fail closed**). 7. **Work cap per `Rebalance`.** Independent of `max_maker_fills`. Bound inserts, CW20 sends, cancel/claim ids (≤ pair `max_batch_rungs`, hard cap 100). Cheap no-op when nothing to do (**A15**). 8. **Blacklist / pause.** Vault queries factory `BlacklistCheck` for the **beneficiary** on deposit / withdraw / param update / `Rebalance`. Pair pause → no-op or revert cheaply; do not retry-loop. Blacklisted beneficiary: no withdraw, no new place; do not keep flipping. 9. **No dApp key custody.** Vault does not hold a user mnemonic. Official keeper is a **separate package** (sibling issue). 10. **Do not build Uniswap-v3 ticks** or mint AMM LP from this vault. 11. **No farm / incentive program** copy or emissions in the contract. 12. **LocalTerra** for gas. Do not report `SKIP (no LocalTerra)` without `make setup-cloud-localterra`. 13. **Never bind-mount `indexer/` into root Docker cargo** (see `AGENTS.md` § Rust / Docker gotchas). ## Relevant files | File | Role | |------|------| | [`smartcontracts/packages/dex-common/src/pair.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/smartcontracts/packages/dex-common/src/pair.rs) | `PlaceLimitOrderBatch` / `PlaceLimitOrderLadder`, hybrid params | | [`smartcontracts/packages/dex-common/src/limit_placement.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/smartcontracts/packages/dex-common/src/limit_placement.rs) | Ladder expand, hard cap 100, **L20** price band | | [`smartcontracts/contracts/pair/src/limit_placement.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/smartcontracts/contracts/pair/src/limit_placement.rs) | Batch execute; discount vs **owner** | | [`smartcontracts/contracts/pair/src/orderbook.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/smartcontracts/contracts/pair/src/orderbook.rs) | Insert/match; deferred maker sends | | [`smartcontracts/contracts/pair/src/contract.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/smartcontracts/contracts/pair/src/contract.rs) | `execute_swap`, `OrderStatus`, pause | | [`smartcontracts/contracts/pair/src/limit_batch_withdraw.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/smartcontracts/contracts/pair/src/limit_batch_withdraw.rs) | Batch cancel/claim | | [`smartcontracts/contracts/community-tax-autolp/`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/smartcontracts/contracts/community-tax-autolp/) | Permissionless crank **pattern only** (not the product) | | [`smartcontracts/contracts/factory/src/contract.rs`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/smartcontracts/contracts/factory/src/contract.rs) | Pair listing, blacklist, code ids | | [`docs/limit-orders.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/limit-orders.md) | Messages, gas, park reasons | | [`docs/contracts-security-audit.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/contracts-security-audit.md) | L5–L22, I13 | | [`docs/reference/fee-discount-tiers.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/docs/reference/fee-discount-tiers.md) | Place-fee vs tier (**I13**) | | [`frontend-dapp/src/services/terraclassic/terraGas.ts`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/frontend-dapp/src/services/terraclassic/terraGas.ts) | dApp gas envelopes (G1–G6 baseline) | | [`smartcontracts/Cargo.toml`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/smartcontracts/Cargo.toml) | Workspace members — add the new crate(s) here | | [`skills/AGENTS_LIMIT_ORDER_BATCH_LADDER.md`](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/blob/main/skills/AGENTS_LIMIT_ORDER_BATCH_LADDER.md) | One side per batch, hints, crossing | ## Recommended direction 1. **New crate** `smartcontracts/contracts/grid-vault` (product name **V3 Grid**; crate name stays `grid-vault`). Optional thin **`grid-factory`** in the same issue if instantiate-per-(beneficiary, pair) needs an index. Do **not** reuse AutoLP. 2. **One vault instance per (beneficiary, pair).** Vault address is the limit `owner`. Isolation for cancel blast radius and a path to park via blacklisting that vault if ops ever need **L19** on the book. Factory (or grid-factory) records `code_id`, listed pair, beneficiary; paginated `Vaults { beneficiary }` / `AllVaults` for the keeper. 3. **Position params (set on create / update, beneficiary-only):** factory-listed pair; human range `[P_low, P_high]`; **spread 1–2500 bps**; rung count (≥ 2, ≤ pair `max_batch_rungs`); equal inventory split unless a later spec says otherwise. Reject spread `0` or `> 2500`. 4. **E2 no-drift flip:** buy fill at human price `P` → next ask at `P * (1 + spread_bps/10000)`; that ask fill → next bid back at **`P`**, not `P*(1+s)*(1-s)`. Prices must stay inside `[P_low, P_high]` and inside **L20**. If the flip would **cross** the live book, skip that rung (post-only); do not take the vault through the spread. 5. **`Rebalance` (permissionless):** - Load live ids; LCD-equivalent `OrderStatus` / pair queries (not indexer). - `ClaimExpiredLimitOrders` for vault-owned parks (**L22** — do not treat parked dust as live inventory). - For each filled (or `Unknown` + not in `ORDERS`) rung, compute the paired opposite and place using **received inventory + idle balances**, after reserving the optional tip. - Compound leftover (realized spread minus tip) into new or existing rungs — “good enough” allocation, not a perfect optimizer. - Two one-sided pair batches max per crank if both sides need places. - `claim_tip = true` → pay 5% of **unclaimed realized grid fees** to caller; write `unclaimed -= paid`. - Emit columnar wasm attrs (`action=grid_rebalance`, position/vault, flips, tip_paid, fees_remaining) so the indexer can ingest without `wasm_attr_last`. 6. **Fee accounting:** snapshot inventory vs outstanding escrow after each completed ping-pong. `unclaimed_grid_fees` is only **realized** surplus (completed buy+sell at the paired prices), never an optimistic mark. Tip cannot exceed `unclaimed_grid_fees` or spend escrow needed to keep in-range rungs live. 7. **Deposit / Withdraw:** beneficiary deposits both CW20s (allowance + send or hook). Withdraw cancels remaining ids (batch), claims parks, returns **all** free + refunded balances to the beneficiary. No partial “claim profits only.” 8. **I13 honesty:** document that place discount is the **vault’s** `GetDiscount`. Do **not** spoof the depositor as `trader` on the pair. UI/APR sibling may show the user’s tier as an estimate; execution stays vault-address. Optional later: vault holds/registers CL8Y — out of band, not required to ship the vault. 9. **Gas appendix (moves #546 G1–G6 here):** measure LocalTerra `gas_used` for existing ladder G1–G4/G6 **and** vault deposit + first grid + `Rebalance` after 1 fill + tip path. Compare to `terraGas.ts`. Paper-only estimate for in-swap flip (G5) — do not prototype on `main`. 10. **Playbook + invariant table** in `docs/contracts-terraclassic.md` + `docs/contracts-security-audit.md` (new G-series). `make verify-issue-<iid>` greps the note + runs crate tests. ## Acceptance criteria - [ ] **AC1 — Vault crate.** `grid-vault` (and factory index if used) is a workspace member; instantiate binds beneficiary + factory-listed pair only. - [ ] **AC2 — Initial grid.** Deposit + create places two one-sided batches (bids below / asks above) using existing pair msgs. No mixed-side pair message. - [ ] **AC3 — E2 flip.** After a bid fill at `P`, `Rebalance` places an ask at `P*(1+s)` (human, **L20**). After that ask fills, the next bid is **`P`**. No multiplicative drift. - [ ] **AC4 — Compound.** Realized surplus remaining after an optional tip is escrowed on subsequent rungs. There is **no** `Claim` / `Reinvest` execute. - [ ] **AC5 — Optional 5% tip.** `claim_tip=false` pays 0. `claim_tip=true` pays exactly 500 bps of `unclaimed_grid_fees` (integer floor) to `info.sender` and reduces the pot. Cannot pay more than the pot; cannot unback live rungs. - [ ] **AC6 — Permissionless + bounded.** Anyone may call `Rebalance`. Work and CW20 sends are capped. Second call with no fills is a cheap no-op. - [ ] **AC7 — Taker-safety.** Pair `execute_swap` is unchanged (no in-swap insert). **L5** still holds. - [ ] **AC8 — Pause / blacklist / park.** Pause and beneficiary blacklist block place/withdraw/re-arm as specified. Dust parks use `reason` (**L22**); flip must not re-place from parked dust. - [ ] **AC9 — Withdraw.** Beneficiary-only; cancels + claims; returns both tokens; vault idle = 0 for that position. - [ ] **AC10 — Tests + verify.** Multitest coverage below; `make verify-issue-<iid>`; LocalTerra gas table attached or in docs. - [ ] **AC11 — Copy boundary.** Docs say **V3 Grid** / book escrow, not “LP shares” or an incentive program. ## Test plan (all paths) | # | Path | Assert | |---|------|--------| | T1 | Instantiate with non-factory pair | Revert | | T2 | Deposit + create 5+5 grid | Two pair txs/submsgs; both sides rest; owner = vault | | T3 | Mixed-side smuggle into one pair batch | Still impossible (pair unchanged) | | T4 | Taker fills 1 bid | Maker payout to **vault**; `OrderStatus` not `Active` | | T5 | `Rebalance` after T4 | Opposite ask at `P*(1+s)`; bid slot empty at `P` | | T6 | Taker fills that ask | Next `Rebalance` restores bid at **`P`** (no drift) | | T7 | Partial fill | Only filled notional flips; remainder stays on the same side | | T8 | `claim_tip=true` with pot > 0 | Caller balance += 5%; pot decreases; rungs still backed | | T9 | `claim_tip=true` with pot = 0 | No extra CW20 to caller; flip still happens if inventory allows | | T10 | `claim_tip=true` pot < 1 unit after floor | Pay 0; no revert | | T11 | Idle `Rebalance` | Cheap no-op; no new order ids | | T12 | Withdraw mid-grid | All vault-owned ids cancelled/claimed; beneficiary receives both tokens | | T13 | Non-beneficiary withdraw / update | Revert | | T14 | Pair paused | `Rebalance` / deposit place / withdraw-cancel blocked or no-op per **L6**; no retry burn | | T15 | Beneficiary blacklisted | Deposit / withdraw / re-arm place blocked | | T16 | Dust park (**L22**) | `Rebalance` claims; does not place a dust opposite | | T17 | Flip would cross mid | Rung skipped (post-only); no self-trade | | T18 | UST1/USTR-style decimals | Range + flip use **L20** human band | | T19 | Spread 0 / 2501 | Reject | | T20 | Existing `place_limit_order_ladder` / fill tests | Still pass (pair unchanged) | ### LocalTerra | # | Path | Assert | |---|------|--------| | M1 | G1–G4, G6 baselines on a deployed pair | `gas_used` recorded vs `terraGas.ts` | | M2 | Vault deposit + 5+5 open | Both sides on `limit-book` | | M3 | Hybrid fill ≥1 vault bid + `Rebalance` | Opposite live; tx hash + `gas_used` | | M4 | Tip path | Caller receives CW20; not LUNC (LUNC swap is the keeper issue) | | M5 | Pause | Vault cannot re-place | ## Test plan — attack, hack, and abuse vectors | # | Vector | What to prove | |---|--------|----------------| | A1 | **Taker griefing** | Filling many tiny vault rungs does not increase **swap** gas. Flip is only on `Rebalance`. | | A2 | **Cross-side batch** | Vault never builds a mixed-side `PlaceLimitOrderBatch`. | | A3 | **Crossing / self-trade** | Flip prices are post-only vs book head (and ref when a side is empty). | | A4 | **Hint poisoning** | Vault-supplied `hint_after_order_id` cannot reorder the book (**L14**). | | A5 | **Tip drain** | Caller cannot set tip bps; 5% is hardcoded. Cannot claim twice on the same pot. Cannot tip from escrow. | | A6 | **Unbacked place** | `Rebalance` fail-closed if inventory < place-fee + escrow + reserved tip. | | A7 | **Reentrancy** | CW20 send → pair hook → `Rebalance` re-entry locked (AutoLP `SKIMMING` pattern). Owner of orders is the vault, never `info.sender` of the crank. | | A8 | **Authz / key theft** | No authz path. Stealing the beneficiary key = withdraw that vault only. Stealing a keeper key = they can only call `Rebalance` (tip at most 5% of **realized** fees), not withdraw. | | A9 | **Indexer lie** | Vault must not trust indexer; on-chain pair queries only. | | A10 | **MEV on re-arm** | Public mempool; delayed opposite can be picked off ([#299](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/299)). Document; post-only reduces self-take. No private relay. | | A11 | **Blacklist / pause loop** | Mid-grid blacklist does not burn crank gas in a hot loop. | | A12 | **Fee drain (place-fee)** | Tight spread vs 90 bps unregistered place can grind inventory. Enforce a **min spread vs vault effective place-fee** (or document + hard-fail when the next place cannot clear the fee). | | A13 | **Range breakout** | Price leaves `[P_low, P_high]`; do not convert leftover to AMM LP. Idle inventory waits; withdraw returns it. | | A14 | **Decimal dust** | 6 vs 18; leftover below `min_remaining_*` stays idle, not a ghost rung. | | A15 | **DoS crank** | Permissionless `Rebalance` with nothing to do is cheap. Attacker pays their own gas; cannot lock the book. | | A16 | **Factory spoof** | Instantiate / update pair must be factory-listed (same spirit as **M610-1**). Immutable factory pointer. | | A17 | **Beneficiary spoof** | Crank cannot change beneficiary or withdraw. | | A18 | **Share / accounting** | One beneficiary per vault. No hidden second owner. | ## Verification criteria 1. Human review can accept the vault without reading a pair `execute_swap` diff (there should be **none**). 2. Gas appendix: G1–G4, G6 + vault open/`Rebalance`/withdraw with `gas_used` and LocalTerra tx hashes. 3. Invariant checklist: L5, L6, L14, L17, L20, L21, L22, I13 marked unchanged. 4. `make verify-issue-<iid>` runs crate tests + doc greps (AC headings, 500 bps tip, no in-swap flip, E2 no-drift). 5. Sibling keeper / `/v3` issues are not blocked on UI, but they **are** blocked on message shapes and events from this issue. ## Out of scope - `grid-keeper` package and tip→LUNC swap. - `/v3` page, APR estimates, Pool/Limits links. - User Claim / Reinvest / Rebalance buttons. - Uniswap-v3 ticks, v2 LP mint, incentive programs. - Changing `max_maker_fills`, mixed-side batch, or taker swap envelopes. - Hosted `authz` keeper. - In-swap auto-flip.
PlasticDigits commented 2026-08-24 03:15:17 +00:00 (Migrated from gitlab.com)

marked as related to #546

marked as related to #546
PlasticDigits commented 2026-08-24 03:15:38 +00:00 (Migrated from gitlab.com)

mentioned in issue #618

mentioned in issue #618
PlasticDigits commented 2026-08-24 03:15:39 +00:00 (Migrated from gitlab.com)

marked as related to #618

marked as related to #618
PlasticDigits commented 2026-08-24 03:15:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #619

mentioned in issue #619
PlasticDigits commented 2026-08-24 03:15:47 +00:00 (Migrated from gitlab.com)

marked as related to #619

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

mentioned in issue #546

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

Implementation issue for the #546 decision (option E / architecture item 5).

Siblings (do not implement here):

  • #618 — grid-keeper/ (2× gas + tip→LUNC). Blocked on this issue’s message shapes and fee-pot query.
  • #619 — /v3 product + indexer. Blocked on this issue’s deposit/withdraw/query shapes. No Claim / Reinvest / Rebalance buttons.

Decision note: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/546#note_3722869567

Implementation issue for the #546 decision (option E / architecture item 5). Siblings (do not implement here): - #618 — `grid-keeper/` (2× gas + tip→LUNC). Blocked on this issue’s message shapes and fee-pot query. - #619 — `/v3` product + indexer. Blocked on this issue’s deposit/withdraw/query shapes. No Claim / Reinvest / Rebalance buttons. Decision note: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/546#note_3722869567
PlasticDigits commented 2026-08-25 13:10:20 +00:00 (Migrated from gitlab.com)

mentioned in issue #650

mentioned in issue #650
PlasticDigits commented 2026-08-25 13:10:21 +00:00 (Migrated from gitlab.com)

marked as related to #650

marked as related to #650
PlasticDigits commented 2026-09-01 08:14:36 +00:00 (Migrated from gitlab.com)

mentioned in issue #717

mentioned in issue #717
PlasticDigits commented 2026-09-01 08:14:37 +00:00 (Migrated from gitlab.com)

marked as related to #717

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