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: 23/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
Judge has assessed an item in Issue #161 as 3 risk. The relevant finding follows:
[L-02] Prime.updateScores will revert if users are added after updating nextScoreUpdateRoundId Vulnerability Details In Prime contract: the updateScores function is meant to update scores of a batch of users when a new round is started (when the nextScoreUpdateRoundId is increased).
A new round is started (by increasing the nextScoreUpdateRoundId variable by one) whenever the admin updates the alpha or any market multipliers, so that the scores of every user needs to be updated as these parameters can modify in a substantial way the calculated scores.
A new round is started by calling the _startScoreUpdateRound; where the nextScoreUpdateRoundId is increased by one and the pendingScoreUpdates is set equal to the current number of token holders (totalIrrevocable + totalRevocable).
Then the updateScores function can be called by anyone to update the scores of a batch of users in all markets; so when a user score is updated; then this user is flagged as being updated by setting isScoreUpdated[nextScoreUpdateRoundId][user]=true so it won’t be updated again in the same round via the same function.
Also the pendingScoreUpdates is decreased by one for each updated user.
Impact The problem arises when new users are added to the Prime program by minting them prime token; and then these users scores are updated in the same round via updateScores function (as this function can be invoked by anyone), but since these users are not accounted for in the pendingScoreUpdates variable that were set before starting the round; then this will lead to disabling the updateScores function for the current round as the pendingScoreUpdates will be eventually equal to zero which will lead to reverting updateScores function after then (this function will be disabled for this round).
These new users scores will be updated after minting them prime token via _initializeMarkets; but they will still be able to get their scores updated again via updateScores function.
demonstartion:
_startScoreUpdateRound function is called after market updates where the totalIrrevocable token holders are 3 users, and totalRevocable token holders are 2 users; so the pendingScoreUpdates will be set to 5. a new 4 users were able to claim revocable tokens after satisfying the prime conditions; so the totalRevocable will be increased to 6, and the total number of users would be 9. the updateScores function is called on a 5 users at first; this will set the pendingScoreUpdates to zero. then the updateScores function is called agin on the remaining 4 users (randomly not specifically the ones that were registered after the round start); so the function will revert as the pendingScoreUpdates can’t be < 0. This will disable this function for the current round, and the remaining users that haven’t got their scores updated will do it manually by calling accrueInterestAndUpdateScore function and update the score for each market. Proof of Concept Prime._startScoreUpdateRound
function _startScoreUpdateRound() internal { nextScoreUpdateRoundId++; totalScoreUpdatesRequired = totalIrrevocable + totalRevocable; pendingScoreUpdates = totalScoreUpdatesRequired; } Prime.updateScores/Line 221
pendingScoreUpdates--; Tools Used Manual Testing.
Recommended Mitigation Steps In _initializeMarkets function :add a flag to indicate that the user got their scores updated when they were added to the Prime program so that they can’t pass the check in the updateScores function:
function _initializeMarkets(address account) internal { address[] storage _allMarkets = allMarkets; for (uint256 i = 0; i < _allMarkets.length; ) { address market = _allMarkets[i]; accrueInterest(market); interests[market][account].rewardIndex = markets[market].rewardIndex; uint256 score = _calculateScore(market, account); interests[market][account].score = score; markets[market].sumOfMembersScore = markets[market].sumOfMembersScore + score; unchecked { i++; } }
}isScoreUpdated[nextScoreUpdateRoundId][account]=true;
#0 - c4-judge
2023-11-08T17:54:39Z
fatherGoose1 marked the issue as duplicate of #555
#1 - c4-judge
2023-11-08T17:54:44Z
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
ID | Title | Severity |
---|---|---|
L-01 | Deprecated markets can't be removed from allMarkets array | Low |
L-02 | Prime.updateScores will revert if users are added after updating nextScoreUpdateRoundId | Low |
L-03 | Prime contract doesn't support vTokens of decimals > 18 | Low |
L-04 | No check on the exchangeRate before calculationg user score | Low |
L-05 | Owner of PrimeLiquidityProvider can sweep registerd (initialized) tokens | Low |
L-06 | The project uses vulnerable node dependencies | Low |
L-07 | Rewards of an asset blockliste user will be stuck in the Prime contract | Low |
allMarkets
array <a id="l-01" ></a>In Prime
contract: markets can be added to prime program by adding the market vToken with it's configuration (supplyMultiplier & borrowMultiplier).
Before adding a new market; a check is made on the length of the allMarkets
array via _ensureMaxLoops(allMarkets.length)
function , where the array length is checked against a maximum value that was set upon contract initialization (_setMaxLoopsLimit(_loopsLimit)
).
With the current mechanism adopted by the protocol; if a prime supported market is deprecated , then the rewards distribution will stop, and this can be achieved by setting the borrow and supply multipliers to 0 and by setting the distribution speeds to zero in the PrimeLiquidityProvider
contract.
But deprecated markets will not be removed from the allMarkets
array in order to keep this market listed so that users that haven't claimed their rewards before market deprecation will still be able to claim them.
This will lead to the allMarket
array grows in size in the future (as the deprecated markets are not removed from it) until it reaches it's maximum limit; which will disable adding any new market in the future.
_setMaxLoopsLimit(_loopsLimit);
function addMarket(address vToken, uint256 supplyMultiplier, uint256 borrowMultiplier) external { _checkAccessAllowed("addMarket(address,uint256,uint256)"); if (markets[vToken].exists) revert MarketAlreadyExists(); bool isMarketExist = InterfaceComptroller(comptroller).markets(vToken); if (!isMarketExist) revert InvalidVToken(); markets[vToken].rewardIndex = 0; markets[vToken].supplyMultiplier = supplyMultiplier; markets[vToken].borrowMultiplier = borrowMultiplier; markets[vToken].sumOfMembersScore = 0; markets[vToken].exists = true; vTokenForAsset[_getUnderlying(vToken)] = vToken; allMarkets.push(vToken); _startScoreUpdateRound(); _ensureMaxLoops(allMarkets.length); emit MarketAdded(vToken, supplyMultiplier, borrowMultiplier); }
Manual Testing.
Add a mechanism to track deprecated markets to enable users from claiming their rewards of these markets; and remove these deprecated markets from the allMarkets
array so it won't reach its maximum limit in the future.
Prime.updateScores
will revert if users are added after updating nextScoreUpdateRoundId
<a id="l-02" ></a>In Prime
contract: the updateScores
function is meant to update scores of a batch of users when a new round is started (when the nextScoreUpdateRoundId
is increased).
A new round is started (by increasing the nextScoreUpdateRoundId
variable by one) whenever the admin updates the alpha or any market multipliers, so that the scores of every user needs to be updated as these parameters can modify in a substantial way the calculated scores.
A new round is started by calling the _startScoreUpdateRound
; where the nextScoreUpdateRoundId
is increased by one and the pendingScoreUpdates
is set equal to the current number of token holders (totalIrrevocable + totalRevocable).
Then the updateScores
function can be called by anyone to update the scores of a batch of users in all markets; so when a user score is updated; then this user is flagged as being updated by setting isScoreUpdated[nextScoreUpdateRoundId][user]=true
so it won't be updated again in the same round via the same function.
Also the pendingScoreUpdates
is decreased by one for each updated user.
The problem arises when new users are added to the Prime program by minting them prime token; and then these users scores are updated in the same round via updateScores
function (as this function can be invoked by anyone), but since these users are not accounted for in the pendingScoreUpdates
variable that were set before starting the round; then this will lead to disabling the updateScores
function for the current round as the pendingScoreUpdates
will be eventually equal to zero which will lead to reverting updateScores
function after then (this function will be disabled for this round).
These new users scores will be updated after minting them prime token via _initializeMarkets
; but they will still be able to get their scores updated again via updateScores
function.
demonstartion:
_startScoreUpdateRound
function is called after market updates where the totalIrrevocable
token holders are 3 users, and totalRevocable
token holders are 2 users; so the pendingScoreUpdates
will be set to 5.totalRevocable
will be increased to 6, and the total number of users would be 9.updateScores
function is called on a 5 users at first; this will set the pendingScoreUpdates
to zero.updateScores
function is called agin on the remaining 4 users (randomly not specifically the ones that were registered after the round start); so the function will revert as the pendingScoreUpdates
can't be < 0.accrueInterestAndUpdateScore
function and update the score for each market.function _startScoreUpdateRound() internal { nextScoreUpdateRoundId++; totalScoreUpdatesRequired = totalIrrevocable + totalRevocable; pendingScoreUpdates = totalScoreUpdatesRequired; }
pendingScoreUpdates--;
Manual Testing.
In _initializeMarkets
function :add a flag to indicate that the user got their scores updated when they were added to the Prime program so that they can't pass the check in the updateScores
function:
function _initializeMarkets(address account) internal { address[] storage _allMarkets = allMarkets; for (uint256 i = 0; i < _allMarkets.length; ) { address market = _allMarkets[i]; accrueInterest(market); interests[market][account].rewardIndex = markets[market].rewardIndex; uint256 score = _calculateScore(market, account); interests[market][account].score = score; markets[market].sumOfMembersScore = markets[market].sumOfMembersScore + score; unchecked { i++; } } + isScoreUpdated[nextScoreUpdateRoundId][account]=true; }
In Prime
contract: _calculateScore
function is intended to calculate the user score that will be used to calculate his rewards based on his supply and borrow.
After the user capital is calculated based on the underlying token price of the market and the user's supply and borrow; this value is scaled based on the vToken decimals:
capital = capital * (10 ** (18 - vToken.decimals()));
The vToken decimals is set equal to the underlying asset that it represents; so if the underlying asset decimals is > 18; then the capital scaling calculation (18 - vToken.decimals()) will revert.
vToken.decimals()
is > 18; then the _calculateScore
function will revert and all the functions implementing it will be disabled.Prime._calculateScore/Line 660
capital = capital * (10 ** (18 - vToken.decimals()));
Manual Testing.
Update _calculateScore
function to account for vTokens with decimals greater than 18.
exchangeRate
before calculationg user score<a id="l-04" ></a>In Prime
contract: _calculateScore
function is intended to calculate the user score that will be used to calculate his rewards based on his supply and borrow.
The supply is calculated based on the exchangeRate from the underlying asset to the vToken:
uint256 exchangeRate = vToken.exchangeRateStored();
uint256 supply = (exchangeRate * balanceOfAccount) / EXP_SCALE;
And this calculated supply is used to calculate the capital of the user (along with the borrow value); which is the the sum of qualified supply and borrow balance of the user.
But as there's no check on the value of the exchangeRate
that's extracted from the vToken; then a zero value could be returned (at some cases); so the calculation of the capital would be incorrect (lesser than actual).
Prime._calculateScore/Line 652
uint256 exchangeRate = vToken.exchangeRateStored();
Manual Testing.
Update _calculateScore
function to revert if the returnd vToken.exchangeRateStored()
is zero.
PrimeLiquidityProvider
can sweep initialized tokens <a id="l-05" ></a>In PrimeLiquidityProvider
contract: the owner can sweep accidental ERC-20 transferred to the contract by calling the sweepToken
function; but as can be noticed; there's no check if the sweeped token is initialized ;i.e. intended to be used to provide funds for the prime rewards program.
So if an initialized token that is meant to fund the prime rewards is fully sweeped from the PrimeLiquidityProvider
; then
the PrimeLiquidityProvider.accrueTokens
will be disabled as it will revert when checking the balanceDiff
when the token started to accrue rewards ( tokenAmountAccrued[token_] > 0):
PrimeLiquidityProvider.accrueTokens / Line 259-261
```solidity uint256 balance = IERC20Upgradeable(token_).balanceOf(address(this)); uint256 balanceDiff = balance - tokenAmountAccrued[token_]; ```
leading to disabling all the functionalities of the Prime feature for this sweeped token market where PrimeLiquidityProvider.accrueTokens
is called.
PrimeLiquidityProvider.sweepToken
function sweepToken(IERC20Upgradeable token_, address to_, uint256 amount_) external onlyOwner { uint256 balance = token_.balanceOf(address(this)); if (amount_ > balance) { revert InsufficientBalance(amount_, balance); } emit SweepToken(address(token_), to_, amount_); token_.safeTransfer(to_, amount_); }
Manual Testing.
Update PrimeLiquidityProvider.sweepToken
function to prevent sweeping initialized (registered) tokens.
The Protocol uses multiple third-party dependencies; some of these were affected by public-known vulnerabilities that may pose a risk to the global application security level.
This may pose a risk to the global application security level.
# npm audit report @openzeppelin/contracts <=4.8.2 Severity: high OpenZeppelin Contracts initializer reentrancy may lead to double initialization - https://github.com/adviso Improper Initialization in OpenZeppelin - https://github.com/advisories/GHSA-88g8-f5mf-f5rj OpenZeppelin Contracts TransparentUpgradeableProxy clashing selector calls may not be delegated - https://g2q-35m2-x2rh OpenZeppelin Contracts ERC165Checker unbounded gas consumption - https://github.com/advisories/GHSA-7grf-83 fix available via `npm audit fix` node_modules/@openzeppelin/contracts-v0.7 flat <5.0.1 Severity: critical flat vulnerable to Prototype Pollution - https://github.com/advisories/GHSA-2j2x-2gpw-g8fm fix available via `npm audit fix` node_modules/eth-gas-reporter/node_modules/flat yargs-unparser <=1.6.3 Depends on vulnerable versions of flat node_modules/eth-gas-reporter/node_modules/yargs-unparser mocha 5.1.0 - 9.2.1 Depends on vulnerable versions of minimatch Depends on vulnerable versions of yargs-unparser node_modules/eth-gas-reporter/node_modules/mocha eth-gas-reporter 0.0.5 - 0.2.25 Depends on vulnerable versions of mocha Depends on vulnerable versions of request Depends on vulnerable versions of request-promise-native node_modules/eth-gas-reporter minimatch <3.0.5 Severity: high minimatch ReDoS vulnerability - https://github.com/advisories/GHSA-f8q6-p94x-37v3 fix available via `npm audit fix` node_modules/eth-gas-reporter/node_modules/minimatch request * Severity: moderate Server-Side Request Forgery in Request - https://github.com/advisories/GHSA-p8p7-x288-28g6 Depends on vulnerable versions of tough-cookie fix available via `npm audit fix` node_modules/request request-promise-core * Depends on vulnerable versions of request node_modules/request-promise-core request-promise-native >=1.0.0 Depends on vulnerable versions of request Depends on vulnerable versions of request-promise-core Depends on vulnerable versions of tough-cookie node_modules/request-promise-native semver 7.0.0 - 7.5.1 Severity: moderate semver vulnerable to Regular Expression Denial of Service - https://github.com/advisories/GHSA-c2qf-rxjj-qq fix available via `npm audit fix --force` Will install @semantic-release/npm@11.0.0, which is a breaking change node_modules/npm/node_modules/semver npm 7.0.0-beta.0 - 9.7.1 Depends on vulnerable versions of semver node_modules/npm @semantic-release/npm 7.1.0 - 10.0.0-beta.4 Depends on vulnerable versions of npm node_modules/@semantic-release/npm semantic-release 18.0.0-beta.1 - 20.1.3 Depends on vulnerable versions of @semantic-release/npm node_modules/semantic-release tough-cookie <4.1.3 Severity: moderate tough-cookie Prototype Pollution vulnerability - https://github.com/advisories/GHSA-72xf-g2v4-qvf3 fix available via `npm audit fix` node_modules/tough-cookie 14 vulnerabilities (8 moderate, 3 high, 3 critical) To address issues that do not require attention, run: npm audit fix To address all issues (including breaking changes), run: npm audit fix --force
Manual Testing.
Update all affected packages to its latest version.
Prime
contract <a id="l-07" ></a>In Prime._claimInterest
function: user can claim his rewards tokens from any listed market directly to his address; but some tokens have a blocklist where certain accounts are prohibited from receiving or transferring any tokens.
So if the user is set in the blocklist of the asset (market) token (if it has a blacklist); then his reward tokens will be stuck in the prime contract .
Blocklisted user in the underlying market asset can't get his accrued rewards.
function claimInterest(address vToken) external whenNotPaused returns (uint256) { return _claimInterest(vToken, msg.sender); }
function claimInterest(address vToken, address user) external whenNotPaused returns (uint256) { return _claimInterest(vToken, user); }
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; } } asset.safeTransfer(user, amount); emit InterestClaimed(user, vToken, amount); return amount; }
Manual Testing.
Update claimInterest
& _claimInterest
functions to have third parameter (receiver) where the user wants to send his rewards tokens.
- function claimInterest(address vToken, address user) external whenNotPaused returns (uint256) { + function claimInterest(address vToken, address user,address receiver) external whenNotPaused returns (uint256) { - return _claimInterest(vToken, user); + return _claimInterest(vToken, user,receiver); }
function _claimInterest(address vToken, address user,address receiver) internal returns (uint256) { //some code - asset.safeTransfer(user, amount); + asset.safeTransfer(receiver, amount); //some code }
#0 - c4-pre-sort
2023-10-07T15:13:29Z
0xRobocop marked the issue as sufficient quality report
#1 - 0xRobocop
2023-10-07T15:13:37Z
L-01 dup of #421 L-02 dup of #555 L-06 dup of #42
#2 - c4-pre-sort
2023-10-07T18:22:41Z
0xRobocop marked the issue as high quality report
#3 - chechu
2023-10-24T19:29:13Z
L-03 | Prime contract doesn’t support vTokens of decimals > 18 Dispute validity. Every vToken in the Venus Protocol has currently 8 decimals. And it will be probably the same in the future. Moreover, the number of decimals of the vTokens are actually irrelevant. The statement identified by the warden contains a different issue, identified at https://github.com/code-423n4/2023-09-venus-findings/issues/588
[L-04] No check on the exchangeRate before calculationg user score
Dispute validity. exchangeRate
, as it's returned by vToken.exchangeRateStored()
, could be zero if:
exchangeRateStored()
_totalSupply
is zero and initialExchangeRateMantissa
is zero. This is not allowed in the initialize
function of the vTokenMoreover, the exchangeRate
is always increasing, every block. So, considering this and the two previous points. We don't think exchangeRate
can be zero, and therefore we don't consider needed to check it in the Prime contract.
[L-05] Owner of PrimeLiquidityProvider can sweep initialized tokens ACK. Similar to https://github.com/code-423n4/2023-09-venus-findings/issues/42
[L-07] Rewards of an asset blocklisted user will be stuck in the Prime contract ACK.
#4 - c4-sponsor
2023-10-24T19:44:11Z
chechu (sponsor) disputed
#5 - c4-judge
2023-11-03T02:04:15Z
fatherGoose1 marked the issue as grade-b
#6 - DevHals
2023-11-07T15:07:05Z
Hi,, L-02 is a duplicate of #555
#7 - fatherGoose1
2023-11-08T17:53:35Z
@DevHals Agreed. Marking as duplicate.