oro-inti
Cantina Security Report
Organization
- @Orogold
Engagement Type
Cantina Reviews
Period
-
Repositories
N/A
Researchers
Findings
Critical Risk
11 findings
11 fixed
0 acknowledged
High Risk
1 findings
1 fixed
0 acknowledged
Medium Risk
5 findings
5 fixed
0 acknowledged
Low Risk
3 findings
3 fixed
0 acknowledged
Informational
5 findings
5 fixed
0 acknowledged
Critical Risk11 findings
Incorrect init Constraint in toggle_liquid Causes Fund Locking and DoS
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
FrankCastle
Incorrect init Constraint in toggle_liquid Causes Fund Locking and DoS
Description
The
toggle_liquid
function is completely frozen because it uses theinit
constraint with theconfig
account. Since theinit
constraint is already used in theinitialize_config
function, any attempt to calltoggle_liquid
will always revert.This results in two critical issues affecting the protocol’s functionality:
- Fund Locking: If
toggle_liquid
is responsible for initialization, the bump seeds used for signing the transfer CPI will not be stored hence the signing will not work permanently. This will lead to a complete lock of funds for all tokens staked by users. - Denial of Service: If
initialize_config
is used for initialization, theliquid_staking
program will be permanently disabled because theliquid
field in theconfig
account will always remainfalse
. As a result, theliquid_stake
function will always revert, preventing staking operations.
Recommendation
Modify the
config
account in thetoggle_liquid
function to avoid reinitialization. The corrected implementation is as follows:#[account(mut,seeds = [b"config".as_ref()],bump = config.bump,)]pub config: Account<'info, State>,Wrong price calculation from Gold to USDC
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
0xhuy0512
Description
In the
mint_gold()
,burn_gold()
,buy()
andsell()
functions, the USDC amount required or received is calculated based on these formulas:// in buy() and mint_gold()amount * price / 100 * (10000 + self.price.fee)// in sell() and buy_gold()amount * price / 100 * (10000 - self.price.fee)These formulas are incorrect due to several issues:
- They don't account for the Pyth price feed exponential value of XAU/USD
- They fail to handle the decimal differences between Gold token and USDC token properly
- They don't divide by
price.fee
's denominator (10000) after multiplication - They divide first and then multiply, which leads to precision loss
Recommendation
The formulas should be corrected as follows (in pseudocode):
// in buy() and mint_gold()- amount * price / 100 * (10000 + self.price.fee)+ amount * price * (10000 + self.price.fee) * pow(10, mint_b.decimals - mint_a.decimals) * pow(10, pyth_price_result?.exponent) / 10000// in sell() and buy_gold()- amount * price / 100 * (10000 - self.price.fee)+ amount * price * (10000 - self.price.fee) * pow(10,mint_b.decimals - mint_a.decimals) * pow(10, pyth_price_result?.exponent) / 10000claim() and unstake() functions will not be useable
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
0xhuy0512
Description
The position account in both
claim()
andunstake()
functions is marked with theinit
constraint, while this same account is already initialized in thestake()
function:#[account(init,payer = user,space = 8 + Position::INIT_SPACE,)]pub position: Box<Account<'info, Position>>,This will cause the
claim()
andunstake()
functions to revert when called, since an account cannot be initialized if it already exists. As a result, all funds staked are stuck indefinitely, as users cannot claim rewards or withdraw their tokens.Recommendation
Replace the
init
constraint with themut
constraint in the position account definition for bothclaim()
andunstake()
functions.Missing seed and bump constraints in position account in claim() and unstake() functions
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
0xhuy0512
Description
The position account in both
claim()
andunstake()
functions is missingseeds
andbump
constraints, which means a user can include any position account belonging to another user when calling these functions. As a result, a malicious user can claim/unstake others' positions to their own wallet, effectively stealing their funds.#[account(init,payer = user,space = 8 + Position::INIT_SPACE,)]pub position: Box<Account<'info, Position>>,Recommendation
Add
seeds
andbump
constraints to the position account in bothclaim()
andunstake()
functions, similar to how they are defined in thestake()
function.Multiple unstake() calls at the same position can drain the vault
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
0xhuy0512
Description
In the
unstake()
function, when a user unstakes their position, all staked tokens plus accrued interest are sent back to the original staker. At this point, the staker should not be allowed to unstake from the same position again. However, there is no restriction in the function preventing multiple unstake calls, which allows users to withdraw the same amount of Gold tokens repeatedly from a single position. A malicious user can exploit this vulnerability to drain the vault by callingunstake()
multiple times on the same position.Recommendation
To prevent this vulnerability, consider implementing one of these solutions:
- Close the position after the unstake operation is complete
- Set the position's amount to zero after a successful unstake
- Add a flag to track whether a position has already been unstaked
Incorrect Reward token calculation enables pool drain via arbitrage
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
0xhuy0512
Description
The
liquid_stake()
function contains a critical mathematical error in calculating the amount of reward tokens to mint to stakers when the rewards mint supply is non-zero. The current implementation calculates the reward amount as:amount * self.config.liquid_amount / self.rewards_mint.supplyThe root cause is in the reward calculation logic where the numerator and denominator are swapped. This creates a situation where malicious users can:
- Perform
liquid_stake()
to receive an inflated amount of reward tokens - Use
liquid_unstake()
to withdraw more underlying tokens than initially deposited - Repeat until the pool is drained
Recommendation
The reward token calculation should be corrected to maintain proper proportions between deposits and rewards. Update the calculation to:
- amount * self.config.liquid_amount / self.rewards_mint.supply+ amount * self.rewards_mint.supply / self.config.liquid_amountIncorrect token amount calculation in liquid unstaking leads to asset loss
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
0xhuy0512
Description
The
liquid_unstake()
function incorrectly handles token amounts during the unstaking process, leading to potential asset loss.The root cause lies in two key areas:
-
Token Amount Mismatch: While
liquid_stake()
accepts GOLD token amount as user input,liquid_unstake()
accepts reward tokens amount as user input. This inconsistency creates an asymmetric relationship between staking and unstaking operations. -
Incorrect State Update: The function decrease
config.liquid_amount
by the reward token amount instead of the GOLD token amount. Sinceliquid_amount
tracks the total GOLD tokens in the vault, this leads to incorrect accounting of the protocol's assets.
This vulnerability can be exploited by users to manipulate the exchange rate between GOLD and reward tokens, potentially draining the vault.
Recommendation
The function should be modified to:
- Use the input amount as GOLD currency to maintain consistency with the
liquid_stake()
function, adjusting calculations accordingly - Update
config.liquid_amount
with the correct GOLD amount
Here's the proposed fix:
pub fn liquid_unstake(&mut self, amount: u64) -> Result<()> {let (canonical_bump_pda, _canonical_bump) =Pubkey::find_program_address(&[b"whitelist", &self.user.key.to_bytes()], &ID);assert_eq!(canonical_bump_pda, self.whitelist.key());let cpi_program = self.token_program.to_account_info();let seeds = &[b"config".as_ref(), &[self.config.bump]];let signer_seeds = &[&seeds[..]];let cpi_accounts = Burn {mint: self.rewards_mint.to_account_info(),from: self.rewards_ata.to_account_info(),authority: self.user.to_account_info(),};let cpi_context: CpiContext<'_, '_, '_, '_, Burn<'_>> =CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds);- let amount_to_burn = amount;- let amount_to_transfer = amount * self.config.liquid_amount / self.rewards_mint.supply;+ let amount_to_burn = amount * self.rewards_mint.supply / self.config.liquid_amount;+ let amount_to_transfer = amount;burn(cpi_context, amount_to_burn)?;let transfer_accounts = TransferChecked {from: self.vault.to_account_info(),mint: self.mint_a.to_account_info(),to: self.user_ata_a.to_account_info(),authority: self.config.to_account_info(),};let cpi_ctx = CpiContext::new_with_signer(self.token_program.to_account_info(),transfer_accounts,signer_seeds,);let _ = transfer_checked(cpi_ctx, amount_to_transfer, self.mint_a.decimals);self.config.liquid_amount -= amount;Ok(())}Missing mutable account aonstraint prevents state updates
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
0xhuy0512
Description
The
config
account inliquid_stake()
,liquid_unstake()
, andreward()
functions lacks the requiredmut
constraint in the account validation, preventing critical state updates from being persisted.In Solana, accounts that need to be modified during instruction execution must be explicitly marked as mutable using the
mut
constraint. Without this constraint, any attempts to modify the account's data will not be persisted at runtime, even though the account is successfully loaded and the modification logic executes correctly.The
config
account, which holds critical protocol state including liquid staking amounts and configuration parameters, is defined without themut
constraint in its account validation struct. As a result, while the code successfully executes state variable updates within this account (e.g.,self.config.liquid_amount += amount
inliquid_stake()
), these changes are not persisted to the blockchain.Recommendation
Add the
mut
constraint to theconfig
account validation inliquid_stake()
,liquid_unstake()
, andreward()
functions. The fix should be applied as follows:#[account(+ mut,seeds = [b"config".as_ref()],bump = config.bump,)]pub config: Account<'info, State>,Excessive Payout in unstake and claim Functions Causes Severe Fund Loss
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
FrankCastle
Excessive Payout in
unstake
andclaim
Functions Causes Severe Fund LossDescription
The
unstake
andclaim
functions incorrectly calculate the amount to transfer, resulting in users receiving 12 times the amount they originally staked. This leads to a significant loss of funds for the protocol.The flawed calculation is shown here:
let amount_to_transfer = self.position.amount+ (self.position.amount * (12 - self.position.claimed as u64) * 10025 / 10000);This miscalculation causes users to receive an excessive reward, far exceeding the intended staking rewards.
Recommendation
The correct implementation should ensure that only a small portion of the staked amount is transferred per claim.
Corrected Claim Function Calculation:
let amount_to_transfer = self.position.amount * 25 / 10000;Corrected Unstake Function Calculation:
To properly transfer the unclaimed amount after the staking duration has passed:
let amount_to_transfer =self.position.amount + ((12 - self.position.claimed as u64) * self.position.amount * 25 / 10000);Fix review Finding : Incorrect Time Validation in unstake Function leads to permanent lock of the staked funds
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
FrankCastle
Incorrect Time Validation in
unstake
Function leads to permanent lock of the staked fundsDescription
The
unstake
function is designed to allow unstaking only after 12 months from the stake start time. However, the current time validation logic incorrectly multiplies the seconds in a year by the number of claimed months, leading to an excessive required time delay.let enlapsed_time = Clock::get()?.unix_timestamp - self.position.start_time;// < SECONDS_IN_A_YEARif enlapsed_time< SECONDS_IN_A_YEAR.checked_mul((self.position.claimed as i64).checked_add(1).ok_or(Overflow)?,).ok_or(Overflow)?{return Err(ErrorCode::NotEnoughTimeElapsed.into());}If a user claims rewards for 6 months and later tries to unstake after 12 months, the function would require over 7 years to elapse, resulting in a significant loss of funds.
Recommendation
Modify the validation logic to check only for a minimum of 12 months (1 year) elapsed time:
let enlapsed_time = Clock::get()?.unix_timestamp - self.position.start_time;// < SECONDS_IN_A_YEARif enlapsed_time < SECONDS_IN_A_YEAR {return Err(ErrorCode::NotEnoughTimeElapsed.into());}Fix review Finding : Incorrect Calculation of amount_to_burn in liquid_unstake
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
FrankCastle
Incorrect Calculation of
amount_to_burn
inliquid_unstake
Description
In the
liquid_unstake
function, the calculation ofamount_to_burn
is incorrect.amount
represents the amount of gold to be unstaked.config.liquid_amount
represents the total amount of gold in the pool.rewards_mint.supply
represents the total minted supply of reward tokens.
The current formula incorrectly calculates
amount_to_burn
as:let amount_to_burn = (amount as u128).checked_mul(self.config.liquid_amount.into()).ok_or(Overflow)?.checked_div(self.rewards_mint.supply.into()).ok_or(Overflow)?;This leads to an incorrect token burn amount, affecting the protocol's balance and potentially causing loss of funds.
Recommendation
Use the correct formula, which ensures
amount_to_burn
is proportional to the unstaked gold relative to the total gold in the protocol:let amount_to_burn = (amount as u128).checked_mul(self.rewards_mint.supply.into()).ok_or(Overflow)?.checked_div(self.config.liquid_amount.into()).ok_or(Overflow)?;
High Risk1 finding
DoS in claim Function Due to init Constraint on an Already Initialized Account
Severity
- Severity: High
≈
Likelihood: High×
Impact: Medium Submitted by
FrankCastle
DoS in
claim
Function Due toinit
Constraint on an Already Initialized AccountDescription
In the
claim
function, theinit
constraint is incorrectly applied to the PDAposition
. However, this account is already initialized in thestake
function. As a result, theclaim
function will fail to execute after the initial staking process, leading to a permanent denial of service (DoS) for claiming rewards. This renders the protocol's claiming functionality unusable.// in claim function#[account(init,payer = user,space = 8 + Position::INIT_SPACE,)]pub position: Box<Account<'info, Position>>,Recommendation
Replace the
init
constraint withmut
to ensure theposition
account can be modified instead of being reinitialized. Additionally, verify the account’s existence before performing operations to prevent unintended reinitialization attempts.#[account(mut)]pub position: Box<Account<'info, Position>>,
Medium Risk5 findings
Missing Slippage Parameter Exposes Users to Unintended Prices, Leading to Potential Fund Loss
Severity
- Severity: Medium
≈
Likelihood: Low×
Impact: High Submitted by
FrankCastle
Missing Slippage Parameter Exposes Users to Unintended Prices, Leading to Potential Fund Loss
Description
In the trading functions
buy
andsell
, trades are executed based on either theoracle price
or a stored constant price, with the higher price being used. While there is a check to ensure price freshness, there is no validation of the price value itself against a reasonable threshold.This lack of validation can result in the use of an inflated price from the Pyth oracle due to market volatility, leading to users unknowingly purchasing assets at significantly higher prices.
For example, if the stored price and the intended trade price are 100, but the Pyth oracle returns an inflated price of 1000, the system will use 1000, causing a significant loss to the user.
Recommendation
Introduce a slippage parameter to allow users to specify an acceptable price deviation, ensuring they are protected from extreme price fluctuations.
Updated
buy
Functionimpl<'info> Buy<'info> {pub fn buy(&mut self, amount: u64, max_amount_in: u64) -> Result<()> {let total_cost = amount * price / 100 * (10000 + self.price.fee)// Ensure the total cost does not exceed the user's maximum acceptable amountrequire!(total_cost <= max_amount_in, CustomError::SlippageExceeded);Ok(())}}Missing Confidence Validation in Pyth Oracle Price
Severity
- Severity: Medium
≈
Likelihood: Medium×
Impact: Medium Submitted by
FrankCastle
Missing Confidence Validation in Pyth Oracle Price
Description
The trading functions that use the Pyth oracle price do not validate confidence values (
conf
andema_conf
). They only useget_price_no_older_than
to check staleness but do not ensure that the confidence percentage remains within an acceptable threshold.This omission can lead to:
- Using prices during high uncertainty periods, which may not reflect accurate market conditions.
- Missing market anomaly signals, potentially overlooking extreme price deviations.
- Potential incorrect liquidations during low market confidence, leading to unfair liquidations or improper risk calculations.
As per Pyth's best practices, confidence intervals should be considered when evaluating price validity.
Recommendation
Introduce a configurable
max_confidence_pct
parameter in the price account and validate the confidence percentage before using the price.Updated
buy
Function with Confidence Validationimpl<'info> Buy<'info> {pub fn buy(&mut self, amount: u64, max_amount_in: u64, max_confidence_pct: u64) -> Result<()> {// Fetch price and confidence from Pyth oraclelet pyth_price_result =price_update.get_price_no_older_than(&Clock::get()?, maximum_age, &feed_id);let (pyth_price, conf) = match pyth_price_result {Ok(price) => (price.price, price.conf),Err(e) => Err(e)?,};// Validate confidence percentagelet confidence_pct = (conf as u128).checked_mul(100)?.checked_div(pyth_price.abs() as u128)?;require!(confidence_pct <= max_confidence_pct as u128,CustomError::PriceConfidenceTooHigh);Ok(())}}User can unstake anytime instead of getting locked for 1 year
Severity
- Severity: Medium
≈
Likelihood: High×
Impact: Medium Submitted by
0xhuy0512
Description
In the
unstake()
function, there is no time restriction for any positions. This means stakers can unstake at any time instead of being locked for 1 year as specified in the requirements.Recommendation
Consider implementing a time check in
unstake()
function that only allows stakers to unstake after 1 year has passed since their initial stake.+ assert!(Clock::get()?.unix_timestamp >= self.position.start_time + (366 * 24 * 60 * 60));Pyth price feed maximum age too high allows stale price data
Severity
- Severity: Medium
≈
Likelihood: Medium×
Impact: Medium Submitted by
0xhuy0512
Description
When using Pyth price feed, the
maximum_age
parameter for Pyth price feed validation is set to 99,999 seconds (approximately 27.8 hours), which is significantly higher than recommended for secure price oracle implementations. This excessive timeout allows the use of stale price data that could be exploited during periods of high price volatility.The issue occurs in the price validation logic where
get_price_no_older_than()
is called with this maximum age parameter. While the function does properly validate that the price update is not older than the specified maximum age, setting such a high threshold effectively weakens this security check.Recommendation
The maximum age for price feed data should be set to a more conservative value that aligns with market dynamics and security best practices. For most DeFi applications, price feeds should not be older than 5-15 minutes.
Implement a more appropriate maximum age limit:
- let maximum_age: u64 = 99999;+ let maximum_age: u64 = 900; // 15 minutes in secondsFix review Finding: Admin Can Reset Critical Configuration by Reinitializing the Contract
Severity
- Severity: Medium
≈
Likelihood: Low×
Impact: High Submitted by
0xhuy0512
Description
In
liquid_staking/src/contexts/init.rs
, there is a vulnerability in theInitialize
context and itsinitialize_config()
function. The root cause is that theconfig
account is created withinit_if_needed
instead ofinit
, allowing the admin to call the initialization function multiple times.The initialization code creates a
config
account as follows:#account(init_if_needed,payer = admin,seeds = [CONFIG_SEED.as_ref()],bump,space = 8 + State::INIT_SPACE,)When
initialize_config()
is called, it sets theState
structure with crucial values includingbump
,liquid
,liquid_amount
, andmonthly_rewards
. Because the function can be called multiple times due to theinit_if_needed
constraint, the admin can reset these properties to arbitrary values at any time.This is particularly dangerous because:
- The
monthly_rewards
parameter can be changed to manipulate rewards - The
liquid
flag andliquid_amount
fields can be reset, causing funds to be locked
Recommendation
Change the constraint from
init_if_needed
toinit
to ensure the initialization can only happen once:#[account(- init_if_needed,+ init,payer = admin,seeds = [CONFIG_SEED.as_ref()],bump,space = 8 + State::INIT_SPACE,)]pub config: Account<'info, State>,
Low Risk3 findings
Broken Access Control in Vault Initialization
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
FrankCastle
Broken Access Control in Vault Initialization
Description
In the function
initialize_vault
, the vault account can be pre-initialized by anyone, leading to this function to revert so this breaks access control mechanism in this function. Additionally, the function itself is redundant, as its sole purpose is to create the vault. Instead of keeping it separate, it can be merged with theinitialize_config
instruction to streamline initialization.Recommendation
Merge the vault creation logic into the
initialize_config
instruction, as shown in the following implementation:pub struct Initialize<'info> {#[account(mut, address = ADMIN)]pub admin: Signer<'info>,#[account(init,payer = admin,seeds = [b"config".as_ref()],bump,space = 8 + State::INIT_SPACE,)]pub config: Account<'info, State>,#[account(init_if_needed,payer = admin,seeds = [b"rewards".as_ref(), config.key().as_ref()],bump,mint::decimals = 6,mint::authority = config,)]pub rewards_mint: Account<'info, Mint>,#[account(init_if_needed,payer = admin,associated_token::mint = mint,associated_token::authority = config,associated_token::token_program = token_program,)]pub vault: Box<InterfaceAccount<'info, TokenAccount>>,pub system_program: Program<'info, System>,pub token_program: Program<'info, Token>,}impl<'info> Initialize<'info> {pub fn initialize_config(&mut self, bumps: &InitializeBumps) -> Result<()> {self.config.set_inner(State {rewards_bump: bumps.rewards_mint,bump: bumps.config,liquid: false,liquid_amount: 0,});Ok(())}}Update price definition from u64 to i64 to accommodate for the possibility of a negative price
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: High Submitted by
jdiggidy
Description
Update price definition from u64 to i64 to accommodate for a "below zero price" as referenced in the kickoff call.
Proof of Concept
Recommendation
-pub fn price(&mut self, bumps: &UpdatePriceBumps, price: u64, fee: u64) -> Result<()> {
+pub fn price(&mut self, bumps: &UpdatePriceBumps, price: i64, fee: u64) -> Result<()> {
Hard Coded Values leading to maintenance issues
Severity
- Severity: Low
≈
Likelihood: High×
Impact: Medium Submitted by
jdiggidy
Description
The use of hardcoded values like 305 * 24 * 6 * 60 for seconds in a month and 10025 / 10000 for the transfer amount calculation can lead to maintenance issues and potential errors if these values need to be updated or changed.
Recommendation
Avoid Hardcoded Values. Replace hardcoded values with constants or configuration parameters that can be easily updated and maintained.
Informational5 findings
Missing Events for Storage-Mutating Functions
Severity
- Severity: Informational
Submitted by
FrankCastle
Missing Events for Storage-Mutating Functions
Description
Functions that modify storage, such as
price
,buy
,sell
,deposit
,andwithdraw
do not emit events. Emitting events is essential for off-chain indexing, transparency, and debugging. Without them, it is harder for users and external applications to track state changes efficiently.Recommendation
Emit an event after modifying storage to ensure state changes can be tracked.
reversible Whitelisting Mechanism should be implemented
Severity
- Severity: Informational
Submitted by
FrankCastle
Irreversible Whitelisting Mechanism
Description
The
inti
program can only whitelist users, but the reverse operation (removing a user from the whitelist) is not possible. This is because the validation of a whitelisted user is done by checking whether the account has been initialized. Once initialized, the account remains permanently whitelisted.Recommendation
To allow for the removal of users from the whitelist, add an
is_whitelisted
flag to the account structure. This will enable dynamic updates to the user's whitelist status without relying solely on account initialization.pub struct Whitelist {pub bump: u8,pub is_whitelisted: bool,}Not Checking for potential overflows
Severity
- Severity: Informational
≈
Likelihood: Low×
Impact: High Submitted by
jdiggidy
Description
The code performs arithmetic operations without checking for potential overflows or underflows. Rust's default behavior is to panic on overflow in debug mode, but in release mode, it wraps around silently.
Recommendation
Use Rust's checked arithmetic methods (e.g., checked_add, checked_mul) to ensure that arithmetic operations do not overflow or underflow
The Comment AUX/USD should be XAU/USD
Severity
- Severity: Informational
≈
Likelihood: High×
Impact: Low Submitted by
jdiggidy
Description
AUX/USD should be XAU/USD. While this has low impact, it can cause maintenance issues down the road and confusion for future auditors.
Recommendation
Change AUX to XAU
Create a robust runnable test suite
Severity
- Severity: Informational
≈
Likelihood: Low×
Impact: Low Submitted by
jdiggidy
Description
The existing test suite sets up a series of tests to interact with the Inti and liquid_staking Solana program using the Anchor framework. It initializes the necessary accounts, defines utility functions for transaction confirmation and logging, and performs various operations such as updating prices, depositing tokens, whitelisting users, buying, selling, withdrawing, minting, burning tokens, initializing a vault, staking, and unstaking tokens. However, several verification tests are needed to ensure critical vulnerabilities are not presented in operation.
Recommendation
Ensure test cases are created for critical vulnerabilities identified during this assessment:
- Testing for incorrect initializations and reinitializations
- Testing price, reward, and staking / unstaking, and transfer calculations to ensure accuracy
- Authorization of functions such as attempting claim/unstake other users' positions to their own wallet
- Replay attacks such as attempting to unstake from the same position after a successful initial unstaking
- Ensuring expected data persists throughout state updates
In addition, in accordance with security best practices, it is recommend to engage in a follow up assessment to ensure critical and high vulnerabilities are mitigated.