fix(contracts): bind cross-vault state in lifecycle authorization - #133
Merged
Merged
Conversation
mellowcroc
marked this pull request as ready for review
September 14, 2026 14:05
graikos
reviewed
Sep 15, 2026
…nt-state-binding # Conflicts: # contracts/.vk-hash # contracts/src/tests/child.test.ts
graikos
approved these changes
Sep 15, 2026
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.
Summary
AccountUpdateobjects before reading cross-vault statepropose()andapproveProposal()Security impact
Fixes F-2026-19300. Previously, parent state reads were recorded on an unattached
AccountUpdate, allowing a prover to substitute a forged approval root and threshold forCREATE_CHILD,RECLAIM_CHILD,DESTROY_CHILD, andENABLE_CHILD_MULTI_SIG. Attaching the RootVault update makes Mina compare those values with the actual RootVault account state.The same detached-read pattern also existed in
assertFreshProposalNonce(): parentpropose()andapproveProposal()could prove against substituted childownersCommitment,parent, andparentNoncevalues. This was not an independent unauthorized-execution path because the child rechecks its real state during execution, but the proposal/approval circuit was not enforcing the child-state validation it claimed to enforce. This PR now attaches that nonce-authority update as well.Both foreign updates are precondition-only: they do not mutate the referenced vault or require its owner signature.
Regression coverage
The fast malicious-prover tests assert that:
propose()andapproveProposal()reject forged SubVault stateAccount_app_state_precondition_unsatisfiedTwo opt-in proof-enabled tests repeat the highest-impact forged-parent
executeDestroyattack and the forged-childpropose()attack. Each asserts that o1js produced a real proof object rather than a dummy proof before confirming rejection.Verification keys
The circuit changed, so both canonical hashes were regenerated after synchronizing with current
main:2454965416820089427434346910577830402844563585696865869814526213712389253085423011579427028232763944993528190930908391338988194510399893639369434906880365Testing
bun run --filter contracts buildbun run --filter contracts typecheckbun run --filter contracts test— 152 passed, 0 failed, 3 intentional skips before the finalmainsyncpropose()andapproveProposal()regressions after sync — 2 passedCREATE_CHILDsetup regression — passedRUN_REAL_PROOF_TESTS=1 bun test src/tests/child.test.ts --test-name-pattern 'genuine MinaGuard proof' --timeout 900000after sync — 2 passed; real proofs produced and rejected for the expected foreign-state precondition mismatches