Quantstamp reviewed UnibotX (UniBot (Diamond Protocol)) on 13 Nov 2022: 34 findings, 0 critical; 25 marked fixed, 7 acknowledged, 2 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 | 0 | 8 |
| Low | 1 | 7 | 15 |
`DiamondFactoryV3._closePosition()` Liquidation fees are applied to the amounts returned by `DiamondFactoryV3._removeUniLiquidity()` which are equivalent to the initial want token deposit minus the reserve and the amount borrowed with leverage. For example, assuming a liquidation fee of 5% and a 4x leverage with zero reserve ratio, the final fees applied on the original deposited amount turn out to be close to 20%, which is a high value. The issue described can lead users if liquidation is allowed for non-keeper addresses (owners of the position e.g) or keepers to get back a value even higher than the initial deposit (worst case scenario).
`DiamondFactoryV3._trySwapExactOutput()` uses the average price tick (`oracleTimeWeightedSec = 900`) to estimate the maximum `quoteAmount` and can be the reason why the swap will possibly throw leading `DiamondFactoryV3._trySwapExactOutput()` to revert due to the requirement stated below: ``` require(_amountIn < amountInWithSlippage, "Slippage protect"); ``` This will cause healthy positions to revert when closing them and possibly turn to unhealthy or even to bad positions. Please note that this issue depends on a random factor (the price volatility during the last `oracleTimeWeightedSec`), and it is hard to predict when a user will be able to claim his position again.
The lending pool used by the contract can be set and updated with the function `setLendingPoolAddress()`. Only the owner of the contract has the right to execute that function. However, if the lending pool is malicious or if it gets exploited, it gets the right to drain all the `borrowToken` owned by the contract.
`LendingPoolV3.repayBorrow()` checks if the caller (which should be factory) is in the whitelist. However, if a factory borrows from a lending pool, and then is removed from the whitelist, it will not be able to call `LendinPool.repayBorrow()`.
The contracts `DiamondFactoryV3` and `BalanceVaultV3` have no constructor but have a function `init()`. As a result, once the contracts are deployed, everyone can call the function `init()` and become the owner of these contracts.
The function `_findPositionIndex()` checks with a for loop if a user owns a given position based on its `_positionId`. However, the loop does not use the library `SafeMath` and uses an `uint8` variable which can store a maximum of 256 values. As a result, an overflow can happen on the variable `i`, which would lead to an infinite loop. Such a situation can be mitigated with the state variable `maxPositionNumber` of the contract `DiamondFactoryController`, but there is no upper limit for its value. If `maxPositionNumber` is not set properly, closing a position that has an id higher than 255 will cause the transaction to throw.
If a particular position reaches a `healthFactor < 1` it can be liquidated through a call to `DiamondFactoryV3.liquidate()`. This function checks that a caller is registered as a keeper in `DiamondFactoryController` when `requireKeeper` flag is set. However, a user can call `DiamondFactoryV3.closePosition()` on an unhealthy position he owns and bypass the keeper requirement imposed in `liquidate()`.
A reentrancy vulnerability is a scenario where an attacker can repeatedly call a function from itself, unexpectedly leading to potentially disastrous results. This is done through a function that does not respect Checks-Effects-Interactions. The [Checks-Effects-Interactions](https://solidity.readthedocs.io/en/latest/security-considerations.html#use-the-checks-effects-interactions-pattern) coding pattern is meant to mitigate any chance of other contracts manipulating the state of the blockchain in unexpected and possibly malicious ways before control is returned to the original contract. As the name implied, only after checking whether appropriate conditions are met and acting internally on those conditions should any external calls to, or interactions with, other contracts be done. All the functions listed below need to have a re-entrancy guard set: - `DiamondFactoryV3.liquidate()` - `DiamondFactoryV3.stopLoss()` - `DiamondFactoryV3.openPosition()` - `DiamondFactoryV3.closePosition()` - `DiamondFactoryV3.addCollateral()` - `DiamondFactoryV3.decreaseCollateral()` - `DiamondFactoryV3.collectFee()` - `DiamondFactoryV3.updateStopLossPrice()` - `BalanceVaultV3.deposit()` - `BalanceVaultV3.withdraw()` - `BalanceVaultV3.transferBalanceFromVault()` - `LendingPoolV3.deposit()` - `LendingPoolV3.withdraw()` Even if a function seems re-entrancy safe due to its interaction with a trusted third-party contract (known token contract, e.g.), it should not be trusted.
A common attack in DeFi is the sandwich attack. Upon observing a position opening or closing, an attacker can front-run the victim transaction by buying one of the assets, letting the victim execute the transaction, and then executing another trade after the victim by trading back the amount gained in the first trade. In both `DiamondFactoryV3._provideUniLiquidity()` and `DiamondFactoryV3._removeUniLiquidity()` the following parameters are not set: - When minting liquidity: - `MintParams.amount0Min` - `MintParams.amount1Min` - When decreasing liquidity (burning): - `DecreaseLiquidityParams.amount0Min` - `DecreaseLiquidityParams.amount1Min` As a slippage prevention mechanism, the official documentation of Uniswap V3 recommends that non-zero values should be used for the parameters `amount0Min` and `amount1Min` when using in production the functions: - `NonfungiblePositionManager.mint()` https://docs.uniswap.org/protocol/guides/providing-liquidity/mint-a-position#calling-mint ; - `NonfungiblePositionManager.decreaseLiquiditymint()` https://docs.uniswap.org/protocol/guides/providing-liquidity/decrease-liquidity#decrease-liquidity ; This best practice is not respected when the functions are called by the functions `_provideUniLiquidity()` and `_removeUniLiquidity()`.
Keepers play an essential role in the protocol when enabled. They liquidate insolvent positions and activate stop losses when a price tick goes below or above stop loss limits defined by the owners of positions. It implies that the protocol solvency highly depends on a limited set of addresses which could easily become single points of failure. On top of that, the function `removeKeeper()` is used to remove a keeper. However, the fact that the last allowed keeper can be removed is not checked. As a result, the protocol can remain without any keeper.
The state variable `openPositionMinimumAmount` acts as a lower limit for the number of tokens for a position. It is set in the function `init()`. However, there is no function to update it.
The function `Oracle.consult()` is used to fetch time-weighted average ticks using Uniswap V3 oracle. It uses the state variable `oracleTimeWeightedSec` as a parameter. However, the call will revert if `oracleTimeWeightedSec` is equal to `0` or if it goes beyond the oldest observation recorded in the pool.
`DiamondFactoryV3.setLendingPoolAddress()` approves the borrow token to be spent by the new pool but does not remove the allowance for the previous address. This represents a risk since a compromised pool will still be able to access possible funds on `DiamondFactoryV3` contract address even after removing them.
`DiamondFactoryV3.setLendingPoolAddress()` allows changing the lending pool address while the contract is deployed. This seems to introduce unnecessary risk since the new address state might not be the same as the previous one. As a consequence, users will likely not be able to withdraw their assets.
Performance fees are collected in the function `collectFee()`. No event is emitted with the number of performance fees that have been collected, which could negatively impact the off-chain monitoring of the protocol.
The names used for the local variables in the function `openPosition()` are close and also mix expected and actual amounts of both tokens. For example, the variables `_strategyParams.wantTokenAmount` and `wantTokenAmount` are very similar. Also, the variables `usdcLiquidityAmount` and `wethLiquidityAmount` have token names in their names, which is not recommended if the contract is meant to be used for other tokens than USDC and WETH. The impact is that it is hard to check that the function behaves as expected.
For several functions, there are differences between the documentation in the interface contract and the implementation contract. The description is sometimes more detailed in the interface and sometimes it is the inverse. The impact is that some integrity constraints could be added or ignored, which could ultimately break the interoperability expected from different contracts implementing the same interface. Also, there is no documentation in the interface `IInterestModel`.
The state variable `feeReceipt` stores the address that will get the fees generated by the contract. Its value is set in the constructor. However, there is no function to update its value. As a result, if the address `feeReceipt` is compromised or blacklisted for the token want, the only way to react would be to set the variable `performanceFee` to 0.
1. The function `find()` of the library `Positions`: - returns `true` if a position exists; - reverts if a position doesn't exist; As a result, it never returns `false` and reverts instead. 2. The function `get()` of the library `Positions` includes an optional check of ownership. However, it does not check if the position does exist. As a result, if the position does not exist, it is possible to: - return an empty position, if `checkOwner == false`; - revert with the incorrect reason `"Invalid position owner"`, if `checkOwner == true`; In both cases, a developer could be misled by the name of these functions if the library is reused in future versions.
In the contract `BalanceVaultV3`, it is possible to manage whitelisted addresses with the functions `addWhiteFactory()` and `removeWhiteFactory()`. However, the documentation of these functions states that an address is whitelisted for a given token and the function expects a parameter `_token`, which is not used in the code. Also, the state variable `whiteFactories` does not record which token should be whitelisted for a given factory.
The following comments in `DiamondFactoryV3._removeUniLiquidity()` seems incorrect: ``` // We charge performance fee based on uniswap fee, but when you removed liquidity // uniswap will not tell you how much fee do you earned. So we have to calculate // how much uniswap fee should we have first. ``` The whole ` IDiamondFactoryHelper(_helper()).getPositionTokenAmount()` is executed to get the fees when only a single call to the line stated below will return the accumulated fees values: ``` (uint256 fee0, uint256 fee1) = PositionValue.fees( INonfungiblePositionManager(UNI_POSITION_MANAGER), _positionId ); ``` The comments are misleading since no real calculations are done to get the fee values.
The contracts do not have specific functions to pull tokens or ETH sent on purpose or by mistake.
`InterestModal` execute multiple arithmetic operations without the use of any safe math library or a solidity version greater than `0.8.0`. Unchecked arithmetic operations can lead to overflows and possible exploits.
In the contract `BalanceVaultV3`, four functions are used by the contract to receive or transfer funds: `deposit()`, `withdraw()`, `transferBalanceFromVault()`, `transferBalanceToVault()`. Tokens received are owned by the contract and the internal accounting of which address owns which amount of token is stored in the state variable `balances`. However, that state variable is updated based on the amount that is expected to be transferred, instead of based on what has been received. As a result, if the contract used an ERC20 where the amount to transfer is different than the amount received (greater or lower), the internal accounting of the contract would be broken and users could claim more or less than the actual amount of tokens owned by the contract.
The state variable `tokenDecimalDiff` is set in the function `init()`. However, it is not used in the contract.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (15)
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