System Program transfer CPI fails for a program-owned PDA: move lamports with try_borrow_mut_lamports instead
Confusion: all accounts start owned by the System Program, which is what lets system_instruction::transfer debit them. After assigning an account to your own program (any PDA holding state), a System Program transfer CPI from that account fails — the System Program will not debit an account it does not own, and it cannot be made a signer for it.
That does not mean the lamports are stuck. A lamport balance is just a field on the account, and a program may modify any field of an account it owns, balance included. So debit and credit directly:
**ctx.accounts.chest_vault.to_account_info().try_borrow_mut_lamports()? -= 100_000; **ctx.accounts.player.to_account_info().try_borrow_mut_lamports()? += 100_000;
Rules the runtime enforces around this:
- Lamports must balance: the sum across all accounts touched by the instruction has to be unchanged, or the transaction fails. Never write a bare decrement without a matching increment.
- You may only *debit* accounts your program owns. Crediting any account is allowed, which is why the destination here can be a plain wallet.
- The account must stay rent-exempt for its data size after the debit, or it becomes closable/garbage-collected.
- The double dereference is required: try_borrow_mut_lamports() yields RefMut<&mut u64>.
Direction of transfer matters for which mechanism to use: moving lamports *into* a program-owned PDA still works fine via System Program CPI, because the debited account is the user's system-owned wallet. Only the outbound direction needs the manual mutation. This is also why "why does my vault withdrawal fail but the deposit works" is such a common report.
Source: adapted from a Stack Exchange answer by Jonas H. (question by Dylan Kerler), https://solana.stackexchange.com/questions/8836/how-can-the-system-program-credit-accounts-if-the-ownership-changes — CC BY-SA 4.0