An approved Squads vault transaction names a target program ID, not the executable that members reviewed. If an independent target upgrade authority redeploys that program before execution, Squads can deliver the unchanged call and its approved vault PDA signer to replacement code. Squads vault operators, proposal executors, governance tooling authors, and auditors therefore cannot treat Approved status alone as proof that the reviewed code will run.
For an ordinary dependency upgrade, the realistic consequence is a stale review: the saved call can fail or produce a state transition the quorum did not simulate. The adversarial consequence requires a malicious or compromised target upgrade authority, an approved call that remains executable with useful signer and writable privileges, and a valid Squads member who later submits it. The upgrade actor can be outside the vault quorum.
An isolated Agave 4.0.2 reproduction establishes the runtime link that source review alone left inferred. A local delayed executor approved a fixed message while reviewed target code was installed, Loader-v3 replaced the target under the same program ID, and the executor later delivered the unchanged message and its vault PDA signer to replacement code. The replacement used that signer in a successful one-lamport System Program transfer. A deployment-slot guard rejected execution after the deployment-slot change and before target CPI.
A read-only finalized mainnet census measures current account shapes. It selected 500 of 33,770 approved Squads proposals by a fixed hash ordering. Of 443 parsable VaultTransactions, 75 called a currently upgradeable Loader-v3 program while forwarding the vault as a writable signer. Five messages approved in the preceding 90 days also supplied the System Program, another writable account, and a system-owned vault with a positive SOL balance.
Four of those five recent messages share one vault holding 571.620909687 SOL at the observation slot. They call the current MPL Core program and supply writable accounts owned by MPL Core. One was approved on July 7, 2026, while the target's current ProgramData deployment is dated July 15, so it remained Approved across an observed deployment-version change. Historical executable bytes were not available, so the timing does not prove a semantic or malicious code change. The census also did not verify that vault's data length or whether any of the proposals still satisfies every execution prerequisite. The measured balance therefore bounds an inferred worst case, not demonstrated live exposure.
The exact review decision is to treat target executable identity as part of the approval. Before execution, verify that every target still matches the version members reviewed. If any identity signal changed, pause or cancel the call, re-simulate it, and require a fresh approval.
What an approval actually binds
The current Squads v4 VaultTransactionMessage contains header counts, account keys, compiled instructions, and address-table lookups. Each compiled instruction identifies a program by its index in that account list, then stores account indexes and instruction data. The schema has no target ProgramData address, deployment slot, executable digest, or reviewed release identifier.
That design closely follows an ordinary Solana transaction message. It is useful because Squads can store arbitrary instructions and execute them later. It also means the authorization has the same semantic limit as a normal signed message: it names a program address, not the code bytes that the runtime will resolve for that address.
The pinned VaultTransaction schema makes that boundary visible. The execution path accepts an approved proposal after its time lock, derives the vault PDA seeds, reconstructs the stored message, and executes its instructions. The source explicitly allows an approved proposal to execute even if it later becomes stale.
Finally, Squads' executable-message utility rebuilds each Instruction, restores the stored signer and writable flags, and calls invoke_signed with the vault seeds. It checks that message accounts and privileges match, and it protects Squads' own proposal account from writable reentrancy. It does not compare target code identity with approval-time state.
The consequential composition sits between those checks:
- Members review and approve program ID P, accounts A, and data D.
- P is a Loader-v3 program with a live upgrade authority independent of the Squads quorum.
- That authority deploys another implementation behind the same P.
- A member with Squads Execute permission executes the unchanged approved message.
- Squads invokes the current implementation of P and gives it the approved vault signer privilege.
Solana's program documentation explains that an upgradeable program keeps its program address while its ProgramData and upgrade authority govern replacement. The CPI documentation explains the other half: signer and writable privileges extend from caller to callee, and invoke_signed lets a program sign for its PDA. Consequently, replacement code does not need a fresh vault signature. It receives the signature that the delayed executor was already authorized to synthesize.
The signer is the security boundary
An upgradeable target alone does not satisfy the finding. A call may touch only read-only public state or omit any authority that replacement code could use. The reusable audit predicate is narrower than "proposal targets an upgradeable program."
Flag a delayed instruction when all of these are true:
- approval stores a program ID and call bytes but no executable identity checked at execution;
- the target is still upgradeable when the delayed call can run;
- a governance, treasury, or vault PDA is forwarded as a signer; and
- the supplied writable accounts let the target make a consequential state transition.
For a direct SOL movement path, strengthen the last condition to require the PDA as both signer and writable, the System Program available to the target call, and another writable recipient account. A system-owned, zero-data vault can then be the from account of a System Program transfer.
The current MPL Core-shaped messages are stronger still. Each supplies two writable accounts currently owned by the target program. If a measured proposal remains executable and its system-owned vault also has zero data bytes, the inferred capability path lets replacement code use the forwarded vault signer to transfer SOL into one of those target-owned accounts. Because the replacement still executes as the same program ID, it owns that account and could later debit its lamports to an attacker-controlled writable account. The census did not establish either prerequisite, and this is not evidence that MPL Core, its upgrade authority, the vault operator, or any proposal is malicious.
The actor distinction matters. A target upgrade authority is expected to choose target code, and that expected power alone is not a vulnerability. Here, the same choice determines which code receives a separate Squads vault signer that another quorum approved earlier. While the saved call remains executable, the composition lets the target-code authority consume that signer without joining the vault quorum. The violated semantic-authorization invariant is separate: the code receiving the signer should match what the quorum reviewed, or the quorum should knowingly approve the new deployment.
A second implementation has the same shape
The pattern is not specific to one Squads struct. Archived SPL Governance source stores InstructionData as program ID, account metadata, and data in its ProposalTransaction state. Its execute processor later turns those fields back into instructions and invokes them with governance or treasury PDA seeds.
That supplies a second meaningful source-level instance of the generic class. The account schemas and proposal lifecycles differ, but the authorization boundary is the same: an earlier vote authorizes a stable program address, and later runtime resolution chooses the executable that receives PDA authority. This research did not establish that a particular deployed SPL Governance binary equals the pinned source.
The detector rejects immutable targets, messages that do not forward the PDA, read-only PDAs, calls without a useful downstream primitive, and messages that can no longer execute. Its scope is particular delayed calls, not all SPL Governance proposals or Squads messages.
A local Agave trace closes the causal gap
The evidence-upgrade artifact builds three small SBF programs and runs them on an isolated solana-test-validator 4.0.2. It is a minimal executor, not Squads bytecode. Its approval account stores a SHA-256 digest over the target program ID, vault PDA, destination, System Program, signer and writable role, and fixed target instruction byte. It also records the Loader-v3 ProgramData deployment slot seen during approval.
Three independent approval accounts stored the same digest, b73d36e0...cbd7243a, while reviewed code was installed at deployment slot 0. A baseline approval then executed through invoke_signed. Agave logs show that reviewed code observed the vault PDA signer, and the vault and destination balances remained 1,000,000 and 890,880 lamports.
The harness next used Loader-v3 to deploy different SBF code under the same target program ID and ProgramData address. ProgramData advanced from deployment slot 0 to 13. After the program-cache delay, both still-pending approval accounts retained the original digest.
The no-signer control deliberately used ordinary invoke instead of invoke_signed. Replacement code logged that the PDA signer was absent and returned custom error 6002. Both balances stayed unchanged. Unguarded execution of the other identical approval then used invoke_signed. Replacement code invoked the real System Program, which transferred one lamport from the system-owned, zero-data vault PDA. The vault fell from 1,000,000 to 999,999 lamports and the destination rose from 890,880 to 890,881.
Finally, guarded execution compared the approval-time slot 0 with current slot 13. It returned custom error 6001 before target CPI, left its approval pending, and changed neither balance. The predeclared baseline, negative control, capability use, and guard observations all passed.
The earlier dependency-free capability-drift-model.js remains as a readable abstraction with immutable-target, read-only-vault, and absent-System-Program controls. The Agave trace now establishes the runtime composition under local Loader-v3 and real CPI semantics. It still does not prove that a measured mainnet Squads proposal is executable, that a measured vault has zero data bytes, that a current deployment changed semantics, or that either pinned governance source matches deployed bytes. No public-chain transaction was submitted.
What the mainnet census measured
The read-only census queried the current Squads v4 mainnet program at finalized commitment. It selected proposals by the lowest values of sha256("hackhack-2026-08-30-approval-code-drift-v1:" + proposal_pubkey). This makes reruns deterministic and avoids choosing visually interesting proposals after seeing their contents.
At finalized slots 442,742,903 through 442,742,921, the population query returned 33,770 approved proposals. The script selected 500, derived all 500 corresponding transaction addresses, and parsed 443 VaultTransactions with zero parse errors. The other 57 transaction accounts were not parsed as VaultTransactions and were not forced through that decoder.
The 443 messages contained:
- 1,178 instructions across 50 distinct static target program IDs;
- 326 Loader-v3 instructions in 252 messages;
- 107 instructions, in 88 messages, whose targets currently retained an upgrade authority;
- 433 instructions forwarding the vault signer, including 389 forwarding it as writable; and
- 94 instructions, in 75 messages, combining a currently upgradeable target with a writable vault signer.
The 75 is a count inside the deterministic sample, not an estimate that the same rate holds across all 33,770 approvals. This version also resolves only static target program IDs. It does not classify targets referenced solely through address lookup tables.
Among the 75 matching messages, 22 were approved within 365 days, six within 90 days, and two within 30 days. Five of the six 90-day messages met the census-visible parts of the direct-SOL account shape: a writable vault signer, the System Program, another writable account, and a positive system-owned vault balance. They referenced two unique vaults with a combined point-in-time balance of 571,621,909,687 lamports. The artifact did not preserve the vaults' full data lengths, so it does not establish the complete System Program transfer predicate for either vault.
Four messages approved between July 7 and August 9 use the same 571.620909687 SOL vault and call MPL Core program CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d. At observation time its ProgramData account recorded deployment slot 433,108,634, block time July 15, and a present upgrade authority. The July 7 message therefore predates the current deployment. A June 11 message into SPL Stake Pool similarly predates its current June 15 deployment, although its measured vault held only 0.001 SOL.
These two timelines prove that an approved message can remain approved across an actual deployment-version change. They do not prove that the before and after bytecode differed, because the public RPC snapshot did not retain the historical ProgramData bytes used for approval. They also do not show that either proposal is intended to execute now.
Expected behavior, unexpected authorization
Every primitive here is intended in isolation:
- a Loader-v3 upgrade authority can replace program code;
- the program ID remains stable;
- a governance proposal can wait after approval;
- approved stale Squads proposals may execute; and
- CPI privileges, including PDA signatures, propagate to callees.
This research treats semantic approval as the independent security invariant: the code receiving a privileged signer at execution should match the code members reviewed, or members should approve the new deployment knowingly. If another authority can replace that code without changing any bytes displayed in the proposal, the old approval is no longer sufficient evidence for the resulting transition.
The realistic benign consequence is operational: an upgrade can change validation, fees, account interpretation, or effects while an old approval remains live. Execution can fail or perform a state transition members did not simulate. The realistic adversarial consequence requires a malicious or compromised target upgrade authority, an approved message that still satisfies its time lock and remaining account checks, and submission by a valid executor. It does not require the upgrade actor to join the vault quorum.
The bounded worst case is conditional on every link. One of the four measured MPL Core messages must remain executable, including its time lock and account checks. Its vault must still be a system-owned, zero-data, writable signer funded with the point-in-time 571.620909687 SOL balance. The target must be maliciously upgraded, a valid Execute member must submit the call, and no executable-identity guard can intervene. Only then does the inferred capability trace let replacement code move up to that measured balance into a target-owned account and later debit it. The census did not verify vault data length, current proposal executability, any of those future events, token holdings, target-authority attribution, intent, compromise, or loss.
How to review and contain the class
Start by inventorying every approved but unexecuted instruction that forwards a privileged PDA. For each target program:
- Resolve whether it is native, immutable, Loader-v3 immutable, or Loader-v3 upgradeable.
- Record the ProgramData address, deployment slot, upgrade-authority state, and a trusted release or executable digest when members review the proposal.
- Repeat the resolution immediately before execution.
- If any identity signal changed, stop. Re-simulate the exact message against current finalized state and obtain a fresh approval.
- Apply a higher review tier when the PDA is signer and writable, especially when the CPI includes System Program, token programs, authority-changing instructions, or target-owned writable accounts.
Deployment slot is a cheap change detector, not a perfect code identity. A redeployment of identical bytes changes the slot, while permissionless ProgramData extension may also change operational metadata and cause a conservative false positive. A verified executable digest is more precise, but hashing a large ProgramData account on chain can be expensive. An off-chain executor policy can compare verified build metadata and block the execute UI. A protocol-level wrapper can pin a compact release commitment or require an attested version account controlled under an appropriate trust model.
Incident response needs the same rule in reverse. Rotating the vault's own keys or a program's upgrade authority does not automatically neutralize unrelated approved messages. After any critical dependency upgrade or authority compromise, enumerate delayed calls into that program, cancel them where possible, and reapprove only after reviewing the deployed version. The cancellation set is defined by target-code dependency, not only by which local authority changed.
Finally, immutability is the strongest negative control. If a dependency no longer needs upgrades, removing its upgrade authority collapses this particular time-of-check/time-of-use boundary. Where mutability is necessary, governance tooling should surface it as part of the object being approved, not as background metadata.
An approved Solana instruction identifies where execution will go. For upgradeable targets, it does not necessarily identify what code will be there when execution arrives. Any delayed executor that supplies a PDA signer should make that distinction visible and enforce it before the signature is synthesized.