fix(close-account): fully close the User PDA and verify the system program account - #3
Open
SwineCoder101 wants to merge 2 commits into
Open
fix(close-account): fully close the User PDA and verify the system program account#3SwineCoder101 wants to merge 2 commits into
SwineCoder101 wants to merge 2 commits into
Conversation
…e-creation and accepts a bogus system program native and pinocchio: close_user must delete the PDA (all lamports back to the user), allow create_user to succeed again afterwards, and reject an instruction whose system-program account is not the System Program.
…ogram account native and pinocchio close_user refunded lamports - minimum_balance(0), leaving 890,880 lamports in a System-owned shell so the PDA was never deleted and create_user could never succeed again for that user. They also assigned the account to whatever key the caller passed as the system program. Drain every lamport to the payer and close the account (resize(0) + assign to the System Program id in native, AccountView::close() in pinocchio), and reject an instruction whose system-program account is not the System Program. Update the tests that asserted the residual account.
SwineCoder101
force-pushed
the
fix/close-account-full-close-native-pinocchio
branch
from
August 27, 2026 12:23
5e5302b to
b98ff4e
Compare
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.
Bug:
close_usernever closes the PDA and permanently blocks re-creationclose_userin the native and pinocchio variants refunds onlylamports - minimum_balance(0), leaving the rent-exempt minimum for a 0-byte account (890,880 lamports) inside the "closed" PDA. The account therefore survives as a System-owned shell, the user is short-changed on the refund, and because the System Program'screate_accountrejects a destination that already holds lamports,create_usercan never succeed again for that user. This is a functional bug (medium): every user who closes their account hits it. A second, low-severity bug in the same instruction is that the system-program account is never verified, so the caller can hand the residual PDA to any program they name.Affected
native/—basics/close-account/native/program/src/instructions/close_user.rspinocchio/—basics/close-account/pinocchio/program/src/lib.rs(process_close)anchor/is correct:close = userdrains every lamport and the PDA can be re-created.Functionality
create_usercreates aUserPDA (["USER", payer]) funded by the payer.close_useris meant to undo that: return all of the PDA's lamports to the payer and delete the account so the same PDA can be created again later.The bug
native/program/src/instructions/close_user.rs:29-36(before the fix) computesdiff = lamports - Rent::minimum_balance(0)and moves onlydiffto the payer, then resizes to 0 and callsassign(system_program.key).pinocchio/program/src/lib.rs:85-93(before the fix) does the same withrent.try_minimum_balance(0), thenassign(system_program.address())in anunsafeblock.Concrete scenario, both variants:
create_user— PDA holds 953,520 lamports (native, 9-byte account) / 1,002,240 lamports (pinocchio, 16-byte account) and is owned by the program.close_user— 890,880 lamports stay in the PDA; it is resized to 0 bytes and reassigned to the System Program. The account still exists.create_useragain — the CPI tosystem_program::create_accountfails withCustom(0)("account ... already in use") because the destination has a non-zero balance. The user is locked out for good.Additionally,
close_user.rs:42/lib.rs:98pass the third account's key straight intoassignwithout checking it. A caller who substitutes any address for the System Program ends up with a 890,880-lamport, 0-byte account owned by that address.The existing TS and Rust tests asserted the residual System-owned account (
account.exists,data.length === 0, owner == System Program), codifying the bug; they have been updated.Reproduce
Tests (both TS suites):
Close with a bogus system program account is rejected,Close the account,Re-create the account after closing it. Rust:test_close_account.Against the unmodified program (native shown; pinocchio is identical apart from line numbers):
Fix
solana_system_interface::program::ID/pinocchio_system::ID) and returnIncorrectProgramIdotherwise.resize(0), andassignto the System Program's known id instead of the caller-supplied key. A zero-lamport account is removed by the runtime at the end of the instruction, so the PDA address is free forcreate_useragain.AccountView::close(), which zeroes the account's owner, lamports and data length (the same patterntokens/escrow/pinocchiouses). The manual rent arithmetic,resizeandunsafe { assign }are gone.The Anchor variant already behaves this way (
close = user), so the three variants are now consistent.Verification
cargo fmtandcargo clippy -- -D warnings(the workspace-wide invocation CI uses) are clean.