sec: generalized CW20 code-id audit harness (decomp + exploit suite; gates #581) #589
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#589
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?
Gate
This issue blocks #581. Do not close #581 with a go to factory
AddWhitelistedCodeId8266, and do notCreatePair/ seed SpaceUSD/UST1, until this harness exists, the standard procedure is documented, and code ID 8266 has a per-code-id audit report plus a green automated suite under that procedure.Parent ops: #558. Policy:
docs/runbooks/cw20-whitelist-policy.md/ #377 H-01 / P2. Pin + write-path re-check: #582 F6 (already live; this issue does not replace F6).Policy change (replaces #581 hash-equal rebuild): a byte-identical optimizer rebuild (
data_hashmatch) is not required and is not a go/no-go input. Old popular CW20 templates (Terraport 8266, TerraSwap 6036, and later IDs) routinely lack a publicCargo.lockand/or use private crates, so hash-equal rebuilds fail even when the LCD wasm is a standard token. The replacement gate is:CodeInfo.data_hash(prove we have the on-chain binary — not that we compiled it).Optional source rebuild remains an appendix only.
Current codebase
Whitelist and listing are already fail-closed on code ID, not on token address:
WHITELISTED_CODE_IDSCreatePairrefuses unknown templatesIsCodeIdWhitelistedcode_idmust match listing pin and stay listed (#582)docs/runbooks/cw20-whitelist-policy.mdscripts/verify-cw20-code-ids.shCodeInfoonlyaudits/CW20-8266-581.md+ hash-repro / terraswap notessmartcontracts/tests/src/adversarial_token.rsHonestvsFeeOnTransfer; documents P2 desync; router dust (R4); hook spoof (H2)Transfer/ pause / flash-mint fixturesdocs/contracts-security-audit.mdcw-multi-testevidencedocs/exploit-replay-matrix.mdSEC-D06GetWhitelistedCodeIds=[6036, 10184]There is no
cw20-codeid-audits/(or equivalent) tree, no standard per-code-id report template, nocosmwasm-vm/ LocalTerra harness that loads arbitrary LCD wasm, and no catalogue that maps ERC-20 “weird token” classes onto CW20 + this DEX.Why this is needed
classic_terraportand pinning rustc 1.66.0 /cosmwasm-std1.3.3 /rust-optimizer:0.12.11. The rebuild was smaller than LCD wasm, exported an extra__getrandom_custom, and never matcheddata_hash953AD60C…. That is the expected outcome for Columbus-5-era tokens without a published lockfile. The same will happen for other popular IDs.tax_mapappeared. 8266 has notax_mapand still needs transfer-path proof. Query enums cannot show hidden skim, sell-side honeypot, or admin-onlyTransferFrom./tmpprobes do not persist. #581 LocalTerra 1:1 probes passed and must be turned into a checked-in, parameterized suite so the next code ID is not a research project.Constraints / guardrails
/cosmwasm/wasm/v1/code/{id}(or equivalent) whose SHA-256 equalsCodeInfo.data_hash. No third-party mirror without that check.indexer/into root Docker to runcargo(AGENTS.md/make test-indexer-target-ownership). Host compile; LocalTerra viamake setup-cloud-localterra/make has-localterra.Relevant files
docs/runbooks/cw20-whitelist-policy.mddocs/runbooks/cw20-code-id-ops.mddocs/contracts-security-audit.mddocs/security-model.mddocs/exploit-replay-matrix.mddocs/terraport.mdscripts/verify-cw20-code-ids.shfetchstepsmartcontracts/tests/src/adversarial_token.rssmartcontracts/tests/src/asset_code_id_pin_tests.rssmartcontracts/tests/src/audit_invariant_tests.rs,security_tests.rs,reentrancy_tests.rs,limit_order_tests.rs,sweep_tests.rsaudits/CW20-8266-581*.mdcw20-codeid-audits/codeids/8266/skills/AGENTS_CW20_CODE_ID_PIN.mdMakefileverify-issue-*patternmake verify-issue-<this>Recommended direction
Layout
Do not dump decomp into
smartcontracts/orfrontend-dapp/. Keep the tree self-contained.Standard procedure (every code ID)
CodeInfo:code_id,data_hash, creator, instantiate permission, contract count. Writemeta.json.data_hash. Fail closed on mismatch.producers/rustc, crate strings (cw20_base,terraport_token,tax_map,requires_terra), custom query/execute enums from error strings.wasm2wat,wasm-decompile(C-like),wasm-objdump -x/-dfortransfer/send/transfer_from/send_from/burn/mintand any tax/hook/admin symbols. Store undercodeids/<id>/decomp/.CATALOG.md. Record hits, misses, and unreadable regions. Extra queries (balance_at,total_supply_at,tax_map) must be classified (snapshot vs live mutation vs tax).cosmwasm-vmin a pair harness). Persist logs/JUnit under the code-id folder or CI artifacts.REPORT.md: fingerprint, decomp findings, suite results vs catalogue, factory-global impact, instance-admin / migrate residual (F6), go / no-go.Harness (two layers)
Layer A — token-only (
cosmwasm-vm): instantiate the LCD wasm (or skip instantiate and execute against a stored instance snapshot if init is exotic), then drive CW20 execute/query. No pair. Fast, CI-friendly, catches FoT/rebase/honeypot/allowance bugs.Layer B — DEX + limits: store the same wasm on LocalTerra (or a
cw-multi-testcustom wasm loader if feasible), whitelist only in the test factory,CreatePairwith a 10184 counter-asset, then re-run the invariant matrix that touches CW20 balances (P1, P2, P3, P4, P10, R1–R4, C4, L1–L3, L6, L10, L11, sweep). Compare to the same scenario with 10184/10184 as control.Prefer parameterized tests (
CODE_ID=8266 make verify-issue-NNN) over copy-paste per ID.Catalogue (research baseline — implement every row)
Sources used for this issue (must be cited in
CATALOG.mdand kept current):Transfer,Send,Burn,Mint,Increase/DecreaseAllowance,TransferFrom,SendFrom,BurnFrom, marketing,UpdateMinter)tax_map(tax ontransfer/send/transfer_from/send_from; contract vs wallet discrimination)Uint256::pow; ASA-2024-007 / Terra–Astroport IBC-hooks infinite mint (token used as pool asset after unbacked mint)docs/exploit-replay-matrix.md; in-repo P2 / P3 / R4 / H1max_spread(existingsecurity_tests)Map every ERC-20 class to a CW20 test (or an explicit N/A with reason, e.g. “no
boolreturn — CosmWasm errors instead”).Acceptance criteria
cw20-codeid-audits/exists withPROCEDURE.md,CATALOG.md(every catalogue row + citation),report-template.md, fetch + decompile scripts, andcodeids/<id>/subfoldersdocs/runbooks/cw20-whitelist-policy.md(and #581) no longer require compile-hash / byte-identical rebuild; they require this procedurecodeids/8266/REPORT.mdfilled (decomp + suite). #581 may then record go/no-go from that report, not from a rebuild hashmake verify-issue-<this>(and agent skill) documented inAGENTS.md/ playbookaudits/CW20-8266-581*.mdare linked or moved so there is one intake pathTest plan — all paths
Framework / intake paths
fetch-lcd-wasm.shhappy pathdata_hashmatch →meta.json+wasm.sha256wabt); do not skip decompREPORT.mdconclusionsCODE_ID=10184(control)CODE_ID=8654or FoT fixturemarketing/ snapshot fieldsCommon CW20 behaviors (Layer A — must implement all)
For each of
Transfer,Send,TransferFrom,SendFrom,Burn,BurnFrom,Mint,IncreaseAllowance,DecreaseAllowance, plus queriesBalance,TokenInfo,Allowance,Minter,AllAllowances/AllAccountsif present:amount; sender debit ==amount(or burn/mint identity). No silent fee.Send/SendFromhookCw20ReceiveMsgwith sameamount; 1:1 credit before hook; receiver revert rolls back balancesTransferFrom/SendFromreject after expiryUpdateMinter/ marketing / logobalanceandtotal_supplyunchanged (anti-rebase)balance_at/total_supply_at)CreatePairbootstrap (P3); no script injection in name/symbol for dApp (record; frontend escape is separate)total_supplyafter every honest op (spot-check + random walk)DEX invariant paths (Layer B — must re-run on candidate wasm)
Reuse IDs from
docs/contracts-security-audit.md. In scope (asset is the candidate CW20; other side 10184 unless noted):k' \ge kwithin documented roundingRESERVES== CW20balanceof pair (no FoT skim, no silent donate credit)MINIMUM_LIQUIDITY; decimals > 18 rejectedCreatePairaccepts only after test whitelist; F6 pin equals stored code ID; migrate-off-template still freezes (already tested inasset_code_id_pin_tests— run once + once with this wasm as the “honest then migrate” subject if admin exists)minimum_receive/ per-hopmin_return; dust on output is not swept (R4)Send1:1 escrow; cancel / claim refunds exact remaining; pause blocks CW20 refunds; batch refunds do not over-payOut of scope for per-code-id Layer B (keep existing
cw-multi-testonly): pure book-structure IDs that do not depend on token semantics (L5, L12–L22 walk/hint/dust-flush/price-band), unless the token can change balances mid-match (then they become in scope).Docs / CI paths
make verify-issue-<this>without LocalTerramake has-localterrahint (do not silent-pass)make setup-cloud-localterrathen Layer B; noSKIP (no LocalTerra)without provisioningrgfor “hash-equal” / “byte-identical” / “optimizer rebuild required” is empty except history notesTest plan — attack, hack, and abuse vectors
Each row is a required automated case (or explicit N/A). “Fail” means the token is not listable (or the DEX correctly rejects / stays solvent). Citations are the research baseline; implementers must add any newer public class found while building
CATALOG.md.A. Malicious / weird CW20 (and ERC-20 analogues)
Transfer/Send/TransferFrom/SendFromrecipient delta < declared; pair P2 desyncrecipientorsenderis pair/router; wallet↔wallet 1:1balanceortotal_supplychanges;balance_atthat rewrites currenttax_map,UpdateTaxMap)TransfertokensToSend/tokensReceived; imBTC Uniswap; Lendf.me; CreamTransfer(not onlySend) dispatchesWasmMsgto sender/recipientSendhook grief / reenter pairtransferAndCall; CosmWasm intendedReceiveSwap/Withdraw/PlaceLimit; pair must fail closed (existing reentrancy tests + this wasm as offer token)IncreaseAllowancesurpriseApprove {amount}overwrite race; if customApproveexists, test double-spend windowtransferFromwithout allowance /_isBotTransferto pair reverts for non-admin after buy; DEX withdraw/swap bricksTransferuint128::MAXthen burn; can break first-deposit / donationMigratesurfaceMsgMigrateContract— F6 tested; report must list adminboolreturn; Tether GoldfalsesuccessBalancedisagrees with transfer events;TokenInfo.total_supply≠ sumdecimals > 18→CreatePairreject (P3);0decimals → overflow/precision testsTransferFromself-semanticsTransferFrom; still testpermitexists and is a no-op, DEX must not assume allowance changedbytes32nameTokenInfounreadableProvide→ P2 desync or trapped yieldamountwhenamount == maxUint128::MAXtransfer credits only balanceuluna/uusd— factory already rejects natives; test candidate is not secretly bank-denomibc_receive, treat as infinite-mint classUint256::powrequires_terra/ classic taxer integrationbalance_atB. DEX exploits that use a malicious or weird token
test_direct_token_donation_does_not_inflate_lp_shares)test_flash_provide_swap_withdraw_no_profit) with this tokenSendreceiver + pair hook both re-enter; no double-payPENDING_ESCROWvs actual balance; cancel/claim honesty (L1). Candidate 1:1 only.MsgMigrateContractFoT → writes fail closed (F6). Already inasset_code_id_pin_tests; include this wasm as the honest side once.max_spread/min_returnstill holds (existing security tests)Transfermid-settlement: treasury unchanged (P10)Balancemakes sweep steal poolC. Harness / process abuse
data_hashcodeids/data_hashVerification criteria
make verify-issue-<this>is green on CI or Cloud Agent for 10184 (and FoT known-bad red).scripts/fetch-lcd-wasm.sh 8266+decompile-wasm.shproducecodeids/8266/decomp/andwasm.sha256matching LCD953AD60C…without any optimizer rebuild.codeids/8266/REPORT.mdexists with every procedure section, every catalogue row checked (pass / fail / N/A+reason), Layer A+B logs referenced, and an explicit go or no-go.rgondocs/runbooks/cw20-whitelist-policy.mdshows decomp+suite as the whitelist evidence; hash-equal rebuild is optional appendix only.Labels / owner
contracts + security + docs + testing + ops (whitelist procedure). Implementation can proceed without a 2-of-3 tx; listing 8266 still needs #581 go after this ships.
Priority
P1 for #558 / #581 SpaceUSD/UST1 intake. Also the standing process for every future CW20 code ID.
marked as related to #581
Opened as the standing CW20 code-id intake: per-ID folder, LCD wasm pin (identity hash only), decompile, catalogue audit, and automated suite.
This gates #581. A rust-optimizer byte-identical rebuild is not required. #581 go/no-go must come from
cw20-codeid-audits/codeids/8266/REPORT.mdafter this ships.GitLab
blocksissue links are unavailable on this license, so the gate is written in both descriptions and this related link.mentioned in issue #581
Supplement — additional automated-test areas for the suite (append-only)
Everything below is new surface relative to the existing plan (A1–A30 token classes, B1–B15 DEX vectors, C1–C7 process rows). Each item links to specific research or gives a coherent justification, and states what the suite must show. The recurring theme: the A-rows are mostly static, point-in-time, honest-probe properties; the largest detection gap is behavior that is honest at audit time / for the probe path and malicious later / for the DEX path, plus attacks on the off-chain trust chain (indexer/oracle/frontend) where pair solvency is never touched.
Additional research base (beyond the sources already in the issue):
hidden_owner,can_take_back_ownership,owner_change_balance,trading_cooldown,is_anti_whale,external_call,fake_token)maxBuy/maxSelldetection; explicit point-in-time caveat) and Cube Exchange on stateful/dynamic honeypot controlswapAndLiquify,setAutomatedMarketMakerPair,setTaxFeePercent,setMaxTxPercent,_isExcludedFromFee) — the token contract as an autonomous trader on its own pairReceivemsg parsing)burn_taxis a live gov parameter; contracts taxed on receive), Terraport $4M post-mortem (CertiK) (centralization/insider class on this exact chain), DeFiHackLabs (850+ incident PoCs — the replay-methodology model this harness should mirror)D. Stateful / conditional / environment-dependent token behavior (detection-gap classes)
block_time/block_height(days, months, years) and re-run the full 1:1 + Layer B slice at each step. Any behavior delta = No.is_anti_whale, SafeMoonsetMaxTxPercent, Honeypot.ismaxBuy/maxSellfields exist precisely because amount-gated behavior is common. Small-amount probes pass by design.extcodesizehoneypot checks: a CosmWasm contract can queryContractInfooninfo.senderand branch. Trapdoor paper catalogs owner/sell-blocking modifiers; A2 covers pair-direction tax but not arbitrary caller classes.Transfer1:1 butSend/TransferFrom/SendFromtaxed (or vice versa)Transfer; the DEX path isSend(limits) andTransferFrom/Send(pair settlement). This is the CW20-specific honeypot shape.external_callrisk class. Also the CosmWasm analogue of Curve read-only reentrancy: token reads pair/router state mid-transfer to compute behavior (circular dependency: pair calls token, token queries pair's half-updated state).swapAndLiquify+lockTheSwap; thousands of forks. In CosmWasm this is a submessage from token → pair while the pair is mid-execution — a reentrancy path B5 doesn't cover (B5 is receiver-hook reentry; this is token-initiated trading).setAutomatedMarketMakerPairpattern; GoPlusowner_change_balance. A permissionless variant means tax/honeypot can activate after listing with no admin action — invisible to admin-key review.hidden_owner+can_take_back_ownershipclasses; Terraport on this chain was a centralization/insider loss.Balancederived from shares × rate; full-balance transfer leaves ghost dust;total_supply≠ Σ balances by designtrading_cooldown; PinkSale "Time Limit Per Trade". Breaks batch and multi-op flows even when single transfers are honest.fundson execute. Pair/router never attach natives → systematic settlement DoS; if any path does attach, value leaks.decimals(or supply) changes after pair bootstrapTokenInforepeatedly across intervening executes/migrates; any mutation = No.Everybodyinstantiate admits hostile configs.AllAccounts/AllAllowancespagination skips entriesAllAccounts/AllAllowances; any state not reachable via queries = No.Balancequeries and flag divergence (see CH16).{"rug":{}}), unknown-variant handlingReceivemsg parser robustness — oversized msg, duplicate JSON keys, deep nesting, unknown fields, nestedSendloopsproducers/crate strings against all CosmWasm advisories, not just CWA-2024-002module_query_safe, unbounded reply recursion, sub-context gas. A token compiled against an affected cosmwasm-std/wasmvm range inherits the class.Balance/Allowance/TokenInfoqueries; unboundedAllAccountsE. Additional DEX-interaction vectors (Layer B extensions)
token0 == token1)CreatePairrejects identical assets; defense-in-depth: pair swap asserts offer ≠ ask even if factory bypassed; run with candidate wasm on both sides.donateToReservesclass ($197M) + OZ ERC-4626 inflation analysis. B2 tests donation-then-provide; the missing case is donation interleaved between simulation and execution of an honest provide.Send(escrow) and claim/cancel are transfers out — same callback surface on the book side (L1–L3).min_returnand dust attribution hold.Everybodyinstantiate → CreatePair spam from the candidate template; token queries that failtest_flash_provide_swap_withdraw_no_profit).F. Off-chain trust chain — the token attacks the displayed truth, not pair solvency
Pair solvency can be perfect while users are robbed via the indexer/oracle/frontend layer. These rows make that explicit and automatable.
textrejects the NUL character (docs) → one malicious instantiation can stall ingest; homoglyph tickers are thefake_tokenclass (GoPlus); SVG XSS is a known payload carrier.G. Harness / methodology additions
CodeInfo.data_hashfrom ≥2 independent LCD/RPC endpoints before pinningtax_map, minter, admin, pause)burn_tax)ibc_receivestrings.CH. Exploit-chaining scenarios (explicit multi-step tests)
The point of this supplement: single-row tests pass while compositions drain value. Each chain is one automated scenario.
CreatePair, anyone registers the pair address; onlySend/TransferFrom(the DEX paths) get taxedTransferFrompulls the pair's escrowed maker balances while the book looks intactasset_code_id_pin_testsstyle; freeze confirmed.Documented N/A mappings (record in CATALOG.md; do not spend test budget)
tx.originauthentication tricks — notx.originin CosmWasm;info.senderonly. (Trapdoor paper catalogstx.originsell-blocks; class is EVM-only.)extcodesize == 0caller checks — CosmWasm analogue exists viaContractInfoquery; covered as D4.selfdestruct/ CREATE2 metamorphic redeploy — no selfdestruct; code IDs are immutable; the metamorphosis surface isMsgMigrateContract, covered by A14/B9/F6 (+CH15).boolreturn / return-data bombs — CosmWasm errors instead of returning false; wasm memory model differs (already noted in the issue).get_virtual_price) — CosmWasm queries cannot re-enter a mid-execution contract, and the indexer reads only committed state (Tendermint finality). The residual analogues are covered: token reading pair state mid-transfer (D6) and display-layer staleness (F5/F7).Suggested landing: append D/E/F/G/CH to
CATALOG.mdas new sections and reference this note from the 8266 REPORT.md checklist so every row gets pass/fail/N/A+reason like the existing A/B/C rows.mentioned in commit
7b01c62ff7mentioned in merge request !1114
Harness MR: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/396
make verify-issue-589is green (16/16 including retest). 8266REPORT.mdis NO-GO until Layer A-lcd + Layer B-lt execute the pinned LCD wasm (CODE_ID=8266 LAYER_B_LT=1after wabt). Do notAddWhitelistedCodeId 8266. #581 stays open.Skill:
skills/AGENTS_CW20_CODE_ID_AUDIT.md.mentioned in commit
fa8550f356Post-merge note (!1114 on
main)Merged: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/396
Sanity:
make verify-issue-58916/16. Layer A+B multi-test 34 passed; known-bad FoT 1:1 / P2 stay red. Decompile fail-closed withoutwabt(C2). H-01 / F6 production contracts unchanged.8266 remains NO-GO.
cw20-codeid-audits/codeids/8266/REPORT.md— do notAddWhitelistedCodeId 8266. #581 was reopened (GitLab auto-closed it from merge text that containedclose #581).Gaps vs full AC (harness landed; binary suite on LCD wasm did not):
token.wasmin Rust. CI is A-mt / B-mt mutants + mintable analogue.LAYER_B_LT=1currently skips with PASS when LocalTerra is up — it does notStoreCodethe LCD wasm. Do not read a green verify as candidate clearance.REPORT.md). 8654 control is in-process FoT mutant, not LCD 8654.apt install wabt,CODE_ID=8266fetch+decomp, then real B-lt once implemented.Byte-identical optimizer rebuild stays appendix-only.
mentioned in issue #590
marked as related to #590
Follow-up tracker: #590 (A-lcd/B-lt + 8266 still NO-GO).
mentioned in issue #591
mentioned in issue #592
mentioned in commit
093b78e43bmentioned in issue #601
mentioned in issue #604
mentioned in issue #605
mentioned in merge request !1122
mentioned in issue #611
marked as related to #611
mentioned in issue #612
mentioned in issue #616
mentioned in commit
d3dba8bfefmentioned in issue #623
marked as related to #623
mentioned in issue #627
marked as related to #627
mentioned in commit
021945811bmentioned in issue #628
mentioned in commit
4379639ceb