feat(treasury): 24h InstantWithdrawCw20 pull limit per spender+CW20 #7

Closed
opened 2026-07-31 02:44:31 +00:00 by PlasticDigits · 8 comments
PlasticDigits commented 2026-07-31 02:44:31 +00:00 (Migrated from gitlab.com)

Summary

Add a governance-configurable 24-hour pull limit for treasury InstantWithdrawCw20, keyed by (spender, CW20 token) so each registered spender has an independent rolling (or fixed-window) quota per token it can pull.

This closes the documented v1 gap from #6 / MR !27: “No on-chain CW20 pull cap (v1); window-side limits are the only product control.” Also tracks audit follow-up in audits/INTERNAL_COMPOSER_1785465508.md (H-2 / M-1 / P3).

Companion consumer remains ust1-window#20 (window may keep its own inventory policy; treasury limit is a hard ceiling).


Current codebase

Component Path Behavior today
Spender registry contracts/contracts/treasury/src/state.rs → CW20_SPENDERS Map<&str, Addr>: one spender per token; overwrite on SetCw20Spender. Doc invariant: registered spender may drain full treasury balance of that token.
Pull path contracts/contracts/treasury/src/contract.rs → execute_instant_withdraw_cw20 Checks cw20_iw_paused, zero amount, sender == CW20_SPENDERS[token], treasury CW20 balance ≥ amount; emits Cw20ExecuteMsg::Transfer. No cumulative / time-window accounting.
Admin msgs contracts/contracts/treasury/src/msg.rs SetCw20Spender / RemoveCw20Spender / SetCw20InstantWithdrawPaused / InstantWithdrawCw20; query Cw20Spenders {}.
Pause CW20_INSTANT_WITHDRAW_PAUSED (cw20_iw_paused) Independent of wrapping_paused.
Pattern to reuse contracts/contracts/wrap-mapper/src/{state,contract}.rs Per-denom RateLimitConfig + RateLimitState (max_amount, tumbling window, amount_used / window_start); SetRateLimit / RemoveRateLimit / check_rate_limit.
Docs docs/CONTRACTS.md decision #10, skills/treasury-cw20-instant-withdraw/SKILL.md invariant #4 Explicitly document no on-chain pull cap in v1.

Implication of “per spender + CW20”: Today auth is token→single spender. Limits must still be keyed by (spender, token) so that (a) the same spender registered on multiple tokens has separate quotas, (b) rotating the spender resets or isolates usage under the new identity, and (c) a future multi-spender-per-token model (if pursued) does not require another storage redesign. Prefer composite keys even if v1 keeps “at most one spender per token.”


Why this is needed

  1. Blast-radius control: A buggy or compromised ust1-window (or any registered spender) can currently empty the entire treasury balance of vFDUSD (or any registered CW20) in one or few txs — audit A4 / H-2.
  2. Defense in depth: Window-side inventory caps are necessary but not sufficient; an on-chain treasury ceiling survives consumer bugs and misconfig.
  3. Per-pair isolation: One global or per-token-only cap is insufficient if multiple spenders / tokens exist (e.g. future window + another integrator, or one window across several CW20s). Limits must be individual per (spender, token).
  4. Ops / IR: Gov can lower or zero a pair’s 24h limit without removing the spender or pausing the whole CW20 InstantWithdraw path (which would halt all tokens’ pulls).
  5. Closes the deferred item from #6 acceptance / MR !27 “Not in this MR” checklist.

Without this, mainnet SetCw20Spender remains an all-or-nothing trust grant for the full token balance.


Constraints / guardrails

  1. Do not break existing InstantWithdrawCw20 auth, pause isolation (wrapping_paused ≠ cw20_iw_paused), native wrap InstantWithdraw, or ProposeWithdraw / ExecuteWithdraw.
  2. Keying: Limits and usage MUST be per (spender, token) — not global, not token-only, not spender-only.
  3. Window: 24 hours (86_400 seconds). Document whether the window is tumbling (reset after 24h from window_start, matching wrap-mapper) or calendar UTC; prefer tumbling for consistency with wrap-mapper unless product requires calendar days.
  4. Default when unset: Choose and document one:
    • Recommended: limit == null / absent ⇒ deny pulls until gov sets a limit (fail-closed for new registrations), or
    • Explicit Uint128::MAX / “unlimited” opt-in for parity with v1 during migration.
      Prefer fail-closed for new SetCw20Spender after this feature, with a migrate path that sets an explicit high limit for any pre-existing mapping if needed.
  5. Governance-only to set/update/remove limits. No timelock required (parity with SetCw20Spender / wrap-mapper SetRateLimit), but document ops risk.
  6. Exceeding the remaining 24h quota MUST fail cleanly before emitting Transfer (no partial transfer).
  7. Solvency check (treasury balance ≥ amount) remains; limit is additional.
  8. Zero amount still rejected; zero / removing limit: define semantics (remove ⇒ deny, or remove ⇒ unlimited — pick fail-closed).
  9. Storage namespaces must not collide with cw20_spenders, cw20_whitelist, denom_wrappers, cw20_iw_paused.
  10. No arbitrary WasmMsg; typed msgs only.
  11. In-place migrate preferred (stable treasury address).
  12. Do not require CW20 whitelist for pulls (whitelist remains orthogonal).
  13. Align intended mainnet quota with ust1-window inventory policy (e.g. ~10_000 vFDUSD) via ops docs — exact number is gov-configurable, not hardcoded.

Relevant files

  • contracts/contracts/treasury/src/msg.rs — new execute/query variants; possibly extend SetCw20Spender or add SetCw20SpenderLimit
  • contracts/contracts/treasury/src/state.rs — limit config + usage maps; extend spender value type if needed
  • contracts/contracts/treasury/src/contract.rs — enforce in execute_instant_withdraw_cw20; gov setters; migrate
  • contracts/contracts/treasury/src/error.rs — e.g. Cw20PullLimitExceeded { spender, token, requested, remaining, reset_at }
  • contracts/contracts/wrap-mapper/src/state.rs + contract.rs — reference rate-limit pattern (RateLimitConfig / check_rate_limit)
  • docs/CONTRACTS.md, docs/ARCHITECTURE.md, docs/DEPLOYMENT.md — replace “no on-chain pull cap” with limit semantics + ops
  • skills/treasury-cw20-instant-withdraw/SKILL.md — update invariants (retire “no pull cap v1”)
  • plans/NATIVE_TOKEN_WRAPPING.md — cross-link if it still describes CW20 IW as uncapped
  • Companion (out of repo): ust1-window should treat treasury limit errors as user-facing redeem failure

Mirror wrap-mapper’s rate-limit shape, keyed by composite (spender, token):

# Governance
SetCw20SpenderLimit {
  token: String,
  spender: String,   # must match registered spender for token (or allow set-with-register)
  limit_24h: Uint128 # max cumulative InstantWithdrawCw20 amount per 24h window
}
RemoveCw20SpenderLimit { token: String, spender: String }  # fail-closed after remove

# Optional DX: extend SetCw20Spender { token, spender, limit_24h: Option<Uint128> }
# so register + limit is one tx (recommended).

# Query
Cw20SpenderLimit { token, spender } → { limit_24h, amount_used, window_start, remaining }
# and/or extend Cw20Spenders {} entries with limit + usage summary

Storage sketch:

Key Namespace Value
Limit config e.g. cw20_pull_limits Map<(spender, token), Uint128> or struct
Usage e.g. cw20_pull_usage Map<(spender, token), { amount_used, window_start }>

On InstantWithdrawCw20:

  1. Existing pause / auth / zero / balance checks.
  2. Load limit for (info.sender, token); if unset → error (fail-closed) or unlimited (if product chooses).
  3. Tumbling window: if now >= window_start + 86400, reset amount_used = 0, window_start = now.
  4. If amount_used + amount > limit → Cw20PullLimitExceeded.
  5. Persist amount_used += amount, then emit Transfer.

Spender rotation: When SetCw20Spender overwrites spender A→B for a token, usage under A no longer applies to B (B starts fresh). Optionally clear A’s usage for that token on remove/overwrite to avoid storage bloat (document).

Do not gate native InstantWithdraw or ProposeWithdraw with this limit.


Acceptance criteria

  • Gov can set / update / remove a 24h pull limit for a specific (spender, token) pair; non-gov cannot.
  • InstantWithdrawCw20 enforces the limit for the caller+token; pulls that would exceed remaining quota fail with a clear error and no Transfer.
  • Limit for spender A + token X does not affect spender A + token Y, nor spender B + token X (isolation).
  • After the 24h window resets, quota replenishes and pulls succeed again up to limit_24h.
  • Multiple pulls within the window accumulate correctly; exact remaining boundary (pull == remaining) succeeds; remaining+1 fails.
  • Pause / wrapping pause / whitelist / native InstantWithdraw / ProposeWithdraw behavior unchanged except docs.
  • Zero amount still rejected; insufficient treasury balance still fails independently of limit.
  • Unregistered spender still fails auth before/without consuming quota.
  • Migrate preserves existing spenders/gov/whitelist/pending/wrappers; new limit storage available; document default for pre-existing spenders.
  • Queries expose limit, used, remaining (and window timing) for ops/monitoring.
  • Docs + skill updated: remove “no on-chain pull cap v1”; describe set-limit → register → window redeem ops.
  • Schema-ready (#[cw_serde]); unit tests green.

Test plan — functional paths

# Path Expect
T1 Gov SetCw20SpenderLimit (or SetCw20Spender with limit) Stored; query shows limit / used=0
T2 Non-gov set/remove limit Unauthorized
T3 Happy pull under limit Transfer emitted; amount_used increases
T4 Pull exactly remaining Success; used == limit
T5 Pull exceeding remaining Cw20PullLimitExceeded; no Transfer; used unchanged
T6 Two tokens, same spender Separate quotas; exhausting X does not block Y
T7 Two spenders / rotate spender Old spender cannot pull; new spender has its own usage (fresh)
T8 Window reset after 24h Used resets; full limit available again
T9 Remove limit (fail-closed) Subsequent pulls fail until reset
T10 Pause still blocks regardless of remaining quota Cw20InstantWithdrawPaused
T11 wrapping_paused does not interact with limit CW20 pull still subject only to cw20 pause + limit
T12 Insufficient balance with room under limit InsufficientBalance (not limit error)
T13 Zero amount ZeroAmount; used unchanged
T14 Unregistered spender Auth error; no usage write
T15 ProposeWithdraw / native InstantWithdraw Unaffected by CW20 pull limits
T16 Migrate smoke Prior state preserved; limit API available
T17 Query remaining mid-window Matches limit − used

Test plan — attack / abuse / hack vectors

# Vector Expect
A1 Drain via many small pulls under limit Stops when cumulative used hits limit
A2 Single pull == full treasury but > limit Limit error (treasury not drained beyond quota)
A3 Bypass via ProposeWithdraw Still gov+timelock only — OK; limit does not apply (document)
A4 Bypass via native InstantWithdraw Different path — OK if denom wrapper; CW20 limit N/A
A5 Spoof spender / wrong token Auth / isolation failures; no quota burn for victim pair
A6 Overflow amount_used + amount Checked add; error, no wrap
A7 Window boundary burst (tumbling) Documented wrap-mapper-style ~edge behavior; no silent unlimited
A8 Gov sets limit=0 All InstantWithdrawCw20 for that pair fail
A9 Race: two txs same block under remaining CosmWasm sequential; second sees updated used; no over-limit success
A10 Storage collision with cw20_spenders / whitelist Distinct namespaces
A11 Rotate to attacker spender with high limit Still gov process risk (A12 of #6); limit bounds blast radius
A12 Remove spender but leave stale usage Next register of same pair: define reset-on-set; no unexpected free quota
A13 Grief: gov sets tiny limit mid-flight Subsequent pulls fail; no stuck funds beyond unused inventory
A14 Pause off + limit exhausted Still blocked by limit until window reset or gov raises limit

Verification criteria

  1. cargo test --package treasury --lib green including new limit cases above.
  2. wrap-mapper / native InstantWithdraw regression still green.
  3. LocalTerra or cw-multi-test: register spender + limit → pulls accumulate → exceed fails → advance 24h → success again; recipient balances match.
  4. After mainnet migrate: query limits for vFDUSD+window; set production limit_24h before or with enabling redeem; confirm exhausted limit cannot drain full treasury balance.
  5. Docs/skill no longer claim “no on-chain pull cap.”
  6. Security review of A1–A14 (especially A3/A7/A11) signed off.

Dependencies

  • Builds on #6 / MR !27 InstantWithdrawCw20 API.
  • Coordinate with ust1-window inventory policy so window UX matches treasury hard ceiling.
  • Optional: land before or with mainnet SetCw20Spender for vFDUSD (strongly preferred for blast-radius control).
## Summary Add a **governance-configurable 24-hour pull limit** for treasury `InstantWithdrawCw20`, keyed by **(spender, CW20 token)** so each registered spender has an independent rolling (or fixed-window) quota per token it can pull. This closes the documented v1 gap from [#6](https://gitlab.com/PlasticDigits2/ustr-cmm/-/issues/6) / [MR !27](https://gitlab.com/PlasticDigits2/ustr-cmm/-/merge_requests/5): *“No on-chain CW20 pull cap (v1); window-side limits are the only product control.”* Also tracks audit follow-up in [`audits/INTERNAL_COMPOSER_1785465508.md`](./audits/INTERNAL_COMPOSER_1785465508.md) (H-2 / M-1 / P3). Companion consumer remains [ust1-window#20](https://gitlab.com/PlasticDigits/ust1-window/-/work_items/20) (window may keep its own inventory policy; treasury limit is a hard ceiling). --- ## Current codebase | Component | Path | Behavior today | |-----------|------|----------------| | Spender registry | `contracts/contracts/treasury/src/state.rs` → `CW20_SPENDERS` | `Map<&str, Addr>`: **one spender per token**; overwrite on `SetCw20Spender`. Doc invariant: *registered spender may drain full treasury balance of that token*. | | Pull path | `contracts/contracts/treasury/src/contract.rs` → `execute_instant_withdraw_cw20` | Checks `cw20_iw_paused`, zero amount, `sender == CW20_SPENDERS[token]`, treasury CW20 balance ≥ amount; emits `Cw20ExecuteMsg::Transfer`. **No cumulative / time-window accounting.** | | Admin msgs | `contracts/contracts/treasury/src/msg.rs` | `SetCw20Spender` / `RemoveCw20Spender` / `SetCw20InstantWithdrawPaused` / `InstantWithdrawCw20`; query `Cw20Spenders {}`. | | Pause | `CW20_INSTANT_WITHDRAW_PAUSED` (`cw20_iw_paused`) | Independent of `wrapping_paused`. | | Pattern to reuse | `contracts/contracts/wrap-mapper/src/{state,contract}.rs` | Per-denom `RateLimitConfig` + `RateLimitState` (`max_amount`, tumbling window, `amount_used` / `window_start`); `SetRateLimit` / `RemoveRateLimit` / `check_rate_limit`. | | Docs | `docs/CONTRACTS.md` decision #10, `skills/treasury-cw20-instant-withdraw/SKILL.md` invariant #4 | Explicitly document **no on-chain pull cap** in v1. | **Implication of “per spender + CW20”:** Today auth is token→single spender. Limits must still be keyed by **(spender, token)** so that (a) the same spender registered on multiple tokens has separate quotas, (b) rotating the spender resets or isolates usage under the new identity, and (c) a future multi-spender-per-token model (if pursued) does not require another storage redesign. Prefer composite keys even if v1 keeps “at most one spender per token.” --- ## Why this is needed 1. **Blast-radius control:** A buggy or compromised ust1-window (or any registered spender) can currently empty the entire treasury balance of vFDUSD (or any registered CW20) in one or few txs — audit A4 / H-2. 2. **Defense in depth:** Window-side inventory caps are necessary but not sufficient; an on-chain treasury ceiling survives consumer bugs and misconfig. 3. **Per-pair isolation:** One global or per-token-only cap is insufficient if multiple spenders / tokens exist (e.g. future window + another integrator, or one window across several CW20s). Limits must be **individual per (spender, token)**. 4. **Ops / IR:** Gov can lower or zero a pair’s 24h limit without removing the spender or pausing the whole CW20 InstantWithdraw path (which would halt all tokens’ pulls). 5. Closes the deferred item from #6 acceptance / MR !27 “Not in this MR” checklist. Without this, mainnet `SetCw20Spender` remains an all-or-nothing trust grant for the full token balance. --- ## Constraints / guardrails 1. **Do not break** existing InstantWithdrawCw20 auth, pause isolation (`wrapping_paused` ≠ `cw20_iw_paused`), native wrap InstantWithdraw, or ProposeWithdraw / ExecuteWithdraw. 2. **Keying:** Limits and usage MUST be per `(spender, token)` — not global, not token-only, not spender-only. 3. **Window:** 24 hours (86_400 seconds). Document whether the window is **tumbling** (reset after 24h from `window_start`, matching wrap-mapper) or **calendar UTC**; prefer **tumbling** for consistency with wrap-mapper unless product requires calendar days. 4. **Default when unset:** Choose and document one: - **Recommended:** `limit == null` / absent ⇒ **deny pulls** until gov sets a limit (fail-closed for new registrations), **or** - Explicit `Uint128::MAX` / “unlimited” opt-in for parity with v1 during migration. Prefer **fail-closed** for new `SetCw20Spender` after this feature, with a migrate path that sets an explicit high limit for any pre-existing mapping if needed. 5. Governance-only to set/update/remove limits. No timelock required (parity with `SetCw20Spender` / wrap-mapper `SetRateLimit`), but document ops risk. 6. Exceeding the remaining 24h quota MUST fail cleanly **before** emitting Transfer (no partial transfer). 7. Solvency check (treasury balance ≥ amount) remains; limit is **additional**. 8. Zero amount still rejected; zero / removing limit: define semantics (remove ⇒ deny, or remove ⇒ unlimited — pick fail-closed). 9. Storage namespaces must not collide with `cw20_spenders`, `cw20_whitelist`, `denom_wrappers`, `cw20_iw_paused`. 10. No arbitrary WasmMsg; typed msgs only. 11. In-place migrate preferred (stable treasury address). 12. Do **not** require CW20 whitelist for pulls (whitelist remains orthogonal). 13. Align intended mainnet quota with ust1-window inventory policy (e.g. ~10_000 vFDUSD) via ops docs — exact number is gov-configurable, not hardcoded. --- ## Relevant files - `contracts/contracts/treasury/src/msg.rs` — new execute/query variants; possibly extend `SetCw20Spender` or add `SetCw20SpenderLimit` - `contracts/contracts/treasury/src/state.rs` — limit config + usage maps; extend spender value type if needed - `contracts/contracts/treasury/src/contract.rs` — enforce in `execute_instant_withdraw_cw20`; gov setters; migrate - `contracts/contracts/treasury/src/error.rs` — e.g. `Cw20PullLimitExceeded { spender, token, requested, remaining, reset_at }` - `contracts/contracts/wrap-mapper/src/state.rs` + `contract.rs` — reference rate-limit pattern (`RateLimitConfig` / `check_rate_limit`) - `docs/CONTRACTS.md`, `docs/ARCHITECTURE.md`, `docs/DEPLOYMENT.md` — replace “no on-chain pull cap” with limit semantics + ops - `skills/treasury-cw20-instant-withdraw/SKILL.md` — update invariants (retire “no pull cap v1”) - `plans/NATIVE_TOKEN_WRAPPING.md` — cross-link if it still describes CW20 IW as uncapped - Companion (out of repo): ust1-window should treat treasury limit errors as user-facing redeem failure --- ## Recommended direction Mirror wrap-mapper’s rate-limit shape, keyed by composite `(spender, token)`: ```text # Governance SetCw20SpenderLimit { token: String, spender: String, # must match registered spender for token (or allow set-with-register) limit_24h: Uint128 # max cumulative InstantWithdrawCw20 amount per 24h window } RemoveCw20SpenderLimit { token: String, spender: String } # fail-closed after remove # Optional DX: extend SetCw20Spender { token, spender, limit_24h: Option<Uint128> } # so register + limit is one tx (recommended). # Query Cw20SpenderLimit { token, spender } → { limit_24h, amount_used, window_start, remaining } # and/or extend Cw20Spenders {} entries with limit + usage summary ``` **Storage sketch:** | Key | Namespace | Value | |-----|-----------|--------| | Limit config | e.g. `cw20_pull_limits` | `Map<(spender, token), Uint128>` or struct | | Usage | e.g. `cw20_pull_usage` | `Map<(spender, token), { amount_used, window_start }>` | On `InstantWithdrawCw20`: 1. Existing pause / auth / zero / balance checks. 2. Load limit for `(info.sender, token)`; if unset → error (fail-closed) or unlimited (if product chooses). 3. Tumbling window: if `now >= window_start + 86400`, reset `amount_used = 0`, `window_start = now`. 4. If `amount_used + amount > limit` → `Cw20PullLimitExceeded`. 5. Persist `amount_used += amount`, then emit Transfer. **Spender rotation:** When `SetCw20Spender` overwrites spender A→B for a token, usage under A no longer applies to B (B starts fresh). Optionally clear A’s usage for that token on remove/overwrite to avoid storage bloat (document). **Do not** gate native InstantWithdraw or ProposeWithdraw with this limit. --- ## Acceptance criteria - [ ] Gov can set / update / remove a 24h pull limit for a specific `(spender, token)` pair; non-gov cannot. - [ ] `InstantWithdrawCw20` enforces the limit for the caller+token; pulls that would exceed remaining quota fail with a clear error and **no** Transfer. - [ ] Limit for spender A + token X does **not** affect spender A + token Y, nor spender B + token X (isolation). - [ ] After the 24h window resets, quota replenishes and pulls succeed again up to `limit_24h`. - [ ] Multiple pulls within the window accumulate correctly; exact remaining boundary (pull == remaining) succeeds; remaining+1 fails. - [ ] Pause / wrapping pause / whitelist / native InstantWithdraw / ProposeWithdraw behavior unchanged except docs. - [ ] Zero amount still rejected; insufficient treasury balance still fails independently of limit. - [ ] Unregistered spender still fails auth before/without consuming quota. - [ ] Migrate preserves existing spenders/gov/whitelist/pending/wrappers; new limit storage available; document default for pre-existing spenders. - [ ] Queries expose limit, used, remaining (and window timing) for ops/monitoring. - [ ] Docs + skill updated: remove “no on-chain pull cap v1”; describe set-limit → register → window redeem ops. - [ ] Schema-ready (`#[cw_serde]`); unit tests green. --- ## Test plan — functional paths | # | Path | Expect | |---|------|--------| | T1 | Gov `SetCw20SpenderLimit` (or SetCw20Spender with limit) | Stored; query shows limit / used=0 | | T2 | Non-gov set/remove limit | Unauthorized | | T3 | Happy pull under limit | Transfer emitted; `amount_used` increases | | T4 | Pull exactly remaining | Success; used == limit | | T5 | Pull exceeding remaining | `Cw20PullLimitExceeded`; no Transfer; used unchanged | | T6 | Two tokens, same spender | Separate quotas; exhausting X does not block Y | | T7 | Two spenders / rotate spender | Old spender cannot pull; new spender has its own usage (fresh) | | T8 | Window reset after 24h | Used resets; full limit available again | | T9 | Remove limit (fail-closed) | Subsequent pulls fail until reset | | T10 | Pause still blocks regardless of remaining quota | `Cw20InstantWithdrawPaused` | | T11 | wrapping_paused does not interact with limit | CW20 pull still subject only to cw20 pause + limit | | T12 | Insufficient balance with room under limit | `InsufficientBalance` (not limit error) | | T13 | Zero amount | `ZeroAmount`; used unchanged | | T14 | Unregistered spender | Auth error; no usage write | | T15 | ProposeWithdraw / native InstantWithdraw | Unaffected by CW20 pull limits | | T16 | Migrate smoke | Prior state preserved; limit API available | | T17 | Query remaining mid-window | Matches limit − used | --- ## Test plan — attack / abuse / hack vectors | # | Vector | Expect | |---|--------|--------| | A1 | Drain via many small pulls under limit | Stops when cumulative used hits limit | | A2 | Single pull == full treasury but > limit | Limit error (treasury not drained beyond quota) | | A3 | Bypass via ProposeWithdraw | Still gov+timelock only — OK; limit does not apply (document) | | A4 | Bypass via native InstantWithdraw | Different path — OK if denom wrapper; CW20 limit N/A | | A5 | Spoof spender / wrong token | Auth / isolation failures; no quota burn for victim pair | | A6 | Overflow `amount_used + amount` | Checked add; error, no wrap | | A7 | Window boundary burst (tumbling) | Documented wrap-mapper-style ~edge behavior; no silent unlimited | | A8 | Gov sets limit=0 | All InstantWithdrawCw20 for that pair fail | | A9 | Race: two txs same block under remaining | CosmWasm sequential; second sees updated used; no over-limit success | | A10 | Storage collision with `cw20_spenders` / whitelist | Distinct namespaces | | A11 | Rotate to attacker spender with high limit | Still gov process risk (A12 of #6); limit bounds blast radius | | A12 | Remove spender but leave stale usage | Next register of same pair: define reset-on-set; no unexpected free quota | | A13 | Grief: gov sets tiny limit mid-flight | Subsequent pulls fail; no stuck funds beyond unused inventory | | A14 | Pause off + limit exhausted | Still blocked by limit until window reset or gov raises limit | --- ## Verification criteria 1. `cargo test --package treasury --lib` green including new limit cases above. 2. wrap-mapper / native InstantWithdraw regression still green. 3. LocalTerra or cw-multi-test: register spender + limit → pulls accumulate → exceed fails → advance 24h → success again; recipient balances match. 4. After mainnet migrate: query limits for vFDUSD+window; set production `limit_24h` **before** or **with** enabling redeem; confirm exhausted limit cannot drain full treasury balance. 5. Docs/skill no longer claim “no on-chain pull cap.” 6. Security review of A1–A14 (especially A3/A7/A11) signed off. --- ## Dependencies - Builds on [#6](https://gitlab.com/PlasticDigits2/ustr-cmm/-/issues/6) / MR !27 InstantWithdrawCw20 API. - Coordinate with ust1-window inventory policy so window UX matches treasury hard ceiling. - Optional: land before or with mainnet `SetCw20Spender` for vFDUSD (strongly preferred for blast-radius control).
PlasticDigits commented 2026-07-31 02:56:54 +00:00 (Migrated from gitlab.com)

mentioned in commit b9cd7cc3f7

mentioned in commit b9cd7cc3f73c38f1d69a687fcbc3e277c2bf9f1a
PlasticDigits commented 2026-07-31 02:57:04 +00:00 (Migrated from gitlab.com)

mentioned in merge request !28

mentioned in merge request !28
PlasticDigits commented 2026-07-31 02:58:59 +00:00 (Migrated from gitlab.com)

mentioned in commit 98fc4de2a0

mentioned in commit 98fc4de2a0d8020a10ba07c9cf1417ff9826242c
PlasticDigits commented 2026-07-31 03:13:52 +00:00 (Migrated from gitlab.com)

mentioned in commit b164ea7523

mentioned in commit b164ea7523a2fdd70604b3fb5f2f1dc5d24a3729
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-07-31 03:19:16 +00:00
PlasticDigits commented 2026-07-31 03:21:16 +00:00 (Migrated from gitlab.com)

mentioned in issue #5

mentioned in issue #5
PlasticDigits commented 2026-08-05 00:10:06 +00:00 (Migrated from gitlab.com)

mentioned in issue #8

mentioned in issue #8
PlasticDigits commented 2026-08-08 00:36:36 +00:00 (Migrated from gitlab.com)

mentioned in merge request PlasticDigits/ust1-window!23

mentioned in merge request PlasticDigits/ust1-window!23
PlasticDigits commented 2026-08-15 09:33:07 +00:00 (Migrated from gitlab.com)

mentioned in issue #10

mentioned in issue #10
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/ustr-cmm#7
No description provided.