Venus Protocol Isolated Pools - peanuts's results

Earn, Borrow & Lend on the #1 Decentralized Money Market on the BNB Chain

General Information

Platform: Code4rena

Start Date: 08/05/2023

Pot Size: $90,500 USDC

Total HM: 17

Participants: 102

Period: 7 days

Judge: 0xean

Total Solo HM: 4

Id: 236

League: ETH

Venus Protocol

Findings Distribution

Researcher Performance

Rank: 12/102

Findings: 3

Award: $1,239.75

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: 0x73696d616f

Also found by: 0xadrii, 0xbepresent, 0xkazim, J4de, peanuts, pontifex, rvierdiiev, volodya

Labels

bug
2 (Med Risk)
partial-50
duplicate-486

Awards

96.0525 USDC - $96.05

External Links

Lines of code

https://github.com/code-423n4/2023-05-venus/blob/8be784ed9752b80e6f1b8b781e2e6251748d0d7e/contracts/Lens/PoolLens.sol#L268

Vulnerability details

Impact

Price oracle may return a stale price if oracle.updatePrice is not called before oracle.getUnderlyingPrice.

Proof of Concept

In RiskFund.sol#_swapAsset, when swapping a single asset to base asset, oracle.updatePrice is called before oracle.getUnderlyingPrice.

_swapAsset( VToken vToken, ... ComptrollerViewInterface(comptroller).oracle().updatePrice(address(vToken)); uint256 underlyingAssetPrice = ComptrollerViewInterface(comptroller).oracle().getUnderlyingPrice( address(vToken) );

Similarly, in ShortFall.sol#_startAuction(), oracle.updatePrice is called before oracle.getUnderlyingPrice.

_startAuction(address comptroller) internal { PoolRegistryInterface.VenusPool memory pool = PoolRegistry(poolRegistry).getPoolByComptroller(comptroller); ... PriceOracle priceOracle = _getPriceOracle(comptroller); ... priceOracle.updatePrice(address(vTokens[i])); uint256 usdValue = (priceOracle.getUnderlyingPrice(address(vTokens[i])) * marketBadDebt) / 1e18;

However, in PoolLens#getPoolBadDebt(),

getPoolBadDebt(address comptrollerAddress) external view returns (BadDebtSummary memory) { ... PriceOracle priceOracle = comptroller.oracle(); ... badDebt.badDebtUsd = VToken(address(markets[i])).badDebt() * priceOracle.getUnderlyingPrice(address(markets[i])); badDebtSummary.badDebts[i] = badDebt; totalBadDebtUsd = totalBadDebtUsd + badDebt.badDebtUsd;

oracle.getUnderlyingPrice is immediately called without calling oracle.updatePrice first.

Tools Used

VSCode

Call updatePrice first in getPoolBadDebt as well.

for (uint256 i; i < markets.length; ++i) { BadDebt memory badDebt; badDebt.vTokenAddress = address(markets[i]); + priceOracle.updatePrice(VToken(address(markets[i]))) badDebt.badDebtUsd = VToken(address(markets[i])).badDebt() * priceOracle.getUnderlyingPrice(address(markets[i])); badDebtSummary.badDebts[i] = badDebt; totalBadDebtUsd = totalBadDebtUsd + badDebt.badDebtUsd; }

Assessed type

Oracle

#0 - c4-judge

2023-05-18T10:45:13Z

0xean marked the issue as duplicate of #88

#1 - c4-judge

2023-06-05T14:09:52Z

0xean marked the issue as satisfactory

#2 - c4-judge

2023-06-08T14:39:49Z

0xean marked the issue as duplicate of #486

#3 - c4-judge

2023-06-08T14:42:01Z

0xean marked the issue as partial-50

Findings Information

🌟 Selected for report: peanuts

Also found by: 0xStalin, jasonxiale, volodya

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
M-05

Awards

951.5947 USDC - $951.59

External Links

Lines of code

https://github.com/code-423n4/2023-05-venus/blob/8be784ed9752b80e6f1b8b781e2e6251748d0d7e/contracts/Lens/PoolLens.sol#L248

Vulnerability details

Proof of Concept

In PoolLens.sol#getPoolBadDebt(), bad debt is calculated as such:

badDebt.badDebtUsd = VToken(address(markets[i])).badDebt() * priceOracle.getUnderlyingPrice(address(markets[i])); badDebtSummary.badDebts[i] = badDebt; totalBadDebtUsd = totalBadDebtUsd + badDebt.badDebtUsd;

In Shortfall.sol#_startAuction(), bad debt is calculated as such:

uint256[] memory marketsDebt = new uint256[](marketsCount); auction.markets = new VToken[](marketsCount); for (uint256 i; i < marketsCount; ++i) { uint256 marketBadDebt = vTokens[i].badDebt(); priceOracle.updatePrice(address(vTokens[i])); uint256 usdValue = (priceOracle.getUnderlyingPrice(address(vTokens[i])) * marketBadDebt) / 1e18; poolBadDebt = poolBadDebt + usdValue;

Focus on the line with the priceOracle.getUnderlyingPrice. In PoolLens.sol#getPoolBadDebt, badDebt in USD is calculated by multiplying the bad debt of the VToken market by the underlying price. However, in Shortfall, badDebt in USD is calculated by the bad debt of the VToken market by the underlying price and divided by 1e18.

The PoolLens#getPoolBadDebt() function forgot to divide the debt in usd by 1e18.

This is what the function is actually counting:

Let's say that the VToken market has a badDebt of 1.3 ETH (1e18 ETH). The pool intends to calculate 1.3 ETH in terms of USD, so it calls the oracle to determine the price of ETH. Let's say the price of ETH is 1500 USD. The total pool debt should be 1.3 * 1500 = 1950 USD. In decimal calculation, the pool debt should be 1.3e18 * 1500e18 (if oracle returns in 18 decimal places) / 1e18 = 1950e18.

Impact

The badDebt in USD in PoolLens.sol#getPoolBadDebt() will be massively inflated.

Tools Used

VSCode

Normalize the decimals of the bad debt calculation in getPoolBadDebt().

badDebt.badDebtUsd = VToken(address(markets[i])).badDebt() * + priceOracle.getUnderlyingPrice(address(markets[i])) / 1e18; badDebtSummary.badDebts[i] = badDebt; totalBadDebtUsd = totalBadDebtUsd + badDebt.badDebtUsd;

Assessed type

Decimal

#0 - c4-judge

2023-05-18T10:40:32Z

0xean marked the issue as primary issue

#1 - c4-sponsor

2023-05-23T21:21:47Z

chechu marked the issue as sponsor confirmed

#2 - c4-judge

2023-06-05T14:31:15Z

0xean marked the issue as satisfactory

#3 - c4-judge

2023-06-05T17:01:05Z

0xean marked the issue as selected for report

Findings Information

🌟 Selected for report: fs0c

Also found by: 0xnev, BPZ, Brenzee, J4de, Team_Rocket, peanuts, rvierdiiev, yongskiws

Labels

bug
2 (Med Risk)
satisfactory
duplicate-222

Awards

192.105 USDC - $192.11

External Links

Lines of code

https://github.com/code-423n4/2023-05-venus/blob/8be784ed9752b80e6f1b8b781e2e6251748d0d7e/contracts/Shortfall/Shortfall.sol#L393

Vulnerability details

Impact

BadDebt calculation in each vToken market may affect the accounting of the total bad debt in the pool if each VToken has a different decimal place.

Proof of Concept

When starting an auction via _startAuction(), the protocol calculates the usd value of the bad debt of all the different markets in the pool.

for (uint256 i; i < marketsCount; ++i) { uint256 marketBadDebt = vTokens[i].badDebt(); priceOracle.updatePrice(address(vTokens[i])); uint256 usdValue = (priceOracle.getUnderlyingPrice(address(vTokens[i])) * marketBadDebt) / 1e18; poolBadDebt = poolBadDebt + usdValue;

The usdValue of each market's debt is calculated and divided by 1e18. The function then accumulates all the different debt in various markets into the variable poolBadDebt.

Dividing all the usdValue by 1e18 assumes that all priceOracle of every vToken and all VTokens have the same decimal place. In VToken.sol, each VToken can have a different decimal when initializing. If all the different VToken have different decimals, then dividing by 1e18 will result in accounting error because some VTokens may have more decimals than other VTokens.

VToken.sol initialize( address underlying_, ComptrollerInterface comptroller_, InterestRateModel interestRateModel_, uint256 initialExchangeRateMantissa_, string memory name_, string memory symbol_, uint8 decimals_,

Assuming that all VToken price oracles return 18 decimals, let's take a look at a hypothetical example. Let's say the vDai market (6 decimals) has a bad debt of 1000e6 vDai, and the vDoge market (18 decimals) has a bad debt of 1000e18 doge. When calculating badDebt in terms of USD, let say vDAI is worth 1 usd and vDoge is worth 0.1 usd

For vDai:

uint256 usdValue = (priceOracle.getUnderlyingPrice(address(vTokens[i])) * marketBadDebt) / 1e18; usdValue = 1e18 * 1000e6 / 1e18 = 1000e6 of bad debt

For vDoge:

uint256 usdValue = (priceOracle.getUnderlyingPrice(address(vTokens[i])) * marketBadDebt) / 1e18; usdValue = 0.1e18 * 1000e18 / 1e18 = 100e18 of bad debt (when it should be 100e6, normalized to 6 decimals wrt vDai market).

Tools Used

VSCode

Recommend dividing according to the decimals of each vToken instead

for (uint256 i; i < marketsCount; ++i) { uint256 marketBadDebt = vTokens[i].badDebt(); priceOracle.updatePrice(address(vTokens[i])); + uint256 usdValue = (priceOracle.getUnderlyingPrice(address(vTokens[i])) * marketBadDebt) / 10 ** vTokens[i].decimals(); poolBadDebt = poolBadDebt + usdValue;

Assessed type

Decimal

#0 - c4-judge

2023-05-17T16:48:11Z

0xean marked the issue as duplicate of #468

#1 - c4-judge

2023-06-05T14:17:30Z

0xean marked the issue as satisfactory

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter