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: 6/115
Findings: 4
Award: $853.09
π Selected for report: 0
π Solo Findings: 0
π Selected for report: Brenzee
Also found by: 0xDetermination, 0xTheC0der, Breeje, SpicyMeatball, Testerbot, ast3ros, ether_sky, pep7siup, santipu_, sces60107, tapir
657.0509 USDC - $657.05
https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L661
Scores are getting incorrectly calculated.
Scores are the critical part of Prime
codebase as Scores are directly correlated to the interest users can claim.
As per the Natspec in Scores.sol
:
14: * @param xvs amount of xvs (xvs, 1e18 decimal places) 15: * @param capital amount of capital (1e18 decimal places)
The xvs
and capital
passed in calculateScore
function must have 18 decimal places.
Now let's focus on _calculateScore
function in Prime.sol
. This internal function calculates the score based on market
and user
and returns the actual score
.
function _calculateScore(address market, address user) internal returns (uint256) { uint256 xvsBalanceForScore = _xvsBalanceForScore(_xvsBalanceOfUser(user)); IVToken vToken = IVToken(market); uint256 borrow = vToken.borrowBalanceStored(user); uint256 exchangeRate = vToken.exchangeRateStored(); uint256 balanceOfAccount = vToken.balanceOf(user); uint256 supply = (exchangeRate * balanceOfAccount) / EXP_SCALE; address xvsToken = IXVSVault(xvsVault).xvsAddress(); oracle.updateAssetPrice(xvsToken); oracle.updatePrice(market); (uint256 capital, , ) = _capitalForScore(xvsBalanceForScore, borrow, supply, market); capital = capital * (10 ** (18 - vToken.decimals())); return Scores.calculateScore(xvsBalanceForScore, capital, alphaNumerator, alphaDenominator); }
Now let's understand decimals precision of each variable in this function:
xvsBalanceForScore
has the decimals of xvs token which is 18 decimals.
borrow
has the number of decimals of the underlying token (for example 18 for USDT and 6 for TRX).
balanceOfAccount
has the number of decimals of the VTokens (every VToken in Venus has 8 decimals now)
exchangeRate
has the decimals of the underlying token + 10 (for example, 28 for USDT and 16 for TRX).
supply
which is (exchangeRate * balanceOfAccount) / EXP_SCALE
will have always the decimals of the underlying token.
Now there is a call on _capitalForScore
to calculate the capital required for calculation of score.
function _capitalForScore( uint256 xvs, uint256 borrow, uint256 supply, address market ) internal view returns (uint256, uint256, uint256) { address xvsToken = IXVSVault(xvsVault).xvsAddress(); uint256 xvsPrice = oracle.getPrice(xvsToken) uint256 borrowCapUSD = (xvsPrice * ((xvs * markets[market].borrowMultiplier) / EXP_SCALE)) / EXP_SCALE; uint256 supplyCapUSD = (xvsPrice * ((xvs * markets[market].supplyMultiplier) / EXP_SCALE)) / EXP_SCALE; uint256 tokenPrice = oracle.getUnderlyingPrice(market); uint256 supplyUSD = (tokenPrice * supply) / EXP_SCALE uint256 borrowUSD = (tokenPrice * borrow) / EXP_SCALE; if (supplyUSD >= supplyCapUSD) { supply = supplyUSD > 0 ? (supply * supplyCapUSD) / supplyUSD : 0; } if (borrowUSD >= borrowCapUSD) { borrow = borrowUSD > 0 ? (borrow * borrowCapUSD) / borrowUSD : 0; } return ((supply + borrow), supply, borrow); }
Here, the response of the ResilientOracle
has 36 - the decimals of the underlying token (for example 18 for USDT and 30 for TRX). This way (tokenPrice * supply) / EXP_SCALE
will have always 18 decimals (same for the borrow).
As supply
and borrow
both have same decimals as underlying token, the return value of capital
will also have same decimals of underlying token.
Now focus on this last 3 lines of _calculateScore
:
(uint256 capital, , ) = _capitalForScore(xvsBalanceForScore, borrow, supply, market); @-> capital = capital * (10 ** (18 - vToken.decimals())); return Scores.calculateScore(xvsBalanceForScore, capital, alphaNumerator, alphaDenominator);
In line above (@->), capital which has decimals of underlying token is multiplied by:
As shown earlier, capital
passed in Scores.calculateScore
must have 18 decimals But the decimal of capital passed will be:
For underlying tokens having 18 Decimals (USDT): 18 + 10 = 28 Decimals. For underlying tokens having 6 Decimals (TRX): 6 + 10 = 16 Decimals.
This means the value of xvs
will have 18 decimal precision but capital
will have precision ranging from 16 to 28 decimals depending on underlying token.
This will lead to incorrect calculation of Scores.
VS Code
Update the codebase in such a way that you subtract decimal of underlying token instead of vToken.decimals()
.
function _calculateScore(address market, address user) internal returns (uint256) { uint256 xvsBalanceForScore = _xvsBalanceForScore(_xvsBalanceOfUser(user)); IVToken vToken = IVToken(market); uint256 borrow = vToken.borrowBalanceStored(user); uint256 exchangeRate = vToken.exchangeRateStored(); uint256 balanceOfAccount = vToken.balanceOf(user); uint256 supply = (exchangeRate * balanceOfAccount) / EXP_SCALE; address xvsToken = IXVSVault(xvsVault).xvsAddress(); oracle.updateAssetPrice(xvsToken); oracle.updatePrice(market); (uint256 capital, , ) = _capitalForScore(xvsBalanceForScore, borrow, supply, market); - capital = capital * (10 ** (18 - vToken.decimals())); + capital = capital * (10 ** (18 - decimalOfUnderlyingToken)); return Scores.calculateScore(xvsBalanceForScore, capital, alphaNumerator, alphaDenominator); }
Error
#0 - c4-pre-sort
2023-10-04T23:27:36Z
0xRobocop marked the issue as duplicate of #588
#1 - c4-judge
2023-11-01T16:21:44Z
fatherGoose1 marked the issue as satisfactory
π Selected for report: 0xDetermination
Also found by: 0xblackskull, 0xweb3boy, ADM, Breeje, Pessimistic, PwnStars, SBSecurity, Satyam_Sharma, ThreeSigma, al88nsk, blutorque, debo, dethera, maanas, neumo, oakcobalt, pina, said, sces60107, tapir, tsvetanovv, xAriextz
32.2731 USDC - $32.27
https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L208
As per the Readme Doc inside Prime Folder:
Market multipliers and alpha can be updated at anytime and need to be propagated to all users. Changes will be gradually applied to users as they borrow/supply assets and their individual scores are recalculated. This strategy has limitations because the scores will be wrong in aggregate.
To mitigate this issue, Venus will supply a script that will use the permission-less function updateScores to update the scores of all users. This script wonβt pause the market or Prime contract. Scores need to be updated in multiple transactions because it will run out of gas trying to update all scores in 1 transaction.
Venus is going to use a script to call updateScores
to update scores for all the users.
Consider the following Situation now:
updateScores
function to update her score for round x
.File: Prime.sol function updateScores(address[] memory users) external { *** for (uint256 i = 0; i < users.length; ) { address user = users[i]; if (!tokens[user].exists) revert UserHasNoPrimeToken(); @-> if (isScoreUpdated[nextScoreUpdateRoundId][user]) continue; // @audit Infinite Loop *** @-> isScoreUpdated[nextScoreUpdateRoundId][user] = true; unchecked { @-> i++; } emit UserScoreUpdated(user); } }
isScoreUpdated
status is set to true
.if (isScoreUpdated[nextScoreUpdateRoundId][user]) continue;
. The idea is not to update scores again if they've already been updated.i++
is used with the unchecked
keyword at the end. But because of the continue
keyword, i++
is effectively skipped, resulting in the same user being processed repeatedly, creating an infinite loop.In simple terms, the code tries to be efficient with gas usage but makes a mistake. Instead of increasing the counter i
within the for
loop itself, it's done later with unchecked
to save gas. However, it ends up getting stuck in an infinite loop for some users like Alice.
VS Code
Update the code as following:
File: Prime.sol function updateScores(address[] memory users) external { *** for (uint256 i = 0; i < users.length; ) { address user = users[i]; if (!tokens[user].exists) revert UserHasNoPrimeToken(); - if (isScoreUpdated[nextScoreUpdateRoundId][user]) continue; + if (isScoreUpdated[nextScoreUpdateRoundId][user]) { + unchecked { + i++; + } + continue; + } *** unchecked { i++; } emit UserScoreUpdated(user); } }
Loop
#0 - c4-pre-sort
2023-10-06T01:51:41Z
0xRobocop marked the issue as duplicate of #556
#1 - c4-judge
2023-11-02T00:50:11Z
fatherGoose1 marked the issue as satisfactory
π 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
Count | Explanation |
---|---|
[L-01] | Solidity Version 0.8.13 used is vulnerable to optimizer issues |
[L-02] | No functionality to change maxLoopsLimit can be problematic in long term |
[L-03] | issue doesn't check the 90 days staking condition to issue Prime Token |
[L-04] | Adding Multiple Markets with same underlying Tokens can be problematic |
Total Low Risk Issues | 4 |
---|
The solidity version 0.8.13 has below two issues:
Use recent Solidity version or atleast above 0.8.15 version which has the fix for these issues.
maxLoopsLimit
can be problematic in long termIn initialize
function of Prime.sol
contract, maxLoopsLimit
is set to _loopsLimit
value through calling function _setMaxLoopsLimit
.
File: Prime.sol `initialize` function: 164: _setMaxLoopsLimit(_loopsLimit);
Issue here is that _setMaxLoopsLimit
function is internal
access function which cannot be called from outside and there is no other method in the contract which uses this function to change the value of maxLoopsLimit
again in future to make sure no DoS issues happens.
Not having this access can lead to issues in future. So it is recommended to add a function that allows priviledged account to update this value.
issue
doesn't check the 90 days staking condition to issue Prime Tokenissue
is a priviledged function used to Directly issue prime tokens to users.
But in implementation, the condition is not checked to make sure that user has staked the xvs tokens for 90 Days. This can lead to issuing of tokens even if user fails to satisfy the condition required to get the token.
File: Prime.sol function issue(bool isIrrevocable, address[] calldata users) external { *** for (uint256 i = 0; i < users.length; ) { _mint(false, users[i]); _initializeMarkets(users[i]); delete stakedAt[users[i]]; // @audit no check for 90 Days staking condition. unchecked { i++; } *** }
addMarket
function in Prime
contract is used to Add a market to prime program.
Also there is a possibility that multiple markets have same underlying tokens.
If case it happens, the line below will overwrite any existing market.
File: Prime.sol function addMarket(address vToken, uint256 supplyMultiplier, uint256 borrowMultiplier) external { *** @-> vTokenForAsset[_getUnderlying(vToken)] = vToken; *** }
This can lead to issues as there will now be 2 markets in existance (markets[vToken].exists = true
) with same underlying token but vTokenForAsset
will only point to new market added.
#0 - 0xRobocop
2023-10-07T15:15:12Z
L-03 dup of #485
#1 - c4-pre-sort
2023-10-07T15:15:23Z
0xRobocop marked the issue as low quality report
#2 - c4-judge
2023-11-03T02:06:20Z
fatherGoose1 marked the issue as grade-b
π Selected for report: 3agle
Also found by: 0x3b, 0xDetermination, 0xweb3boy, ArmedGoose, Bauchibred, Breeje, J4X, hunter_w3b, jamshed, kaveyjoe, oakcobalt, radev_sw, versiyonbir
159.397 USDC - $159.40
Count | Topic |
---|---|
1 | Introduction |
2 | High Level Contract Flow |
3 | Audit Approach |
4 | Codebase Quality |
5 | Architecture Recommendation |
6 | Centralization Risk |
7 | Systemic Risks |
8 | Code Review Insights |
9 | Time Spent |
This technical analysis report examines the security aspects of Venus Prime, a vital component of the Venus Protocol's incentive program. The analysis explores the underlying smart contracts and different components, including reward distribution calculations, claiming mechanism, and unique Soulbound Tokens, while identifying security insights and potential vulnerabilities within the Venus Prime contracts.
Initial Scope and Documentation Review: Thoroughly examined the Contest Readme File and Venus Prime Readme file to understand the contract's objectives and functionalities.
High-level Architecture Understanding: Performed an initial architecture review of the codebase by skimming through all files without exploring function details.
Test Environment Setup and Analysis: Set up a test environment and executed all tests.
Comprehensive Code Review: Conducted a line-by-line code review focusing on understanding code functionalities.
Report Writing: Write Report by compiling all the insights I gained throughout the line by line code review.
Codebase Quality Categories | Comments |
---|---|
Unit Testing | The codebase has undergone extensive testing. |
Code Comments | In general, the code had comments for what each function does, which is great. However, it could be better if we also added comments for the custom data types (struct) used in the code. This would help make it clearer how each data type is used in the contract. |
Documentation | Prime Documentation was to the point, containing all the major details about how the contract is expected to perform. |
Organization | Code orgranization was great for sure. The flow from one contract was perfectly smooth. |
Block.number Vs Block.timestamp
: Since the contracts are planned for deployment on multiple chains, including those like Optimism where block.number
may not accurately represent time, it is advisable to use block.timestamp
consistently. In the PrimeLiquidityProvider
(PLP) contract, variations in average block time can lead to fluctuations in the rate at which tokens accrue.
Solidity Version Used
: Consider upgrading the Solidity version from 0.8.13 to 0.8.15 or a higher version. Although the current version does not impact any contracts within scope, it's a important to avoid any potential issues which 0.8.13 version brings related to ABI-encoding and optimizer bugs regarding memory side effects of inline assembly.
Handling Multiple Markets with Same Underlying Token
: Address the limitation in the Prime contract, which currently cannot handle multiple markets with the same underlying token. This issue could have long-term implications, especially if multiple markets with identical underlying tokens need to be added to the Prime
program simultaneously. It's recommended to enhance the contract's ability to handle such scenarios for improved flexibility and scalability.
Token Sweep Power
: The owner has the authority to sweep all tokens held in the PrimeLiquidityProvider contract. This centralized control could potentially pose serious risk.
Claim Functionality Pause
: The owner retains the capability to temporarily "pause" the claim functionality whenever necessary. This centralized ability to halt operations could affect user access and functionality.
Privileged Role Impact
: Privileged roles have the ability to modify the values of alpha
and multiplier
, which can have a substantial impact on the interest earned by users.
Sybil Risk
: The contract employs a cap named MAXIMUM_XVS_CAP
to prevent users from earning interest on amounts exceeding this limit. However, in practice, a user can potentially exploit this by using multiple accounts to accumulate more interest. As a result, the existing protection measures in place are not entirely immune to Sybil attacks.
DoS in Key Functionities
: sweepToken
function is intended to recover accidentally transferred ERC-20 tokens to the contract. While this function is restricted to the contract owner (onlyOwner
modifier), it does not account for previously accrued tokens, which poses a significant risk to the protocol's stability. In case it sweeps the token which are previously added to total accrued tokens but not released to Prime yet, then it will lead to DoS across both this contracts.
Gas Limit Issues
: There is no way to update Max Loops value in Future. While addMarket
controls the number of markets that can be add so it not that big a risk. But still as there is no way to even remove the market, it is a possible risk if block limit is reached.
Key Contracts:
Prime.sol
: Soulbound token, the central contract of this audit, allows Prime holders to accumulate rewards, which they can later claim at their convenience.
PrimeLiquidityProvider.sol
: This contract offers liquidity to the Prime token consistently over a specific duration. This liquidity serves as one of the two fund sources supported by Prime tokens.
The rate at which tokens accumulate in PrimeLiquidityProvider
may not be consistent across different blockchain networks.
The sweepToken
function has the potential to sweep already accrued rewards, which could result in Denial-of-Service (DoS) vulnerabilities and financial losses for users.
There is a risk of encountering an infinite loop within the updateScores
function.
There is a decimal precision error which is leading to incorrect calculation of Score.
The value of BLOCKS_PER_YEAR
may be unpredictable during deployment, particularly on blockchain networks with inconsistent block times.
Tokens that have fees associated with them may lead to exaggerated interest claims for some users, potentially causing issues for the remaining users.
Irrevocable tokens can be burned.
There is currently no mechanism in place to update the maximum loop count in the future.
The issue
function allows the issuance of Prime Tokens to users without enforcing the 90-day staking condition.
The Prime
contract does not support the use of multiple markets with the same underlying tokens.
Total Number of Hours | 16 |
---|
16 hours
#0 - c4-pre-sort
2023-10-07T06:16:33Z
0xRobocop marked the issue as sufficient quality report
#1 - c4-judge
2023-11-03T20:08:50Z
fatherGoose1 marked the issue as grade-a