Governance rotation fans SetLpAdmin to every pair in one unbatched message #277
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#277
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: Informational
Reachability: Governance-only, and only on an actual governance change — not attacker-reachable. Latent scaling cliff.
Affected:
execute_update_configSetLpAdmin fanout (smartcontracts/contracts/factory/src/contract.rs).Summary
When governance rotates,
execute_update_configloopsfor idx in 0..pair_countand pushes oneSetLpAdminmessage per pair into a single response. With enough pairs that one message batch can exceed gas / message-size limits, and the rotation tx fails — meaning governance can't actually be rotated once the DEX has many pairs.Gated by
ensure_governanceand only fires whennew_governance != old_governance, so there's no attacker angle. But it's an unbounded fanout on a critical, rare operation, so it should be paginated before pair count grows.Current codebase
execute_update_config:if new_governance != old_governance { for idx in 0..pair_count { messages.push(SetLpAdmin{...}) } }— no batching, no continuation.Recommended direction
Acceptance criteria
Governance should be stored centrally and read on demand.
Implementation plan — and a real caveat on the "read on demand" framing.
Reading the code:
SetLpAdmindoes NOT write a pair-storedlp_adminvalue you can read on demand — it emitsWasmMsg::UpdateAdmin, which rewrites each LP token's chain-level x/wasm migration admin. So the only way to make rotation literally O(1) (your intent) is:PRIMARY (O(1)): at LP-token instantiate set
admin: Some(<factory>)(pair/contract.rs ~523) instead of the pair, and delete the per-pair rotation fanout entirely. The factory'sCONFIG.governancebecomes the single source of truth; rotation touches no pairs. Caveat: this only takes effect for LP tokens instantiated AFTER redeploy — existing pairs need a one-timeupdate-admin(pair→factory) op (runbook).FALLBACK (lower-risk, admin stays literally == governance): #242-style
SetLpAdminAll(capped via calc_limit) +SetLpAdminBatch(cursor) — the exact template isexecute_set_discount_registry_all/_batch(factory/contract.rs:386-487) +DiscountRegistryAllTooManyPairserror. Operators loop untilhas_more=false. Keeps it a fanout but bounded + resumable.Files: factory/contract.rs (delete fanout for PRIMARY, or add batch arms for FALLBACK), pair/contract.rs (LP instantiate admin, PRIMARY), dex-common/factory.rs + error.rs (FALLBACK variants). Test gap that let this ship:
test_update_configcreates ZERO pairs, so the rotation fanout is completely untested — add a many-pairs rotation test either way. Your call on PRIMARY vs FALLBACK; PRIMARY matches your O(1) intent, FALLBACK is the proven in-repo pattern. @PlasticDigitsRe-confirmed the root cause for the record: execute_update_config fans a SetLpAdmin message to every pair in one tx when governance changes (contract.rs ~631), unbounded — that's the issue. The fix is the PRIMARY-vs-FALLBACK call from my earlier note: PRIMARY = set the LP token's admin to the factory at instantiate and delete the rotation fanout entirely (only affects pairs created after redeploy; existing need a one-time update-admin); FALLBACK = #242-style bounded SetLpAdminAll + SetLpAdminBatch cursor. Both change rotation behavior, so I didn't want to pick for you. Say which and I'll ship it — the FALLBACK is the exact same shape as the SetDiscountRegistryAll/Batch already in the factory. @PlasticDigits
Went ahead and implemented the FALLBACK — MR above. Removed the unbounded SetLpAdmin fanout from execute_update_config and added bounded SetLpAdminAll (one tx, fails over the pagination cap) + SetLpAdminBatch (cursor, rerun until has_more=false), the exact shape of the SetDiscountRegistryAll/Batch already in the factory.
The tradeoff to sign off on: governance rotation no longer auto-rotates the LP-token admins — the operator runs SetLpAdminAll/Batch after the UpdateConfig, same as the discount registry works. So between the rotation and the batch, LP tokens keep their old admin; that goes in the rotation runbook. Rewrote the rotation test accordingly; suite 415/0.
If you'd rather go PRIMARY (set LP admin = factory at instantiate and drop the per-pair rotation entirely — existing pairs need a one-time update-admin), say so and I'll swap this for it. @PlasticDigits
mentioned in merge request !760
Rejected. Admin for LP tokens must be kept up to date.
mentioned in commit
091ca26061mentioned in merge request !783
mentioned in merge request !785
mentioned in issue #381
mentioned in issue #424