As of 3 Nov 2026, Hawksight has 1 audit report on Trustblock, by CertiK, dated 28 Sep 2022. That report lists no open critical findings.
Record last updated . It holds audits and findings, contracts and incidents up to Nov 2026. Trustblock does not review code.
Newest first
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 2 | 0 | 2 |
| Medium | 0 | 0 | 3 |
| Low | 3 | 0 | 20 |
The `create_state` function is designed to initialize a globally unique `state` account. This `state` account will be used to identify the project owner and to ensure that only the owner can create a `strategy` and set the `token_per_second` of the farm. ```rust pub struct CreateState<'info> { #[account( init, seeds = [b"state".as_ref()], bump, payer = authority, space = 8 + size_of::<StateAccount>() )] pub state: Account<'info, StateAccount>, ... } ``` ```rust pub fn create_state(&mut self, bump: u8) -> Result<()> { let state = &mut self.state; state.authority = self.authority.key(); state.bump = bump; state.reward_mint = self.reward_mint.key(); state.reward_vault = self.reward_vault.key(); emit!(StateCreated {}); Ok(()) } ``` Any given program_id can only have one `state` account. This may allow malicious users to pre-empt the creation of `state` by posting `create_state` transactions first, thus making the program unavailable. Additionally, if a malicious user manages to call `create_state` prior to admin of the program, the malicious attacker would then have a parallel running program who's state is controlled by them. The user could use this to impersonate the real program to exploit users.
Open the reportThe unit tests here are not enough. Testing programs are a very important aspect of proving program correctness, preventing regressions, and release engineering. Without tests, there is no way to know if the program works as expected. The fact that code was shipped for review with such major flaw, is considered a signal of some problems with release engineering procedures, which may lead to shipping not intended changes to production.
Open the reportIn the `farm` account, the `asset_infos` array is initialized by `weights` and `remaining_accounts`. - `mints` vector is initialized by iterating over `ctx.remaining_accounts`. ```rust for mint in _ctx.remaining_accounts.iter(){ require!(*mint.owner == token::ID, ErrorCode::InvalidMint); Mint::unpack(&mint.to_account_info().try_borrow_data()?)?; mints.push(mint.key()); } ``` - `asset_size` is defined by the length of `weights`. ```rust let asset_size = weights.len(); let asset_count = asset_size.try_into().unwrap(); ``` - `mints` is used to calculate the `farm`'s `seed`. ```rust for mint in mints.iter() { seeds.push(mint.as_ref()); } ``` - `farm.asset_infos` is filled by iterating over the elements of `mints` and `weights` up to `asset_size`. ```rust for i in 0 .. asset_size{ total_weight += u128::from(weights[i]); farm.asset_infos.push(FarmAssetInfo{ weight: weights[i], mint: mints[i] }) } ``` However, the length of `weights` and `remaining_accounts`/`mints` are not checked during this initialization process. This makes it possible that the farm's `seed` and its `asset_infos` to not match, making the service unusable.
Open the reportThe function `change_token_per_second` updates the farms `token_per_second` parameter. The function provides no validation for when the function should be called. ```rust impl<'info> ChangeTokenPerSecondMulti<'info>{ pub fn change_token_per_second(&mut self, token_per_seconds: Vec<u128> ) -> Result<()> { let farm = &mut self.farm; let farm_key = farm.key(); farm.validate_address(farm_key)?; let asset_count:u8 = token_per_seconds.len().try_into().unwrap(); require!( asset_count == farm.asset_count, ErrorCode::InvalidAssetCount); let mut index:usize = 0; for token_per_second in token_per_seconds.iter() { farm.reward_infos[index].token_per_second = *token_per_second; index += 1; } Ok(()) } } ``` In the `state.rs`, the `update()` function will calculate the rewards that have been generated for a farm. ```rust pub fn update<'info>(&mut self, clock: &Sysvar<'info, Clock>) -> Result<()> { let seconds = u128::try_from( clock .unix_timestamp .checked_sub(self.last_reward_time) .unwrap(), ) .unwrap(); let mut reward_per_share: u128 = 0; if self.amount > 0 && seconds > 0 { reward_per_share = u128::from(self.token_per_second) .checked_mul(seconds) .unwrap() .checked_mul(ACC_PRECISION) .unwrap() .checked_div(u128::from(self.amount)) .unwrap(); } self.acc_reward_per_share = self .acc_reward_per_share .checked_add(reward_per_share) .unwrap(); self.last_reward_time = clock.unix_timestamp; Ok(()) } ``` Neither `update` nor `change_token_per_second` have any validation for _when_ `token_per_second` should be changed. In practice, this could mean someone calling `change_token_per_second` prior to when they are supposed to, which would result in unexpected results when calling `update`. The logic of when `token_per_second` should change is totally off-chain and in the hands of the owner, without any validation. Both human error and malicious intent are risks with the current architecture of this function.
Open the reportNote that with the `UST` price now below $0.05, and no longer has the role of a stable coin. This may easily affect functionality of UST in the project. ```rust pub const UST_MAINNET: Pubkey = static_pubkey::static_pubkey!("9vMJfxuKxXBoEa7rM12mYLMwTacLMLDJqHozw96WQL8i"); pub const STABLE_MINTS: [Pubkey; 2] = [USDC_MAINNET, UST_MAINNET]; ```
Open the reportAfter withdrawing reward, the user's `last_action` is set as `FinishAction`, however the users `last_amount` is not reset to 0 after processing. For comparison, `stake_to_farm()` and `redeem_stable_token()`, after a `FinishAction` reset the `last_amount` to zero. This could cause undefined behavior in the future. For example ```rust=919 pub fn withdraw_reward(&mut self) -> Result<()> { ... for asset_index in 0..usize::from(farm.asset_count) { ... user_asset.calculate_reward_debt(&farm_reward)?; user_asset.last_action = Action::FinishAction as u8; user_asset.last_amount = 0; // reset as zero before FinishAction } ... } ```
Open the report`total_weight` will be used in the future to assign `amount` to different `asset_info.last_amount`. ```rust user.asset_infos[index].last_amount = u128::from(new_amount) .checked_mul(u128::from(asset_info.weight)) .unwrap() .checked_div(farm.total_weight) .unwrap() .try_into() .unwrap(); ``` But the instruction doesn't have a validation over the weights and the total weight. It is important to make sure that `total_weight` is not equal to 0 in order to prevent the construction of unavailable `farm` accounts. ```rust pub fn create_farm(&mut self, ... ) -> Result<()> { for i in 0 .. asset_size{ total_weight += u128::from(weights[i]); ... } farm.total_weight = total_weight; } ```
Open the reportBy adding an empty `bump` constraint to the `#[account(...)]` macro, you signal Anchor to find the canonical bump on its own for the initialization of the account. The user defined `bump` will be written to storage, but will not be used for account initialization. ```rust = 21 pub fn create_state(&mut self, bump: u8, ) -> Result<()> { ... state.bump = bump; ... } ``` Anywhere that the storage `state.bump` is used will result in incorrect validation of account addresses. Reference material on bump creation can be found [here](https://docs.rs/anchor-lang/latest/anchor_lang/context/struct.Context.html#structfield.bumps) and [here](https://www.anchor-lang.com/docs/pdas)
Open the report`MAX_ASSET_COUNT` is used to determine the total size that is to be allocated for accounts in `create_or_allocate_account_raw`. ```rust pub const MAX_ASSET_COUNT:usize = 10; ``` However, when this upper limit is not checked when allocating `asset_infos` in `farm`. This may result in the allocated space of the account being depleted. ```rust for i in 0..asset_size { total_weight += u128::from(weights[i]); farm.asset_infos.push(FarmAssetInfo { weight: weights[i], mint: mints[i], }) } ```
Open the report`Mint` is the only basis for identifying different types of tokens in Solana, and it is important to make sure that the mint in the a given Token Accounts data, which is input by the user, is correct. The current program only checks whether the address of the Token Account is constructed by the right seeds and program id, via the `check_token_account` function. It is best practice to include mint and ownership checks when interacting with the SPL Token program. For example, a validation that guarantees the Token Accounts mint address is the same as the one expected in the `strategy`. ```rust require!(token_account.mint == strategy.src_mint, ErrorCode::CustomErrorCode); ``` This would guarantee the token account was not created incorrectly.
Open the reportAccording to the logic, **Hawksight** program forms a complete process in strict accordance with the `Action` steps. The project strictly controls the process execution through the inspection of `Action`. For example, in `supply_liquidity`, the function can only be run if the `last_action` was a `SwapAction` ```rust pub fn supply_liquidity(&mut self, min_amount_out: u64) -> Result<()> { ... require!( (user_asset_info.last_action == Action::SwapAction as u8) || ((user_asset_info.last_action == Action::FundAction as u8) && (strategy.token_mint == USDC_MAINNET) && ((strategy.src_mint == USDC_MAINNET) || (strategy.dst_mint == USDC_MAINNET))), ErrorCode::InvalidOperationOrder ); ... ``` However, `Action` is not verified in `stake_to_farm()` and `unstake_from_farm()` function, which may lead to unexpected errors in cross-step operations.
Open the reportThe current computation of `creator_fee` does not conform to the multiply first, divide later specification. This will result in a loss of precision in the final value. ```rust let fee_amount = u128::from(input_amount) .checked_mul(fee_pct) .unwrap() .checked_div(FEE_DENOMINATOR) .unwrap(); let creator_fee = fee_amount .checked_mul(CREATOR_FEE_WEIGHT) .unwrap() .checked_div(...).unwrap() .unwrap(); ```
Open the report[`mem::size_of<T>()`](https://doc.rust-lang.org/std/mem/fn.size_of.html) should be used for size calculations with caution. Borsch will always serialize an option as 1 byte for the variant identifier and then additional x bytes for the content if it's Some. However, Rust uses null-pointer optimization to make Option's variant identifier 0 bytes when it can, so an option is sometimes just as big as its contents, such as with `Sign`.
Open the reportThere are some unnecessary checks in the current code. For example: - **Already checked by `check_sub`** ```rust = 341 user.asset_infos[index].last_amount = u128::from(new_amount) .checked_mul(u128::from(asset_info.weight)) .unwrap() .checked_div(farm.total_weight) .unwrap() .try_into() .unwrap(); user.asset_infos[index].last_action = Action::FundAction as u8; require!( rest_amount >= user.asset_infos[index].last_amount, ErrorCode::IntegerUnderflow );//Duplicate check rest_amount = rest_amount .checked_sub(user.asset_infos[index].last_amount) .unwrap(); index += 1; ``` - **Already checked by `#[account(mut,seeds = [..], bump)]`** ```rust check_token_account( self.user_pda_stable_token.key(), farm.stable_mint, farm_key, user_key, )?; ``` - **Already checked by `Program<'info, Token>`** ```rust #[account(constraint = token_program.key == &token::ID)] ```
Open the reportIn `require!` macro, checking the `x == true` expression is redundant. ```rust require!(found == true, ErrorCode::AssetNotFound); ```
Open the reportThe code in the following loop uses `index`, but it can be further simplified. ```rust=341 for asset_info in farm.asset_infos.iter(){ user.asset_infos[index].last_amount = u128::from(new_amount) .checked_mul(u128::from(asset_info.weight)).unwrap() .checked_div(farm.total_weight).unwrap() .try_into().unwrap(); user.asset_infos[index].last_action = Action::FundAction as u8; require!(rest_amount >= user.asset_infos[index].last_amount, ErrorCode::IntegerUnderflow); rest_amount = rest_amount.checked_sub(user.asset_infos[index].last_amount).unwrap(); index += 1; } ``` ```rust = 170 for token_per_second in token_per_seconds.iter() { farm.reward_infos[index].token_per_second = *token_per_second; index += 1; } ```
Open the reportIn the `stake_to_farm()` function, the user can stake the LP obtained by adding liquidity to `saber_farm` to obtain rewards. ```rust stake_lp_token_pda( self.saber_farm_program.to_account_info(), self.token_program.to_account_info(), user.to_account_info(), &self.saber_farm, self.user_pda_lp_token.to_account_info(), self.saber_farm_rewarder.to_account_info(), // Stake full LP token balance of pass through account self.user_pda_lp_token.amount, // user.asset_infos[asset_index].last_amount, authority_seeds, )?; ``` However, the stake amount is `user_pda_lp_token.amount`, but the billed amount is `user_asset.last_amount`. ```rust user_asset.amount = user_asset .amount .checked_add(user_asset.last_amount) .unwrap(); ``` If there is an external transaction that transfers the additional LPs to the `user_pda_lp_token`, this will cause the actual stake amount to be inconsistent with the billed amount, users cannot get back all staked LPs when unstaking from the farm.
Open the reportThe comment includes a spelling mistake of `further`.
Open the reportThe references on the linked lines would be dereferenced immediately by the compiler, so the borrow operations are unnecessary.
Open the reportThere is an unnecessary `return` statement in the current code base. ```rust = 620 if exp_token_address != address { return Err(Error::ProgramError(ProgramErrorWithOrigin::from(ProgramError::InvalidArgument))); } ```
Open the reportThe `CreateUserToken` struct declares a `strategy` account, however the only field used in that account is the token mint. Passing around unnecessary data can increase complexity of the program, as well as computational requirements. ```rust pub struct CreateUserToken<'info> { #[account( seeds = [ b"saber-farm-strategy".as_ref(), strategy.token_mint.as_ref(), strategy.src_mint.as_ref(), strategy.dst_mint.as_ref(), strategy.lp_mint.as_ref(), strategy.spl_swap.as_ref(), strategy.saber_swap.as_ref(), strategy.quarry.as_ref(), ], bump, )] pub strategy: Account<'info, SaberFarmStrategy>, // ... } ``` ```rust pub fn create_user_token(&mut self) -> Result<()> { let farm = &mut self.farm; let strategy = &mut self.strategy; let farm_key = farm.key(); farm.validate_address(farm_key)?; farm.find_asset(strategy.token_mint)?; // Used here Ok(()) } ```
Open the reportThe references on the linked lines make unnecessary conversions to the same type.
Open the reportIn the `utils.rs`, `authority_seeds` was re-sliced, which was unnecessary: ```rust 393 with_signer(&[&authority_seeds[..]]) ```
Open the reportThe variable `_res` is declared but is not used in the code logic. ```rust=550 let _res = redeem_all_tokens_from_mint_proxy(CpiContext::new(...)); ```
Open the reportThe QuarryProtocol provides [`create_miner`](https://github.com/QuarryProtocol/quarry/blob/f41219dd9e60fbabe5494a6dffbbc17dc3778a46/programs/quarry-mine/src/lib.rs#L232) to initialize the miner. However, since the current `create_miner` still requires `bump` for initialization, QuarryProtocol provides a more standardized [`create_miner_v2`](https://github.com/QuarryProtocol/quarry/blob/f41219dd9e60fbabe5494a6dffbbc17dc3778a46/programs/quarry-mine/src/lib.rs#L242) to replace `create_miner`. ```rust create_miner(CpiContext::new( self.quarry_mine_program.to_account_info(), CreateMiner{ authority: self.user.to_account_info(), miner: self.miner.to_account_info(), quarry: self.quarry.to_account_info(), rewarder: self.rewarder.to_account_info(), system_program: self.system_program.to_account_info(), payer: self.authority.to_account_info(), token_mint: self.lp_mint.to_account_info(), miner_vault: self.miner_vault.to_account_info(), token_program: self.token_program.to_account_info(), }).with_signer(&[&authority_seeds[..]]), miner_bump)?; ```
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="hawksight"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
Something 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