Skip to content

close() in account.rs: unwrap() on checked_add should return a graceful error #4224

@Mctursh

Description

@Mctursh

Hello,

I was reading through the close function in anchor-lang and spotted an unwrap() on a checked_add that I think should be replaced with a proper error propagation. Here's the line in question:

pub fn close<'info>(info: AccountInfo<'info>, sol_destination: AccountInfo<'info>) -> Result<()> {
    let dest_starting_lamports = sol_destination.lamports();
    **sol_destination.lamports.borrow_mut() =
        dest_starting_lamports.checked_add(info.lamports()).unwrap(); // <-- this one
    **info.lamports.borrow_mut() = 0;

    info.assign(&system_program::ID);
    info.resize(0).map_err(Into::into)
}

Is this exploitable?

Honestly, not really, at least not today. Current total supply is around ~567M SOL (~567 * 10^15 lamports), which is nowhere near u64::MAX (~18.4 * 10^18). And unlike tokens with a hard cap, SOL has no max supply — it's inflationary by design. That said, even with inflation, we're still orders of magnitude away from overflow territory for the foreseeable future.

But I still think it's worth fixing, and here's why:

The real issue: CPI composability

This is the one that matters. close() gets called by basically every Anchor program that closes accounts. If another program calls into a close instruction via CPI and this somehow panics, the entire transaction dies. There's no way for the calling program to catch a panic from invoke / invoke_signed — it just kills everything.

If this returned a proper error instead, the caller would get back a ProgramError they can actually match on and handle gracefully. Right now, any program composed with an Anchor program's close instruction has a (theoretical) unrecoverable failure path baked in.

Given that close() lives in the framework itself and gets inherited across the ecosystem, the surface area here is pretty wide. On top of that, a panic just gives you a cryptic Program failed to complete in explorer logs — a proper error code is something integrators can actually work with. The SPL token program already handles this exact same operation gracefully in their close account path:

// From solana-program/token processor.rs — CloseAccount
let destination_starting_lamports = destination_account_info.lamports();
**destination_account_info.lamports.borrow_mut() = destination_starting_lamports
    .checked_add([source](https://github.com/solana-program/token/blob/main/program/src/processor.rs)_account_info.lamports())
    .ok_or(TokenError::Overflow)?;

(source)

Same operation, same context, but they propagate the error instead of panicking. I think Anchor should hold itself to the same standard.

Suggested fix

One-liner:

**sol_destination.lamports.borrow_mut() = dest_starting_lamports
    .checked_add(info.lamports())
    .ok_or(ProgramError::ArithmeticOverflow)?;

Zero risk of regression, function already returns Result<()>, and now it composes cleanly.

Happy to open a PR for this if you think it's worth merging. Let me know!

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions