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
Rank: 10/36
Findings: 4
Award: $5,772.78
π Selected for report: 1
π Solo Findings: 0
π Selected for report: SBSecurity
Also found by: 0xCiphky
2444.3801 USDC - $2,444.38
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
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.
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.
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.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; }
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
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.
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.
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".
π Selected for report: serial-coder
Also found by: SBSecurity, serial-coder
1880.2924 USDC - $1,880.29
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
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.
If we take a look at the current gas prices on Mainnet:
Priority | Low | Average | High |
---|---|---|---|
Gwei | 75 | 75 | 80 |
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:
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.
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;
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.
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
π Selected for report: WoolCentaur
Also found by: AM, SBSecurity
1128.1754 USDC - $1,128.18
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
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:
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
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 liquidationpseudoTotalPool
totalDepositShares
- reset to 0 in liquidationFrom 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.
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.
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.
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
π Selected for report: aariiif
Also found by: 0xepley, Myd, SBSecurity, fouzantanveer, foxb868, kaveyjoe
319.9291 USDC - $319.93
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:
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.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.Stage | Action | Details | Information |
---|---|---|---|
1 | Compile and Run Test | Installation | A bit complex setup, caused by the big codebase. |
2 | Documentation review | Documentation | Provides more general information showing Wise's advantages over competitors, not enough technical explanations. |
3 | Contest details | Audit Details | Lacking any contract explanation. Has an extensive known issues page, significantly lowering the attack surface. |
4 | Diagramming | Excalidraw | Drawing diagrams through the entire process of codebase evaluation. |
5 | Test Suits | Tests | Mix between Foundry and Hardhat makes the test review stage harder for the auditor. |
6 | Manual Code Review | Scope | Reviewed all the contracts in scope. |
7 | Special focus on Areas of Concern | Areas of concern | Observing the known issues and bot report. |
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:
_removePositionData
can be simplified, removing the unnecessary if statements and optimizing the execution of the function.Lasa Algorithm explanation
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:
stETH
, consider extending the functionality to support different feeds such as USD
ones.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.
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:
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.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); }
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
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)); }
PENDLE_MARKET
and UNDERLYING_PENDLE_MARKET
, PENDLE_CONTROLLER
and PENDLE_POWER_FARM_CONTROLLER
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.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.
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 intentionallyWe 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 Categories | Comments |
---|---|
Code Maintainability and Reliability | The 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 Comments | While 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. |
Documentation | Mostly 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 Formatting | The 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. |
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.
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
:
FeeManager
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.
Severity | Findings |
---|---|
High | 0 |
Medium | 5 |
Low/NC | 7 |
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.PendlePowerFarmToken
will consume big amount of gas, possibly resulting in a DOS for the contract when many rewardTokens
are presented in PendleMarket
.Learned about:
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.
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.
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.