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: 16/36
Findings: 2
Award: $1,378.00
π Selected for report: 0
π Solo Findings: 0
π Selected for report: serial-coder
Also found by: 0x11singh99, Jorgect, Mrxstrange, Rhaydden, josephdara, nonseodion, unix515
249.8161 USDC - $249.82
Some implementations of transfer and transferFrom functions could return βfalseβ on failure instead of reverting. It is safe to wrap such calls into require() statements to these failures.
There is also another important check for low level external calls, if the address doesn't not have bytecode the call success.
This is been doing for the contract in the _callOptionalReturn function:
file:/contracts/TransferHub/TransferHelper.sol function _callOptionalReturn( address token, bytes memory data ) internal returns (bool call) { ( bool success, bytes memory returndata ) = token.call( data ); bool results = returndata.length == 0 || abi.decode( returndata, (bool) ); if (success == false) { revert(); } call = success && results && token.code.length > 0; }
The problem is that the result variable is not been checked if it was false.
The safeTransfer function is not checking if the transfer and transferFrom return βfalseβ on failure instead of reverting. so if a token return false the executing of the logic is continuing but the transfer was never made it.
contract TransferHelper is CallOptionalReturn { /** * @dev * Allows to execute safe transfer for a token */ function _safeTransfer(address _token, address _to, uint256 _value) internal { _callOptionalReturn(_token, abi.encodeWithSelector(IERC20.transfer.selector, _to, _value)); } /** * @dev * Allows to execute safe transferFrom for a token */ function _safeTransferFrom(address _token, address _from, address _to, uint256 _value) internal { _callOptionalReturn( _token, abi.encodeWithSelector(IERC20.transferFrom.selector, _from, _to, _value)); } }
As you can see the safeTransfer function is not checking whether the return value of the _callOptionalReturn is true or false, in consequence is not reverting on false
Manual review
Consider implement well known and tested librarys for safe erc20 calling, The most used are SafeERC20.sol or SafeTransferLib.sol.
Oher solution is check for false and rever in this cases.
Other
#0 - c4-pre-sort
2024-03-17T14:30:20Z
GalloDaSballo marked the issue as duplicate of #212
#1 - c4-pre-sort
2024-03-18T16:27:10Z
GalloDaSballo marked the issue as sufficient quality report
#2 - c4-judge
2024-03-26T14:31:04Z
trust1995 marked the issue as satisfactory
#3 - c4-judge
2024-03-26T19:10:54Z
trust1995 changed the severity to 2 (Med Risk)
1128.1754 USDC - $1,128.18
If the total borrowed for a borrower is more than the total collateral plus some threshold they can get liquidated, that is common in all lending platforms.
When a position is ready to be liquidate, there is a important check in the wiseSecurity contract:
function liquidatePartiallyFromTokens( uint256 _nftId, uint256 _nftIdLiquidator, address _paybackToken, address _receiveToken, uint256 _shareAmountToPay ) external syncPool(_paybackToken) syncPool(_receiveToken) returns (uint256) { CoreLiquidationStruct memory data; data.nftId = _nftId; data.nftIdLiquidator = _nftIdLiquidator; data.caller = msg.sender; data.tokenToPayback = _paybackToken; data.tokenToRecieve = _receiveToken; data.shareAmountToPay = _shareAmountToPay; data.maxFeeETH = WISE_SECURITY.maxFeeETH(); data.baseRewardLiquidation = WISE_SECURITY.baseRewardLiquidation(); ( data.lendTokens, data.borrowTokens ) = _prepareAssociatedTokens( _nftId, _receiveToken, _paybackToken ); data.paybackAmount = paybackAmount( _paybackToken, _shareAmountToPay ); _checkPositionLocked( _nftId, msg.sender ); _checkLiquidatorNft( _nftId, _nftIdLiquidator ); WISE_SECURITY.checksLiquidation( <------ _nftId, _paybackToken, _shareAmountToPay ); return _coreLiquidation( data ); }
function checksLiquidation( uint256 _nftIdLiquidate, address _tokenToPayback, uint256 _shareAmountToPay ) external view { ( uint256 weightedCollateralETH, uint256 unweightedCollateralETH ) = overallETHCollateralsBoth( _nftIdLiquidate ); uint256 borrowETHTotal = overallETHBorrowHeartbeat( _nftIdLiquidate ); canLiquidate( borrowETHTotal, weightedCollateralETH ); checkMaxShares( <------ _nftIdLiquidate, _tokenToPayback, borrowETHTotal, unweightedCollateralETH, _shareAmountToPay ); }
function checkMaxShares( uint256 _nftId, address _tokenToPayback, uint256 _borrowETHTotal, uint256 _unweightedCollateralETH, uint256 _shareAmountToPay ) public view { uint256 totalSharesUser = WISE_LENDING.getPositionBorrowShares( _nftId, _tokenToPayback ); uint256 maxShares = checkBadDebtThreshold(_borrowETHTotal, _unweightedCollateralETH) ? totalSharesUser : totalSharesUser * MAX_LIQUIDATION_50 / PRECISION_FACTOR_E18; if (_shareAmountToPay <= maxShares) { <------- return; } revert TooManyShares(); }
The problem is that if a liquidator try to paid the whole position, the borrower can just payback 1 share just before the transaction of the liquidation making the transaction revert because _shareAmountToPay will be major than maxShares:
if (_shareAmountToPay <= maxShares) { <------- return; } revert TooManyShares();
User can avoid get liquidate if liquidator try to liquidate the whole amount, this can represent serious problems in the protocol due is breaking a core functionality, in the worse case can generate bad debt to the protocol.
function paybackExactShares( uint256 _nftId, address _poolToken, uint256 _shares ) external syncPool(_poolToken) returns (uint256) { uint256 repaymentAmount = paybackAmount( _poolToken, _shares ); _validateNonZero( repaymentAmount ); _handlePayback( msg.sender, _nftId, _poolToken, repaymentAmount, _shares ); _safeTransferFrom( _poolToken, msg.sender, address(this), repaymentAmount ); return repaymentAmount; }
As you can see there is not check for health or bad deb on the payback functions, which means that borrowers can pay whatever amount that he want. So borrowers can pay 1 share to make revert this transaction if the liquidator try to liquidate the whole amount:
if (_shareAmountToPay <= maxShares) { <------- return; } revert TooManyShares();
Manual review
instead of revert if _shareAmountToPay > maxShares
refund the different between _shareAmountToPay - maxShares .
Other
#0 - c4-pre-sort
2024-03-17T15:42:10Z
GalloDaSballo marked the issue as duplicate of #237
#1 - c4-pre-sort
2024-03-18T16:28:05Z
GalloDaSballo marked the issue as sufficient quality report
#2 - c4-judge
2024-03-26T19:05:29Z
trust1995 marked the issue as satisfactory