#37314 [SC-High] Vault creators can not withdraw their fees without being recursively charged (vault and program) fees on their own fees which causes permanent loss of funds
Submitted on Dec 2nd 2024 at 05:56:27 UTC by @niroh for Audit Comp | Jito Restaking
Report ID: #37314
Report Type: Smart Contract
Report severity: High
Target: https://github.com/jito-foundation/restaking/tree/master/vault_program
Impacts:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
The following fees are collected in the Jito Restaking system: Vault fees - include withdraw fee, deposit fee and reward fee, set as bps by vault_fee_admin and collected as vrt_tokens during withdrawal/deposit/vault_update_balance. Program fee - set as bps by the config_admin and collected during withdrawals. all fee bps changes can only take effect at the start of the next process_initialize_vault_update_state_tracker. vault fees are sent to the vault_fee_wallet, and program fees to the program_fee_wallet, all in the form of vrt_tokens.
Vulnerability Details
The problem is that when a vault creator/admin wishes to withdraw their fees from the fee_wallet, they are subject to the same fee collection rules of any other withdrawal, including a "recursive" charge of withdrawal_fee_bps from the wallet to itself. This causes both a loss through paying excessive program fees, and a DOS on full fee withdrawal, as the following fee withdrawal flow demonstrates:
Assume vault_withdrawal_fee and program_fee are 10% each
A user withdraws 100_000 vrts
10_000 vrts are sent to the vault_fee_wallet and 10_000 vrts are sent to the program_fee_wallet, and the user receives the ST value of 80_000 vrts
Next the owner of vault_fee_wallet wants to withdraw the 10_000 vrts it received, and so it enqueues a withdrawal ticket for its 10_000 vrts.
Two epocs later, when the withdrawal ticket matures and burned, the burn_withdrawal_ticket instruction will "send back" to the vault_fee_wallet 1000 vrts, send 1000 vrts to the program_fee_wallet and send the ST value of 8000 vrts to the vault_fee_wallet.
The owner of vault_fee_wallet will now have to enqueue a new withdrawal ticket for the "sent back" fee of 1000 VRTs, which will again, only enable withdrawing 80% of the amount, charging the rest as vault/program fees. For a full withdrawal the vault_fee_wallet will have to recursively enqueue withdrawals multiple times until the amount left is so small it will be fully withdrawn. (see POC for detailed example).
Notes
While the vault creator controls the withdrawal_fee_bps, they can not resolve this by setting the withdrawal_fee_bps to zero before they burn the withdrawal ticket and back up after the burn. The reason is that fee changes only take effect on the next process_initialize_vault_update_state_tracker. If the vault owner changes the fee to zero before the update cycle of the epoc in which they burn their withdrawal ticket, this change will remain in effect atleast until the next epoc, causing an unpredictable loss from forgone withdrawal fees during that epoc.
The same problem exists for the program fee. When the program_fee_wallet owner tries to withdraw the fee, program_fee_wallet will recursively "charge to itself" the program fee in addition to paying the vault withdrawal_fee. Similarly to the vault_fee_wallet, full withdrawal will be DOSed for a prologed time due to the multiple calls required, and will cause the program to over-pay vault withdrawal fees.
Recomendation
Make withdrawals signed by the vault_fee_wallet/program_fee_wallet exempt from fees. They can be either entirely exempt (from both vault and program fees), or each exempt only from recursively paying themselves fees. (depending on the protocol's desired behavior)
Impact Details
This bug results in two main impacts:
loss of funds for the owner of vault_fee_wallet, through overpaying program fees (see POC example where 25% higher program fees are paid). While technically not "theft", the impact from the vault owner perspective is the same, as their funds end up in someone else's hands.
A secondary impact is DOS (and additional transaction costs) of full fee withdrawal. This is due to the need to perform multiple withdraws that require each a couple of epocs to complete. In the POC example, a full withdrawal of fees will take atleast 42 epocs or 84 days. Considering that during this period new fees are expected to accrue, this practically means the vault owner is never able to fully withdraw fees.
References
https://github.com/jito-foundation/restaking/blob/406903e569da657035a2ca71ad16f8a930db6940/vault_program/src/burn_withdrawal_ticket.rs#L94 https://github.com/jito-foundation/restaking/blob/406903e569da657035a2ca71ad16f8a930db6940/vault_core/src/vault.rs#L1008
Proof of Concept
Proof of Concept
How to run
In integration_tests/tests/fixtures/vault_client.rs line 236, change the program_fee_bps parameter from 0 to 1000 (quick workaround to set a 10% program fee)
Copy the code below to a new test file under integration_tests/tests/vault/
Run
RUST_LOG=off RUST_BACKTRACE=1 cargo nextest run --nocapture test_vault_fee_withdrawal