Quantstamp reviewed Fragmetric Restaking Program on 1 Nov 2024: 17 findings, 0 critical; 4 marked fixed, 12 acknowledged, 1 open.
Report hosted on certificate.quantstamp.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 2 |
| Medium | 1 | 1 | 1 |
| Low | 0 | 11 | 1 |
A given `RewardAccount` can contain up to 4 pools of the type `RewardPool`. When `fragSol` moves from one user to another, or during minting and burning, the function `update_reward_pools_token_allocation` is invoked to update the reward pools in the given `rewardAccount`. This happens in the following loop: ``` for reward_pool in self.get_related_pools(&from.user, receipt_token_mint)? { ... reward_pool.update(effective_deltas, current_slot)?; ... } ``` However, if a `RewardPool` is closed, the `update()` function will throw an error: ``` fn update( &mut self, deltas: Vec<TokenAllocatedAmountDelta>, current_slot: u64, ) -> Result<Vec<TokenAllocatedAmountDelta>> { if self.is_closed() { err!(ErrorCode::RewardPoolClosedError)? } ... } ``` Thus, closing a single `RewardPool` in any `RewardAccount` will prevent updating all the reward pools. And also all user reward pools.
The protocol updates `fragSol` price with respect to `SOL` in order to maintain an up-to-date price. The price update is triggered through the following statement: `fund.update_token_prices(ctx.remaining_accounts)?;` The `update_token_prices()` first verifies that the `ctx.remaining_accounts` contain the address of a pricing source that is specified by the admin when they add a `supportedToken`. Then, it proceeds to get the current price of the asset by getting the spot price from either `Marinade pool` or `SPL Stake pool`. When a user requests withdrawal, by calling the `request_withdrawal()` function, a withdrawal request is created and added to a batch. Then an operator needs to call the `process()` method to start processing the batch. During processing, the `fragSol` amount is converted into `SOL` and the total `SOL` amount to be withdrawn is reserved in the `fundAccount.withdrawalStatus.reserved_fund`. Then, the user can call the `withdraw()` function to finally receive their `SOL` amount. The issue is, the token price can still be updated between the user calling `request_withdrawl()`. This can happen if another user calls the `deposit_sol()` because it will trigger a price update. Or if an operator calls the `update_prices()` function. Currently, even the `withdraw()` function performs a price update. and `withdraw()`. - Some users may not be able to withdraw because the subtraction from `sol_remaining` in `reserved_fund` could fail due to underflow. - The calculation for the value of `SOL` per `fragSol` would be inaccurate since the amount of `SOL` could be greater than the amount of SOL subtracted from `sol_operation_reserved_amount` which is used in `assets_total_sol_value()`. This would result in an artificially inflated token value. The inflated token price would allow more `SOL` to be withdrawn per token than expected. If `SOL` is withdrawn more than what is subtracted from `sol_operation_reserved_amount`, then the SOL stored for the rent-exemption could be transferred out.
The function `set_capacity_amount()` allows the admin to set a new `capacity_amount` for the `SupportedTokenInfo` type. However, it currently compares between two state variables instead of comparing against the input provided: ``` if self.capacity_amount < self.accumulated_deposit_amount { err!(ErrorCode::FundInvalidUpdateError)? } ``` The same issue also exists in the function `set_sol_capacity_amount()`: ``` if self.sol_capacity_amount < self.sol_accumulated_deposit_amount { err!(ErrorCode::FundInvalidUpdateError)? } ``` It is also worth noting that the `sol_accumulated_deposit_amount` and `accumulated_deposit_amount` do not seem to be decreasing at any point. So it will always be needed to update the capacity; and if the admin mistakenly sets the capacity to a value lower than `accumulated_deposit_amount`, the admin will not be able to update the capacity again.
There are three issues related to using the function `from_utf8_trim_null()` for the objects `RewardPool`, `Holder` and `Reward`. 1. There is a mismatch between the code and the spec for the function `from_utf8_trim_null()`, where the comment says: ``` /// Truncates null (0x0000) at the end. ``` But the function removes all `null` in `v`, not only at the end. 2. When objects mentioned above are initialized, the value of `name` or `description` is saved without being trimmed. However, what is returned by the functions `name()` and `description()` is trimmed. As a result, trimming is done `n` times instead of only once at initialization. Also, when calling `add_reward_pool()`, `add_holder()`, and `add_reward()`, the check of an existing item with the same name can be bypassed by using a name with added `null` chars. The values will differ, but once the object is added, the function `name()` will return the same value (trimmed) for both objects. 3. These objects can be created with an empty name or description.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (1)
236 projects reviewed · 332 reports · latest Oct 2026
See Quantstamp’s auditsSomething wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us