Wise Lending - SBSecurity's results

Decentralized liquidity market that allows users to supply crypto assets and start earning a variable APY from borrowers.

General Information

Platform: Code4rena

Start Date: 21/02/2024

Pot Size: $200,000 USDC

Total HM: 22

Participants: 36

Period: 19 days

Judge: Trust

Total Solo HM: 12

Id: 330

League: ETH

Wise Lending

Findings Distribution

Researcher Performance

Rank: 10/36

Findings: 4

Award: $5,772.78

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: SBSecurity

Also found by: 0xCiphky

Labels

bug
2 (Med Risk)
downgraded by judge
:robot:_88_group
sufficient quality report
primary issue
satisfactory
selected for report
M-03

Awards

2444.3801 USDC - $2,444.38

External Links

Lines of code

https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmController.sol#L53-L111 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L452-L463 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L465-L476

Vulnerability details

Impact

In certain scenarios, shares of a subsequent depositor can be heavily reduced, losing a large amount of his deposited funds, the attacker can increase the right side of the previewMintShares by adding rewards for compounding.

That way victim can lose 6e17 of his assets for a deposit of 1e18.

Proof of Concept

Let’s see how a first user, can grief a subsequent deposit and reduce his shares from the desired 1:1 ratio to 1:0000000000000005.

First, he needs to choose PowerFarmToken with no previous deposits.

  1. He calls depositExactAmount with 2 wei which will also call syncSupply β†’ _updateRewards which is a key moment of the attack, this will make it possible PowerFarmController::exchangeRewardsForCompoundingWithIncentive to be called when performing the donation.
  2. With 2 wei user will mint 2 shares, so totalSupply = 3, underlyingLpAssetsCurrent = 3
  3. Then, the victim comes and tries to deposit 1e18 of assets, but is front ran by the first user calling PowerFarmController::exchangeRewardsForCompoundingWithIncentive β†’ addCompoundRewards with 999999999999999996 that will increase the totalLpAssetsToDistribute, which is added to underlyingLpAssetsCurrent in the _syncSupply function, called from the modifier before the main functions.

The attacker does not lose his deposit of 1e18 because it is converted to rewardTokens and sent to him, basically making the inflation free.

PendlePowerFarmController.sol#L53-L111

function exchangeRewardsForCompoundingWithIncentive(
      address _pendleMarket,
      address _rewardToken,
      uint256 _rewardAmount
  ) external syncSupply(_pendleMarket) returns (uint256) {
      CompoundStruct memory childInfo = pendleChildCompoundInfo[_pendleMarket];

      uint256 index = _findIndex(childInfo.rewardTokens, _rewardToken);

      if (childInfo.reservedForCompound[index] < _rewardAmount) {
          revert NotEnoughCompound();
      }

      uint256 sendingAmount = _getAmountToSend(_pendleMarket, _getTokensInETH(_rewardToken, _rewardAmount));

      childInfo.reservedForCompound[index] -= _rewardAmount;
      pendleChildCompoundInfo[_pendleMarket] = childInfo;

      _safeTransferFrom(_pendleMarket, msg.sender, address(this), sendingAmount);

      IPendlePowerFarmToken(pendleChildAddress[_pendleMarket]).addCompoundRewards(sendingAmount);//@audit inflate

      _safeTransfer(childInfo.rewardTokens[index], msg.sender, _rewardAmount);//@audit receive back + incentive

      emit ExchangeRewardsForCompounding(_pendleMarket, _rewardToken, _rewardAmount, sendingAmount);

      return sendingAmount;
  }
  1. After that totalSupply = 3, underlyingLpAssetsCurrent = 1e18 - 1
  2. Victim transaction is executed:

PendlePowerFarmToken.sol#L452-L463

function previewMintShares(uint256 _underlyingAssetAmount, uint256 _underlyingLpAssetsCurrent)
        public
        view
        returns (uint256)
    {
        return _underlyingAssetAmount * totalSupply() / _underlyingLpAssetsCurrent;
        // 1e18 * 3 / 1e18 - 1 = 2 
    }

Both attacker and victim have 1 share, because of the fee that is taken in the deposit function.

After victim deposit:

totalSupply: 5, underlyingLpAssetsCurrent = 2e18 - 1

  1. Then victim tries to withdraw all his shares - 1 (1 was taken as a fee on deposit)

PendlePowerFarmToken.sol#L465-L476

function previewAmountWithdrawShares(uint256 _shares, uint256 _underlyingLpAssetsCurrent)
        public
        view
        returns (uint256)
    {
        return _shares * _underlyingLpAssetsCurrent / totalSupply();
	        //1 * (2e18 - 1) / 5 = 399999999999999999 (0.3e18)
    }

User has lost 1e18 - 0.3e18 = 0.6e18 tokens.

Tools Used

Manual Review

Since there will be many PowerFarmTokens deployed, there is no way team to perform the first deposit for all of them. Possible mitigation will be to have minimum deposit amount for the first depositor in the PendlePowerToken, which will increase the cost of the attack exponentially and there will be no enough of reward tokens making the exchangeRewardsForCompoundingWithIncentive revert, due to insufficient amount.

Or just mint proper amount of tokens in the initialize function.

Assessed type

Other

#0 - GalloDaSballo

2024-03-17T10:28:32Z

This finding asserts that the rebase changes the price Other 2 reports assert that the rebase will break functionality

#1 - c4-pre-sort

2024-03-17T15:39:22Z

GalloDaSballo marked the issue as duplicate of #88

#2 - c4-pre-sort

2024-03-17T15:39:26Z

GalloDaSballo marked the issue as sufficient quality report

#3 - GalloDaSballo

2024-03-17T15:39:47Z

Seems to show a less impact than primary

#4 - c4-judge

2024-03-26T17:03:16Z

trust1995 changed the severity to 3 (High Risk)

#5 - c4-judge

2024-03-26T18:58:25Z

trust1995 marked the issue as partial-50

#6 - c4-judge

2024-03-26T18:59:01Z

trust1995 marked the issue as satisfactory

#7 - c4-judge

2024-03-27T14:32:15Z

trust1995 marked the issue as not a duplicate

#8 - c4-judge

2024-03-27T14:32:18Z

trust1995 marked the issue as primary issue

#9 - c4-judge

2024-03-27T14:34:50Z

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

#10 - c4-judge

2024-03-27T20:49:02Z

trust1995 marked the issue as not a duplicate

#11 - c4-judge

2024-03-27T20:49:13Z

trust1995 marked the issue as primary issue

#12 - c4-judge

2024-03-27T20:52:51Z

trust1995 marked the issue as selected for report

#13 - c4-judge

2024-03-27T20:54:34Z

trust1995 changed the severity to 2 (Med Risk)

#14 - vm06007

2024-04-02T14:17:23Z

Since there will be many PowerFarmTokens deployed, there is no way team to perform the first deposit for all of them.

I think this assumption is wrong here, team deploys each token and farm by admin - one by one, and each time a new token/farm is created team can perform first deposit or necessary step, it is not a public function to create these that team cannot handle something like that or to say "there is no way to perform the first deposit for all of them" thats just blown off and far fetched.

#15 - thebrittfactor

2024-04-29T14:30:41Z

For transparency and per conversation with the sponsors, see here for the Wise Lending team's mitigation.

#16 - vm06007

2024-05-02T12:31:00Z

Additional notes: before any farm is publicly available, admin creating farms can ensure no further supplier to the farm would experience any loss due to described far-fetched scenario in this "finding".

Findings Information

🌟 Selected for report: serial-coder

Also found by: SBSecurity, serial-coder

Labels

bug
2 (Med Risk)
:robot:_33_group
sufficient quality report
satisfactory
duplicate-255

Awards

1880.2924 USDC - $1,880.29

External Links

Lines of code

https://github.com/code-423n4/2024-02-wise-lending/blob/main/contracts/WiseSecurity/WiseSecurity.sol#L1092-L1109 https://github.com/code-423n4/2024-02-wise-lending/blob/main/contracts/WiseLending.sol#L1553-L1581

Vulnerability details

Impact

There is no incentive for liquidators to liquidate small positions (even under $100), especially on Mainnet, this will result in bad debt accrued for the WiseLending system because no one will be willing to remove the position and FEE_MANAGER will not be able to split the bad debt between depositors.

Proof of Concept

If we take a look at the current gas prices on Mainnet:

PriorityLowAverageHigh
Gwei757580
Price$7$7$7,45

We can see the current transaction prices varying and in peak hours they can be up to 5 times more, without additional costs of liquidation logic execution.

That way if there are many borrow positions eligible for liquidation but with unsatisfactory incentives for liquidators (liquidator receives less than the cost of executing the liquidation) and they approach insolvency, receiving roughly 5.90 USD incentive for the 100 USD borrow, it will lead to big harm for the protocol since they will result in an unaccounted bad debt for the system, without a proper way to be mitigated.

We can see in the code that there is no validation for the minimum borrowable amount, it has only to be enough to keep the position healthy.

In fact, there is a check for the minDepositEthValue when depositing:

WiseSecurity.sol#L1092-L1109

function _checkMinDepositValue(
    address _token,
    uint256 _amount
)
    private
    view
    returns (bool)
{
    if (minDepositEthValue == ONE_WEI) {
        return true;
    }

    if (_getTokensInEth(_token, _amount) < minDepositEthValue) {
        revert DepositAmountTooSmall();
    }

    return true;
}

But it can do nothing to prevent this malicious action to be executed.

Important note is that positions can become insolvent unintentionally, by borrowers leaving dust amounts of borrowed tokens when closing their positions.

For the borrow flow, there is no minimum borrowable amount.

WiseLending.sol#L1553-L1581

function _handleBorrowExactAmount(
    uint256 _nftId,
    address _poolToken,
    uint256 _amount,
    bool _onBehalf
)
    private
    returns (uint256)
{
    uint256 shares = calculateBorrowShares(
        {
            _poolToken: _poolToken,
            _amount: _amount,
            _maxSharePrice: true
        }
    );

    _coreBorrowTokens(
        {
            _caller: msg.sender,
            _nftId: _nftId,
            _poolToken: _poolToken,
            _amount: _amount,
            _shares: shares,
            _onBehalf: _onBehalf
        }
    );

    return shares;

Tools Used

Manual Review

Similar to the deposit functions, consider adding minBorrowEthValue to prevent such scenarios, otherwise to keep the solvency of the whole poolMarket admins of Wise will have to liquidate such a positions, losing money in gas fees without receiving anything back.

Assessed type

Invalid Validation

#0 - GalloDaSballo

2024-03-16T16:53:35Z

Worth considering

#1 - c4-pre-sort

2024-03-16T16:53:39Z

GalloDaSballo marked the issue as sufficient quality report

#2 - c4-pre-sort

2024-03-17T15:40:34Z

GalloDaSballo marked the issue as primary issue

#3 - vonMangoldt

2024-03-18T14:01:24Z

Can also be circumvented by just paying back after borrowing. Doesnt really add any value imo

#4 - c4-judge

2024-03-26T16:38:04Z

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

#5 - c4-judge

2024-03-26T19:03:32Z

trust1995 marked the issue as satisfactory

Findings Information

🌟 Selected for report: WoolCentaur

Also found by: AM, SBSecurity

Labels

bug
2 (Med Risk)
sufficient quality report
satisfactory
duplicate-116

Awards

1128.1754 USDC - $1,128.18

External Links

Lines of code

https://github.com/code-423n4/2024-02-wise-lending/blob/main/contracts/WiseCore.sol#L460-L536 https://github.com/code-423n4/2024-02-wise-lending/blob/main/contracts/MainHelper.sol#L752-L788

Vulnerability details

Impact

When the pool lacks sufficient funds to fully compensate the liquidator, it will reset both totalPool and totalLendingShares to 0, but will increase the lendingShares of the liquidator by the difference.

This enables the liquidator to retrieve it later when the pool liquidity rises once again, either by paybacks or lp deposits. Consequently, when someone deposits into the pool, the liquidator can claim the remaining portion of their liquidation reward, preventing the depositor from withdrawing their full amount. As we can observe this creates a weird scenario when every next liquidity provider tokens are β€œstolen” from the previous withdrawer.

This is extremely bad for the market because unlike borrowing, where you are sure that these funds will be back into the pool either by repaying or liquidation when the liquidator that has received surplus shares withdraws them it will lead to a domino effect and the last withdrawer for this market will bear all the cost risking up to 100% of his deposits to be stolen.

When pureCollateral of a user isn’t enough for the liquidator to seize, _withdrawOrAllocateSharesLiquidation is executed:

WiseCore.sol#L460-L536

function _withdrawOrAllocateSharesLiquidation(
    uint256 _nftId,
    uint256 _nftIdLiquidator,
    address _poolToken,
    uint256 _percentWishCollateral
)
    private
    returns (uint256)
{
 ...Code when withdrawAmount <= totalPoolToken
 

    uint256 totalPoolInShares = calculateLendingShares(
        {
            _poolToken: _poolToken,
            _amount: totalPoolToken,
            _maxSharePrice: false
        }
    );

    uint256 shareDifference = cashoutShares
        - totalPoolInShares;

    _coreWithdrawBare(
        _nftId,
        _poolToken,
        totalPoolToken,
        totalPoolInShares
    ); 
   
    _decreaseLendingShares(
        _nftId,
        _poolToken,
        shareDifference
    );

    _increasePositionLendingDeposit(
        _nftIdLiquidator,
        _poolToken,
        shareDifference
    );

    _addPositionTokenData(
        _nftIdLiquidator,
        _poolToken,
        hashMapPositionLending,
        positionLendTokenData
    );

    _removeEmptyLendingData(
        _nftId,
        _poolToken
    );
    
    //@audit after: 
    //totalPool = 0
    //totalDepositShares = 0
    //shares of liquidator = diff of cashout - total

    return totalPoolToken;
}

When assets of a pool are less than the amount that liquidator receives we can see that both

  • lendingShares
  • totalPool

are being reset to 0.

And when borrowers payback, mappings that get increased are:

  • totalPool
  • totalBorrowAmount

MainHelper.sol#L752-L788

function _corePayback(
    uint256 _nftId,
    address _poolToken,
    uint256 _amount,
    uint256 _shares
)
    internal
{
    _updatePoolStorage(
        _poolToken,
        _amount,
        _shares,
        _increaseTotalPool,
        _decreasePseudoTotalBorrowAmount,
        _decreaseTotalBorrowShares
    );

    _decreasePositionMappingValue(
        userBorrowShares,
        _nftId,
        _poolToken,
        _shares
    );

    if (userBorrowShares[_nftId][_poolToken] > 0) {
        return;
    }

    _removePositionData({
        _nftId: _nftId,
        _poolToken: _poolToken,
        _getPositionTokenLength: getPositionBorrowTokenLength,
        _getPositionTokenByIndex: getPositionBorrowTokenByIndex,
        _deleteLastPositionData: _deleteLastPositionBorrowData,
        isLending: false
    });
}

This is not enough for a liquidator to redeem his difference because on withdraw the following mappings should hold enough values:

  • totalPool - reset to 0 in liquidation
  • pseudoTotalPool
  • totalDepositShares - reset to 0 in liquidation

From there we can see in order liquidator to be able to redeem his shares he needs someone else to deposit his funds in the same market, we saw that payback won’t work. Then after totalDepositShares holds enough shares that satisfy the liquidator he can freely withdraw them β€œstealing” from the original depositor. Then when he wants to withdraw his tokens and exit the protocol it won’t be possible because previous user has redeemed tokens that belonged to him. Lets consider the example that deposits and withdrawals will be alternating. Then the scenario will continue to happen for each new deposit in the system being stolen from the previous person.

Proof of Concept

Here is a gist containing test that can be placed in test/extraFunctionality2.test.js:

https://gist.github.com/AydoanB/1c8012f8e078d5b5de8981dee96a929e

For simplicity all the other tests can be removed, then in the terminal: npm run test-extra2 will execute it.

The main idea is to verify that when such type of liquidation occurs totalPool is zeroed and after Bob deposits in the system liquidator can redeem shares accrued from liquidation using Bob’s assets.

After that when Bob wants to withdraw big part of his deposit WiseCore::_coreWithdrawBare will revert with underflow.

Tools Used

Manual Review

Finding an appropriate solution is indeed challenging. One approach could be to allow the liquidator to withdraw the remaining funds only when there is a substantial increase in liquidity, such as 5 times or 10 times the amount he has left to withdraw.

Assessed type

Other

#0 - c4-pre-sort

2024-03-16T16:53:07Z

GalloDaSballo marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-03-17T14:27:35Z

GalloDaSballo marked the issue as duplicate of #238

#2 - c4-judge

2024-03-26T19:00:07Z

trust1995 marked the issue as satisfactory

Findings Information

🌟 Selected for report: aariiif

Also found by: 0xepley, Myd, SBSecurity, fouzantanveer, foxb868, kaveyjoe

Labels

analysis-advanced
grade-b
high quality report
edited-by-warden
A-03

Awards

319.9291 USDC - $319.93

External Links

Logo

πŸ› οΈ Analysis -Β WiseLending

Overview

Wise Lending is a protocol that serves as a DeFi liquidity market with multiple sources of yield integrated. The yield strategies approach makes the protocol unique by removing the complexity for a normal user to manually manage his assets. In a trust-minimized approach protocol gives the freedom to its users to decide how they want to operate within the protocol, they can choose from pure lending/borrowing or one of the many configurations provided by the development team:

  • No APY, pure lending/borrowing
  • APY from Wise + APY from Aave
  • APY from Wise + APY from Pendle
  • APY from Wise + APY from Pendle + APY from Aave

As simple as that.

Key contracts of WiseLending for this audit are:

  • WiseLending: A main contract used for all actions in the protocol like lending, borrowing, repaying, and liquidations. It also manages the following important features for the protocol - borrow rates, LASA algorithm, users’s positions, and configuring pool token markets.
  • PositionNFT: ERC721 contract which takes care of minting and reserving all the positions across the WiseLending, it makes possible for positions to be transferred on third-party markets in a P2P.
  • PendlePowerFarm: Contract serving as an entry point for WiseLending to Pendle Finance, with the help of Balancer’s flash loans, users can open and close leveraged positions with Pendle LP tokens.
  • PendlePowerFarmToken: ERC20 contract representing the Pendle LP tokens of a particular Pendle market, also serves as a powerFarm yield reward distributor.
  • PendlePowerFarmController: Contract that manages both Pendle LP and Pendle market tokens, a person with master role can vote and claim Pendle rewards from it.
  • FeeManager: Contract managing bad debt in the system, also organizes fee distribution and rewards claiming accumulated from borrowers paying interest.

System Overview

We can observe 6 main parts in the WiseLending system:

  • WiseLending - Consists of many contracts inheriting each other, each one serving a different purpose, storage modification, pool configuration, core lending, borrowing, repaying and liquidation logic, share price calculation, and scaling algorithm.
  • DerivativesOracles & OraclesHub - The Entire WiseLending system relies on oracles as all calculations are performed in ETH value, OraclesHub wraps the logic of all DerivativesOracles and exposes a generic interface, exposing only the price-deriving functions.
  • FeeManager - Entity used to receive fees from borrow interest accrued and manage bad debt that is split amongst all the participants in the system.
  • PowerFarms - Set of contracts, allowing full interaction with Pendle and WiseLending, serving as an entry point for swapping Standardised Yield tokens for Pendle LPs and opening WETH borrow positions, with the help of Balancer flash loans.
  • WrapperHub - Set of contracts, allowing full interaction with Aave yield and WiseLending, serving as an atomic way to provide liquidity to Aave and receive additional yield from depositing the ATokens in Wise and vice versa.
  • WiseSecurity - Helper contracts, used for preparing the Curve pools, setting liquidation configuration, calculating the health state of positions, and additional safe checks to not push the system into a bad state.

Approach Taken in Evaluating WiseLending

StageActionDetailsInformation
1Compile and Run TestInstallationA bit complex setup, caused by the big codebase.
2Documentation reviewDocumentationProvides more general information showing Wise's advantages over competitors, not enough technical explanations.
3Contest detailsAudit DetailsLacking any contract explanation. Has an extensive known issues page, significantly lowering the attack surface.
4DiagrammingExcalidrawDrawing diagrams through the entire process of codebase evaluation.
5Test SuitsTestsMix between Foundry and Hardhat makes the test review stage harder for the auditor.
6Manual Code ReviewScopeReviewed all the contracts in scope.
7Special focus on Areas of ConcernAreas of concernObserving the known issues and bot report.

Architecture Recommendations

WiseLending:

Developers did a great job, extracting common logic into contracts and utilizing inheritance to make the understanding easy, despite the complexity and size of the protocol. Understandably attack surface is big, but parts that provide economic incentives for the attacker are well-thought-out and developed in a precise manner. Post-exploit modifications applied, completely remove the chance of it happening again. We can observe best development principles are being applied with some minor mistakes as not utilizing the power of Solidity and repetitive variables in some places. Separation of concerns and abstraction are applied in a great way, external functions contain reusable functions.

Notes for improvement:

  • Add events emissions in most of the important functions, currently, there are only for the solely deposits.
  • When possible move the validations, such as zero values checks, to the external functions.
  • _removePositionData can be simplified, removing the unnecessary if statements and optimizing the execution of the function.
  • liquidation, where total pool assets are less than the asked from the liquidator, should be reconsidered and potentially reimplemented.
Lasa Algorithm explanation

Lasa Algorithm explanation

DerivativesOracles & OraclesHub:

The decision to use both Uniswap TWAP and Chainlink oracle minimizes the oracle manipulations, however, this increases the gas costs of the transactions, adds additional complexity, and exposes potential denial-of-services. Wise lending heavily relies on the oracles for all types of actions across the entire system, but there is no room for decimal errors as everything is converted to ETH value and scaled to 18 decimals.

OracleHub exposes 2 functions: getTokensInETH and getTokensFromETH for converting the assets to/from ETH price and scaling to proper decimals. Also, sequencer and oracle state checks are added which is important for L2 chains.

DerivativeOracles are modular with an easy way to be used by the OracleHub, this makes the upgradeability of oracles an easy task.

Notes for improvement:

  • Consider adding functionality to replace oracles
  • Most of the assets in the Wise Lending will be ETH derivatives such as stETH, consider extending the functionality to support different feeds such as USD ones.

FeeManager:

Contracts with unique approach, serving two main purposes to handle bad debt and fee management. Bad debt incentive is implemented well with a possibility to attract users and cover the bad debt accrued, this greatly decreases the chance of Wise system to end up insolvent.

PendlePowerFarm:

Set of farming strategy contracts are implemented well, with a good architecture, following the inheritance pattern, similar to WiseLending. The idea to reserve the powerFarm NFTs and not let the users use them in the other parts of the system is a big limitation, but indeed it removes the complexity of managing them and allowing malicious actions to be performed. Increasing the position leverage with flash loans will greatly attract users, especially in bullish markets.

Notes for improvement:

  • Hardcoded UniswapV3 pool fee will render most of the PowerFarms useless due to the price impact calculation after depositing into the PendleToken contract, caused by the low liquidity pools and the price change that will be caused when swapping.
  • PENDLE_ROUTER.addLiquiditySingleSy should be able to set minLpOut parameter instead of hardcoding 0.
  • When Balancer starts taking fees on flash loans, there will be failed transactions due to the slippage caused from the 4 swaps done in _logicOpenPosition and inability to borrow exact same amount and cover the flash loan + fees.
    function _logicOpenPosition(
          bool _isAave,
          uint256 _nftId,
          uint256 _depositAmount,
          uint256 _totalDebtBalancer,
          uint256 _allowedSpread
      ) internal {
          uint256 reverseAllowedSpread = 2 * PRECISION_FACTOR_E18 - _allowedSpread;
    
          if (block.chainid == ARB_CHAIN_ID) {
              _depositAmount = _getTokensUniV3(
                  _depositAmount,
                  _getEthInTokens(ENTRY_ASSET, _depositAmount) * reverseAllowedSpread / PRECISION_FACTOR_E18,
                  WETH_ADDRESS,
                  ENTRY_ASSET
              );
          } //@audit first swap
    
          uint256 syReceived = PENDLE_SY.deposit({
              _receiver: address(this),
              _tokenIn: ENTRY_ASSET,
              _amountTokenToDeposit: _depositAmount,
              _minSharesOut: PENDLE_SY.previewDeposit(ENTRY_ASSET, _depositAmount)
          }); //@audit second swap
    
          (, uint256 netPtFromSwap,,,,) =
              PENDLE_ROUTER_STATIC.addLiquiditySingleSyStatic(address(PENDLE_MARKET), syReceived);
    
          (uint256 netLpOut,) = PENDLE_ROUTER.addLiquiditySingleSy({
              _receiver: address(this),
              _market: address(PENDLE_MARKET),
              _netSyIn: syReceived,
              _minLpOut: 0,
              _guessPtReceivedFromSy: ApproxParams({
                  guessMin: netPtFromSwap - 100,
                  guessMax: netPtFromSwap + 100,
                  guessOffchain: 0,
                  maxIteration: 50,
                  eps: 1e15
              })
          });//@audit third swap
    
          uint256 ethValueBefore = _getTokensInETH(ENTRY_ASSET, _depositAmount);
    
          (uint256 receivedShares,) = IPendleChild(PENDLE_CHILD).depositExactAmount(netLpOut);//@audit fourth swap
    
          uint256 ethValueAfter = _getTokensInETH(PENDLE_CHILD, receivedShares) * _allowedSpread / PRECISION_FACTOR_E18;
    
          if (ethValueAfter < ethValueBefore) {
              revert TooMuchValueLost();
          }
    
          WISE_LENDING.depositExactAmount(_nftId, PENDLE_CHILD, receivedShares);
    
          _borrowExactAmount(_isAave, _nftId, _totalDebtBalancer);
    
          if (_checkDebtRatio(_nftId) == false) {
              revert DebtRatioTooHigh();
          }
    
          _safeTransfer(WETH_ADDRESS, BALANCER_ADDRESS, _totalDebtBalancer);
      }

PendlePowerFarmController:

Pendle LP rewards are tracked properly and the incentive to provide Pendle tokens is a good way to have more APY. Both Controller and PowerFarmToken contracts are at a mutual benefit and made it possible for Wise users to increase their rewards.

Notes for improvement:

  • syncSupply modifier of PendlePowerFarmToken can be refactored
    • sharePrice can be calculated only by the underlyingLpAssetsCurrent , previewDistribution will return 0 after 1st call.
    • updateRewards should call redeemRewards in order to not have discrepancies with the rewards in Pendle.
modifier syncSupply() {
    _triggerIndexUpdate();
    _overWriteCheck();
    _syncSupply();
    _updateRewards();
    _setLastInteraction();
    _increaseCardinalityNext();
    uint256 sharePriceBefore = _getSharePrice();
    _;
    _validateSharePriceGrowth(_validateSharePrice(sharePriceBefore));
}
  • state variables holding the same addresses should be merged into single ones and reused on all places, such as PENDLE_MARKET and UNDERLYING_PENDLE_MARKET, PENDLE_CONTROLLER and PENDLE_POWER_FARM_CONTROLLER
  • most of the functions in PendlePowerFarmToken will cost huge amount of gas to be called because of the syncSupply making ~25 external calls.
  • _getSharePrice can use only underlyingLpAssetsCurrent, without calling previewUnderlyingLpAssets, because it will return 0 in a subsequent calls.
  • lockPendle misses chain check, as locking in vePENDLE can happen only on Ethereum.

WrapperHub:

This wrapper contracts utilizing Aave , reduce the complexity of a user’s side to first receive ATokens then deposit them in Wise in order to earn additional APY. They make the process straightforward, containing all the needed logic to ensure seamless lending and borrowing of ATokens. Nothing significant to be improved there, only events should be renamed. Approvals are done on configuring in setAaveTokenAddress, removing the need of additional Aave pool approvals on supply.

WiseSecurity:

These contracts are implemented well, with big set of functions, used all across the codebase, proper access control is applied. Both WiseSecurity and WiseSecurityHelper contain the right set of functions, also separated by access modifiers.

Notes for improvement:

  • getLiveDebtRatio should not revert on blacklisted tokens, because liquidations are possible for them intentionally

Codebase Quality Analysis

We consider the quality of WiseLending codebase as a bit overegineered, but on other hand developers managed to keep the overall complexity medium.

All the contracts are modular, each one serving its own purpose, there are contracts such as WiseSecurity and PositionNFTs that all others reuse. They are implemented well with a low likelihood denial-of-service to be caused, blocking the entire application.

Approach taken for the lending and borrowing part of the system is good, it handles the bad debt realization different from the other protocols.

Single entity controls the entire protocol which exposes risk if it gets compromised or lost.

Tests are extensive but without any structure and proper order, making them extremely hard to be understood and auditor to grasp the flows of the actions in the system, additionally using two testing frameworks (hardhat and foundry), increases the complexity and should be avoided.

Codebase Quality CategoriesComments
Code Maintainability and ReliabilityThe WiseLending contracts demonstrate good maintainability through modular structure, consistent naming, and efficient use of libraries. It also prioritizes reliability by handling errors, and conducting security checks. However, for enhanced reliability, consider professional security audits and documentation improvements. Regular updates are crucial in the evolving space.
Code CommentsWhile comments provided high-level context for certain constructs, lower-level logic flows and variables were not fully explained within the code itself. Following best practices like NatSpec could strengthen self-documentation. As an auditor without additional context, it was challenging to analyze code sections without external reference docs. To further understand the implementation intent for those complex parts, referencing supplemental documentation was necessary.
DocumentationMostly non-technical documentation is provided on the WiseLending website and a decent amount of known issues are on the contest page, it would be more beneficial for both sponsors and auditors to have more technical documentation, explaining the scenarios in the system.
Code Structure and FormattingThe codebase contracts are well-structured and formatted. but some order of functions do not follow the Solidity Style Guide According to the Solidity Style Guide, functions should be grouped according to their visibility and order: constructor, receive, fallback, external, public, internal, and private. Within a grouping, place the view and pure functions last.

Test analysis

The overall test coverage is high, most of them are not really unit tests but more of a component testing. Development team should put additional effort to convert all the Hardhat tests to Foundry, as it will make subsequent audits more easier for both parties. Additionally tests, should be even more modular, testing each function in isolation.

Fuzz & Invariant testing will suit this type of protocol, so that type of service should be considered, even 5% improvement is worth it in the world of smart contracts.

Using both mocks and forks speaks well for the tests as they are put into real environment. After the exploit, there are separate files focusing on critical parts of the code, which is also good.

On the other hand, we faced difficulties executing Hardhat tests, as most of them either were no compiling or reverting because of the wrong asserts in all files.

Systemic & Centralization Risks

As stated in the protocol’s documentation - decentralization is a key element of the ecosystem and they are working towards it.

WiseLending consist of small amount of roles, with one that controls everything across the contracts - master.

Leaving everything to governance will be the best possible approach but due to the nature of the protocol the chosen approach finds the sweet point.

However, having single entity responsible for everything can expose certain centralization risks, as it can be compromised or even behave maliciously (Wonderland rugpull), but observing the contracts we can see that there is no functions that can make this easily achievable, without going unnoticed by users.

Master user can set two other roles inside the WiseLending:

  • Beneficials in FeeManager
  • SecurityWorkers in WiseSecurity

securityWorkers can put the pool markets in a bad state if they decide, since they can freely pause the protocol.

Protocol admins should consider giving some of these roles to the governance in the future, when test period passes and everything is settled and working seamlessly.

All important functions have proper access controls minimising the third party systemic and centralization risks.

Team Findings

SeverityFindings
High0
Medium5
Low/NC7

Most of the findings were identified in the WiseLending and PendlePowerFarm.

The ones with a significant impact are:

  • PowerFarm enters will fail when Balancer starts taking fees on flash loans.
  • Small positions without incentive for liquidations will lead to insolvency of the entire pool token market.
  • All transactions in PendlePowerFarmToken will consume big amount of gas, possibly resulting in a DOS for the contract when many rewardTokens are presented in PendleMarket.
  • Liquidator will brick the entire pool market, also stealing from the subsequent depositors.

New insights and learning from this audit

Learned about:

Conclusion

In general, WiseLending protocol presents a well-designed but a bit overengineered architecture. It exhibits, such as modularity, entity separation, proven integrations with third-party protocols and we believe the team has done a good job regarding the code. However, there are areas for improvement, including tests, code documentation and extensive technical documentation. Systemic and Centralization risks do not pose any significant impact to the system.

Main code recommendations include simplifying redundant functions, if-statements rework, and extracting redundant state variables, holding same addresses.

It is also highly recommended that the team continues to invest in security measures such as mitigation reviews, audits, and bug bounty programs to maintain the security and reliability of the project.

Time spent:

70 hours

#0 - c4-pre-sort

2024-03-17T10:51:42Z

GalloDaSballo marked the issue as sufficient quality report

#1 - c4-pre-sort

2024-03-18T16:23:10Z

GalloDaSballo marked the issue as high quality report

#2 - c4-judge

2024-03-26T12:47:55Z

trust1995 marked the issue as grade-b

#3 - blckhv

2024-03-27T12:21:22Z

Hey @trust1995, Thanks for the fast judging process, I would like to ask why this analysis is marked as grade-b when it follows all the outlined judging criteria here.

  • There is no code functionality repeated
  • All Wise Lending parts described have recommendations
  • No template writing, without explicitly saying which part of the code they target
  • Codebase quality is evaluated
  • Potential weak points are explained, according to my understanding of the codebase

Will be happy to have this analysis re-reviewed, thanks!

#4 - trust1995

2024-03-27T14:08:11Z

Firstly, your analysis was good! Very few made it to A/B grade. What you can improve on is providing more actionable content - improvement points, high-depth insight and so on.

#5 - blckhv

2024-03-27T14:17:24Z

Will definitely work on that, but still I think compared to other reports it stands slightly better, mainly for not using text from the contest's readme and one-fit-all text as well as information available in Solidity Metrics extension.

#6 - Slavchew

2024-03-27T14:57:57Z

I might also add that in all the other 'grade b' analyses most of the text explains the code structure, code flows, and diagrams generated by vs code extensions, except ours and other 'grade a' ones where the emphasis is on recommendations and problems.

#7 - radeveth

2024-03-29T12:21:27Z

Hey, @trust1995!

Yes, this analysis report is not the best, but compared to other grade b analysis reports, it is for grade a. The report contains only pros and valuable information about the protocol, emphasizing the recommendations and problems of the protocol instead of writing code structure, code flows and diagrams generated by vs code extensions that simply do not provide any value to the protocol.

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