As of 30 Oct 2026, Primex Finance has 5 audit reports by 2 security firms on Trustblock; the latest is by Resonance Security, dated 14 Oct 2024. Those reports list no open critical findings.
Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Primex Finance | Security firm: Resonance Security | Date: | Scope: 401 addresses on Ethereum, Polygon (POS) and Arbitrum | No open critical findings | Open · IPFS |
| Primex Finance | Security firm: Resonance Security | Date: | Scope: 401 addresses on Ethereum, Polygon (POS) and Arbitrum | 1 critical finding, fixed | Open · IPFS |
| Primex Finance | Security firm: Resonance Security | Date: | Scope: 401 addresses on Ethereum, Polygon (POS) and Arbitrum | No open critical findings | Open · IPFS |
| Primex Finance | Security firm: Resonance Security | Date: | Scope: 401 addresses on Ethereum, Polygon (POS) and Arbitrum | 1 critical finding, fixed | Open · IPFS |
| Primex Finance | Security firm: Quantstamp | Date: | Scope: Files named in the report | No open critical findings | Open · certificate.quantstamp.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 1 |
| High | 0 | 0 | 7 |
| Medium | 3 | 4 | 11 |
| Low | 9 | 11 | 46 |
The function flashLoan() does not update important variables right after the transfer of the assets. Essentially, there are three steps to the function flashLoan():Calculate fees, available liquidities and transfer the assets to the borrower.Give control to the borrower to execute the intended operation.Handle the repayment of the flash loan.During the first step, the transfer of the assets to the receiver is performed but the calls to the functions _updateRates() and _updateIndexes() are not performed. These are only performed later after the executeOperation() function call on the third step during _handleFlashLoanRepayment(). This effectively means that malicious borrowers may execute operations within the flash loan transaction, such as deposit() and withdraw(), that will operate under outdated rates and indexes variables. Taking into additional consideration that flash loans can amount to big numbers and different buckets, the execution of such functions may yield unfavorable results to the protocol.The same attack vector is true for the variable availableLiquidity.
Open the reportThe PriceOracle contract is responsible for dictating the prices for given assets. The prices could be fetched using multiple of sources - Uniswap, Pyth and Chainlink to be precise. However, the Pyth and Chainlink sources support only prices denominated in USD. The PriceOracle contract keeps the prices in a mapping that is indexed with the asset price and returns that asset's price in USD. Hence, if the requested price is for a pair that does not include USD (for instance BTC/ETH) and Pyth/Chainlink is used as a price source, then the _getExchangeRate() function would actually return the price denominated in USD which will lead to invalid calculations.
Open the reportThe functions performPathsSwap() and swapExactTokensForTokens() make use of the internal function _swapExactTokensForTokens() and expect a uint256[3]value to be returned. Further calculations are made with this returned value. However, for the DexType.Paraswap, no value is returned, both leading to incorrect calculations of values presented to the caller of the functions.
Open the reportThe function updatePullOracle() does not properly validate the amount of Ether sent to the smart contract and allows for funds to be drained to an external contract. This can occur when the validation on the variable msg.value is being done after funds have been transferred. While it seems counterintuitive, the variable msg.value does not change throughout the transaction, even after successful outbound transfers of native funds.The following describes an example of an attack scenario for this specific case:User calls updatePullOracle() while sending 10 Wei. The variable msg.value will contain the value 10.For the Pyth if branch, the variable updateFee is calculated via pyth.getUpdateFee().Supposing a value of 9 for the variable updateFee, the require statement will pass successfully.9 Wei is sent to the external contract pyth via updatePriceFeeds(). 1 Wei is left from the funds deposited by the user.Supposing input parameters that will result in the execution of multiple iterations of the for loop, and supposing a calculation result of 9 for the second updateFee, the next iteration of the require statement will still pass successfully (9 <= 10) since the value of msg.value does not change, even though funds have been transferred away on the first iteration. This means that if the contract PriceOracle contains funds, these will be used along with the 1 Wei left from the user.Next iterations will drain the funds of the PriceOracle smart contract completely.
Open the reportThe function updatePullOracle() is payable and therefore, it can receive native Ether from callers of this function. When using UpdatePullOracle.Pyth as an oracle type, the calculated updateFee is required to be less than or equal to the amount of native Ether received via msg.value. When the caller of the function sends a higher amount of Ether than the one required by updateFee, the smart contract PriceOracle will retain the excess funds and it will be impossible to retrieve them, making the contract act as a sink for unrecoverable funds.
Open the reportIn the PrimeX protocol, the keeper can attack orders using spot trades or swaps. The protocol includes slippage protection by comparing the DEX execution price with the oracle price data within a certain gap. However, this protection is not applied to swap and spot trade positions. As a result, if a limit order is created for a swap or spot trade, there can be high slippage without the oracle price gap protection. The limit orders do not have the slippage protection by `amountOutMin` as well.
Open the reportWhen an asset was removed from a bucket through `removeAsset()`, the assets of all open positions may be transferred to the treasury. The affected traders will lose their entire position. The issue arises from the fact that `health()` in `PositionLibrary` returns 0 when `position.positionAsset` is not an allowed asset in the bucket. This can only occur if the asset was removed, as this position could not exist otherwise. Because `health()` returns 0, those positions can be closed through `closePosition()` with `CloseReason.RISKY_POSITION`. Such closures will lead to the traders not receiving any profits (`topUpAvailableBalance()` will not be called) AND not receiving their deposit back, as `decreaseTraderDebt()` will be called with `params.primexDNS.treasury()` as a receiver. Furthermore, when a position is closed due to liquidation, the trader will not receive any funds back. The deposit and potential profit will be sent to the treasury in the `PositionLibrary.closePosition()`.
Open the reportThe Chainlink Volatility Feed measures the [realized volatility](https://docs.chain.link/data-feeds/rates-feeds#realized-volatility) of one asset against another. Note that realized volatility can be greater than or equal to 100% (see: [formula here](https://www.wallstreetmojo.com/realized-volatility/#h-realized-volatility-formula)), and such an instance would create one of two problems: * In the case where the volatility is exactly 100% any user holding this asset would have a health of 0 and hence be able to liquidate. * In the case where the volatility is over 100% the calculation of a user's health will fail due to overflow and hence no one will be able to liquidate.
Open the reportThe function updatePullOracle() does not validate the input arrays _data and _oracleTypes. It is possible for this function to be called with mismatching input array lengths and allowing transactions to occur on only a subset of the input elements.
Open the reportThe protocol currently utilizes OpenZeppelin's `ECDSAUpgradeable` version `4.3.1` in contracts `PMXBonusNFT`, `ReferralProgram`, and `WhiteBlackListReferral` for validating registration using `ECDSAUpgradeable.recover(bytes, bytes)`. However, it is important to note that version `4.7.3` introduced a high-severity patch to address a [vulnerability related to signature malleability](https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories/GHSA-4h98-2769-gh6h) in the `ECDSA.recover()` and `ECDSA.tryRecover()` functions. These functions were found to be vulnerable due to accepting EIP-2098 compact signatures in addition to the traditional 65-byte signature format. This vulnerability affected the functions that accept a single bytes argument, not the ones that take `r, v, s` or `r, vs` as separate arguments. As a result, the `PMXBonusNFT`, `ReferralProgram`, and `WhiteBlackListReferral` contracts are impacted since they utilize the single bytes argument version of `ECDSA`, and version `4.3.1` is affected by this vulnerability.
Open the reportIn the `FeeExecutor`, lists `updatedTimestamps` and `indexes` are used to track all index and timestamp updates for a bucket. The function `FeeExecutor._searchApproxIndex()` is designed to return an approximate index corresponding to a given bonus deadline and the current index. However, its implementation, which employs a binary search approach, assumes that only one bucket updates are recorded in the `updatedTimestamps` and `indexes` lists. This assumption is incorrect because multiple buckets can update the lists using the function `FeeExecutor._updateIndex()`. Based on `FeeDecreaser` and `InterestIncreaser` functions, the `_updateIndex()` function pushes different bucket indexes which can be different based on the bucket's specific `bar` rate. This incorrect assumption can lead to inaccurate results returned by the `_searchApproxIndex()` function.
Open the reportThe `BestDexLens.getBestMultipleDexes()` function filters out DEX results and attempts to optimize share splits across DEX results to return the best DEX routing path for the amount requested. However, several concerns arise from the current implementation: 1. **Overwriting the Negative Value**: Within the loop, a condition checks if `amount` equals 0 and assigns `VERY_NEGATIVE_VALUE` to `amountByDexByShare`. However, in the subsequent lines, this value is immediately overwritten without any related checks. 1. **Unsafe Gas Estimation**: The gas, represented in ETH units (`vars.gas`), is directly added or subtracted to/from the `amount`. Mixing gas estimations in ETH with an amount that might represent another token or asset unit could lead to inaccurate calculations or misrepresentations of value. 1. **Unsafe Casting to `int256`**: The `amount`, an unsigned integer, is directly cast to `int256`. Without ensuring the `amount` is within safe bounds, this could introduce potential overflows or unexpected negative values. 1. **Arbitrarily Large Negative Value Concern**: A value `VERY_NEGATIVE_VALUE` defined as `-1e72` is used to indicate an undesirable amount obtained from the DEX. The problem arises when `currentValue` is assigned this large negative value and subsequently compared to other values using the conditional `_params.isAmountToBuy ? (currentValue < bestValue || bestValue == 0) : currentValue > bestValue`. If `_params.isAmountToBuy` is `true`, then a `currentValue` of `VERY_NEGATIVE_VALUE` will almost always be considered smaller than `bestValue`, even if `bestValue` represents a more desirable outcome. This bug can lead to incomplete or suboptimal solutions.
Open the reportWhen batch closing positions through `BatchManager.closeBatchPositions()`, each position listed in the array `_ids` is validated. If the position does not exist through `PositionManager.getPosition()`, the function will catch the error and continue to the next position id in the `_ids` array. Therefore the `i` variable may not track to `vars.length` and `vars.length` can be less than `i`. ```solidity if ( _closeReason == PositionLibrary.CloseReason.BATCH_STOP_LOSS || _closeReason == PositionLibrary.CloseReason.BATCH_TAKE_PROFIT ) vars.closeConditions[vars.length] = positionManager.getCloseCondition(vars.ids[i], _conditionIndexes[i]); ``` Therefore, the variable `i` used to obtain the position close condition can be incorrect. Since `i` can be ahead of `vars` the position id can be zero. This will return the close condition from position id `0` for the current position `_ids[i]` and can incorrectly close the current position being evaluated.
Open the reportThe architecture graph suggests that exclusive roles should possess substantial privileges, which should be meticulously time-locked for enhanced security. The factories inherit the `UpgradeableBeacon` contract for beacon upgrades. However, the `UpgradeableBeacon` contract inherits the `Ownable` contract, making the deployer the owner and granting them the ability to upgrade implementations through the `UpgradeableBeacon.upgradeTo()` function (see: [code](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4d4a509b1f8e521583d82c042ef4bce919fe8e12/contracts/proxy/beacon/UpgradeableBeacon.sol#L52C14-L52C23)). As a result, the deployer holds a significant privilege to upgrade all contracts, highlighting the need to secure this role and its associated responsibilities.
Open the reportThe `ActivityRewardDistributor.withdrawPmx()` function enables the admin to withdraw unused PMX tokens from the contract. During the execution of this function, the `bucketInfo.totalReward` is reduced by the withdrawal amount. However, the `bucketInfo.rewardPerDay` and `bucketInfo.endTimestamp` remain unchanged. As a result, there may not be enough PMX tokens available to cover the reward until the end timestamp. Additionally, the `lastUpdatedRewardTimestamp` is not updated as expected. As a consequence, the time span since this timestamp was last refreshed is inaccurately accounted for during subsequent reward accumulation. Specifically, when rewards are withdrawn, the `bucketInfo.fixedReward` value increases based on the duration since the `lastUpdatedRewardTimestamp`. Failure to update the `lastUpdatedRewardTimestamp` can result in the period being double-counted if it is not properly updated.
Open the reportAccording to the [documentation](https://docs.primex.finance/guides/credit-buckets/liquidity-mining), it states that "During the stabilization period, Traders can now borrow funds from the Credit Bucket, and Lenders start earning interest on the liquidity they provide for margin trading." Based on this information, we assume that lenders only start earning interest after moving to the stabilization period, which occurs when the bucket launches. Additionally, the implementation of `_depositLM()` does not update indexes and rates. However, the `withdraw()` function allows lenders to withdraw funds before the bucket launches. Within this function, both `_updateIndexes()` and `_updateRates()` are called, causing the index to be updated and start accumulating before the bucket launches.
Open the reportIn both the `setupBucket()` and `decreaseRewardPerDay()` functions of the `ActivityRewardDistributor` contract, if `rewardPerDay` is equal to zero, the `bucketInfo.endTimestamp` is overridden to `type(uint256).max`. Additionally, the `getBucketAccumulatedReward()` and `_updateBucketInfo()` functions have the logic of `endTimestamp += unusedTime` when `rewardPerDay == 0`. The line `endTimestamp += unusedTime` will overflow if the `endTimestamp` is set to `type(uint256).max`. Consequently, almost all functions relying on `getBucketAccumulatedReward()` and `_updateBucketInfo()` will fail. From our discussion with the team, the `rewardPerDay` can be set to zero as emergency mitigation in case there is a bug to stop the accrual of incorrect rewards. Despite the likelihood of this occurring being low, it would cause extra chaos and worsen the emergency situation, which is why it is a medium-severity issue.
Open the reportThe `Reserve` currently uses `pToken.scaledTotalSupply()` instead of actual token supply `pToken.totalSupply()`, when validating the minimum percentage of token supply in the `Reserve` during the function `transferToTreasury()`. This methodology could result in inaccuracies in the calculated minimum percentage, which can lead to a lower than expected minimum percentage in the reserve. ``` _require( reserveBalance >= (restristions.minAmountToBeLeft + amount) && reserveBalance >= (pToken.scaledTotalSupply().wmul(restristions.minPercentOfTotalSupplyToBeLeft) + amount), Errors.NOT_SUFFICIENT_RESERVE_BALANCE.selector ); ```
Open the reportWhen closing a spot trade position (or more specifically, on `vars.borrowedAmountIsNotZero` in `PositionManager._closePosition()#L682-690`) through `PositionManager._closePosition()` the `oracleTolerableLimit` is set to zero between `depositAsset` and `positionAsset`. This means when closing the position through `PositionLibrary.closePosition()`, the swapped amount returned by the DEX in the `PrimexPricingLibrary.multiSwap()` must be equivalent to or better than the price provided by the price oracle. This check is validated in the `multiSwap()` where the `getOracleAmountsOut()` must be greater or equal to the amount obtained from the DEX because `_needOracleTolerableLimitCheck` is set to `true` in the `PositionLibrary.closePosition()` function. This strict requirement can prevent spot traders from closing their positions.
Open the reportThe function getEstimatedMinProtocolFee() does not account for gas calculations for the newly introduced Optimistic chain integration. The variable l1CostWei will always yield a value of 0. While this may be intended, it does not conform with code implemented across other smart contracts on the protocol.
Open the reportIssue identified by Primex:Gas price oracle was not set for Arbitrum, and defaultMaxGasPrice was used instead of it that caused incorrect min protocol fee and min position size calculations. We’ve added a wrapper contract on top of Arb gas price precompiles that returns gas price in the required format.
Open the reportIssue identified by Primex:In createLimitOrder, there is a check that the expected position size is higher than minPositionSize. However this method dont include pullOracleData so it was impossible to create an order for assets with pull oracles.
Open the reportIssue identified by Primex:Executing a limit order through primexUpkeep is not possible as the method is not payable, and it cannot pay the Pyth pull oracle fee in the native token.
Open the reportIssue identified by Primex:Method getL1BaseLengthForTradingOrderType returned the sum of lengths for position opening and closing for limit orders that caused incorrect calculations of minProtocolFee for these actions separately.
Open the reportIssue identified by Primex:The positions opened before the upgrade paid the protocol fee at the moment of opening, and the fee from them is not taken again upon closing, but this fee was taken into account in the health calculation. A check was added to avoid it.
Open the reportIssue identified by Primex:An incorrect token amount was used in the protocol fee calculation for batch closing, tokenOut and amountIn were used instead of tokenOut and amoutOut.
Open the reportIssue identified by Primex:The following functions do not emit relevant events after executing sensitive actions:withdrawFromincreaseLockedBalancetopUpAvailableBalance
Open the reportIssue identified by Primex:After the changes in fee structure, limit orders created before the upgrade worked incorrectly - the fee associated with them was locked after the order execution or editing, also the order became unexecutable and unclosable after editing.
Open the reportIssue identified by Primex:We renamed minPositionSizeMultiplier to minPositionSizeAddend because the old name didn’t correspond to its meaning, and irrelevant restrictions were removed.
Open the reportIssue identified by Primex:Added a new method to Lens to calculate min protocol fee for liquidation.
Open the reportIssue identified by Primex:The OpenPositionData struct had no extraParams field in src/contracts/interfaces/IPrimexLens.sol
Open the reportIssue identified by Primex:The protocolFeeCoefficient was missed in minPositionSize calculation, but this coefficient is a part of min protocol fee, which defines the min position size.
Open the reportIssue identified by Primex:The initialize functions of SwapManager and BatchManager check the interfaces of the contracts they receive as parameters, including IPositionManagerV2 and IPriceOracleV2, which will be added with this upgrade. However, the initialize method was called before the upgrade of the PositionManager and PriceOracle contracts, causing the check to fail. To resolve this, the initialization was split into two steps, with the second step executed after the upgrade of these contracts.
Open the reportIssue identified by Primex:Add revert to Uni v3 TWAP oracle for the case if the pool is unavailable.
Open the reportIssue identified by Primex:It was impossible to swap tokens through an arbitrary contract using DexAdapter.swapExactTokensForTokens as it required that the router had to be whitelisted. This restriction was removed.
Open the reportIssue identified by Primex:The setPoolUpdateInterval function in src/contracts/UniswapPriceFeed/UniswapPriceFeed.sol is designed to set poolUpdateInterval. However it will not work due to parameter name mismatch.
Open the reportThe FlashLoanManager contract contains logic related to flashloans. Most notably - the flashLoan function used to initiated the loan. Among other arguments, this function takes two arrays - buckets and amounts which specify assets to be used in the flashloan. It was observed that the flashLoan function does not verify the uniqueness of entries in the buckets array. Hence, it is possible to use the same asset multiple times during the same flashloan. As a consequence, in this scenario the contract will need to perform additional cross-contract calls and use more memory for the execution which would be inefficient.
Open the reportThe function _withdrawBucketLiquidityFromAave() is duplicated on both Bucket and BucketExtension contracts. This fact makes the code more complex and less readable, while also wasting unnecessarily more bytecode on the blockchain.
Open the reportIssue identified by Primex:The borrowed amount for margin limit orders was calculated during the order execution based on the oracle prices. At the same time, keepers calculate the swap route before the transaction execution, and if the amountIn used in the route and the amount calculated in tx differs, the transaction fails. To fix it, we allowed keepers to set the borrowed amount, but we also check that the leverage of the opened position can’t differ from the leverage of the order for more than leverageTolerance percentage.
Open the reportIssue identified by Primex:There is a difference in minProtocolFee calculation for single and batch position closing which leads to different health values and the liquidation price for the same position depending on the closing method.
Open the reportThe contract KeeperRewardDistributorStorageV2 implements the state variable minPositionSizeMultiplier as an int256. The usage of this state variable throughout the code assumes that it will always contain an unsigned integer. Therefore, various checks may be removed to optimize gas.
Open the reportIssue identified by Primex:Change admin roles for some methods.
Open the reportThe `TraderBalanceVault.depositAndLock()` function allows the `VAULT_ACCESS_ROLE` to add any amount of any asset to any trader's locked balance. However, this function does not actually require that a deposit takes place, contrary to what the function name suggests.
Open the reportWhen increasing the deposit of a position through `PositionLibrary.increaseDeposit()`, the following check is performed: ``` _require(depositedAmountInBorrowed >= params.amountOutMin, ...) ``` However, this check will also be performed in case `params.asset == borrowedAsset`, meaning that no swap is executed. Therefore, this swap may revert if the user has provided `_amountOutMin` in `increaseDeposit(..)` of `PositionManager.sol` anyway.
Open the reportThe `limitOrderManager.orderExists` modifier may run into a problem when `orders.length == 0`. In this case, if it is called with input `0` then it will return true since the check `orders[index].id == _orderId` essentially amounts to a check that `0 == 0`.
Open the reportThe deployment process grants `NO_FEE_ROLE` to `PositionManager` and `BatchManager` in addition to `LimitOrderManager` and `Bucket`. The `NO_FEE_ROLE` is given to contracts to avoid protocol fees when swapping with `SwapManager`. The `PositionManager` and `BatchManager` do not need `NO_FEE_ROLE` because the contracts do not use `SwapManager` to conduct swaps.
Open the reportIn order to validate the proper deployment and initialization of the contracts, it is a good practice to emit events. Also, any important state transitions can be logged, which is beneficial for monitoring the contract, and also tracking eventual bugs or hacks. Below we present a non-exhaustive list of events that could be emitted to improve application management: 1. `Bucket.setReserveRate()` 1. `Bucket.setWithdrawalFee()` 1. `Bucket.setInterestRateStrategy()` 1. `PositionManager.setKeeperRewardDistributor()` 1. `PositionManager.setMinPositionSize()` 1. `PositionManager.setOracleTolerableLimitMultiplier()` 1. `Reserve.setTransferRestrictions()` 1. `PrimexDNS.setConditionalManager()` 1. `PrimexDNS.setPMX()` 1. `PrimexDNS.setProtocolRate` 1. `PrimexDNS.setProtocolRateInPMX` 1. `PrimexDNS.setAavePool()` 1. `PrimexDNS.activateBucket()` 1. `PrimexDNS.freezeBucket()` 1. `PrimexDNS.setDexAdapter()` 1. `PrimexDNS.activateDEX()` 1. `PrimexDNS.freezeDEX()` 1. `PriceOracle.updatePriceFeed()` 1. `PriceOracle.updateVolatilityFeed()` 1. `PriceOracle.setGasPriceFeed()` 1. `KeeperRewardDistributor.setDefaultMaxGasPrice()` 1. `KeeperRewardDistributor.setOracleGasPriceTolerance()` 1. `KeeperRewardDistributor.setMaxGasAmount` 1. `KeeperRewardDistributor.setPmxPartInReward()` 1. `KeeperRewardDistributor.setNativePartInReward()` 1. `KeeperRewardDistributor.setPositionSizeCoefficients()` 1. `KeeperRewardDistributor.setAdditionalGas()` 1. `SpotTradingRewardDistributor.decreaseRewardPerPeriod()` 1. `SpotTradingRewardDistributor.setRewardPerPeriod()` 1. `SpotTradingRewardDistributor.topUpAvailablePmxBalance()` 1. `SpotTradingRewardDistributor.withdrawPmx()` 1. `Treasury.setMaxSpendingLimit()` 1. `Treasury.decreaseLimits()` 1. `ReferralProgram.setReferrals()` 1. `DexAdapter.setQuoter()` 1. `DexAdapter.setDexType()` 1. `Redeemer.changeRate()` 1. `Redeemer.adminClaimEarlyTokens()` 1. `Redeemer.adminClaimRegularTokens()` 1. `SwapManager.setSwapRate()` 1. `SwapManager.setSwapRateInPmx()` 1. `PMXBonusNFT.setExecutor()` 1. `PMXBonusNFT.blockNFT()` 1. `PMXBonusNFT.unblockNFT()`
Open the reportThe `setReferrers()` function permits the configuration of multiple referrers. However, it currently does not verify the existence of a referrer in the `referrers` list prior to the addition of a new referrer. This can result in the same referrer address being duplicated in the `referrers` list. Furthermore, the `setReferrals()` function neglects to validate the existence of referrals or referrers before updating the `referralsOf` and `referrerOf` mappings. It also fails to add new referrers to the `referrers` list. This can lead to inconsistencies in the tracking of referrers and referrals.
Open the reportThe current structure of the `Treasury.setMaxSpendingLimit()` and `Treasury.decreaseLimits()` functions, crucial for managing the treasury spending, exhibit inconsistencies and lack the necessary validation checks. The `setMaxSpendingLimit()` function allows the `Treasury` to add a new spender address with spending limits or update existing limits. This function does not validate the `_newSpendingLimits` used and but the function is locked behind a `MEDIUM_TIMELOCK_ADMIN`. Although the function name implies modifying the maximum spending limits for a spender, the implementation allows any data in `SpendingInfo.limits` to be updated for new and existing spenders. The `Treasury.decreaseLimits()` function allows `Treasury` to update the spending limits of existing spenders. This function validates that the new limits are decreased and time durations are increased. However, the function does not validate the existence of the spender address. This lack of validation could allow for unintentional increases in the time duration for non-existent spenders in the `spenders` mapping. The following input validations are recommended, but not limited to: 1. When adding or updating spending limits for a spender address, 1. `maxTotalAmount` should exceed zero, 2. `maxAmountPerTransfer` should exceed zero, 3. `maxPercentPerTransfer` should be less than `WAD` or reasonably bounded, 4. `minTimeBetweenTransfers` should exceed zero or reasonably bounded, 5. `maxAmountDuringTimeFrame` should exceed zero or reasonably bounded. 2. When decreasing spending limits for a spender address, 1. `isSpenderExist` should be validated,
Open the reportIf `PositionManager` contract is paused, it is still possible to open new positions via the `LimitOrderManager` contract through the `LimitOrderManager.openPositionByOrder()` function. However, this can be mitigated once the `LimitOrderManager` contract is paused as well.
Open the reportAn order for a swap does not require, and should not have, a close condition attached to it. This is because a swap order does not open a position, so there is no need for a way to close it. The `LimitOrderLibrary.setCloseConditions()` function includes a check on `_order.shouldOpenPosition` to prevent this situation. However, it is important to note that this check can be bypassed if a user changes a spot trade order to a swap order using the `LimitOrder.updateOrder()` function. In such cases, the originally attached closing condition will remain attached, potentially leading to unexpected behavior.
Open the reportSeveral contracts approves token infinitely for another contract. While we do not find any exploit from this, and the risk is mitigated as it is only approved to another contract within the protocol, it increase the risk surface as a vulnerability can cause the token on the approved contracts to be all taken away. We found the pattern in the following places: 1. `ActivityRewardDistributor.initialize()` 2. `Bucket.initialize()` 3. `LiquidityMiningRewardDistributor.initialize()` 4. `SpotTradingRewardDistributor.initialize()`
Open the reportThe `getLenderInfo()` function calculates the percentage of funds a lender holds with the formula: ``` (lenderInfo.points * 100) / buckets[_bucketName].totalPoints; ``` This calculation can lead to a loss of precision. For example, a lender with 1 point in a 1000-point bucket would be represented as holding 0% rather than the correct 0.1%. As the total points accumulate in `totalPoints`, the inaccuracy magnifies.
Open the reportDuring `Registry` deployment, if `guardianAddress` is undefined from the `addresses.json` configuration, the role `GUARDIAN_ADMIN` will not be added to the `Registry`. The `guardianAddress` is not specified in the configuration. The guardian role is needed in PrimexTimelock to pause and unpause functionalities.
Open the reportAny user can call the `PToken.setBucket()` function and change the bucket corresponding to the `PToken` as long as they chose an address conforming to a valid PrimeX bucket interface. However, once this bucket is set it cannot be changed. Thus, this attack has a very small window of opportunity but can be disruptive if successfully executed. Additionally, this issue is mitigated in the `BucketsFactory.createBucket()` function. Essentially, the pToken is deployed/created, and `setBucket()` is immediately called, ensuring that this scenario would not occur in practice due to their deployment mechanism. Nonetheless, if the `PToken` contract is ever used independently, this issue should be a concern.
Open the reportThe `PTokensFactory.createPToken()` function seems only needed for the `BucketFactory.createBucket()` function. However, there is no authorization check for the `PTokensFactory.createPToken()` function. In other words, anyone can call the function and deploy a new pToken. We do not find any exploit due to this, but if any of the off-chain components are to listen to the `PTokenCreated` event, they might be wrongly notified and have unexpected behavior.
Open the reportDuring the deployment process of `PrimexTimelock`, the proposers are incorrectly parsed from executors. Therefore the proposers are set as the executors regardless of the proposers provided in the task parameters.
Open the reportIn the `transfer()` function of `PToken`, there is no check to ensure that the recipient is not blacklisted. In accordance with these checks in other places, blacklisted addresses should not be able to receive any pTokens. **Note:** We discussed this with the team during the audit, and the team has clarified that this is the intended behavior.
Open the reportAn order for a swap or spot trade should not have the `depositedAsset` being the same as the `positionAsset`, as there is no need for them to be identical. The `LimitOrderLibrary.createLimitOrder()` function includes validation on the assets when `vars.isSpot` to prevent this scenario. However, it is important to note that when using the `LimitOrderManager.updateOrder()` function, one can change the `depositedAsset` without the same validation. As a result, it is possible to create a spot trade or swap order with the `depositedAsset` being the same as the `positionAsset`, which may lead to unexpected behavior or unnecessary operations.
Open the reportThe `LimitOrderManager` contract currently only allows the trader to cancel an order with the `cancelLimitOrder()` function, unless the bucket has been delisted. However, if the `deadline` of the order has passed, it may be kept in the contract indefinitely unless the trader actively cancels it. These legacy orders will be stored in the contract, including arrays such as `traderOrderIds` and `bucketOrderIds`. When there are too many legacy orders, it can potentially block certain features, such as the `getBucketOrders()` function, from executing due to running out of gas. Additionally, directly accessing the public array may also raise gas concerns, although this can be configurable through the node (refer to the gas limit on `eth_call` discussion [here](https://community.infura.io/t/gas-limit-on-eth-call/1115)).
Open the reportWhen swapping from the wallet through the `swap()` function, the input token `params.tokenA` can be `NATIVE_CURRENCY`. The current implementation does not accept `NATIVE_CURRENCY` when swapping directly from the wallet through the `TokenTransfersLibrary.doTransferFromTo()` function. However, the implementation accepts `NATIVE_CURRENCY` when using funds from the `traderBalanceVault.lockAsset()`. Nonetheless, the call to `PrimexPricingLibrary.multiSwap()` will eventually fail. However, it might be great to clarify whether native token should be supported or not and add the corresponding validation.
Open the reportLeaving an implementation contract uninitialized poses a security risk. An uninitialized implementation contract can be exploited by attackers, potentially compromising the associated proxy. To mitigate this risk, it is recommended to invoke the `_disableInitializers()` function in the constructor during deployment (see: [OpenZeppelin doc](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#initializing_the_implementation_contract)). This action will automatically lock the implementation contract, preventing any unauthorized usage. The following is the list of impacted contracts found with the `grep` command: ```sh ▶ grep -rl --include="*.sol" --exclude="I*.sol" --exclude-dir="mocks" "initializer" . ./SpotTradingRewardDistributor/SpotTradingRewardDistributor.sol ./TraderBalanceVault/TraderBalanceVault.sol ./PMXBonusNFT/PMXBonusNFT.sol ./ActivityRewardDistributor/ActivityRewardDistributor.sol ./PToken/PToken.sol ./PriceOracle/PriceOracle.sol ./WhiteBlackList/WhiteBlackListReferral/WhiteBlackListReferral.sol ./WhiteBlackList/WhiteBlackList/WhiteBlackList.sol ./LimitOrderManager/LimitOrderManager.sol ./KeeperRewardDistributor/KeeperRewardDistributor.sol ./Bucket/Bucket.sol ./LiquidityMiningRewardDistributor/LiquidityMiningRewardDistributor.sol ./DebtToken/DebtToken.sol ./Reserve/Reserve.sol ./ReferralProgram/ReferralProgram.sol ./PrimexDNS/PrimexDNS.sol ./BonusExecutor/FeeDecreaser.sol ./Treasury/Treasury.sol ./PositionManager/PositionManager.sol ```
Open the reportEvery Solidity file specifies in the header a version number of the format `pragma solidity (^)0.8.*`. The caret (`^`) before the version number implies an unlocked pragma, meaning that the compiler will use the specified version _and above_, hence the term "unlocked". Note that Solidity `0.8.20` might only work on Ethereum Mainnet at this moment as it introduced a new OP Code that other EVM-compatible chains might not support yet (see: [this article](https://medium.com/coinmonks/push0-opcode-a-significant-update-in-the-latest-solidity-version-0-8-20-ea028668028a) for more information).
Open the reportThe `PrimexPricingLibrary.getLiquidiationPriceByOrder()` function has an unresolved `TODO` stating: "This function is only used in tests. Maybe it can be removed."
Open the reportThe function `AccessControl._setupRole()` has been [deprecated](https://docs.openzeppelin.com/contracts/4.x/api/access#AccessControl-_setupRole-bytes32-address-) in favor of `AccessControl._grantRole()`, which performs the same operations.
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="primex-finance"></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