fix(dao): audit H-2 + H-5 + H-6 (per internal notes)

H-2: drop ExUnits to 5M/2G for spend, 2M/1G for mint
  Was 14M/10G each = per-tx Conway cap. With 3 plutus contracts running
  (governor spend + stake spend + ProposalST mint), total claim 42M/30G
  exceeds per-tx limit and node rejects pre-phase-2.

H-5: propagate malformed wallet asset keys instead of silently dropping
  Previous filter_map silently dropped any key < 56 chars. Could let a
  corrupt Koios response burn assets on submit. Now returns explicit
  Err with the offending UTxO + key.

H-6: tighten StakeST detection to asset_name == stake_validator_hash
  Per Stake/Scripts.hs:188-190 (pscriptHashToTokenName), StakeST
  asset_name is the stake validator's script hash. Previous code took
  "first non-gov-token asset" which would silently pick a wrong policy
  if a stake UTxO accidentally carried a junk NFT. Regression test
  h6_junk_token_does_not_pollute_stake_st_detection added.

3 of 7 audit punch-list items closed. C-1 + C-2 + C-3 next.
This commit is contained in:
Sulkta 2026-05-05 20:52:22 -07:00
parent 4644fb525f
commit 6955377c61
3 changed files with 128 additions and 43 deletions

View file

@ -1602,35 +1602,44 @@ impl WalletService {
.ok_or_else(|| format!("governor utxo {governor_utxo_ref} no longer present on chain"))?
.lovelace;
let wallet_utxos = self
.inner
.chain
.get_utxos(&self.inner.address)
.await
.map_err(|e| format!("koios get wallet utxos: {e}"))?
.into_iter()
.map(|u| DaoWalletUtxo {
tx_hash_hex: u.tx_hash,
output_index: u.output_index,
lovelace: u.lovelace,
// Chain backend gives `assets: BTreeMap<concatenated_key, qty>`
// where the key is `policy_id_hex || asset_name_hex` with the
// policy taking the first 56 chars (28 bytes). Split for
// pallas-txbuilder which wants the parts separate.
assets: u
.assets
.into_iter()
.filter_map(|(k, q)| {
if k.len() >= 56 {
let (p, n) = k.split_at(56);
Some((p.to_string(), n.to_string(), q))
} else {
None
}
})
.collect(),
})
.collect();
let wallet_utxos: Vec<DaoWalletUtxo> = {
let raw = self
.inner
.chain
.get_utxos(&self.inner.address)
.await
.map_err(|e| format!("koios get wallet utxos: {e}"))?;
// AUDIT-H5 fix: assets in the chain backend are
// `BTreeMap<policy_id_hex || asset_name_hex, qty>`. Previous
// implementation silently dropped any key < 56 chars via filter_map
// — that could let a corrupt Koios response burn assets on submit.
// Now: any malformed key surfaces as an explicit error.
let mut out = Vec::with_capacity(raw.len());
for u in raw {
let mut assets = Vec::with_capacity(u.assets.len());
for (k, q) in u.assets {
if k.len() < 56 {
return Err(format!(
"malformed asset key in wallet utxo {tx_hash}#{idx}: \
{k:?} is {len} chars, need 56 (policy_id_hex || asset_name_hex)",
tx_hash = u.tx_hash,
idx = u.output_index,
len = k.len(),
));
}
let (p, n) = k.split_at(56);
assets.push((p.to_string(), n.to_string(), q));
}
out.push(DaoWalletUtxo {
tx_hash_hex: u.tx_hash,
output_index: u.output_index,
lovelace: u.lovelace,
assets,
});
}
out
};
// ScriptRefs must be populated before this tool can build a tx.
// For Sulkta the values are known from the audit; user must pass