wormhole: write nullifiers after settlement checks + rollback regression tests#568
Open
n13 wants to merge 2 commits into
Open
wormhole: write nullifiers after settlement checks + rollback regression tests#568n13 wants to merge 2 commits into
n13 wants to merge 2 commits into
Conversation
adding unit tests making sure failures roll back correctly
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Audit item
Why the bug as described is not exploitable
The audit's premise — "FRAME dispatchables do not automatically promise rollback unless explicitly wrapped transactionally" — has not been true for
#[pallet::call]dispatchables since paritytech/substrate#11431 (followed by #11927, which moved the wrap directly into each call body).Every
#[pallet::call]dispatchable is automatically rewritten by the macro to be:This is visible in
frame-support-procedural-36.0.0/src/pallet/expand/call.rs:241-247(the version this workspace uses) and documented on the#[pallet::call]rustdoc:So if the original
verify_aggregated_proofreturnedErr(TransferAmountBelowMinimum)orErr(ArithmeticError::Overflow)after writingUsedNullifiers, the macro's storage layer would already roll those writes back. We confirmed this empirically while building the regression tests — both attacker-grief and normal-user failure modes correctly roll nullifiers back without any code change.What this PR does anyway (defense-in-depth)
Even though the bug isn't exploitable today, the audit raises two genuine concerns we want to lock in:
Order of operations is not self-evident. A future maintainer reading
verify_aggregated_proofshouldn't have to know about#[pallet::call]'s implicit wrap to understand why writing nullifiers before fallible work is safe. So we reorder the write to happen only after all cheap checks pass (TransferAmountBelowMinimum, account decoding, balance conversion). After this PR, even if the storage layer were ever removed, the most obvious failure modes can never consume nullifiers.No regression test pinned the property. We add two tests covering both scenarios the audit calls out:
..._below_minimum_does_not_consume_nullifiers— normal-user case: proof becomes under-minimum at settlement time, nullifiers must remain spendable...._settlement_overflow_rolls_back_nullifiers— attacker grief case: an exit account is pre-funded toBalance::MAXso the settlement mint overflows after nullifiers are written, validating the storage-layer rollback works end-to-end.If
#[pallet::call]'s auto-wrap is ever removed in a future FRAME upgrade, or if anyone moves the nullifier write back to the top of the dispatchable, both tests fail loudly.Files changed
pallets/wormhole/src/lib.rs— moveUsedNullifiers::<T>::insertloop to after theMinimumTransferAmountcheck (the only substantive behavior change).pallets/wormhole/src/tests.rs— two new regression tests; small DRY refactor that extractssetup_valid_proof_state,nullifier_bytes,exit_accounts, andassert_nullifiers_unusedhelpers and uses them in three existing tests as well.pallets/wormhole/src/mock.rs—MinimumTransferAmountbecomes apub storageparameter so the new test can override it (same pattern aspallets/multisig/src/mock.rs).Test plan
cargo test -p pallet-wormhole— 22/22 pass.cargo check -p quantus-runtime— builds clean.#[pallet::call]'s storage layer protects the existing code.