Skip to content

Verify Wormhole VAA fields properly and bound governance proposal length - #469

Merged
abayomicornelius merged 4 commits into
Heliobond:mainfrom
Temi-suwa18:fix/bridge-security-452-453-454-455
Aug 24, 2026
Merged

Verify Wormhole VAA fields properly and bound governance proposal length#469
abayomicornelius merged 4 commits into
Heliobond:mainfrom
Temi-suwa18:fix/bridge-security-452-453-454-455

Conversation

@Temi-suwa18

Copy link
Copy Markdown
Contributor

Summary

Four independent fixes: three tighten complete_bridge_transfer's validation of an inbound Wormhole VAA, one bounds an unbounded string field in project_registry.

Trust decision used unverified payload field instead of VAA-verified one (#452)

The trusted-emitter lookup keyed off transfer.source_chain — decoded from the payload bytes themselves, which parse_bridge_payload doesn't independently validate — instead of parsed.emitter_chain, the chain ID Wormhole's guardian network actually cryptographically attests as the VAA's real origin. Switched both the TrustedEmitter lookup and the BridgeTransferCompleted event to use parsed.emitter_chain.

token_address decoded but never checked (#453)

BridgeTransferPayload.token_address identifies which asset a message concerns, but complete_bridge_transfer decoded it and never read it — minting was gated purely by the trusted-emitter check plus recipient/amount. Not exploitable in isolation today, but if the same trusted emitter is ever reused for a multi-asset bridge, a VAA about a different asset would be silently minted as HBS. Added an assertion that transfer.token_address matches this vault's own contract address.

target_chain decoded but never checked (#454)

Same pattern: transfer.target_chain is meant to confirm a message is actually destined for Stellar, but was decoded and ignored. Added an assertion that transfer.target_chain == wormhole::chain_id::STELLAR.

create_proposal's description has no length bound (#455)

create_project's uri is strictly bounded (MIN_URI_LEN/MAX_URI_LEN) specifically to prevent excessively large ledger entries, but create_proposal's description had no equivalent bound — any whitelisted address could store an arbitrarily large description indefinitely. Added MAX_PROPOSAL_DESCRIPTION_LEN (2048 bytes), mirroring the URI pattern. Distinct from #332 (unbounded Vec params / missing voting_duration_secs upper bound).

Why these are safe, minimal fixes

All four are additional validation checks on an already-gated entry point (the trusted-emitter allowlist already governs who can trigger a mint at all) or a straightforward length bound matching an existing pattern in the same file — no changes to any existing authorized behavior's happy path, no storage layout changes. stellar contract build passes clean across the whole workspace.

Closes

Closes #452
Closes #453
Closes #454
Closes #455

Test plan

  • stellar contract build (full workspace, wasm32v1-none) — passes clean
  • CI

complete_bridge_transfer's trusted-emitter lookup keyed off
transfer.source_chain — a field decoded from the payload bytes themselves,
which parse_bridge_payload doesn't independently validate — instead of
parsed.emitter_chain, the chain ID Wormhole's guardian network actually
cryptographically attests as the VAA's real origin. Using a sender-supplied
payload field for a trust decision instead of the VAA-verified envelope
metadata is exactly backwards for a security check: if payload content and
VAA envelope ever diverge (e.g. a misconfigured or malicious source-chain
contract embedding the wrong chain ID in its own outbound payload), the
allowlist lookup would key off the wrong value. The BridgeTransferCompleted
event had the same issue, reporting the unverified field.

Use parsed.emitter_chain for both the TrustedEmitter lookup and the
completion event.

Closes Heliobond#452
…ge_transfer

BridgeTransferPayload.token_address exists specifically to identify which
asset a cross-chain message concerns, and initiate_bridge_transfer sets it
to the vault's own encoded address on the outbound side. But
complete_bridge_transfer decoded the full payload and never once read
transfer.token_address — it minted HBS purely from the trusted-emitter
check plus recipient/amount, with no assertion the incoming message was
actually about this vault's own token. Not exploitable in isolation today
since the trusted-(chain, emitter) allowlist already gates who can trigger
a mint, but if the same trusted emitter is ever reused for a multi-asset
bridge (a common pattern) or a future version adds more token types, a VAA
meant for a different asset would be silently accepted and minted as HBS.

Assert transfer.token_address matches this vault's own contract address
before minting. New VaultError::BridgeTokenMismatch.

Closes Heliobond#453
…e_transfer

BridgeTransferPayload.target_chain is set by the sender on the outbound
side to indicate which chain a transfer is destined for. Same gap as the
token_address issue: complete_bridge_transfer decoded this field into
transfer.target_chain but never read it anywhere, relying entirely on the
trusted-emitter allowlist to prevent misuse rather than also checking the
message actually claims Stellar as its destination.

Assert transfer.target_chain == wormhole::chain_id::STELLAR before
proceeding to mint. New VaultError::BridgeWrongTargetChain.

Closes Heliobond#454
create_project's uri is strictly bounded (MIN_URI_LEN/MAX_URI_LEN)
explicitly to prevent excessively large ledger entries, but
create_proposal's description had no equivalent bound — any whitelisted
address (not just the owner) could call create_proposal with an
arbitrarily large description, stored indefinitely as part of the
Proposal persistent entry. Distinct from Heliobond#332, which covers
compact_storage's unbounded Vec params and the missing
voting_duration_secs upper bound, not description's length.

Add MAX_PROPOSAL_DESCRIPTION_LEN (2048 bytes), mirroring the URI-length
pattern, with a new RegistryError::ProposalDescriptionTooLong variant.

Closes Heliobond#455
@drips-wave

drips-wave Bot commented Aug 24, 2026

Copy link
Copy Markdown

@Temi-suwa18 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@abayomicornelius
abayomicornelius merged commit b1802f8 into Heliobond:main Aug 24, 2026
1 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment