Platform: Code4rena
Start Date: 28/09/2023
Pot Size: $36,500 USDC
Total HM: 5
Participants: 115
Period: 6 days
Judge: 0xDjango
Total Solo HM: 1
Id: 290
League: ETH
Rank: 26/115
Findings: 2
Award: $202.85
π Selected for report: 0
π Solo Findings: 0
π Selected for report: 0xDetermination
Also found by: DeFiHackLabs, Norah, Pessimistic, PwnStars, SpicyMeatball, Testerbot, ThreeSigma, bin2chen, blutorque, deadrxsezzz, dirk_y, ether_sky, hals, neumo, rokinot, said, seerether, turvy_fuzz
198.4834 USDC - $198.48
https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L331 https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L397
In issue() and claim() to mint new prime token, I don't see totalScoreUpdatesRequired and pendingScoreUpdates are updated. it might cause function performing updateScores(address[] memory users) always revert.
totalScoreUpdatesRequired and pendingScoreUpdates only updated when perform updateAlpha(), updateMultipliers() and addMarket().
Scenario 1:
Scenario 2:
function updateScores(address[] memory users) external { if (pendingScoreUpdates == 0) revert NoScoreUpdatesRequired(); //@audit-issue revert if (nextScoreUpdateRoundId == 0) revert NoScoreUpdatesRequired(); for (uint256 i = 0; i < users.length; ) { address user = users[i]; if (!tokens[user].exists) revert UserHasNoPrimeToken(); if (isScoreUpdated[nextScoreUpdateRoundId][user]) continue; address[] storage _allMarkets = allMarkets; for (uint256 j = 0; j < _allMarkets.length; ) { address market = _allMarkets[j]; _executeBoost(user, market); _updateScore(user, market); unchecked { j++; } } pendingScoreUpdates--; //audit-issue underflow isScoreUpdated[nextScoreUpdateRoundId][user] = true; unchecked { i++; } emit UserScoreUpdated(user); } }
VSC
When claim and issue should keep totalScoreUpdatesRequired and pendingScoreUpdates are updated.
DoS
#0 - c4-pre-sort
2023-10-06T20:48:48Z
0xRobocop marked the issue as low quality report
#1 - c4-pre-sort
2023-10-06T20:49:01Z
0xRobocop marked the issue as duplicate of #555
#2 - c4-judge
2023-11-02T02:01:04Z
fatherGoose1 marked the issue as satisfactory
#3 - c4-judge
2023-11-05T00:52:22Z
fatherGoose1 changed the severity to 3 (High Risk)
π Selected for report: Bauchibred
Also found by: 0x3b, 0xDetermination, 0xMosh, 0xScourgedev, 0xTheC0der, 0xTiwa, 0xWaitress, 0xdice91, 0xfusion, 0xpiken, 0xprinc, 0xweb3boy, ArmedGoose, Aymen0909, Breeje, Brenzee, Daniel526, DavidGiladi, DeFiHackLabs, Flora, Fulum, HChang26, Hama, IceBear, J4X, Krace, KrisApostolov, Maroutis, Mirror, MohammedRizwan, Norah, PwnStars, SPYBOY, TangYuanShen, Testerbot, ThreeSigma, Tricko, al88nsk, alexweb3, ast3ros, berlin-101, bin2chen, blutorque, btk, d3e4, deth, e0d1n, ether_sky, ge6a, gkrastenov, glcanvas, hals, imare, inzinko, jkoppel, jnforja, joaovwfreire, josephdara, kutugu, lotux, lsaudit, mahdirostami, merlin, n1punp, nadin, neumo, nisedo, nobody2018, oakcobalt, orion, peanuts, pep7siup, pina, ptsanev, rokinot, rvierdiiev, said, santipu_, sashik_eth, seerether, squeaky_cactus, terrancrypt, tonisives, twicek, vagrant, xAriextz, y4y
4.3669 USDC - $4.37
The addMarket function can only be executed with admin permissions. Using _ensureMaxLoops to limit the maximum number of markets doesn't make much sense, especially since prime.sol
doesn't have a setMaxLoopsLimit. If _ensureMaxLoops is set to 10, it means there can only ever be 10 markets.
https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L109
constructor(address _wbnb, address _vbnb, uint256 _blocksPerYear) { if (_wbnb == address(0)) revert InvalidAddress(); if (_vbnb == address(0)) revert InvalidAddress(); if (_blocksPerYear == 0) revert InvalidBlocksPerYear(); WBNB = _wbnb; VBNB = _vbnb; BLOCKS_PER_YEAR = _blocksPerYear;
Same issue reported last time : https://github.com/code-423n4/2023-05-venus-findings/issues/178
Recommnendation:
Add setMaxLoopsLimit function
function setMaxLoopsLimit(uint256 limit) external onlyOwner { _setMaxLoopsLimit(limit); }
Venus Protocol is deployed on the BNB chain, which has a block-time of 15 seconds. Previously, the blocksPerYear
constant was set without validation. To accurately reflect the number of blocks per year on the BNB chain, the blocksPerYear
constant should be fixed. The correct value is 1,051,200.
https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L109
constructor(address _wbnb, address _vbnb, uint256 _blocksPerYear) { if (_wbnb == address(0)) revert InvalidAddress(); if (_vbnb == address(0)) revert InvalidAddress(); if (_blocksPerYear == 0) revert InvalidBlocksPerYear(); WBNB = _wbnb; VBNB = _vbnb; BLOCKS_PER_YEAR = _blocksPerYear; // Note that the contract is upgradeable. Use initialize() or reinitializers // to set the state variables. _disableInitializers(); }
Recommnendation:
Validate the correct blocksPerYear
isΒ 10512000
All contracts are using 0.8.13. Consider updating to the latest version 0.8.21 to ensure the compiler contains the latest security fixes.
Storage Write Removal Bug On Conditional Early Termination The bug was introduced in version 0.8.13 and Solidity version 0.8.17, released on September 08, 2022, provides a fix. The bug is significantly easier to trigger with optimized via-IR code generation, but can theoretically also occur in optimized legacy code generation.
REF: https://soliditylang.org/blog/2022/09/08/storage-write-removal-before-conditional-termination/
// SPDX-License-Identifier: BSD-3-Clause pragma solidity 0.8.13;
Recommnendation:
updating to the latest version 0.8.21
Currently use OZ 4.8.3
This version has 6 medium issues CVE-2023-40014 CVE-2023-34459 CVE-2023-34234 CVE-2023-40014 CVE-2023-34459 CVE-2023-34234
REF: https://security.snyk.io/package/npm/@openzeppelin%2Fcontracts/4.8.3
@openzeppelin/contracts": "^4.8.3",
Recommnendation:
Upgrade to version 4.9.3 or higher.
There is no implementation for fund transfers while paused in protocolShareReserve.sol
. However, PrimeLiquidityProvider
has a pause/unpause modifier. Yet, I don't see any checks for pause status in _claimInterest()
. If FundsTransferIsPaused
is set to paused, claimInterest
will always revert.
function _claimInterest(address vToken, address user) internal returns (uint256) { uint256 amount = getInterestAccrued(vToken, user); amount += interests[vToken][user].accrued; interests[vToken][user].rewardIndex = markets[vToken].rewardIndex; interests[vToken][user].accrued = 0; address underlying = _getUnderlying(vToken); IERC20Upgradeable asset = IERC20Upgradeable(underlying); if (amount > asset.balanceOf(address(this))) { address[] memory assets = new address[](1); assets[0] = address(asset); IProtocolShareReserve(protocolShareReserve).releaseFunds(comptroller, assets); if (amount > asset.balanceOf(address(this))) { IPrimeLiquidityProvider(primeLiquidityProvider).releaseFunds(address(asset)); unreleasedPLPIncome[underlying] = 0; } }
function releaseFunds(address token_) external { if (msg.sender != prime) revert InvalidCaller(); if (paused()) { revert FundsTransferIsPaused(); }
Recommnendation:
Check the pause status before performing an action.
In initialize and constructor only check zero address, It is recommended to add a "checkContract" function to verify the integrity of the contract code during the deployment process. This can help prevent attacks that aim to exploit vulnerabilities in the contract's code or its dependencies. By including an integrity check, the contract can ensure that it is running as intended and that it has not been tampered with or modified in any way.
https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L130
constructor(address _wbnb, address _vbnb, uint256 _blocksPerYear) { if (_wbnb == address(0)) revert InvalidAddress(); if (_vbnb == address(0)) revert InvalidAddress(); if (_blocksPerYear == 0) revert InvalidBlocksPerYear(); WBNB = _wbnb; VBNB = _vbnb; BLOCKS_PER_YEAR = _blocksPerYear; // Note that the contract is upgradeable. Use initialize() or reinitializers // to set the state variables. _disableInitializers(); } function initialize( address _xvsVault, address _xvsVaultRewardToken, uint256 _xvsVaultPoolId, uint128 _alphaNumerator, uint128 _alphaDenominator, address _accessControlManager, address _protocolShareReserve, address _primeLiquidityProvider, address _comptroller, address _oracle, uint256 _loopsLimit ) external virtual initializer { if (_xvsVault == address(0)) revert InvalidAddress(); if (_xvsVaultRewardToken == address(0)) revert InvalidAddress(); if (_protocolShareReserve == address(0)) revert InvalidAddress(); if (_comptroller == address(0)) revert InvalidAddress(); if (_oracle == address(0)) revert InvalidAddress(); if (_primeLiquidityProvider == address(0)) revert InvalidAddress(); _checkAlphaArguments(_alphaNumerator, _alphaDenominator); alphaNumerator = _alphaNumerator; alphaDenominator = _alphaDenominator; xvsVaultRewardToken = _xvsVaultRewardToken; xvsVaultPoolId = _xvsVaultPoolId; xvsVault = _xvsVault; nextScoreUpdateRoundId = 0; protocolShareReserve = _protocolShareReserve; primeLiquidityProvider = _primeLiquidityProvider; comptroller = _comptroller; oracle = ResilientOracleInterface(_oracle); __AccessControlled_init(_accessControlManager); __Pausable_init(); _setMaxLoopsLimit(_loopsLimit); _pause(); }
function initialize( address accessControlManager_, address[] calldata tokens_, uint256[] calldata distributionSpeeds_ ) external initializer { __AccessControlled_init(accessControlManager_); __Pausable_init(); uint256 numTokens = tokens_.length; if (numTokens != distributionSpeeds_.length) { revert InvalidArguments(); } for (uint256 i; i < numTokens; ) { _initializeToken(tokens_[i]); _setTokenDistributionSpeed(tokens_[i], distributionSpeeds_[i]); unchecked { ++i; } } }
Recommnendation:
It is recommended to add a "checkContract" function.
Example:
It is recommended to add a "checkContract" function.Example:
Description:
In Prime.sol
, the calculateAPR/estimateAPR
function relies on fetching prices from the ResilientOracle
to perform calculations. While this approach is generally effective, it presents a potential issue related to the ResilientOracle
's pausing or unavailability. If the oracle used in calculateAPR/estimateAPR
is paused or experiences downtime, the function may not execute as expected, leading to inaccurate or incomplete results. This poses a risk to the contract's reliability and functionality.
Recommendation:
The ResilientOracle
itself relies on multiple sources to fetch prices and has a fallback mechanism built into it. The ability to pause or unpause the oracle is under the control of the protocol's owners. Users must trust the protocol and hope that the protocol owners act responsibly to ensure the system's integrity. It's essential for protocol owners to maintain transparency and keep users informed about any actions related to the oracle to maintain user trust.
burn
the Prime SoulBound Token of any userDescription:
Prime Protocol includes a feature where users are rewarded with Prime SoulBound tokens when they stake 1000 XVS tokens for 90 days. While this incentive system is designed to encourage long-term participation, there is a potential centralization risk associated with the burn
function.
The burn
function allows the protocol owner to burn Prime SoulBound tokens, even if the user acquired them by fulfilling the staking requirement. This centralization of power in the hands of the protocol owner poses a risk to the fairness and trustworthiness of the protocol.
The test case titled manually burn irrevocable token
demonstrates the issue.
Recommendation:
Users must trust the protocol and hope that the protocol owners act responsibly to ensure the system's integrity. It's essential for protocol owners to maintain transparency and keep users informed about any actions related to the oracle to maintain user trust.
"manually burn irrevocable token"
#0 - c4-pre-sort
2023-10-07T02:04:25Z
0xRobocop marked the issue as low quality report