Incident,
5M.
Source: De.Fi incident database.
- Audits before it
- 4.
- Audits since
- 63, the latest dated 13 Sep 2025.
As of 30 Oct 2026, Venus Protocol has 67 audit reports by 7 security firms on Trustblock; the latest is by Quantstamp, dated 13 Sep 2025. Those reports list no open critical findings. It also holds 2 deployed contracts on 2 chains and 2 incidents (latest 12 May 2022).
Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.
Newest first
67 audit reports, 40 in CertiK’s series, 11 in Quantstamp’s series
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Venus - Multichain Governance | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus wstETH Oracle | Security firm: Quantstamp | Date: | Scope: Repository at commit f638cef | No open critical findings | Open · certificate.quantstamp.com |
| Venus - Token Converter Addendum | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus TWAP Oracle | Security firm: HashEx | Date: | Scope: 1 address on BNB Chain | No open critical findings | Open · hashex.org |
| Venus - Oracle for wstETH on Ethereum | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus - Time based contracts & SeizeVenus | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus - XVS Token Bridge | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus - Prime and Oracle Changes | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus Multichain Support | Security firm: Quantstamp | Date: | Scope: Repository at commit 5299454 | No open critical findings | Open · certificate.quantstamp.com |
| Venus - Private Conversions | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus - Prime | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus - Token Converter | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus Token Converter Audit | Security firm: OpenZeppelin | Date: | Scope: Files named in the report | No open critical findings | Open · blog.openzeppelin.com |
| Venus - Forced liquidations in Isolated pools | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus Income Allocation | Security firm: Quantstamp | Date: | Scope: Repository at commit 2e0f5f6 | No open critical findings | Open · certificate.quantstamp.com |
| Venus - Force Liquidations | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus - Allocation of Income | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus Protocol Diamond Comptroller Audit | Security firm: OpenZeppelin | Date: | Scope: Files named in the report | No open critical findings | Open · blog.openzeppelin.com |
| Venus - RiskFund & Shortfall | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus - Isolated Pools Allow Multiple Rewards Dist | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus - Diamond Comptroller (Subscription Audit 5) | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus Protocol V2 | Security firm: Quantstamp | Date: | Scope: Repository at commit a9c6e07 | No open critical findings | Open · certificate.quantstamp.com |
| Venus Liquidator | Security firm: Quantstamp | Date: | Scope: Repository at commit 457c8cd | No open critical findings | Open · certificate.quantstamp.com |
| Venus - Oracle Update | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
| Venus - RewardsDistributor | Security firm: CertiK | Date: | Scope: Files named in the report | No open critical findings | Open · skynet.certik.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 3 |
| High | 16 | 0 | 13 |
| Medium | 5 | 4 | 32 |
| Low | 71 | 60 | 222 |
`setDirectPrice()` allows a user with sufficient access control to manually set a price for any asset in the `prices[]` mapping. `_getPriceInternal()` will return the `prices[]` mapping if it is non-zero, preferring it over the price returned by `_getChainlinkPrice()`. The comment specifies that the manually set price is "useful under extenuating conditions such as price feed failure". However, the price is currently used in preference of the price feed, even if it is successful. Additionally, the price never becomes stale. Even if the price is accurately set on initialization, it may not remain so and the protocol would continue operating under a false price.
Open the reportDuring the `_releaseFund()` function call, `DistributionConfig`s are iterated over, and the funds are divided between the provided addresses via an ERC20 transfer. After the ERC20 token transfer, the `updateAssetsState()` method of `IIncomeDestination` is invoked, so that the tokens are accounted for correctly. The issue arises from the fact that the method is called on an arbitrary contract at the address `_config.destination`, which is added to the array. Therefore, if an address is added that does not have the `updateAssetsState()` method implemented, this call will revert; hence, every call to `_releaseFund()` will revert, and the contract will not be usable, as it is not possible to remove items from the distribution array.
Open the report`updateAssetsState()` function is usually called when a transfer of ERC20 assets has been made to the contract. It can also be called by any user of the platform. The idea behind this function is to account for newly transferred ERC20 tokens by taking the difference between the current balance of the contract and the accounted balance of the contract. It is assumed that there should be no case where the `currentBalance` will be less than the already accounted `assetReserve`. This is not strictly the case, as if the asset is a rebasing token such as Ampelforth, the number of tokens in the contract balance will vary depending on the current market conditions and the underlying peg-keeping algorithm. These types of balance changes are also common in other types of non-standard tokens. When `releaseFunds()` is called with the correct parameters, the income of such rebasing token would be distributed based on the last `updateAssetsState()` balance, which might not be the same as the current contract balance and could be potentially lower than the `schemaBalances` accounted balance. Leading to an issue where the last transfer will fail due to insufficient funds in the contract.
Open the reportThe report gives no description here.
When a proposal is received from the layer zero endpoint, it is queued with the relevant timelock based on the proposal type. Timelock contracts can be updated via `addTimelocks()`, but there is no check that enforces them to be unique. Hence, the contract can use the same timelock for all 3 proposal types. This could lead to issues where `proposalTimelocks[proposalType_].queuedTransactions` transactions will revert if the same transaction has been sent with multiple proposal types, for example, to get it expedited.
Open the reportThe `OmnichainProposalSender` and `OmnichainGovernanceExecutor` include mechanisms to limit the number of commands that can be sent and received within 24 hours. These daily limits may prevent critical proposals from being executed promptly if the limit is reached, potentially delaying important governance actions.
Open the reportThe owner of the contract can call `transferOwnership()` to transfer the ownership to a new address. If an uncontrollable address is accidentally provided as the new owner address then the contract will no longer have an active owner, and functions with the `onlyOwner` modifier can no longer be executed.
Open the reportThe `addTimelocks()` function allows the admin to update the timelock contracts. However, it does not check if the timelocks to be updated have queued transactions, which could lead to an inconsistent state where transactions can never be executed. The usage of Timelock functions such as `cancelTransaction()` and `executeTransaction()` rely on the fact that the underlying timelock has not changed; if that is the case, the old queued transactions would need to be resent as a new proposal since the old ones could neither be canceled nor executed. However, this could make the whole process questionable for users since proposals can be silently canceled or skipped.
Open the reportThe execution order of proposals is not guaranteed in the Venus multichain governance. Due to the non-blocking nature of the message receiver, proposals sent across chains may not be executed in the order they were intended. This lack of a guaranteed execution order can be particularly problematic for interdependent or sequence-sensitive proposals. Although the scenario presented below is unlikely, it demonstrates the potential impact of out-of-order proposal execution. Additionally, this misordering can be compounded by potential race conditions such as: 1. Calling `setSrcChainId()` will invalidate any proposals that are yet to be received from that chain or any proposals in the `failedMessages` mapping. 2. If `execute()` and `cancel()` are called at a similar time, the behavior of the proposal will be based on the ordering of the transactions in the block.
Open the reportWhen a proposal is sent via the `execute()` method in the `OmnnichainProposalSender` contract, the `_validateProposal()` function increments the command count within a 24-hour window. If the proposal dispatch fails, this increment is not reverted, potentially obstructing future proposals in the same window. Moreover, retrying the execution could erroneously increase the command count twice if `_validateProposal()` is called again within the same period, further risking the execution of subsequent proposals.
Open the reportAccording to the NatSpec comments of the Resilient oracle, the function `updateAssetPrice()` should be called before calling `getPrice()`. However, the `CorrelatedTokenOracle` calls the latter without calling the former which could result in an outdated price from the pivot oracle inside the Resilient oracle.
Open the reportThe above mentioned custom oracles rely on a price feed that has an upgradable component behind it, meaning at some point, the implementation of the price feed used can change, and hence, such an oracle can start producing incorrect or inaccurate prices. These upgrade actions should be carefully monitored, and adjustments should be made to the custom oracle if needed.
Open the reportThe new function added to the `RewardFacet` in the core protocol called `seizeVenus()` allows the owner of the protocol to seize tokens from a given number of holders and transfer them to a provided recipient. A compromised owner could drain the protocol.
Open the reportThe function `rewardTokenAmountsPerBlock(address _rewardToken)` returns the value stored in the `rewardTokenAmountsPerBlockOrSecond` mapping at the given `_rewardToken`. If the value stored in the mapping is in seconds, a misleading value would be returned.
Open the reportThe protocol already allows users the ability to approve delegates to borrow and pay on their behalf. The protocol's upgrade extends the functionalities available to delegates, now includes the ability to redeem underlying collateral on behalf of the vToken holder. This extension is built upon the pre-existing delegate feature, which already permitted delegates to mint, repay, or borrow. The issue is that users approved those borrow delegates under the assumption that they would only be able to borrow on their behalf. However, with this new update, those same borrow delegates are automatically redeem delegates as well.
Open the reportThe `NativeTokenGateway` allows the owner to transfer all native and ERC20 tokens in the contract to themselves through the functions `sweepNative()` and `sweepToken()`. However, it is worth noting that this is only related to stuck native and ERC20s tokens since users should only send native tokens and receive its wrapped version through specific functions defined by the contract.
Open the reportThe isolated pools repository uses Solidity version `0.8.13` in most of the contracts in scope except for the `NativeTokenGateway` contract which uses `0.8.20`. Using different compiler versions brings risks of inconsistent contract behavior. Additionally, some chains do not support the latter version, and deployment might not be possible, or the contracts usage might be hindered by this.
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".
Open the report`WstETH` contract contains a specific function that allows getting the amount of `stETH` from `wstETH`: ``` contract WstETH is ERC20Permit { ... function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256) { return stETH.getPooledEthByShares(_wstETHAmount); } ... } ``` <br> Using the `stETH` contract directly to compute the `stETH` value from a fixed share value by calling `STETH.getPooledEthByShares(1 ether)` in the `WstETHOracle.getPrice()` function is correct. However, any future upgrade might create unknown results so we strongly recommend using `WstETH.getStETHByWstETH()` instead of the `stETH` contract.
Open the reportWhile there are non-zero address checks, consider increasing the robustness of the input validation by using OpenZeppelin's `isContract()` function to ensure that all the addresses provided are smart contract addresses and not EOA.
Open the reportThe `ResilientOracle` contract attempts to manage extreme scenarios where a substantial number of dependent oracles fail. Nonetheless, the oracle function used does not return the last update time, meaning that the risk of utilizing stale price data still remains with a low likelihood. We also note, the oracle also assumes that the `ResilientOracle` will accurately return the price of a `WETH` in terms of USD. In case this assumption fails, the oracle will return inaccurate results.
Open the reportThe `getPrice()` function accepts a parameter but reverts unless it is `WSTETH_ADDRESS`. In this case the parameter is not necessary, however, may still be kept in order to conform to the `OracleInterface`.
Open the reportThe in-scope contract is compiled using Solidity version 0.8.13. According to the [Solidity official's compiler bug list](https://docs.soliditylang.org/en/latest/bugs.html), this version contains several known compiler bugs, specifically 1 medium/high-severity, 2 medium-severity, and 6 low-severity bugs. According to our examination, these bugs seem to not affect the code in scope.
Open the reportThe protocol is planned to be deployed on Arbitrum and opBNB. Both chains allow users to send cross-chain messages to call L2 contracts from L1 so that the interaction with L2 protocols can continue even if the sequencer is down. However, when an L2 contract receives a cross-chain call from L1, the `msg.sender` value is usually not the sender who initiates the cross-chain call on L1. On Arbitrum, the `msg.sender` is the [aliased address](https://docs.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing) of the original sender. On opBNB, which is built on the OP Stack, the `msg.sender` can be the [L2 messenger contract](https://community.optimism.io/docs/developers/bridge/messaging/#accessing-msg-sender) or an [aliased address](https://community.optimism.io/docs/developers/bedrock/differences/#deposits-from-the-underlying-l1-ethereum-goerli-etc-to-l2-op-mainnet-op-goerli-etc), depending on which L1 contract is called and whether the original sender is a contract or not. Therefore, access control mechanisms based on the `msg.sender` value should take such changes into account if a privileged role needs to call an L2 contract from L1 in certain circumstances, for example, in an emergency but directly sending L2 transaction is not possible since the sequencer is down. In the past, the Uniswap Factory deployed on Arbitrum did not consider address aliasing, and its owner was set to the address of the Timelock contract on L1, causing the execution of governance proposals on Arbitrum to be temporarily blocked ([reference](https://gov.uniswap.org/t/consensus-check-fix-the-cross-chain-messaging-bridge-on-arbitrum/18547)).
Open the reportThe `BaseXVSProxyOFT` contract takes `sharedDecimals_` as a constructor parameter and sets the `ld2sdRate`. According to the code comments in `OFTCoreV2`, > _sharedDecimals should be the minimum decimals on all chains The XVS tokens will have 18 decimals on each EVM chain. However, it should be noted that there is an upper limit of `type(uint64).max` on `sd` amounts. This is required for bridging to non-EVM chains such as Aptos and Solana. If `sharedDecimals_` is set to 18, at most ~18 XVS tokens can be bridged in each call. As mentioned by the Layer Zero docs, ["OFTV2 is intended to be used with no more than 10 shared decimals"](https://layerzero.gitbook.io/docs/evm-guides/layerzero-omnichain-contracts/oft/oft-v1-vs-oftv2-which-should-i-use#what-are-the-differences-between-the-two-versions).
Open the reportThe XVS vault internal mapping `rewardTokenAmountsPerBlock`, as the name implies, stores the amount of rewards per block for each registered token. On chains with a fixed block time, since each block is created (approximately) periodically, this implies “per unit of time”. Therefore, as seen in the table below, special care needs to be taken when setting this value in the different chains (these values can and might change in the [future](https://etherscan.io/chart/blocktime)). A wrong value could result in unexpected rewards being provided to users. For chains with irregular block times, please refer to the other related issues. | Chain | Avg Blocktime [s] | |----------|:-----------------:| | Ethereum | 12 | | BSC | 3 | | Optimism | 2 | | opBNB | 1 | | Arbitrum | ~12 (L1), Irregular (L2) | | zkEVM | Irregular | <br> **References:** * [opBNB vs other Layer2 Networks | BNB Optimistic Rollup](https://docs.bnbchain.org/opbnb-docs/docs/core-concepts/difference-L2) * [Arbitrum Block Numbers and Time](https://docs.arbitrum.io/time)
Open the reportIn the function `AccessControlManager.giveCallPermission()`, the event `PermissionGranted` is bypassable by computing the `role` off-chain and calling the function `AccessControlManager.grantRole()` directly. Similarly, the event `PermissionRevoked` is bypassable by calling `AccessControlManager.revokeRole()` directly.
Open the reportThe contract `AccessControlManager` allows the `DEFAULT_ADMIN_ROLE` to grant an account access to a specified function accross **all** managed contracts by passing the zero address when calling `AccessControlManager.giveCallPermission()`. While code comments indicate that this is the intended design, it is crucial to note that such a design *could* lead to uninteded access when a new contract is managed by the `AccessControlManager`, or when a currently managed contract is upgraded with new functions.
Open the reportThe `ChainlinkOracle.getPrice()` and `_getPriceInternal()` functions fetch the given ERC-20 token's `decimal` and multiply the asset price by `10 ** (18 - decimal)`. However, this approach cannot handle ERC-20 tokens with decimals larger than 18, as the subtraction will trigger an integer overflow.
Open the reportSmart contracts will often have `owner` variables to designate the person with special privileges to make modifications to the smart contract. In this protocol, access control is also granted on a per-function basis through `_checkAccessAllowed()`. **VTreasuryV8** The `VTreasuryV8.sol` contract contains the following privileged roles: 1. An owner (`owner`, `onlyOwner` modifier) as initialized during the constructor execution to `msg.sender`: 1. Transfer any amount of any token to any address by calling `withdrawTreasuryToken()`. 2. Transfer any amount of the native token to any address by calling `withdrawTreasuryNative()`. <br> **XVS Bridge & XVS** The `BaseXVSProxyOFT.sol` contract contains the following privileged roles: 1. An owner (`owner`, `onlyOwner` modifier) as initialized during the constructor execution to `msg.sender`: 1. `setOracle()`: Set the address of the `ResilientOracle` contract. 1. `setMaxSingleTransactionLimit()`: Sets the limit of a single transaction amount. 1. `setMaxDailyLimit()`: Sets the limit of daily (24-hour) transactions amount. 1. `setMaxSingleReceiveTransactionLimit()`: Sets the maximum limit for a single receive transaction. 1. `setMaxDailyReceiveLimit()`: Sets the maximum daily limit for receiving transactions. 1. `setWhitelist()`: Sets the whitelist address to skip checks on transaction limit. 2. `pause()`: Pauses the bridge. 3. `unpause()`: Unpauses the bridge. <br> The `XVSBridgeAdmin.sol` contract contains the following privileged roles: 1. An owner (`owner`, `onlyOwner` modifier) as initialized during the constructor execution to `msg.sender`: 1. Insert or modify the mapping between function signatures and names to allow specific roles to call the privileged functions on the bridges. <br> The `XVS.sol` contract contains the following privileged roles: 1. An Access Control Manager (`accessControlManager_`), as initialized during the constructor execution to `accessControlManager_`: 1. Control who is allowed to mint tokens by indirectly affecting `_ensureAllowed("mint(address,uint256)")`. 2. Control who is allowed to burn tokens by indirectly affecting `_ensureAllowed("burn(address,uint256)")`. <br> The following functions in this contract are protected through `_ensureAllowed()`: 1. `mint(address,uint256)`: Mints a capped `amount_` of tokens to a specified `account_`. 2. `burn(address,uint256)`: Burns any `amount_` of tokens from a specified `account_`. <br> **XVS Vault** The `XVSVault.sol` contract contains the following privileged roles: 1. An administrator (`admin`, `onlyAdmin()` modifier), as initialized during the constructor execution to `msg.sender`: 1. Change the XVS store contract address (`xvsStore`) that controls reward token transfers by calling `setXvsStore()`. 2. Change the access control manager implementation address (and thereby indirectly all `_checkAccessAllowed()`-controlled access checks) by calling `setAccessControl()`. <br> The following functions in this contract are protected through `_checkAccessAllowed()` with their corresponding function signatures: 1. `pause()`: Pause the contract (and thereby all calls to `deposit()`, `claim()`, `executeWithdrawal()`, `requestWithdrawal()`, `updatePool()`, `delegate()` and `delegateBySig()`) by calling `pause()`. 1. `resume()`: Resume the contract from a paused state by calling `resume()`. 1. `add(address,uint256,address,uint256,uint256)`: Add a new pool by calling `add()`. 1. Using an existing reward token, but a new pool token allows the caller to globally update the `rewardTokenAmountsPerBlock[]` variable for all pools using this reward token, **without explicitly calling `setRewardAmountPerBlock()`**. In extreme cases, this could either drain the reward token (by choosing a high reward value) or halt any future rewards (by choosing zero). 1. **Accidentally or with malicious intent the caller could add enough new pools to make calling `massUpdatePools()` prohibitively gas-expensive and thereby deny the service to the following functions**: 1. `add()` 1. `set()` 1. `setRewardAmountPerBlock()` 1. `set(address,uint256,uint256)`: Update the accumulated rewards per share for all pools and change the allocation point of a pool by calling `set()`. 1. `setRewardAmountPerBlock(address,uint256)`: Update the accumulated rewards per share for all pools and arbitrarily change the reward amount per block by calling `setRewardAmountPerBlock()`. 1. **By setting it to zero no additional rewards would be accumulated.** 1. **By setting it to an unusually high value the reward token could be drained.** 1. `setWithdrawalLockingPeriod(address,uint256,uint256)`: Change the lockup period for withdrawing deposited funds by calling `setWithdrawalLockingPeriod()`. <br> The `XVSStore.sol` contract contains the following privileged roles: 1. An administrator (`admin`, `onlyAdmin()` modifier), as initialized during the constructor execution to `
Open the reportIn the contract `AccessControlManager`, the `DEFAULT_ADMIN_ROLE` is granted to the deployer, which can then grant and revoke privileged roles for access to restricted protocol functions. In the case where the deployer calls `AccessControlManager.renounceRole(DEFAULT_ADMIN_ROLE, deployerAddress)`, and no other addresses are granted the `DEFAULT_ADMIN_ROLE`, the `AccessControlManager` contract will no longer be able to grant or revoke roles. In certian cases, a renounced `DEFAULT_ADMIN_ROLE` could prevent a managed contract from accessing pausable functionality or other critical functionality.
Open the reportIf a cross-chain transfer (from `XVSProxyOFTSrc` to `XVSProxyOFTDest`) fails on the destination chain, the `fallbackWithdraw()` function can be called on the source chain to reclaim the deposited tokens. However, there is no way of checking that there has ever been a failed transfer on a destination chain, and furthermore, there is no way of verifying the transfer amount. Therefore, it is possible to withdraw tokens on the source chain that were not ever "stuck" on a destination chain, which would prohibit users from bridging their tokens from a destination chain back to the source chain.
Open the reportEvery Solidity file specifies in the header a version number of the format `pragma solidity (^)0.x.y`. 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".
Open the reportThe above-listed contracts are compiled using Solidity version 0.5.16 or 0.8.13, which contains known compiler bugs, according to the [Solidity official's compiler bug list](https://docs.soliditylang.org/en/latest/bugs.html). Specifically, Solidity version 0.8.13 includes one bug labeled as `medium/high` severity and two bugs labeled as `medium` severity. According to our examination, these bugs seem to not affect the code in scope. For 0.5.16, it is considered a relatively outdated version and is not recommended for production use.
Open the reportAccording to Arbitrum’s [documentation](https://docs.arbitrum.io/time), its `block.number` is updated to sync with Ethereum’s `block.number` every minute. Therefore, any time calculations based on the block number would be inaccurate. The `XVSVault` relies on the `block.number` to update `pool.accRewardPerShare` in `_updatePool()` based on the duration since the last update. The pool is updated internally before every major action is taken in the contract, and there is also the external function `updatePool()` without any restrictions on the caller. If `_updatePool()` is called less frequently, this could cause inaccuracies in `accRewardPerShare` in the short term, leading to users receiving fewer rewards than expected. On the other hand, block numbers and timestamps on Arbitrum are generally reliable in the longer term, according to the documentation: > As a general rule, any timing assumptions a contract makes about block numbers and timestamps should be considered generally reliable in the longer term (i.e., on the order of at least several hours) but unreliable in the shorter term (minutes). (It so happens these are generally the same assumptions one should operate under when using block numbers directly on Ethereum!) If the rewards need to be accurate on the order of several hours, this issue is of less concern.
Open the reportThe report gives no description here.
Open the reportThe contract has a function with `setProtocolShareReserve()` functionality that allows to set a new address for the `protocolShareReserve` contract. There are similar implementations of setters in the contract, but the main difference is that the new address is verified by a call to a `is<ExpectedContract>()` call, where Expected Contract is, for example, Comptroller. This is not the case for the `protocolShareReserve` contract. Hence, it is possible for the new address not to be the expected contract and not to have an `updateAssetsState()` method implemented. This would lead to `_reduceReservesFresh()` failing on every call in the `VToken` context and hence the DoS of the usability of the contract, as the method would not exist and revert. In the context of `vBNBAdmin`, the function failing would be `reduceReserves()`. This issue affects both the `VToken` contract implementations and the `vBNBAdmin` contract.
Open the reportDuring the `_releaseFund()` call, the correct distribution patterns are applied to the collected ERC20 Income. The income is divided as a percentage between previously allocated addresses. The `transferAmount` is calculated via the following formula: ``` uint256 transferAmount = (schemaBalances[uint256(_config.schema)] * _config.percentage) / MAX_PERCENT ``` The division operator could lead to rounding errors, and hence, it is possible that the total transfer amount might not add up to the exactly expected `totalBalance`. These leftover ERC20 tokens will be returned to the `assetsReserves` and reallocated at the next round, but potentially under a different allocation scheme. Hence, the users entitled to them might not receive them in the next allocation.
Open the reportFor the core pool, access to the `vToken._reduceReserves()` function is controlled by the `AccessControlManager`, which checks whether the caller is authorized to invoke this function. However, in the case of `vBNB`, the `vBNBAdmin.reduceReserves()` function operates without access control. This means that anyone can call the `reduceReserves()` function, which triggers the `vBNBAdmin` contract to call `vBNB._reduceReserves()`. This inconsistency between `vBNB` and the other core pool vTokens could potentially lead to different behaviors in their operations.
Open the reportThe idea of the `ProtocolShareReserve` contract is to accumulate income from ERC20 tokens and distribute it in the expected patterns between different addresses. It is not expected for the contract to receive native tokens, hence the absence of `payable` functions or the `receive()` method. The EVM currently still supports the `SELFDESTRUCT` opcode. It is used to terminate a contract, remove the bytecode from the blockchain, and send any contract funds to a specified address. This transfer cannot be rejected, so if another contract self-destructs and points to `ProtocolShareReserve` as the recipient, then the contract will be funded from an unexpected source. This issue is also applicable to the `vBNBAdmin` contract, as the specifications of the contract state that BNB tokens should only be received from the associated `vBNB` contract. Currently, the `receive()` function is protected with a required statement that only accepts funds from the `vBNB` contract. But with the issue described above, it is still possible to receive native tokens not from the expected source.
Open the reportDuring the `addOrUpdateDistributionConfigs()` function, it is possible to add a distribution config with a zero percentage. This would mean that no funds would be sent to that address, but the address would still be iterated over, hence wasting gas.
Open the reportThese contracts are using solidity version `0.8.13`. While no high-severity issues are present in that version, it is currently recommended to use version `0.8.18`.
Open the reportThe `VBNBAdmin` contract implements a `fallback()` function that is only callable by the admin, and this function forwards the call to the `vBNB` contract. This design allows the admin to call any function on the `vBNB` contract as needed. However, if the call to the `vBNB` contract results in a BNB transfer back to the `VBNBAdmin` contract, no mechanism is currently in place to retrieve the received BNB specifically. These BNB will be considered token reserves and forwarded to the `ProtocolShareReserve` contract when the `reduceReserves()` is called.
Open the reportThe `_ensureMaxLoops()` check is only used once in `addOrUpdateDistributionConfigs()` to limit the number of iterations through the distribution targets. It is not used to check other loops in the contract, such as the number of configs being added or updated, the number of assets used in `releaseFunds()`, the number of markets in `_accrueAndReleaseFundsToPrime()` and `_accruePrimeInterest()`, and the number of schemas or distribution targets in `_releaseFund()`. The absence of these checks could lead to a potential gas issue and the transaction failing.
Open the reportIn the `ProtocolShareReserve._releaseFund()` function, funds are first transferred to destinations, followed by the call of the `updateAssetsState()` function on the destination contracts. Then, the `assetsReserves` and `totalAssetReserve` state variables are updated. Note that since the state changes are applied after the external calls, the read of the contract state during the external call will be inaccurate. It is important to note that the state changes are applied after the external calls. This means that during the external call of the `_releaseFund()` function if any destination contract (or a contract subsequently called by the destination contract during `updateAssetsState()`) reads the `assetsReserves` or `totalAssetReserve` state variables or invokes the `getUnreleasedFunds()` function, it will receive inaccurate values. These inaccuracies can potentially lead to accounting issues within the calling contracts.
Open the reportIn both `VToken` contracts, the values for `reduceReservesBlockDelta` and `protocolShareReserve` are not set during initialization. This could cause unintended behavior at the time of deployment.
Open the reportThe core pool's `VToken._reduceReservesFresh()` function can return a non-zero error code to indicate any failures during its execution. However, in the `accrueInterest()` function, when `_reduceReservesFresh()` is called, the returned error code is unhandled. Therefore, even though an error occurs during the execution of `reduceReservesFresh()`, the `accrueInterest()` function continues to execute, returning a `uint(Error.NO_ERROR)` to indicate that no errors have occurred. External contracts may incorrectly assume that `_reduceReservesFresh()` has executed successfully based on the `Error.NO_ERROR` return value.
Open the reportSome of the contracts audited are upgradeable, and their functionality may be changed by privileged roles in the future.
Open the reportThe report gives no description here.
Open the reportThe report gives no description here.
Open the reportThe function `_checkForceVAILiquidate()` initiates by verifying whether the VAI liquidation is currently paused. If a pause is detected, it bypasses the subsequent checks and continues with the rest of the execution. However, when the `vToken` is identified as the `VAI` token, the `liquidateBorrow()` function neglects to confirm the pausing status of the `vaiController` prior to attempting the liquidation. This oversight may potentially cause a revert within the `vaiController` contract, as confirmed by the Venus team. However, the Liquidator contract has the capability to manage this situation before invoking the `vaiController` contract.
Open the reportThe `_reduceReservesInternal()` function may not carry out its logic in a liquidation event in case `pendingRedeemChunkLength`remains unset (defaulted to zero). It does not matter how many items are in `pendingRedeem`, as long as `pendingRedeemChunkLength`remains unset, the `range` and, therefore, the ìndex` variables will be set to zero and the loop will be skipped.
Open the reportThe `comptroller.liquidationIncentiveMantissa()` is used to determine the amount of collateral to be seized based on the amount of debt that the liquidator wants to pay, and to limit the maximum percentage value that the `treasuryPercentMantissa` can have. If the `liquidationIncentiveMantissa` gets reduced under the value of the `treasuryPercentMantissa` without updating the latter, it would lead to the protocol getting more collateral than what should be.
Open the reportThe `forceVAILiquidate` variable enforces the protocol to check the debt amount of `VAI` token. If it is greater than the minimum threshold (`minLiquidatableVAI`), then it should revert the liquidation procedure unless the liquidated token is the `VAI` token. Additionally, the documentation states that the protocol will enforce the liquidation of `VAI` first over other tokens. However, the `forceVAILiquidate` variable is not set during the initialization step and will default to a paused state, which would essentially bypass the need to validate the `VAI` debt.
Open the reportAs security standards develop, so does the Solidity language. In order to stay up to date with current practices, it's important to use a recent version of Solidity and recent conventions. The version used in all the contracts in scope, `0.8.13`, is known to have vulnerabilities, for more information [see here](https://github.com/ethereum/solidity/blob/11be2f4881d1644f15b026d1022388f06a034a34/docs/bugs_by_version.json#L1738C9-L1738C9).
Open the reportPrivileged users can call `withdrawBep20()` to transfer any amount of any token held by the contract to an address of their choice. While this is useful for transferring any unsupported tokens sent to the contract, this allows any privileged user to drain the contract of its entire VRT balance.
In 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. There is currently no event indicating an update of `VAIVault`'s pending rewards.
The report gives no description here.
The report gives no description here.
Smart contracts will often have `owner` variables to designate the person with special privileges to make modifications to the smart contract. Certain contracts have state variables, e.g. `owner`, which provide certain addresses with privileged roles. Such roles may pose a risk to end-users. Contracts `VAIVault.sol`, `VRTVault.sol` and `XVSVault.sol` had their access controls setup via the two custom contracts (which themselves were however out-of-scope for this audt) [`AccessControlledV5.sol`](https://github.com/VenusProtocol/governance-contracts/blob/main/contracts/Governance/AccessControlledV5.sol) and [`AccessControlManager.sol`](https://github.com/VenusProtocol/governance-contracts/blob/main/contracts/Governance/AccessControlManager.sol): - Access is checked via function `AccessControlledV5._checkAccessAccessControlledV5Allowed()`, having a string of the function signature of the to-be-checked function as the parameter. - Access is granted via `AccessControlManager.giveCallPermission()`, where addresses are granted access to specific function signature strings (and if non-zero, tied to specific contract addresses). - Access is revoked via `AccessControlManager.revokeCallPermission()`, where addresses are revoked access to specific function signature strings (and if non-zero, tied to specific contract addresses). The `XVSVault.sol` contract contains the following privileged roles: 1. An administrator (`admin`, `onlyAdmin()` modifier), as initialized during the constructor execution to `msg.sender`: 1. Renounce the role (**and thereby preventing any future calls to the followingly listed funcitons!**) by calling `burnAdmin()`. 1. Change the XVS store contract address (`xvsStore`) that controls reward token transfers by calling `setXvsStore()`. 1. Change the access control manager implementation address (and thereby indirectly all `_checkAccessAllowed()`-controlled access checks) by calling `setAccessControl()`. The following functions in this contract are protected through `_checkAccessAllowed()` with their corresponding function signatures: 1. `"pause()"`: Pause the contract (and thereby all calls to `deposit()`, `claim()`, `executeWithdrawal()`, `requestWithdrawal()`, `updatePool()`, `delegate()` and `delegateBySig()`) by calling `pause()`. 1. `"resume()"`: Resume the contract from a paused state by calling `resume()`. 1. `"add(address,uint256,address,uint256,uint256)"`: Add a new pool by calling `add()`. 1. Using an existing reward token, but a new pool token allows the caller to globally update the `rewardTokenAmountsPerBlock[]` variable for all pools using this reward token, **without explicitly calling `setRewardAmountPerBlock()`**. In the extreme cases this could either drain the reward token (by choosing a high reward value) or halt any future rewards (by choosing zero). 1. **Accidentally or with malicious intent the caller could add enough new pools to make calling `massUpdatePools()` prohibitively gas-expensive and thereby denial the service to the following functions**: 1. `add()`. 1. `set()`. 1. `setRewardAmountPerBlock()`. 1. `"set(address,uint256,uint256)"`: Update the accumulated rewards per share for all pools and change the allocation point of a pool by calling `set()`. 1. `"setRewardAmountPerBlock(address,uint256)"`: Update the accumulated rewards per share for all pools and arbitrarily change the reward amount per block by calling `setRewardAmountPerBlock()`. 1. **By setting it to zero no additional rewards would be accumulated.** 1. **By setting it to an unusually high value the reward token could be drained.** 1. `"setWithdrawalLockingPeriod(address,uint256,uint256)"`: Change the lockup period for withdrawing deposited funds by calling `setWithdrawalLockingPeriod()`. 1. **Funds could be held indefinitely by choosing high lockup times and/or always extending an existing period.** The `XVSStore.sol` contract contains the following privileged roles: 1. An administrator (`admin`, `onlyAdmin()` modifier), as initialized during the constructor execution to `msg.sender`: 1. Transfer the role to another address (which has to accept it by calling `acceptAdmin()`) by calling `setPendingAdmin()`. 1. Designate a new owner role by calling `setNewOwner()`. 1. Add/remove token addresses from the `rewardTokens[]` array by calling `setRewardToken()`. 1. An owner (`owner`, `onlyOwner()` modifier), as set through `setNewOwner()` by `admin`: 1. Transfer an arbitrary amount of reward tokens by calling `safeRewardTransfer()`. 1. **A compromised owner could drain all funds from this contract by**: 1. Adding a to-be-drained token to the `rewardTokens[]` array, if not already a reward token. 1. Transfer an arbitrary amount by calling `safeRewardTransfer()`. 1. **A comrpomised admin could drain all funds from this contract by**: 1. Adding a to-be-drained token to the `rewardTokens[]` array, if not already a rewar
Every 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".
The contracts are compiled under Solidity `v0.5.16`, which is considered outdated and may contain known compiler bugs that can affect the logic of the contracts. The use of experimental features, e.g., `pragma experimental ABIEncoderV2;` should also be avoided since they were not tested thoroughly at the time of the outdated Solidity versions and [several known bugs](https://blog.ethereum.org/2019/03/26/solidity-optimizer-and-abiencoderv2-bug/) existed in the past.
The [VAI token is an algorithmic stable coin](https://github.com/VenusProtocol/venus-protocol/#venus-protocol) and pegged to 1 USD, similar to [MakerDAO's DAI](https://developer.makerdao.com/dai/1/). However, it is not guaranteed to always hold its peg to 1 USD under all circumstances. Should the token lose its peg and be (much) less valuable than 1 USD it could lead to a high increase of users depositing their `VAI` tokens in the `VAIVault` and earning `XVS` tokens as interest at a (high) discount. This could lead to an unexpectedly quick depletion of `XVS` funds in the `VAIVault` and in turn destabilize the `XVS` token value and all its associated pools in the `XVSVault`.
Although the privileged roles can pause the deposit and withdrawals on the VRT vault, users still earn the interests during the pausing period since the interests are calculated block-by-block until the last accruing block. Users can claim their interest during this period when the vault is unpaused.
Attached through a shared contract address. Our incident data ends in Oct 2026.
5M.
Source: De.Fi incident database.
Source: De.Fi incident database.
| Address | Chain | Source code |
|---|---|---|
| 0xd3cc…894a0xd3cc9d8f3689b83c91b7b59cab4946b063eb894a | Chain: Ethereum | Source code: Verified source |
| 0x67c5…62a70x67c549a18abfad127b13f8d56738f43a21bb62a7 | Chain: BNB Chain | Source code: Verified source |
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="venus-protocol"></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