#37079 [SC-Insight] Withdrawals can be DOSed by reviving tickets in the same burn tx
Submitted on Nov 24th 2024 at 23:54:29 UTC by @NinetyNineCrits for Audit Comp | Jito Restaking
Report ID: #37079
Report Type: Smart Contract
Report severity: Insight
Target: https://github.com/jito-foundation/restaking/tree/master/vault_program
Impacts:
Permanent freezing of funds
Description
Brief/Intro
When a withdrawal ticket gets closed, its data gets zeroed out but not resized to zero. When someone refunds the ticket account in the same transaction to prevent the account to be garbage collected, then trying to enqueue a new withdrawal for the same account will fail, because the acount is asserted to be empty. This will cause funds to be locked for third party programs building on top of the Jito vault-program.
Vulnerability Details
The process_burn_withdrawal_ticket calls the close_program_account function like this:
close_program_account(program_id, vault_staker_withdrawal_ticket_info, staker)?;This will transfer out all the lamports from the ticket account, assign its owner to the system program and zero out the data on the account:
If an account has zero lamports at the end of a tx, then the system will garbage collect it and remove all the data. If a subsequent instruction in the same tx transfers enough lamports back in, then the garbage collection will not happen. This is commonly known as a revival attack.
When a withdrawal gets enqueued in process_enqueue_withdrawal and a corresponding ticket needs to be created the load_system_account function will perform some checks on the ticket account prior to creation. Notably, it checks that the account has no data:
If the base seed used for the ticket creation is the same as a previously burned ticket that has been revived, the ticket account has not been garbage collected. The account will contain data (filled with zeroes) and the returned Err will cause process_enqueue_withdrawal to fail.
Impact Details
No withdrawals can be done for a base (seed of a withdrawal ticket) that has been previously used for a ticket that has been burned and revived. For cases, where there is no mint_burn_admin, this will only be an inconvenience, that is solvable on the client side by switching the base with a new keypair. But if the mint_burn_admin is a third party program that was built on top of Jito, then this will cause withdrawals to fail if base is a PDA that is reused, for which there can be various plausible scenarios:
The third-party program buffers withdrawals and then performs them in bulk reusing the same PDA to sign for
baseUsers can withdraw individually and for each user there will be a PDA created using only the users pubkey
The impact is ultimately frozen funds, that can not be withdrawn.
Recommendation and addressing scope
It can be argued that the underlying issue is that the function close_program_account, which is technically not listed as in scope, is at fault for setting all bytes to 0 instead of resizing the data to zero.
But just to showcase a simple fix on the in-scope code, a solution is to resize the data to zero before invoking close_program_account in process_burn_withdrawal_ticket (tested with the POC):
References
not applicable
Proof of Concept
POC
The following helper function needs to be added to fixtures/vault_client.rs within the VaultProgramClient impl:
This function works mostly like do_burn_withdrawal_ticket (same file), except that it adds a transfer to the instruction list.
Now add the following test to integration_tests/tests/vault/burn_withdrawal_ticket.rs and run it with SBF_OUT_DIR=$(pwd)/target/sbf-solana-solana/release cargo nextest run --all-features --no-capture test_99crits_prevent:
This will log:
Which is caused by load_system_account snippet mentioned in the issue description.
Last updated
Was this helpful?