2023-10-real-wagmi-judging's People
Forkers
tredesboi bugsluck tuannda1986 kaur-d23 rejalkarim66666 md-assembla natali9t9 ravenerzz deloreskbruns tomblindi akenta5678 mrparsa313 hamzaaylak sedbangi2023-10-real-wagmi-judging's Issues
feelereth - Vulnerability where the current daily rate could be unintentionally defaulted to the Constants.DEFAULT_DAILY_RATE if it has not been properly set.
feelereth
medium
Vulnerability where the current daily rate could be unintentionally defaulted to the Constants.DEFAULT_DAILY_RATE if it has not been properly set.
Summary
The _getHoldTokenRateInfo() function does assume that the currentDailyRate is already set, but defaults it to Constants.DEFAULT_DAILY_RATE if it is not set. This could potentially allow the rate to be unintentionally defaulted
Vulnerability Detail
the _getHoldTokenRateInfo() function does have a potential vulnerability where the current daily rate could be unintentionally defaulted to the Constants.DEFAULT_DAILY_RATE if it has not been properly set.
Here is how it works:
In _getHoldTokenRateInfo(), the currentDailyRate is first read from the TokenInfo struct in storage:
currentDailyRate = holdTokenRateInfo.currentDailyRate;
Then this is checked:
if (currentDailyRate == 0) {
currentDailyRate = Constants.DEFAULT_DAILY_RATE;
}
If the currentDailyRate has not been explicitly set, it will be 0. So in this case, it will get defaulted to Constants.DEFAULT_DAILY_RATE.
This could lead to unintended behavior, as any calling contracts or users expecting a specific rate to be set would instead get the default rate.
For example, if a lending contract calls this to calculate fees owed, it may end up charging less interest than intended if the rate gets defaulted.
Impact
Reduced expected revenue for lending activities due to lower interest rates. Incorrect interest fee calculations over time, leading to lost revenue for the protocol or unfair overcharging of users. The error compounds the more the contract is used.
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L42
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L43-L45
Tool used
Manual Review
Recommendation
You should explicitly prevent defaulting in _getHoldTokenRateInfo():
if (currentDailyRate == 0) {
revert("Current daily rate not set");
}
And require the calling contract to properly set the rate first via a separate write function or constructor before calling _getHoldTokenRateInfo(). This way there is no risk of accidentally relying on the default rate.
Soltho - Do not use abi.encode to interact with ERC20 functions
Soltho
medium
Do not use abi.encode to interact with ERC20 functions
Summary
Wrongly encoded calls are not detected by the compiler and revert at runtime.
Vulnerability Detail
While sometimes the contract uses the IERC20 interface to interact with tokens, other times it does use raw call encoding. This results in a incosistent implementation. Also, all the token interactions can be done safe and efficiently with specific libraries such as Solady's SafeTransferLib
/**
* @dev This internal function attempts to approve a specific amount of tokens for a spender.
* It performs a call to the `approve` function on the token contract using the provided parameters,
* and returns a boolean indicating whether the approval was successful or not.
* @param token The address of the token contract.
* @param spender The address of the spender.
* @param amount The amount of tokens to be approved.
* @return A boolean indicating whether the approval was successful or not.
*/
function _tryApprove(address token, address spender, uint256 amount) private returns (bool) {
(bool success, bytes memory data) = token.call(
abi.encodeWithSelector(IERC20.approve.selector, spender, amount)
);
return success && (data.length == 0 || abi.decode(data, (bool)));
}
Impact
Low, since the code has been tested.
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/package.json#L16
Tool used
Manual Review
Recommendation
Use Solady's SafeTransferLib
IvanFitro - _pay() will always revert in some functions for not approving VAULT_ADDRESS address to spend the tokens of the borrower.
IvanFitro
medium
_pay() will always revert in some functions for not approving VAULT_ADDRESS address to spend the tokens of the borrower.
Summary
_pay()
is employed to transfer tokens from the borrower to the VAULT_ADDRESS. To achieve this, a transferFrom()
operation is used, which transfer the tokens from the borrower's address (EOA) indicated by msg.sender() to the VAULT_ADDRESS, but the VAULT_ADDRESS is never approved to spend the tokens of the borrower.
Vulnerability Detail
In several functions _pay()
is called. These functions involve two addresses: msg.sender, representing the borrower (EOA) and VAULT_ADDRESS, which is where the tokens are intended to be transferred
_pay(
params.holdToken,
msg.sender,
VAULT_ADDRESS,
borrowingCollateral + liquidationBonus + cache.dailyRateCollateral + feesDebt
);
When the payer is not the contract, the safeTransferFrom()
function is employed to do the token transfers. However, the crucial step in this process is approving VAULT_ADDRESS to enable to expend the tokens belonging to the payer (borrower). This approval is managed by _maxApproveIfNecessary()
.
The issue arises because the _maxApproveIfNecessary()
is never invoked. This omission causes all transactions to revert.
function _pay(address token, address payer, address recipient, uint256 value) public {
if (value > 0) {
if (payer == address(this)) {
IERC20(token).safeTransfer(recipient, value);
} else {
IERC20(token).safeTransferFrom(payer, recipient, value);
}
}
}
function _maxApproveIfNecessary(address token, address spender, uint256 amount) internal {
if (IERC20(token).allowance(address(this), spender) < amount) {
if (!_tryApprove(token, spender, type(uint256).max)) {
if (!_tryApprove(token, spender, type(uint256).max - 1)) {
require(_tryApprove(token, spender, 0));
if (!_tryApprove(token, spender, type(uint256).max)) {
if (!_tryApprove(token, spender, type(uint256).max - 1)) {
true.revertError(ErrLib.ErrorCode.ERC20_APPROVE_DID_NOT_SUCCEED);
}
}
}
}
}
}
Impact
In increaseCollateralBalance()
, takeOverDebt()
and borrow()
will always revert.
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L381
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L451
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L498-L503
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L91-L104
Tool used
Manual Review
Recommendation
Before call _pay()
approve the VAULT_ADDRESS to spend the tokens of the borrower using _maxApproveIfNecessary()
.
kaysoft - Use of UniswapV3 slot0() function to get sqrtPriceLimitX96 can lead to price manipulation.
kaysoft
high
Use of UniswapV3 slot0() function to get sqrtPriceLimitX96 can lead to price manipulation.
Summary
The UniswapV3 slot0() function is used to get the sqrtPriceX96 as shown below.
function _getCurrentSqrtPriceX96(
bool zeroForA,
address tokenA,
address tokenB,
uint24 fee
) private view returns (uint160 sqrtPriceX96) {
if (!zeroForA) {
(tokenA, tokenB) = (tokenB, tokenA);
}
address poolAddress = computePoolAddress(tokenA, tokenB, fee);
(sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0();//@audit prone to price manipulation search slot0
}
Vulnerability Detail
The _getCurrentSqrtPriceX96(...) function above is used in _getHoldTokenAmountIn to calculate values which were used for swap here: https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L229-L308
LINK TO THE UNISWAP SLOT0 FUNCTION USED TO GET sqrtPriceX96
- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L341
- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L507
Impact
Using Uniswap's slot0 function to get sqrtPriceX96 can be manipulated which will cause loss of asset for the user.
Code Snippet
Tool used
Manual Review
Recommendation
Use the TWAP to get the value of sqrtPriceX96
Reference
Duplicate of #109
feelereth - Malicious users can avoid liquidation penalties by setting the liquidation bonus to 0
feelereth
high
Malicious users can avoid liquidation penalties by setting the liquidation bonus to 0
Summary
The liquidation bonus calculations make assumptions about default values if no bonus is set. An attacker could set the bonus to 0 to avoid penalties.
Vulnerability Detail
An attacker could manipulate the liquidation bonus calculation to avoid penalties by setting the bonus to 0. Here is an explanation:
The getLiquidationBonus function first retrieves the Liquidation struct for the given token:
Liquidation memory liq = liquidationBonusForToken[token];
It then checks if the bonusBP field is 0:
if (liq.bonusBP == 0) {
// use default bonus
}
If bonusBP is 0, it will use the default bonus defined in the Constants contract instead of applying a token-specific bonus.
An attacker could exploit this by:
-
Calling setLiquidationBonus as the owner to set bonusBP to 0 for a token:
setLiquidationBonus(token, 0, 0);
-
Borrowing that token.
-
Defaulting on the loan.
When the loan is liquidated, getLiquidationBonus will be called and use the default bonus instead of applying a higher token-specific bonus. This allows the attacker to avoid the intended larger liquidation penalty
Impact
Attackers could borrow assets without being penalized for defaulting. This makes lending riskier and less viable, and exposes lenders to potential exploitation.
Code Snippet
Tool used
Manual Review
Recommendation
getLiquidationBonus could be changed to enforce a minimum bonus percentage if the token-specific bonus is 0:
function getLiquidationBonus(
address token,
uint256 borrowedAmount,
uint256 times
) public view returns (uint256 liquidationBonus) {
Liquidation memory liq = liquidationBonusForToken[token];
if (liq.bonusBP == 0) {
// Enforce minimum bonus
liq.bonusBP = MINIMUM_BONUS_BP;
}
// Rest of function
}
This would prevent attackers from avoiding liquidation penalties by setting the bonus to 0.
feelereth - Blindly adding 1 to amount0 and amount1 in _increaseLiquidity() can result in providing more liquidity than intended.
feelereth
high
Blindly adding 1 to amount0 and amount1 in _increaseLiquidity() can result in providing more liquidity than intended.
Summary
When calculating amounts for increaseLiquidity(), it blindly adds 1 to amount0 and amount1. This could result in providing more liquidity than intended if those amounts were already at non-zero value.
Vulnerability Detail
Blindly adding 1 to amount0 and amount1 in _increaseLiquidity() can result in providing more liquidity than intended.
The relevant code is:
function _increaseLiquidity(
address saleToken,
address holdToken,
LoanInfo memory loan,
uint256 amount0,
uint256 amount1
) private {
if (amount0 > 0) ++amount0;
if (amount1 > 0) ++amount1;
// Call increaseLiquidity with increased amounts
}
Here is how it can lead to excess liquidity:
• Let's say amount0 and amount1 originally had non-zero values, say 100 and 200 respectively
• The _increaseLiquidity() function blindly adds 1 to these amounts
• So now amount0 becomes 101 and amount1 becomes 201
• These new inflated amounts are passed to increaseLiquidity()
• More liquidity will be added than required, based on the original amount0 and amount1 values of 100 and 200
This introduces a vulnerability where an attacker could extract some liquidity from the position, and then exploit this blind increment to add more liquidity than they returned, effectively stealing funds.
Impact
The impact is that if amount0 and amount1 already had non-zero values before calling _increaseLiquidity(), adding 1 to those values would increase the liquidity provided.
For example, if amount0 was 10 and amount1 was 20 before calling this function, they would be increased to 11 and 21 respectively. So the final liquidity provided would be higher than if the original amounts were used.
This introduces a vulnerability where an attacker could exploit this to steal funds by providing more liquidity than they should be able to.
In summary, the blind increment allows attackers to artificially modify liquidity holdings, disrupt pool accounting, and steal funds by depositing less assets than the liquidity added.
Code Snippet
Tool used
Manual Review
Recommendation
The increment should only happen if amount0 and amount1 values are 0 originally:
if (amount0 == 0) {
++amount0;
}
if (amount1 == 0) {
++amount1;
}
This ensures the increment only happens to avoid rounding down from non-zero amounts to zero, but doesn't blindly add 1 in all cases.
So in summary, the blind increment can lead to excess liquidity vulnerability, and should be guarded to only apply when amount values are originally zero.
feelereth - completeRepayment flag just checks loans.length == 0. But loans could be manipulated inside the loop
feelereth
high
completeRepayment flag just checks loans.length == 0. But loans could be manipulated inside the loop
Summary
The key issue is that loans is a storage array that can be manipulated inside the _calculateEmergencyLoanClosure function.
Vulnerability Detail
The completeRepayment flag just checks loans.length == 0. But loans could be manipulated inside the loop, so this can not be accurate
The key issue here is that loans is a storage array that can be manipulated inside the _calculateEmergencyLoanClosure function.
Specifically, this line is problematic:
loans[i] = loans[loans.length - 1];
This replaces the loan at index i
with the last loan in the array. Then loans.pop()
is called to remove the last element.
The problem is that loans.length
is not decremented when an element is replaced, so it remains the same even though an element was removed.
This means completeRepayment = loans.length == 0
may not accurately reflect if all loans were removed or not.
For example:
loans = [A, B, C] // loans.length = 3
Replace loan A with loan C
loans = [C, B, C]
Pop last element
loans = [C, B]
loans.length still equals 3 even though A was removed
So a malicious caller could remove loans but make it appear that loans still exist by manipulating the length in this way.
Impact
This could allow attackers to steal funds, avoid repaying debts, or wrongly claim liquidation bonuses. The contract's accounting could become corrupted. In essence, it jeopardizes the integrity of the borrowing and repayment process. It could lead to loss of funds or improper accounting by the contract.
Code Snippet
Tool used
Manual Review
Recommendation
loans.length should be decremented whenever an element is removed:
function _calculateEmergencyLoanClosure(...) {
...
if (owner matches) {
loans[i] = loans[loans.length - 1];
loans.pop();
// Decrement loans.length
loans.length--;
}
...
}
Additionally, keeping a separate counter variable that tracks the number of loans removed would be more robust than relying on loans.length alone.
Bauer - Lack of slippage protection when reducing liquidity
Bauer
high
Lack of slippage protection when reducing liquidity
Summary
When calling Uniswap V3's decreaseLiquidityParams()
, both amount0Min
and amount1Min
are set to zero. This absence of minimum acceptable amounts (slippage protection) could lead to unintended consequences during liquidity reduction.
Vulnerability Detail
In the function LiquidityManager._decreaseLiquidity()
, when calling Uniswap V3's decreaseLiquidity()
, setting both amount0Min
and amount1Min
to 0 essentially means that there is no slippage protection in place. This omission of slippage protection can lead to several issues, mainly due to the lack of a minimum expected amount for each asset when decreasing liquidity in a Uniswap V3 position.
function _decreaseLiquidity(uint256 tokenId, uint128 liquidity) private {
// Call the decreaseLiquidity function of underlyingPositionManager contract
// with DecreaseLiquidityParams struct as argument
(uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(
INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: liquidity,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp
})
);
Impact
Users may incur losses
Code Snippet
Tool used
Manual Review
Recommendation
It is advisable to set appropriate minimum amounts for both amount0Min
and amount1Min
when interacting with Uniswap V3's decreaseLiquidityParams()
.
feelereth - Incorrect collateral amount due to changes between when currentDailyRate is retrieved and when the final collateralAmt is calculated
feelereth
high
Incorrect collateral amount due to changes between when currentDailyRate is retrieved and when the final collateralAmt is calculated
Summary
The currentDailyRate can change right after it is retrieved, but before the final collateralAmt is calculated. This can lead to an incorrect collateral amount
Vulnerability Detail
The currentDailyRate can change between when it is retrieved and when the final collateralAmt is calculated, leading to an incorrect collateral amount. Here is a more detailed explanation:
In the calculateCollateralAmtForLifetime function, it first retrieves the currentDailyRate for the holdToken:
(uint256 currentDailyRate, ) = _getHoldTokenRateInfo(
borrowing.saleToken,
borrowing.holdToken
);
Later in the function, it uses this currentDailyRate value to calculate the collateralAmt:
uint256 everySecond = (
FullMath.mulDivRoundingUp(
borrowing.borrowedAmount,
currentDailyRate * Constants.COLLATERAL_BALANCE_PRECISION,
1 days * Constants.BP
)
);
collateralAmt = FullMath.mulDivRoundingUp(
everySecond,
lifetimeInSeconds,
Constants.COLLATERAL_BALANCE_PRECISION
);
The issue is that between retrieving currentDailyRate and using it to calculate collateralAmt, the currentDailyRate could be updated via the updateHoldTokenDailyRate function:
function updateHoldTokenDailyRate(
address saleToken,
address holdToken,
uint256 value
) external {
// update currentDailyRate
}
So the collateralAmt calculation would be using an outdated rate.
Impact
The unpredictable collateral requirements from the race condition introduce instability, capital inefficiency, unexpected liquidations, and can erode user trust in the platform over time
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L343-L346
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L348-L360
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L211
Tool used
Manual Review
Recommendation
Ensure the currentDailyRate value used in the calculation is the latest value. This can be done by moving the collateralAmt calculation right after retrieving the currentDailyRate. A suggestive example below:
(uint256 currentDailyRate, ) = _getHoldTokenRateInfo(
borrowing.saleToken,
borrowing.holdToken
);
// calculate collateralAmt using currentDailyRate
uint256 everySecond = (
FullMath.mulDivRoundingUp(
borrowing.borrowedAmount,
currentDailyRate * Constants.COLLATERAL_BALANCE_PRECISION,
1 days * Constants.BP
)
);
uint256 collateralAmt = FullMath.mulDivRoundingUp(
everySecond,
lifetimeInSeconds,
Constants.COLLATERAL_BALANCE_PRECISION
);
This ensures the collateralAmt is calculated atomically using the latest currentDailyRate value.
IceBear - _v3SwapExactInput() lack of the deadline parameter
IceBear
medium
_v3SwapExactInput() lack of the deadline parameter
Summary
_v3SwapExactInput() lack of the deadline parameter
Vulnerability Detail
Without a deadline parameter, the transaction may sit in the mempool and be executed at a much later time potentially resulting in a worse price.
Impact
Please refer to the Uniswap V3 doc for the design of the "swapExactInputSingle" parameters.
Includes a deadline parameter to protect against long-pending transactions and wild swings in prices
In ApproveSwapAndPay.sol, when using _v3SwapExactInput() to perform a token swap using Uniswap V3 with exact input, deadline parameter should be added to avoid transactions waiting for an extended period in the mempool before execution.
Code Snippet
Tool used
Manual Review
Recommendation
Recommend the protocol add deadline check
Soltho - Confusing function
Soltho
medium
Confusing function
Summary
This confusing function is more error-prone and can make the owner set the values wrong
Vulnerability Detail
The updateSettings
function in OwnerSettings
has a very confusing parameters. It uses array of 3 uint256 values that sometimes requires the first value to be a casted address, and in other cases it only uses the first value of the array, even tough you need to provide a array of 3.
function updateSettings(ITEM _item, uint256[] calldata values) external onlyOwner {
if (_item == ITEM.LIQUIDATION_BONUS_FOR_TOKEN) {
require(values.length == 3);
if (values[1] > Constants.MAX_LIQUIDATION_BONUS) {
revert InvalidSettingsValue(values[1]);
}
if (values[2] == 0) {
revert InvalidSettingsValue(0);
}
liquidationBonusForToken[address(uint160(values[0]))] = Liquidation(
values[1],
values[2]
);
} else if (_item == ITEM.DAILY_RATE_OPERATOR) {
require(values.length == 1);
dailyRateOperator = address(uint160(values[0]));
} else {
if (_item == ITEM.PLATFORM_FEES_BP) {
require(values.length == 1);
if (values[0] > Constants.MAX_PLATFORM_FEE) {
revert InvalidSettingsValue(values[0]);
}
platformFeesBP = values[0];
} else if (_item == ITEM.DEFAULT_LIQUIDATION_BONUS) {
require(values.length == 1);
if (values[0] > Constants.MAX_LIQUIDATION_BONUS) {
revert InvalidSettingsValue(values[0]);
}
dafaultLiquidationBonusBP = values[0];
}
}
}
Impact
Medium, since it directly affects to the settings of the protocol.
Code Snippet
Tool used
Manual Review
Recommendation
Use a setter function for each of the values: dailyRateOperator
, platformFeesBP
,dafaultLiquidationBonusBP
,liquidationBonusForToken
, and get rid of the ITEM
struct
0xDetermination - DoS of lenders and gas griefing by packing tokenIdToBorrowingKeys arrays
0xDetermination
high
DoS of lenders and gas griefing by packing tokenIdToBorrowingKeys arrays
Summary
In LiquidityBorrowingManager
, tokenIdToBorrowingKeys
arrays can be packed to gas grief and cause DoS of specific loans for an arbitrary period of time.
Vulnerability Detail
LiquidityBorrowingManager.borrow()
calls the function _addKeysAndLoansInfo()
, which adds user keys to the tokenIdToBorrowingKeys
array of the borrowed-from LP position:
function _addKeysAndLoansInfo(
bool update,
bytes32 borrowingKey,
LoanInfo[] memory sourceLoans
) private {
// Get the storage reference to the loans array for the borrowing key
LoanInfo[] storage loans = loansInfo[borrowingKey];
// Iterate through the sourceLoans array
for (uint256 i; i < sourceLoans.length; ) {
// Get the current loan from the sourceLoans array
LoanInfo memory loan = sourceLoans[i];
// Get the storage reference to the tokenIdLoansKeys array for the loan's token ID
bytes32[] storage tokenIdLoansKeys = tokenIdToBorrowingKeys[loan.tokenId];
// Conditionally add or push the borrowing key to the tokenIdLoansKeys array based on the 'update' flag
update
? tokenIdLoansKeys.addKeyIfNotExists(borrowingKey)
: tokenIdLoansKeys.push(borrowingKey);
...
A user key is calculated in the Keys
library like so:
function computeBorrowingKey(
address borrower,
address saleToken,
address holdToken
) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(borrower, saleToken, holdToken));
}
So every time a new user borrows some amount from a LP token, a new borrowKey
is added to the tokenIdToBorrowingKeys[LP_Token_ID]
array. The problem is that this array is iterated through by calling iterating methods (addKeyIfNotExists()
or removeKey()
) in the Keys
library when updating a borrow (as seen in the first code block). Furthermore, emergency repays call removeKey()
in _calculateEmergencyLoanClosure()
, non-emergency repays call removeKey()
in _removeKeysAndClearStorage()
, and takeOverDebt()
calls removeKey()
in _removeKeysAndClearStorage()
. The result is that all exit/repay/liquidation methods must iterate through the array. Both of the iterating methods in the Keys
library access storage to compare array values to the key passed as argument, so every key in the array before the argument key will increase the gas cost of the transaction by (more than) a cold SLOAD
, which costs 2100 gas (https://eips.ethereum.org/EIPS/eip-2929). Library methods below:
function addKeyIfNotExists(bytes32[] storage self, bytes32 key) internal {
uint256 length = self.length;
for (uint256 i; i < length; ) {
if (self.unsafeAccess(i).value == key) {
return;
}
unchecked {
++i;
}
}
self.push(key);
}
function removeKey(bytes32[] storage self, bytes32 key) internal {
uint256 length = self.length;
for (uint256 i; i < length; ) {
if (self.unsafeAccess(i).value == key) {
self.unsafeAccess(i).value = self.unsafeAccess(length - 1).value;
self.pop();
break;
}
unchecked {
++i;
}
}
}
Let's give an example to see the potential impact and cost of the attack:
- An LP provider authorizes the contract to give loans from their large position. Let's say USDC/WETH pool.
- The attacker sees this and takes out minimum borrows of USDC using different addresses to pack the position's
tokenIdToBorrowingKeys
array. InConstants.sol
,MINIMUM_BORROWED_AMOUNT = 100000
so the minimum borrow is $0.1 dollars since USDC has 6 decimal places. Add this to the estimated gas cost of the borrow transaction, let's say $3.9 dollars. The cost to add one key to the array is approx. $4. The max block gas limit on ethereum mainnet is30,000,000
, so divide that by 2000 gas, the approximate gas increase for one key added to the array. The result is 15,000, therefore the attacker can spend 60000 dollars to make any new borrows from the LP position unable to be repaid, transferred, or liquidated. Any new borrow will be stuck in the contract. - The attacker now takes out a high leverage borrow on the LP position, for example $20,000 in collateral for a $1,000,000 borrow. The attacker's total expenditure is now $80,000, and the $1,000,000 from the LP is now locked in the contract for an arbitrary period of time.
- The attacker calls
increaseCollateralBalance()
on all of the spam positions. Default daily rate is .1% (max 1%), so over a year the attacker must pay 36.5% of each spam borrow amount to avoid liquidation and shortening of the array. If the gas cost of increasing collateral is $0.5 dollars, and the attacker spends another $0.5 dollars to increase collateral for each spam borrow, then the attacker can spend $1 on each spam borrow and keep them safe from liquidation for over 10 years for a cost of $15,000 dollars. The total attack expenditure is now $95,000. The protocol cannot easily increase the rate to hurt the attacker, because that would increase the rate for all users in the USDC/WETH market. Furthermore, the cost of the attack will not increase that much even if the daily rate is increased to the max of 1%. The attacker does not need to increase the collateral balance of the $1,000,000 borrow since repaying that borrow is DoSed. - The result is that $1,000,000 of the loaner's liquidity is locked in the contract for over 10 years for an attack cost of $95,000.
Impact
Array packing causes users to spend more gas on loans of the affected LP token. User transactions may out-of-gas revert due to increased gas costs. An attacker can lock liquidity from LPs in the contract for arbitrary periods of time for asymmetric cost favoring the attacker. The LP will earn very little fees over the period of the DoS.
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L100-L101
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L790-L826
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Keys.sol
Tool used
Manual Review
Recommendation
tokenIdToBorrowingKeys
tracks borrowing keys and is used in view functions to return info (getLenderCreditsCount() and getLenderCreditsInfo()). This functionality is easier to implement with arrays, but it can be done with mappings to reduce gas costs and prevent gas griefing and DoS attacks. For example the protocol can emit the borrows for all LP tokens and keep track of them offchain, and pass borrow IDs in an array to a view function to look them up in the mapping. Alternatively, OpenZeppelin's EnumerableSet library could be used to replace the array and keep track of all the borrows on-chain.
newt - Not using deadline while swapping on UniswapV3
newt
medium
Not using deadline while swapping on UniswapV3
Summary
While making a swap on UniswapV3 the caller should use the slippage parameter amountOutMinimum and deadline parameter to avoid losing funds. Though the slippage parameter (amountOutMinimum) is there, the deadline param is missing.
Vulnerability Detail
deadline lets the caller specify a deadline parameter that enforces a time limit by which the transaction must be executed. Without a deadline parameter, the transaction may sit in the mempool and be executed at a much later time potentially resulting in a worse price for the user.
Impact
Inconvinience for the user and might be subjected to pay more.
Code Snippet
function _v3SwapExactInput(
v3SwapExactInputParams memory params
) internal returns (uint256 amountOut) {
// Determine if tokenIn has a 0th token
bool zeroForTokenIn = params.tokenIn < params.tokenOut;
// Compute the address of the Uniswap V3 pool based on tokenIn, tokenOut, and fee
// Call the swap function on the Uniswap V3 pool contract
(int256 amount0Delta, int256 amount1Delta) = IUniswapV3Pool(
computePoolAddress(params.tokenIn, params.tokenOut, params.fee)
).swap(
address(this), //recipient
zeroForTokenIn,
params.amountIn.toInt256(),
(zeroForTokenIn ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1),
abi.encode(params.fee, params.tokenIn, params.tokenOut)
);
// Calculate the actual amount of output tokens received
amountOut = uint256(-(zeroForTokenIn ? amount1Delta : amount0Delta));
// Check if the received amount satisfies the minimum requirement
if (amountOut < params.amountOutMinimum) {
revert SwapSlippageCheckError(params.amountOutMinimum, amountOut);
}
}
Tool used
Manual Review
Recommendation
Use parameter deadline correctly to avoid loss of funds.
feelereth - The emergency loan closure logic can lead to inconsistent state and abuse compared to normal repayment.
feelereth
high
The emergency loan closure logic can lead to inconsistent state and abuse compared to normal repayment.
Summary
• Emergency loan closure can lead to inconsistent state compared to normal repayment
• Only removing the lender's loans causes this
• Should fully close out position if all loans removed
• Properly closing out state avoids inconsistencies
Vulnerability Detail
The key functions are _calculateEmergencyLoanClosure and repay:
function _calculateEmergencyLoanClosure(
// args
) private returns (
uint256 removedAmt,
uint256 feesAmt,
bool completeRepayment
) {
// Loop through loans
for(uint i = 0; i < loans.length; i++) {
// Only remove loans owned by msg.sender
// Update removedAmt and feesAmt
}
// Return removedAmt, feesAmt, and completeRepayment
}
function repay(
// args
) external {
// Call _calculateEmergencyLoanClosure
// If completeRepayment is true
// Fully close out position (clear state)
else
// Partially close out position
// Transfer funds
}
The issue is that _calculateEmergencyLoanClosure only partially closes out loans, but repay doesn't fully reset state if completeRepayment is true.
The emergency loan closure logic can lead to inconsistent state and abuse compared to normal repayment. Let's dive deep into how this works:
The key functions are _calculateEmergencyLoanClosure and repay.
In _calculateEmergencyLoanClosure, the contract loops through the loans for a borrowing position and removes any loans owned by the msg.sender (the lender). It calculates the removedAmt and feesAmt based on only the removed loans.
If all loans are removed, it sets completeRepayment = true.
The issues arise in repay function:
• If completeRepayment = true, it does NOT fully close out the position like in normal repayment. It only:
• Reduces borrowedAmount and feesOwed based on removedAmt
• Reduces totalBorrowed based on removedAmt
• Transfers removedAmt + feesAmt to lender
• So the position can still exist with borrowedAmount and feesOwed inconsistent with remaining loans
• And if completeRepayment = true, it indicates all loans removed but the position still exists
This can allow the borrower to continue using the position improperly.
For example:
• Borrower opens position with 2 loans, Loan A and Loan B
• Loan A owner (Lender A) uses emergency repayment
• This removes Loan A, and sets completeRepayment = true
• But position still exists with Loan B
• Borrower adds back more loans, increasing debt improperly
Impact
Potential for borrower abuse: If loans and collateral balances are not fully reset, a malicious borrower could try to reuse a partially closed position to take out more loans. The incomplete reset enables this potential abuse.
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L716
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532
Tool used
Manual Review
Recommendation
This can be mitigated by doing a full cleanup of the position's storage if completeRepayment is true.:
function repay(
// args
) external {
// Call _calculateEmergencyLoanClosure
if (completeRepayment) {
// Full cleanup
delete loansInfo[borrowingKey]
delete borrowingsInfo[borrowingKey]
// Etc
} else {
// Partial cleanup
}
// Transfer funds
}
This ensures the position storage is fully reset when all loans are removed.
seeques - takeOverDebt function assigns loans and adds keys to the previous borrowing key, not the new one
seeques
high
takeOverDebt function assigns loans and adds keys to the previous borrowing key, not the new one
Summary
A main goal of the LiquidityBorrowingManager.takeOverDebt()
function is to transfer ownership of the debt to a function caller if the caller pays collateral for the undercollateralized position. The position's transfer happens at _addKeysAndLoansInfo()
function. However, the argument for the function is not the newBorrowingKey
, but the old one from which the position should be transferred from.
Vulnerability Detail
takeOverDebt()
function takes a borrowingKey
as its input which is then used to remove the borrowing key from the storage and to delete all information associated with it in a corresponding call to the _removeKeysAndClearStorage()
function. Then, an initialization of a borrowing happens at the _initOrUpdateBorrowing()
function, which returns a newBorrowingKey
to which the ownership of a position should be assigned. However, the _addKeysAndLoansInfo()
function does not use it.
_removeKeysAndClearStorage(oldBorrowing.borrower, borrowingKey, oldLoans);
// Initialize a new borrowing using the same saleToken, holdToken
(
uint256 feesDebt,
bytes32 newBorrowingKey,
BorrowingInfo storage newBorrowing
) = _initOrUpdateBorrowing(
oldBorrowing.saleToken,
oldBorrowing.holdToken,
accLoanRatePerSeconds
);
// Add the new borrowing key and old loans to the newBorrowing
_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans); //@audit
Since borrowing keys are hashes of msg.sender's address and tokens, the ownership stays with the previous owner. This means that the caller of the function loses his collateral by repaying the debt for the loan which should be transferred to him but which instead stays with the previous owner.
Impact
Loss of funds.
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L429-L441
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L921
Tool used
Manual Review
Recommendation
Pass the newBorrowingKey
as an argument to _addKeysAndLoansInfo()
_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans);
Duplicate of #53
mstpr-brainbot - The internal _upRestoreLiquidityCache function is a view function and does not return the supposed struct variable "cache."
mstpr-brainbot
high
The internal _upRestoreLiquidityCache function is a view function and does not return the supposed struct variable "cache."
Summary
The internal _upRestoreLiquidityCache
function does not return anything, and it is a view function. To ensure that the function _restoreLiquidity
works as intended, the internal _upRestoreLiquidityCache
function must return the RestoreLiquidityCache memory cache
variable. This is necessary for the parent function to function correctly. Currently, these values default to Solidity's default type values, which is definitely not the intended behavior for this function.
Vulnerability Detail
Inside the _upRestoreLiquidityCache
function, there is an internal function call as follows:
Link to Code
Within this function, several variables are assigned values to populate the struct "cache," as demonstrated here:
Link to Code
However, since the "cache" struct is never returned to the parent function, all the variables that begin with "cache.variableName" default to Solidity's default type values.
Link to Code
As these values are not correctly returned, the function does not operate as intended. Consequently, all calculations within the parent function are inaccurate.
Impact
This is definitely a major bug and needs to be corrected!
Code Snippet
Tool used
Manual Review
Recommendation
Inside the _upRestoreLiquidityCache
return the "cache" variable
Milad-Sha - Unchecked return value for low level call in _tryApprove() function
Milad-Sha
medium
Unchecked return value for low level call in _tryApprove() function
Summary
Low-level calls will never throw an exception, instead they will return false if they encounter an exception, whereas contract calls will automatically throw.
Vulnerability Detail
If the return value of a low-level message call is not checked then the execution will resume even if the called contract throws an exception. If the call fails accidentally or an attacker forces the call to fail, then this may cause unexpected behavior in the subsequent program logic.
In the case that you use low-level calls, be sure to check the return value to handle possible failed calls.
Impact
Unchecked returns can cause unexpected behavior, As a result, a certain amount of tokens is not approved
Code Snippet
(bool success, bytes memory data) = token.call(
abi.encodeWithSelector(IERC20.approve.selector, spender, amount)
);
return success && (data.length == 0 || abi.decode(data, (bool)));
Tool used
Manual Review
Recommendation
Ensure that the return value of a low-level call is checked or logged.
(bool success, bytes memory data) = token.call(
abi.encodeWithSelector(IERC20.approve.selector, spender, amount)
);
if (success){
return success && (data.length == 0 || abi.decode(data, (bool)));
}
or
(bool success, bytes memory data) = token.call(
abi.encodeWithSelector(IERC20.approve.selector, spender, amount)
);
require(success, "...");
return success && (data.length == 0 || abi.decode(data, (bool)));
peanuts - Low decimal tokens such as EURS will not work as dailyRateCollateral will be overinflated
peanuts
medium
Low decimal tokens such as EURS will not work as dailyRateCollateral will be overinflated
Summary
Some tokens have an extremely low amount of decimals, like EURS which has 2 decimals. When calculating dailyRateCollateral
, low decimal tokens will result in overinflated payments.
Vulnerability Detail
dailyRateCollateral
is calculated by first calling _updateTokenRateInfo
and setting the currentDailyRate
. If there is no set amount, the currentDailyRate
will be set to 10, which is the DEFAULT_DAILY_RATE
. Once currentDailyRate
is set, the borrowedAmount
is calculated and this calculation will be executed. If the calculated dailyRateCollateral
is less than 1000, it will be set to 1000.
//@audit DailyRateCollateral calculation
cache.dailyRateCollateral = FullMath.mulDivRoundingUp(
cache.borrowedAmount,
cache.dailyRateCollateral,
Constants.BP
);
// Check if the dailyRateCollateral is less than the minimum amount defined in the Constants contract
if (cache.dailyRateCollateral < Constants.MINIMUM_AMOUNT) {
cache.dailyRateCollateral = Constants.MINIMUM_AMOUNT;
}
dailyRateCollateral
means the amount of collateral the user has to pay daily. For example, if the user borrows 1000 USDT, his daily rate collateral will be 1000e18 * 10 / 10000 = 1e18 , which is about 1 USDT. This means that the user has to pay 1 USDT per day. Since 1e18 is greater than 1000, dailyRateCollateral
will be 1e18.
However, if the borrowed token has extremely low decimals, for example EURS, and the user borrows 1000 EURS, his daily rate collateral will be 1000e2 * 10 / 10000 = 100. Since 100 < 1000, dailyRateCollateral
will be set to 1000. This means that if the user borrows 1000 EURS, he has to pay 1000 EURS, instead of 0.1% of that amount.
Since the protocol uses UniswapV3 positions, it is expected to accommodate to all types of ERC20 tokens.
Impact
Low token decimals will not work in this protocol, and if not careful, borrowers will overpay when calling borrow()
.
Code Snippet
Tool used
Manual Review
Recommendation
Recommend not adding the minimum sum. Also, recommend having a minimum amount to borrow and also probably whitelisting the types of tokens accepted to prevent low decimal issues all round.
Duplicate of #181
kutugu - Some chains do not support the solidity shanghai fork version
kutugu
medium
Some chains do not support the solidity shanghai fork version
Summary
Solidity >= 0.8.20 version introduces push0 instruction, which is still not supported by many chains, like Arbitrum and might be problematic for projects compiled.
Vulnerability Detail
pragma solidity 0.8.21;
Real Wagmi is compiled using the latest solidity version, which may cause problems on the L2 chain.
This could also become a problem if different versions of Solidity are used to compile contracts for different chains. The differences in bytecode between versions can impact the deterministic nature of contract addresses, potentially breaking counterfactuality.
Impact
- The contract may not compile/run properly
- Compiling with different versions of solidity may result in different contract addresses.
Code Snippet
Tool used
Manual Review
Recommendation
Change the Solidity compiler version to 0.8.19
Duplicate of #84
feelereth - Rounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity.
feelereth
medium
Rounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity.
Summary
Rounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity.
Vulnerability Detail
Rounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity. Here is a more detailed explanation:
The collect function in _decreaseLiquidity rounds the collected amounts to uint128:
(amount0, amount1) = underlyingPositionManager.collect(
INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: address(this),
amount0Max: uint128(amount0),
amount1Max: uint128(amount1)
})
);
Later in _increaseLiquidity, these uint128 amounts are passed directly to increaseLiquidity:
(uint128 restoredLiquidity, , ) = underlyingPositionManager.increaseLiquidity(
INonfungiblePositionManager.IncreaseLiquidityParams({
tokenId: loan.tokenId,
amount0Desired: amount0,
amount1Desired: amount1,
...
})
);
The issue is that increaseLiquidity expects the full uint256 amounts, so passing the rounded uint128 values can lead to lost precision.
For example, let's say the actual collected amounts are:
• amount0 = 10000000000000001
• amount1 = 20000000000000001
When rounded to uint128, these become:
• amount0 = 10000000000000001
• amount1 = 20000000000000000
Now the lost precision in amount1 results in less liquidity being restored than expected.
Impact
Some amount of tokens may be left unaccounted for when increasing liquidity back in _increaseLiquidity().
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L368-L374
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L398-L402
Tool used
Manual Review
Recommendation
The collect amounts should be kept as uint256 and only rounded when passing to increaseLiquidity:
// Collect amounts as uint256
(uint256 amount0, uint256 amount1) = underlyingPositionManager.collect(...);
// Only round down when passing to increaseLiquidity
(uint128 restoredLiquidity, , ) = underlyingPositionManager.increaseLiquidity(
INonfungiblePositionManager.IncreaseLiquidityParams({
amount0Desired: uint128(amount0),
amount1Desired: uint128(amount1),
...
})
);
This ensures no precision is lost before increasing liquidity. The uint128 conversion is still needed for increaseLiquidity but done as late as possible.
kaysoft - Slippage loss because `amount0Min` and `amount1Min` are set to zero
kaysoft
high
Slippage loss because amount0Min
and amount1Min
are set to zero
Summary
Setting amount0Min
and amount1Min
for increasing and adding liquidity can result in loss of funds due to sandwich attack.
- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L356-L357
- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L403-L404
Vulnerability Detail
amount0Min
and amount1Min
are set to zero in the links below which indicates that the user is okay with 100% slippage loss. The only check done afterwards is to check that amount received are not zero which can be bypassed by allowing user receive just tiny amount of asset.
INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: liquidity,
amount0Min: 0, //@audit sliipage loss.
amount1Min: 0,
deadline: block.timestamp//@audit deadline.
})
INonfungiblePositionManager.IncreaseLiquidityParams({
tokenId: loan.tokenId,
amount0Desired: amount0,
amount1Desired: amount1,
amount0Min: 0,
amount1Min: 0,//@audit slippage loss
deadline: block.timestamp
})
Impact
Loss of fund due to slippage
Code Snippet
INonfungiblePositionManager.IncreaseLiquidityParams({
tokenId: loan.tokenId,
amount0Desired: amount0,
amount1Desired: amount1,
amount0Min: 0,
amount1Min: 0,//@audit slippage loss
deadline: block.timestamp
})
Tool used
Manual Review
Recommendation
Allow amount0Min
and amount1Min
to be passed as input parameter offchain.
p-tsanev - LiquidityBorrowingManager#_addKeysAndLoansInfo() - user can have more than the allowed positions
p-tsanev
medium
LiquidityBorrowingManager#_addKeysAndLoansInfo() - user can have more than the allowed positions
Summary
The _addKeysAndLoansInfo
is used to add loans and in case of new positions, push them to an array of positions owner by a specific user. There is a constant Constants.MAX_NUM_USER_POSOTION
that limits the max positions to 10, but the check for it is incorrect, thus leading to an off-by-one error.
Vulnerability Detail
Upon calling the _addKeysAndLoansInfo
function, requesting to add loans to a newly open positions, when adding the loans they are first pushed into the loans array and then the array's length is checked against the constant for the invariant.
However, when opening a new positions the reverse is done: first the invariant is checked that the positions array's length does not exceed the limit and then the new item is pushed. Thus if we are at the limit of 10 and try to open a new positions, instead of reverting we would create an 11th one since the pushing happens after the if
check.
Impact
Broken protocol constant, above intended positions per user.
Code Snippet
Tool used
Manual Review
Recommendation
Swap the pushing operation and the if
check's orders like so:
bytes32[] storage allUserBorrowingKeys = userBorrowingKeys[msg.sender];
allUserBorrowingKeys.push(borrowingKey);
(allUserBorrowingKeys.length > Constants.MAX_NUM_USER_POSOTION).revertError(
ErrLib.ErrorCode.TOO_MANY_USER_POSITIONS
);
kevinkien - Inadequate Validation of Token Balance Data in ``getBalances`` Function
kevinkien
medium
Inadequate Validation of Token Balance Data in getBalances
Function
Summary
The getBalances
function in the provided smart contract lacks proper validation of the token balance data returned by the staticcall
. This can potentially lead to security vulnerabilities and inaccurate token balance reporting.
Vulnerability Detail
The getBalances
function iterates through an array of token addresses and uses staticcall
to invoke the balanceOf
function on each token contract. However, the code only checks the success of the staticcall
and the length of the data returned. It does not verify the actual content of the data, making it susceptible to malicious or misbehaving token contracts.
Impact
The inadequate validation of token balance data can have the following security and usability impacts:
- Security Risks: Malicious or misconfigured token contracts can return incorrect data, leading to security vulnerabilities or manipulation of reported token balances.
- Inaccurate Reporting: Users relying on the
getBalances
function may receive incorrect balance information, affecting the integrity of their transactions and decisions.
Code Snippet
function getBalances(
address[] calldata tokens
) external view returns (uint256[] memory balances) {
bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, address(this));
uint256 length = tokens.length;
balances = new uint256[](length);
for (uint256 i; i < length; ) {
(bool success, bytes memory data) = tokens[i].staticcall(callData);
require(success && data.length >= 32);
balances[i] = abi.decode(data, (uint256));
unchecked {
++i;
}
}
}
Tool used
Manual Review
Recommendation
It is recommended to enhance the security and accuracy of the getBalances function by performing thorough data validation. This can be achieved by implementing the following changes to the code:
- Check that the staticcall is successful.
- Verify the length of the data returned to ensure it matches the expected length for an uint256.
- Validate the content of the data to ensure it represents a non-negative token balance.
function getBalances(
address[] calldata tokens
) external view returns (uint256[] memory balances) {
bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, address(this));
uint256 length = tokens.length;
balances = new uint256[](length);
for (uint256 i = 0; i < length; i++) {
(bool success, bytes memory data) = tokens[i].staticcall(callData);
require(success && data.length == 32);
+ uint256 tokenBalance = abi.decode(data, (uint256));
- balances[i] = abi.decode(data, (uint256));
+ require(tokenBalance >= 0, "Negative token balance");
+ balances[i] = tokenBalance;
unchecked {
++i;
}
}
}
0xMAKEOUTHILL - Whenever a user wants to `takeOverDebt` will never work
0xMAKEOUTHILL
high
Whenever a user wants to takeOverDebt
will never work
Summary
In LiquidityBorrowingManager.sol
a user can takeOverDebt
for a specific borrower by providing the borrower's borrowingKey
:
function takeOverDebt(bytes32 borrowingKey, uint256 collateralAmt)
Vulnerability Detail
In order for a user to successfully take over a borrower's debt, he has to provide :
(collateralAmt <= minPayment).revertError(
ErrLib.ErrorCode.COLLATERAL_AMOUNT_IS_NOT_ENOUGH
);
collateralAmt
is the amount of collateral to be provided by the new borrower.
minPayment
is the minimum payment required based on the collateral balance for the old borrower.
Then loans and keys are removed from the old borrower
_removeKeysAndClearStorage(oldBorrowing.borrower, borrowingKey, oldLoans);
After that the
(uint256 feesDebt, bytes32 newBorrowingKey, BorrowingInfo storage newBorrowing) = _initOrUpdateBorrowing(
oldBorrowing.saleToken,
oldBorrowing.holdToken,
accLoanRatePerSeconds
);
is called which returns the msg.sender's (new borrower) bytes32 newBorrowingKey
then:
// Add the new borrowing key and old loans to the newBorrowing
_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);
The problem is that the old loans and the initialization of the borrower are added again to the OLD borrower because borrowingKey
is used in _addKeysAndLoansInfo
rather than the new borrower's newBorrowingKey
.
Impact
User can't take over another borrower's debt.
Code Snippet
Tool used
Manual Review
Recommendation
-- _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);
++ _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans);
Duplicate of #53
Japy69 - Anyone can block any borrowing
Japy69
high
Anyone can block any borrowing
Summary
The LiquidityBorrowingManager.sol smart contract in the Real Wagmi project has a vulnerability that can lead to a situation where no one can borrow a specific ERC20 token. It works with any ERC20 token.
Vulnerability Detail
In the LiquidityBorrowingManager.sol smart contract, specifically in the borrow function, there is a check on borrowingCollateral
. The relevant code snippet is as follows:
uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;
(borrowingCollateral > params.maxCollateral).revertError(ErrLib.ErrorCode.TOO_BIG_COLLATERAL);
The borrowingCollateral
variable is calculated as the difference between the borrowed amount and cache.holdTokenBalance
. cache.holdTokenBalance
is equivalent to the balance of the params.holdToken
token held by the contract. In a normal world, this is equivalent to the tokens the contract just received by the position used here. Since anyone can send ERC20 tokens to this contract, an attacker can manipulate cache.holdTokenBalance
by sending an amount of the params.holdToken
token to the contract directly. This manipulation can result in cache.holdTokenBalance
being larger than cache.borrowedAmount
, causing the transaction and all the next one to revert.
To PoC this vulnerability, in the test file WagmiLeverageTests.ts, we just need to modify this line
[owner.address, alice.address, bob.address, aggregatorMock.address],
by
[owner.address, alice.address, bob.address, aggregatorMock.address, borrowingManager.address],
By this modification, we also send tokens to the smart contract. Then all the tests (run npx hardhat test
) when someone borrows (and logically then other actions after borrowing) fail.
Impact
The impact of this vulnerability is significant. An attacker can effectively prevent anyone from borrowing the specific params.holdToken
token. Moreover, the cost is not high since borrowingCollateral
is normally not very high (in some cases it is equal to 1).
Code Snippet
The vulnerability comes from this line: https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L869-L872
Tool used
Manual Review
Recommendation
- Call the
balanceOf
function of the token at the beginning of the borrow function. When computingcache.holdTokenBalance
, compare it to the previous balance. - Add a function allowing to withdraw ERC20 tokens of this contract (and not in the vault!). Because the balance should be empty, in case of receipt, someone should be able to withdraw it.
Duplicate of #86
0xDetermination - Price changes after borrowing or slippage during borrowing can cause non-emergency repay() to revert
0xDetermination
medium
Price changes after borrowing or slippage during borrowing can cause non-emergency repay() to revert
Summary
Price changes after borrowing or slippage during borrowing can cause non-emergency repay() to revert. Lenders are forced to use emergency liquidation; borrowers have to use a non-obvious workaround to fix the issue since they cannot do emergency repays, and they will have to pay more fees until they figure out how to get their collateral un-stuck.
Vulnerability Detail
The Uniswap liquidity borrowed is stored in the LoanInfo
struct to restore the loaner's liquidity when repay()
is called to close/liquidate a position. When non-emergency repay()
is called, an error will be thrown if the restored liquidity is less than the liquidity stored in the LoanInfo
. See below:
//BELOW CODE IS FROM THE _increaseLiquidity() FUNCTION, WHICH IS CALLED BY repay() DURING NON EMERGENCY CALLS
// Check if the restored liquidity is less than the loan liquidity amount
// If true, revert with InvalidRestoredLiquidity exception
if (restoredLiquidity < loan.liquidity) {
...
revert InvalidRestoredLiquidity(
This protects the loaner, but the problem is that the original liquidity amount is set by the borrower and is equal to the liquidity reduced from the loaner's position, which does not account for slippage or price changes.
/**
* @dev Decreases the liquidity of a position by removing tokens. CALLED BY _extractLiquidity()
* @param tokenId The ID of the position token.
* @param liquidity The amount of liquidity to be removed. THIS VALUE IS SET BY THE BORROWER
*/
function _decreaseLiquidity(uint256 tokenId, uint128 liquidity) private {
// Call the decreaseLiquidity function of underlyingPositionManager contract
// with DecreaseLiquidityParams struct as argument
(uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(
INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: liquidity,
amount0Min: 0,
amount1Min: 0,
deadline: block.timestamp
})
);
So if the liquidity restored during repayment is smaller due to price changes or borrow slippage, repay()
will revert. Example:
- Loaner's position is 10
saleTokens
to 10holdTokens
. The price ratio of the tokens is 1:1. - Borrower calls
borrow()
to open a position, taking all the loaner's liquidity. Uniswap liquidity is calculated as$L=\sqrt{x*y}$ , so the liquidity here is$\sqrt{100}$ . - All the
saleTokens
are swapped toholdTokens
, so the balance of the loan is now 20holdTokens
. - The price ratio of the pool changes over time to 1
saleToken
to 2holdTokens
.$\sqrt{100}$ liquidity is now approx. equal to a LP position of 7saleTokens
and 14holdTokens
. - The borrower calls
repay()
, and 14holdTokens
are swapped for 7saleTokens
to prepare for restoring liquidity (liquidity should be provided at the current price/ratio). 6holdTokens
are left over. - 7
saleTokens
and only 6holdTokens
are restaked, so the liquidity restored is far below$\sqrt{100}$ .InvalidRestoredLiquidity
error is thrown.
Impact
The borrower's collateral balance is used in the restoring liquidity swap, so the borrower could call increaseCollateralBalance()
to enable repay() to work. However, there is a check in this function that prevents anyone who's not the borrower from increasing the borrower's collateral balance: (borrowing.borrowedAmount == 0 || borrowing.borrower != address(msg.sender)).revertError( ErrLib.ErrorCode.INVALID_BORROWING_KEY );
.
So borrowers will have to call increaseCollateralBalance()
(borrowers cannot do emergency liquidation) to get repay() to succeed. Loaners will be forced to use emergency liquidation for repay()
to succeed, since they can't increase the borrower's collateral balance (emergency liquidation doesn't do swapping). The protocol's functionality is not working properly, and users will also lose gas from the reverted calls to repay()
.
This is particularly an issue for borrowers, because the solution to this issue is not obvious, and there is no functionality in the protocol that makes clear how much the borrower needs to increase their collateral to successfully call repay()
. Borrowers will have their collateral stuck in the contract until they figure out the workaround, and during that time their loan will be collecting extra fees, so they will have to pay extra fees.
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L408-L425
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L344-L360
https://uniswapv3book.com/docs/introduction/uniswap-v3/#the-mathematics-of-uniswap-v3
https://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/providing-liquidity
Tool used
Manual Review
Recommendation
Add a transferFrom into repay()
so that borrowers don't need to separately call increaseCollateralBalance()
for repay()
to succeed. If their collateral funds are not sufficient to restore liquidity, throw an error notifying them that they need to approve the contract for X amount of holdToken
and have that amount in their balance.
shogoki - Protocol is not usable & possible lock of funds on zksync because of wrong address computation
shogoki
medium
Protocol is not usable & possible lock of funds on zksync because of wrong address computation
Summary
The Wagmi Leverage Protocol is supposed to be deployed on many chains, including the zksync Era chain.
On multiple occurences the address of the UniswapV3 pool to use is computed by using the deterministic way the create2
opcode for EVM works, which is different for zkSync ERA.
This results in the protocol not being correctly usable and the possibility for funds to be locked.
Vulnerability Detail
The contracts interact with UniswapV3 pools several times. The address of the pool is always computed by the contract using the UNDERLYING_V3_POOL_INIT_CODE_HASH
the UNDERLYING_V3_FACTORY_ADDRESS
and the salt
which is the hash of the tokens and the fee.
This works, as the UniswapV3 pools are created by the Factory using the create2
opcode, which gives us an deterministic address based on these parameters. (see here
However, the address derivation works different on the zkSync Era chain, as they are stating in their docs.
export function create2Address(sender: Address, bytecodeHash: BytesLike, salt: BytesLike, input: BytesLike) {
const prefix = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("zksyncCreate2"));
const inputHash = ethers.utils.keccak256(input);
const addressBytes = ethers.utils.keccak256(ethers.utils.concat([prefix, ethers.utils.zeroPad(sender, 32), salt, bytecodeHash, inputHash])).slice(26);
return ethers.utils.getAddress(addressBytes);
}
This will result in the computed address inside the contract to be wrong, which make any calls to these going to be reverted, or giving unexpected results.
As there is a possibility for the borrow
function to go work fine, as the computed address will not be used, there is a potential for getting funds into the Vault. However, these would be locked forever, as every possible code path of the repay
function is relying on the computed address of the UniswapV3 pool.
Impact
- Protocol is not usable on zkSync Era network
- Funds could be locked.
Code Snippet
Tool used
Manual Review
Recommendation
As zkEVM differs in some points from the EVM, i would consider writing a slightly adjusted version of the contract for zkEVM, in regards to the differences mentioned in their docs
Duplicate of #104
Milad-Sha - Uniswap callback is incorrectly protected
Milad-Sha
high
Uniswap callback is incorrectly protected
Summary
callback functions in Uniswap V3 are not properly protected and cause this function to always revert.
Vulnerability Detail
Uniswap callback is incorrectly protected
Considering that callback functions in Uniswap v2 or v3 are dangerous and must be properly protected, if this is not done, a malicious person can do things such as stealing money depending on the conditions.
Each callback must be validated to verify that the call originated from a genuine V3 pool. Otherwise, the pool contract would be vulnerable to attack via an EOA manipulating the callback function.
In the ApproveSwapAndPay.sol contract, authorization is done incorrectly for the uniswapV3SwapCallback function at all, and this is very dangerous.
A malicious person creates malicious contract and passes arbitrary data which calls the malicious contract.
The malicious Pool calls back the ApproveSwapAndPay.uniswapV3SwapCallback function by swapping and considering that authentication is done incorrectly here, it can easily steal users' assets and funds.
Unsafe downcast
When a type is downcast to a smaller type, the higher order bits are truncated, effectively applying a modulo to the original value. Without any other checks, this wrapping will lead to unexpected behavior and bugs
Solidity does not check if it is safe to cast an integer to a smaller one. Unless some business logic ensures that the downcasting is safe, a library like SafeCast should be used.
According to these two introductions:
The computePoolAddress() function is implemented incorrectly and occurs in the Unsafe downcast bug, and eventually the calculations and the result of the function will be wrong.
As a result of this mistake, the check performed in the uniswapV3SwapCallback function to compare Pool Address and msg.sender will not be established and this function will probably always revert.
Impact
A miscalculation in the computePoolAddress() function causes Unsafe downcast and the output of this function is used as an authentication mechanism on the uniswapV3SwapCallback function, which leads to a wrong result and finally the uniswapV3SwapCallback function always reverts.
But in theory, it is possible to consider a case where the malicious person finds arguments that the output of the computePoolAddress function is equal to msg.sender by checking a lot. In this case a malicious person can write uniswapV3SwapCallback in such a way that a callback is made by swapping and implement the process of stealing tokens and assets of other users in this function.
Code Snippet
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata data
) external {
(amount0Delta <= 0 && amount1Delta <= 0).revertError(ErrLib.ErrorCode.INVALID_SWAP); // swaps entirely within 0-liquidity regions are not supported
(uint24 fee, address tokenIn, address tokenOut) = abi.decode(
data,
(uint24, address, address)
);
(computePoolAddress(tokenIn, tokenOut, fee) != msg.sender).revertError(
ErrLib.ErrorCode.INVALID_CALLER
);
uint256 amountToPay = amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);
_pay(tokenIn, address(this), msg.sender, amountToPay);
}
uint160(
uint256(
Tool used
Manual Review
Recommendation
To fix the problem, you can use the verifyCallback function
CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);
This line defers the validation of the LP address (which should equal msg.sender) to the CallbackValidation library. The function verifyCallback executes this code:
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
require(msg.sender == address(pool));
Where PoolAddress is another library.
The computeAddress function executes this somewhat complex check:
function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) {
require(key.token0 < key.token1);
pool = address(
uint256(
keccak256(
abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encode(key.token0, key.token1, key.fee)),
POOL_INIT_CODE_HASH
)
)
)
);
}
Or you can use the SafeCast library to prevent Unsafe downcast.
Duplicate of #158
kevinkien - Vulnerability in Address Check within the ``transferToken`` Function
kevinkien
medium
Vulnerability in Address Check within the transferToken
Function
Summary
In the transferToken
function of the application, there is no check to verify whether the _to
address is address 0 (0x0) before conducting a fund transfer, which can potentially pose a security issue.
Vulnerability Detail
Within the source code, the transferToken
function allows transferring funds from the _token
address to the _to address if the _amount
is positive. However, the function does not check the _to
address before executing the transfer. If _to
is the address 0 (0x0), the transaction will proceed without any validation, resulting in funds being sent to an invalid address, with no possibility of recovery.
Impact
Risk of fund loss: This vulnerability can lead to funds being sent to an invalid address (0x0) with no possibility of recovery.
Code Snippet
function transferToken(address _token, address _to, uint256 _amount) external onlyOwner {
if (_amount > 0) {
IERC20(_token).safeTransfer(_to, _amount);
}
}
Tool used
Manual Review
Recommendation
To address this vulnerability, it is recommended to perform a check on the _to address before executing the fund transfer. Here's an improved code snippet:
function transferToken(address _token, address _to, uint256 _amount) external onlyOwner {
require(_to != address(0), "Invalid recipient address");
require(_amount > 0, "Amount must be greater than 0");
IERC20(_token).safeTransfer(_to, _amount);
}
By adding two require statements, ensure that _to
is not address 0 and that _amount
is greater than 0 before executing the transfer.
feelereth - Borrowers can abuse the ability to repay loans while still owing fees.
feelereth
high
Borrowers can abuse the ability to repay loans while still owing fees.
Summary
The repay function in LiquidityBorrowingManager contract allows the borrower to repay their loan even if the collateralBalance is negative.
Vulnerability Detail
Borrowers are allowed to repay loans even when the collateralBalance is negative.
Here is how it works:
In the repay() function, there is a check that allows the borrower to proceed if either:
- msg.sender == borrowing.borrower OR
- collateralBalance >= 0
This means that even if the collateralBalance is negative, the borrower can still call repay() and proceed, since the first condition is satisfied (msg.sender will be the borrower).
The impact of this is that borrowers could potentially walk away without fully repaying the lender if the collateralBalance is negative. When collateralBalance is negative, it means the borrower owes more in fees/interest than what they originally deposited as collateral. By allowing them to repay even in this scenario, they can get away without paying those additional fees they owe.
The relevant code is here:
(msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(
ErrLib.ErrorCode.INVALID_CALLER
);
Impact
• Borrowers can walk away from their debt by repaying only a portion of what they owe. This leaves the lender at a loss.
• It incentivizes borrowers to take risky leveraged positions without needing to repay the full debt if the trade goes against them.
In essence - Borrowers can abuse the ability to repay loans while still owing fees.
Code Snippet
Tool used
Manual Review
Recommendation
This check should be updated to:
(msg.sender != borrowing.borrower || collateralBalance >= 0).revertError(
ErrLib.ErrorCode.INVALID_CALLER
);
By changing the AND to an OR, it will prevent borrowers from repaying unless they either are the original borrower OR have a non-negative collateral balance. This mitigates the potential vulnerability by ensuring borrowers cannot walk away without paying fees they owe if collateralBalance is negative. . This will prevent borrowers from abusing the ability to repay loans while still owing fees.
IceBear - Using block.timestamp as deadline is still dangerous
IceBear
medium
Using block.timestamp as deadline is still dangerous
Summary
Using block.timestamp as deadline is still dangerous
Vulnerability Detail
shouldn't set the deadline to block.timestamp as a validator can hold the transaction and the block it is eventually put into will be block.timestamp, so this offers no protection.
similar findings:
https://code4rena.com/reports/2023-05-maia#m-20-some-functions-in-the-talos-contracts-do-not-allow-user-to-supply-slippage-and-deadline-which-may-cause-swap-revert
Impact
It may be more profitable for a miner to deny the transaction from being mined until the transaction incurs the maximum amount of slippage.
A malicious miner can hold the transaction as deadline is set to block.timestamp which means that whenever the miner decides to include the transaction in a block, it will be valid at that time, since block.timestamp will be the current timestamp. The transaction might be left hanging in the mempool and be executed way later than the user wanted. The malicious miner can hold the transaction and execute the transaction only when he is profitable and no error would also be thrown as it will be valid at that time, since block.timestamp will be the current timestamp.
Code Snippet
Tool used
Manual Review
Recommendation
Add deadline argument
IvanFitro - Users do not pay the plataformFees
IvanFitro
medium
Users do not pay the plataformFees
Summary
_pickUpPlatformFees()
is designed to calculate protocol fees that users are required to pay. However and error in the formula to calculate it provocates that never will be paid.
Vulnerability Detail
_pickUpPlatformFees()
calulcates the fees protocol platformFees = (fees * platformFeesBP) / Constants.BP
.
Next calulates currentFees = fees - platformFees
.
However, a critical issue arises when calculating currentFees as it incorrectly subtracts the platformFees from the total user fees. This error results in the user being charged fewer fees than required, rather than more.
function _pickUpPlatformFees(
address holdToken,
uint256 fees
) private returns (uint256 currentFees) {
uint256 platformFees = (fees * platformFeesBP) / Constants.BP;
platformsFeesInfo[holdToken] += platformFees;
currentFees = fees - platformFees;
}
Impact
Users pays less fees and never pays the plataform fees.
Code Snippet
Tool used
Manual Review
Recommendation
Change the formula for the following:
currentFees = fees + platformFees
feelereth - Vulnerability where minPayment could be 0 if the collateral balance is positive, allowing takeover without proper collateral.
feelereth
high
Vulnerability where minPayment could be 0 if the collateral balance is positive, allowing takeover without proper collateral.
Summary
The calculation of minPayment depends on the collateral balance being negative. If positive, minPayment could be 0 which would allow takeover without proper collateral.
Vulnerability Detail
There is a potential vulnerability in the takeOverDebt function where minPayment could be 0 if the collateral balance is positive, allowing takeover without proper collateral.
If the collateral balance is positive, minPayment could potentially be 0 which would allow a takeover of the debt without providing proper collateral.
The key parts of the code related to this are:
-
Calculating the collateral balance:
(int256 collateralBalance, uint256 currentFees) = _calculateCollateralBalance( oldBorrowing.borrowedAmount, oldBorrowing.accLoanRatePerSeconds, oldBorrowing.dailyRateCollateralBalance, accLoanRatePerSeconds );
-
Calculating minPayment based on the collateral balance:
minPayment = (uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1;
-
Validating the provided collateralAmt against minPayment:
(collateralAmt <= minPayment).revertError( ErrLib.ErrorCode.COLLATERAL_AMOUNT_IS_NOT_ENOUGH );
The vulnerability arises because if collateralBalance is positive, minPayment will be calculated as 0 + 1 = 1.
This means the collateral validation would pass as long as collateralAmt >= 1, allowing the takeover without proper collateral.
This means if the old borrower has a positive collateral balance, the new borrower can take over the debt by only providing 1 wei of collateral, which is insecure.
Attackers could take over loans that are well collateralized by providing minimal collateral. This could drain collateral from the protocol when the attackers default on the loans.
Impact
-
It would allow someone to take over a debt without providing proper collateral, leaving the protocol undercollateralized.
-
Undercollateralization of the lending protocol over time as debts are taken over without proper collateral being provided. This could lead to insolvency of the protocol if debts are not able to be repaid.
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L422
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L423-L424
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L410-L414
Tool used
Manual Review
Recommendation
The minPayment calculation should be changed to:
minPayment = collateralBalance < 0 ?
(uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1 :
1;
This will ensure minPayment is at least 1 even if collateralBalance is positive. The takeover collateral validation will then properly enforce a minimum amount of collateral for the debt takeover.
HHK - Borrower cannot `repay()` if lender burns its NFT
HHK
high
Borrower cannot repay()
if lender burns its NFT
Summary
Lenders approve their Uniswapv3 NFTs on the wagmi contract. When borrowers borrow()
liquidity to long an asset, the NFT position's liquidity is decreased.
Later when the borrower wants to repay()
its loan, the tokens he borrowed are added back to the liquidity position. But if the liquidity position doesn't exist anymore then the call will revert not allowing borrowers to close their positions.
Vulnerability Detail
In the repay()
function of the LiquidityBorrowingManager contract, we try to recreate the liquidity position of the lenders.
This happens in the _restoreLiquidity()
function, where we pass all the loans that belong to the borrowing position.
There for each loan the function queries the NonfungiblePositionManager contract with the tokenId
to get all the infos of the position and then will try to increaseLiquidity()
.
But if one of the loan position was burned by the lender after being borrowed, the call to the NonfungiblePositionManager will revert here as the tokenId
doesn't exist anymore.
Consider this POC that can be copied and pasted in the test files (replace all tests and just keep the setup & NFT creation):
it("LEFT_OUTRANGE_TOKEN_1 borrowing liquidity (long position WBTC zeroForSaleToken = false) will be successful", async () => {
//create the borrowing position
const amountWBTC = ethers.utils.parseUnits("0.05", 8); //token0
const deadline = (await time.latest()) + 60;
const minLeverageDesired = 50;
const maxCollateralWBTC = amountWBTC.div(minLeverageDesired);
const loans = [
{
liquidity: nftpos[3].liquidity,
tokenId: nftpos[3].tokenId,
},
];
const swapParams: ApproveSwapAndPay.SwapParamsStruct = {
swapTarget: constants.AddressZero,
swapAmountInDataIndex: 0,
maxGasForCall: 0,
swapData: swapData,
};
let params: LiquidityBorrowingManager.BorrowParamsStruct = {
internalSwapPoolfee: 500,
saleToken: WETH_ADDRESS,
holdToken: WBTC_ADDRESS,
minHoldTokenOut: amountWBTC.mul(2), //<=TooLittleReceivedError
maxCollateral: maxCollateralWBTC,
externalSwap: swapParams,
loans: loans,
};
await expect(borrowingManager.connect(bob).borrow(params, deadline)).to.be.reverted;
params = {
internalSwapPoolfee: 500,
saleToken: WETH_ADDRESS,
holdToken: WBTC_ADDRESS,
minHoldTokenOut: amountWBTC,
maxCollateral: maxCollateralWBTC,
externalSwap: swapParams,
loans: loans,
};
await borrowingManager.connect(bob).borrow(params, deadline);
//Alice burns her NFT
nonfungiblePositionManager.connect(alice).burn(nftpos[3].tokenId);
});
it("repay borrowing and restore liquidity (long position WBTC zeroForSaleToken = false) will be unsuccessful because NFT burned", async () => {
const borrowingKey = await borrowingManager.userBorrowingKeys(bob.address, 0);
const deadline = (await time.latest()) + 60;
const swapParams: ApproveSwapAndPay.SwapParamsStruct = {
swapTarget: constants.AddressZero,
swapAmountInDataIndex: 0,
maxGasForCall: 0,
swapData: swapData,
};
let params = {
isEmergency: false,
internalSwapPoolfee: 500,
externalSwap: swapParams,
borrowingKey: borrowingKey,
swapSlippageBP1000: 990, //1%
};
//BOB cannot repay his loan and loose his liquidation bonus and potential profits
await expect(borrowingManager.connect(bob).repay(params, deadline)).to.be.revertedWith("Invalid token ID");
});
Impact
High.
- Borrower will not be able to close its position, loosing his liquidation bonus and potential profits.
- Position cannot be liquidated by liquidators/bots.
- Since borrower will stop paying fees, lenders that didn't go rogue will not be earning anymore until they monitor their position and find out that they need to do an emergency liquidity restoration.
Code Snippet
Tool used
Manual Review
Recommendation
Consider adding a vault/router contract that will hold the NFT positions so lenders cannot burn their positions.
Duplicate of #78
newt - Modifier lacks appropriate symbole
newt
medium
Modifier lacks appropriate symbole
Summary
In LiquidityBorrowingManager.sol the modifier in this case is suppose to check if the current block timestamp is before or equal to the deadline according to the documentation. But the modifier only checks if the blocktimestamp is before and not equal to the deadline.
Vulnerability Detail
Every function that uses checkDeadline() modifier will only check if deadline is before not on the specific deadline date.
Impact
Borrowers at borrow() line465 can only borrow tokens before the deadline and not on the deadline date same goes for repaying the loans at repay() line532. This can cause a huge inconvenience for the users.
Code Snippet
modifier checkDeadline(uint256 deadline) {
(_blockTimestamp() > deadline).revertError(ErrLib.ErrorCode.TOO_OLD_TRANSACTION);
_;
}
Tool used
Manual Review
Recommendation
Add a equal symbol to the modifier like so,
modifier checkDeadline(uint256 deadline) {
(_blockTimestamp() => deadline).revertError(ErrLib.ErrorCode.TOO_OLD_TRANSACTION);
_;
}
nmirchev8 - No slippage protection on _decreaseLiquidity call
nmirchev8
medium
No slippage protection on _decreaseLiquidity call
Summary
Inside liquidityManager
we call NonfungiblePositionManager v3 to decrease the liquidity of a position, but the problem is that we don't provide valid params for amount0Min
amount1Min
, which it the protection against slippages.
Vulnerability Detail
The only check is wether the returned values are greater than 0, which is not enough
if (amount0 == 0 && amount1 == 0) {
revert InvalidBorrowedLiquidity(tokenId);
}
You can see in the docs it is written that In production, amount0Min and amount1Min should be adjusted to create slippage protections.
Impact
This could result in potential lost of funds
Code Snippet
Tool used
Manual Review
Recommendation
Implement recommended params for slippage protection with valid values and check the returned values.
(uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(
INonfungiblePositionManager.DecreaseLiquidityParams({
tokenId: tokenId,
liquidity: liquidity,
amount0Min: validValue,
amount1Min: validValue,
deadline: block.timestamp
})
);
// Check if both amount0 and amount1 are zero after decreasing liquidity
// If true, revert with InvalidBorrowedLiquidity exception
if (amount0 < amount0Min || amount1 < amount1Min) {
revert InvalidBorrowedLiquidity(tokenId);
}
ali_shehab - DOS blocking users from opening positions on loans
ali_shehab
high
DOS blocking users from opening positions on loans
Summary
It is possible to DOS the borrow function to always revert when users call it while passing a targeted holdToken, this is done simply by transferring a small amount of the targeted holdToken directly to the LiquidityBorrowingManager, which will mess up the _getPairBalance
function and will return messed up values that violate the actual/expected balance.
Vulnerability Detail
The difference between cache.borrowedAmount
and cache.holdTokenBalance
in borrow function will always be small for example, if we print the values of cache.borrowedAmount
and cache.holdTokenBalance
you will notice it is too small.
So if a user send small amount of token to LiquidityBorrowingManager it will cause the cache.holdTokenBalance
to be greater than cache.borrowedAmount
then line
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492
will always revert when anyone try to call the borrow function.
POC:
it("poc", async () => {
const amountWBTC = ethers.utils.parseUnits("0.05", 8); //token0
const deadline = (await time.latest()) + 60;
const minLeverageDesired = 50;
const maxCollateralWBTC = amountWBTC.div(minLeverageDesired);
const loans = [
{
liquidity: nftpos[3].liquidity,
tokenId: nftpos[3].tokenId,
},
];
const swapParams: ApproveSwapAndPay.SwapParamsStruct = {
swapTarget: constants.AddressZero,
swapAmountInDataIndex: 0,
maxGasForCall: 0,
swapData: swapData,
};
const params: LiquidityBorrowingManager.BorrowParamsStruct = {
internalSwapPoolfee: 500,
saleToken: WETH_ADDRESS,
holdToken: WBTC_ADDRESS,
minHoldTokenOut: amountWBTC,
maxCollateral: maxCollateralWBTC,
externalSwap: swapParams,
loans: loans,
};
await borrowingManager.connect(bob).borrow(params, deadline);
await WBTC.connect(alice).transfer(borrowingManager.address, ethers.utils.parseUnits("1", 1));
const params2: LiquidityBorrowingManager.BorrowParamsStruct = {
...params,
loans: [
{
liquidity: nftpos[4].liquidity,
tokenId: nftpos[4].tokenId,
},
],
};
await expect(borrowingManager.connect(bob).borrow(params2, deadline)).to.be.reverted;
});
you will see that it will always revert.
Impact
DOS blocking users from opening positions on loans
Code Snippet
uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;
function _getBalance(address token) internal view returns (uint256 balance) {
bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, address(this));
(bool success, bytes memory data) = token.staticcall(callData);
require(success && data.length >= 32);
balance = abi.decode(data, (uint256));
}
Tool used
Manual Review
Recommendation
use local variables to track balances instead of balance(this)?
Duplicate of #86
jonatascm - Solidity version not run in all chains
jonatascm
high
Solidity version not run in all chains
Summary
The Solidity version 0.8.21 includes the push0 instruction, which is not supported in some chains, such as [Arbitrum](https://docs.arbitrum.io/solidity-support).
Vulnerability Detail
In newer versions of Solidity, the push0 instruction is included. However, some chains do not support this instruction. The Real Wagmi should be deployed on multiple chains. However, some of them do not support the push0 instruction. This means that the contract will be deployed but will not be executed.
Impact
Code deployed in not supported chains will not execute.
Code Snippet
In all files the solidity version is set to 0.8.21
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.21;
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L2
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L2
Tool used
Manual Review
Recommendation
Downgrade the version of solidity for deployment.
Duplicate of #84
peanuts - Max collateral check is not done when increasing collateral balance
peanuts
medium
Max collateral check is not done when increasing collateral balance
Summary
There is no max collateral check when increasing the collateral balance using increaseCollateralBalance()
Vulnerability Detail
When a user calls borrow()
, there is a check for borrowingCollateral
. The check makes sure that borrowingCollateral
is not greater than maxCollateral
File: LiquidityBorrowingManager.sol
492: uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;
493: (borrowingCollateral > params.maxCollateral).revertError(
494: ErrLib.ErrorCode.TOO_BIG_COLLATERAL
495: );
maxCollateral
is defined as the maximum amount of collateral that can be provided for the loan.
File: LiquidityBorrowingManager.sol
35: /// @notice The maximum amount of collateral that can be provided for the loan
36: uint256 maxCollateral;
However, when increaseCollateralBalance
is called, the maxCollateral
variable is not checked.
File: LiquidityBorrowingManager.sol
371: function increaseCollateralBalance(bytes32 borrowingKey, uint256 collateralAmt) external {
372: BorrowingInfo storage borrowing = borrowingsInfo[borrowingKey];
373: // Ensure that the borrowed position exists and the borrower is the message sender
374: (borrowing.borrowedAmount == 0 || borrowing.borrower != address(msg.sender)).revertError(
375: ErrLib.ErrorCode.INVALID_BORROWING_KEY
376: );
377: // Increase the daily rate collateral balance by the specified collateral amount
//@audit -- No max collateral balance check
378: borrowing.dailyRateCollateralBalance +=
379: collateralAmt *
380: Constants.COLLATERAL_BALANCE_PRECISION;
381: _pay(borrowing.holdToken, msg.sender, VAULT_ADDRESS, collateralAmt);
382: emit IncreaseCollateralBalance(msg.sender, borrowingKey, collateralAmt);
383: }
Impact
Without checking maxCollateral
, the leverage position may be unnecessarily overcollaterized.
Code Snippet
Tool used
Manual Review
Recommendation
Recommend checking the max collateral amount when calling increase collateral balance
Soltho - Using outdated OpenZeppelin libraries
Soltho
medium
Using outdated OpenZeppelin libraries
Summary
The contract is using outdated contracts
Vulnerability Detail
The protocol uses "@openzeppelin/contracts": "4.9.3"
, but the latest version is 5.0.0
Impact
Low, but should always use the latest.
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/package.json#L16
Tool used
Manual Review
Recommendation
Update to latest version
feelereth - New borrowers can manipulate the accrued loan rate per second when taking over the debt
feelereth
high
New borrowers can manipulate the accrued loan rate per second when taking over the debt
Summary
The takeOverDebt function allows the new borrower to provide the accrued loan rate instead of persisting the rate from the old borrowing
• The accrued loan rate is passed in to _initOrUpdateBorrowing from the new borrower.
• It is not inherited from the old borrowing's rate.
• This allows the new borrower to provide a lower rate, reducing their interest fees.
Vulnerability Detail
The takeOverDebt function allows the new borrower to provide the accrued loan rate instead of persisting the rate from the old borrowing
The key points are:
- In takeOverDebt, the new borrower provides the accLoanRatePerSeconds
- This is passed to _initOrUpdateBorrowing and set as the rate for the new borrowing
- The old rate is not carried over or validated against the new rate
- This allows the new borrower to manipulate the accrued rate lower than the actual rate
- Reducing the accrued rate would make their collateral requirements lower
Here is the relevant code:
function takeOverDebt(bytes32 borrowingKey, uint256 collateralAmt) external {
// New borrower provides accLoanRatePerSeconds
accLoanRatePerSeconds = holdTokenRateInfo.accLoanRatePerSeconds;
// Rate passed to _initOrUpdateBorrowing
_initOrUpdateBorrowing(
oldBorrowing.saleToken,
oldBorrowing.holdToken,
accLoanRatePerSeconds
);
}
function _initOrUpdateBorrowing(
// Sets provided rate without validation
borrowing.accLoanRatePerSeconds = accLoanRatePerSeconds;
This could allow the new borrower to take over debt cheaply by manipulating the accrued rate.
This means the new borrower can provide any value they want for accLoanRatePerSeconds.
The accLoanRatePerSeconds represents the accumulated loan interest rate per second for the debt. It is used to calculate the fees owed when closing the position.
By letting the new borrower manipulate this value on takeover, they can incorrectly calculate the fees owed to underpay when closing the position.
Impact
• It allows the new borrower to potentially commit fraud by underpaying the loan interest fees on closure.
• The lender will lose money, since the new borrower repays less than they owe due to the lower manipulated accrued loan rate.
• It compromises the integrity of the accrued loan rates in the system
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L408
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L435-L439
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L955
Tool used
Manual Review
Recommendation
It could be mitigated by persisting the rate from the previous borrowing:
// Take over debt
// Retrieve previous rate
uint256 oldRate = oldBorrowing.accLoanRatePerSeconds;
// Pass old rate to _initOrUpdateBorrowing
_initOrUpdateBorrowing(
oldBorrowing.saleToken,
oldBorrowing.holdToken,
oldRate
);
// _initOrUpdateBorrowing
// Set rate from previous borrowing
borrowing.accLoanRatePerSeconds = oldRate;
This would prevent the new borrower from manipulating the accrued loan rate.
peanuts - MAX_NUM_USER_POSOTION can be bypassed
peanuts
medium
MAX_NUM_USER_POSOTION can be bypassed
Summary
MAX_NUM_USER_POSOTION can be bypassed by 1 position.
Vulnerability Detail
In LiquidityBorrowingManager.sol
, if the user calls borrow()
or takeOverDebt()
and if the borrowed amount is 0, then the borrow position is a new position. This position will be added to the allUserBorrowingKeys
struct.
However, the check for maximum number of position is called first before pushing the array:
File: LiquidityBorrowingManager.sol
817: if (!update) {
818: // If it's a new position, ensure that the user does not have too many positions
819: bytes32[] storage allUserBorrowingKeys = userBorrowingKeys[msg.sender];
820: (allUserBorrowingKeys.length > Constants.MAX_NUM_USER_POSOTION).revertError(
821: ErrLib.ErrorCode.TOO_MANY_USER_POSITIONS
822: );
823: // Add the borrowingKey to the user's borrowing keys
824: allUserBorrowingKeys.push(borrowingKey);
825: }
If the max number of user position is 10, and there are 10 positions so far, the check will pass and a new, 11 position will be pushed into the mapping, which should not be the case.
Impact
Max user position will be overcounted by 1
Code Snippet
Tool used
Manual Review
Recommendation
Add the borrowingKey to the user's borrowing keys first before checking for the max number of position.
Soltho - ERC20 transfer return value not checked
Soltho
medium
ERC20 transfer return value not checked
Summary
ERC20 transfer
function returns a boolean, should be checked. Found in transferToken
of Vault
Vulnerability Detail
Some ERC20 tokens dont revert when transfer
fails but they return false instead.
/**
* @notice Transfers tokens to a specified address
* @param _token The address of the token to be transferred
* @param _to The address to which the tokens will be transferred
* @param _amount The amount of tokens to be transferred
*/
function transferToken(address _token, address _to, uint256 _amount) external onlyOwner {
if (_amount > 0) {
IERC20(_token).safeTransfer(_to, _amount);
}
}
Impact
Low in this case, because it doesn't affect to the protocol, but it could be worse.
Code Snippet
Tool used
Manual Review
Recommendation
Always check the return value of the transfers and require they are true
feelereth - The fees are always rounded up in _calculateCollateralBalance(), which benefits the protocol over the user
feelereth
medium
The fees are always rounded up in _calculateCollateralBalance(), which benefits the protocol over the user
Summary
Rounding fees up benefits the protocol at the cost of users paying slightly higher fees.
Vulnerability Detail
The code in _calculateCollateralBalance() does round up fees in a way that benefits the protocol over the user.
The key lines are:
currentFees = FullMath.mulDivRoundingUp(
borrowedAmount,
accLoanRatePerSeconds - borrowingAccLoanRatePerShare,
Constants.BP
);
This uses FullMath.mulDivRoundingUp() to calculate the fees. As the name suggests, this rounds the result up to the nearest integer.
Normally in division, remainders are rounded down. But here, rounding up ensures any remainder is added to the fees. This increases the fees collected by the protocol.
For example, if the actual fee calculation resulted in a value like 100.000001, rounding down would give fees of 100. But rounding up makes it 101.
Over many transactions, these small roundings add up to significant extra fees for the protocol. The user pays more fees than they should based on the exact calculation.
Impact
Users pay slightly higher fees, reducing their collateral balance.
Code Snippet
Tool used
Manual Review
Recommendation
This could be mitigated by using standard rounding instead of always rounding up:
currentFees = FullMath.mulDiv(
borrowedAmount,
accLoanRatePerSeconds - borrowingAccLoanRatePerShare,
Constants.BP
);
Now remainders are rounded to the nearest integer, rather than always up. This removes the bias and calculates fees fairly.
Bauer - Revert on Large Approvals & Transfers
Bauer
medium
Revert on Large Approvals & Transfers
Summary
Some tokens (e.g. UNI, COMP) revert if the value passed to approve or transfer is larger than uint96.
Vulnerability Detail
Some tokens (e.g. UNI, COMP) have special case logic in approve that sets allowance to type(uint96).max .
function approve(address spender, uint rawAmount) external returns (bool) {
uint96 amount;
if (rawAmount == uint(-1)) {
amount = uint96(-1);
} else {
amount = safe96(rawAmount, "Uni::approve: amount exceeds 96 bits");
}
allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
if the approval amount is type(uint256).max), which may cause issues with systems that expect the value passed to approve to be reflected in the allowances mapping.
function _maxApproveIfNecessary(address token, address spender, uint256 amount) internal {
if (IERC20(token).allowance(address(this), spender) < amount) {
if (!_tryApprove(token, spender, type(uint256).max)) {
if (!_tryApprove(token, spender, type(uint256).max - 1)) {
require(_tryApprove(token, spender, 0));
if (!_tryApprove(token, spender, type(uint256).max)) {
if (!_tryApprove(token, spender, type(uint256).max - 1)) {
true.revertError(ErrLib.ErrorCode.ERC20_APPROVE_DID_NOT_SUCCEED);
}
}
}
}
}
}
Impact
The function's multiple attempts will fail
Code Snippet
Tool used
Manual Review
Recommendation
It is advisable to dynamically adjust the approval amount.
p-tsanev - LiquidityManager.sol - usage of ``slot0`` is a bad practice
p-tsanev
medium
LiquidityManager.sol - usage of slot0
is a bad practice
Summary
The LiquidityManager
abstract contract introduces possible operations with liquidity such as adding, removing, extracting and restoring liquidity. It uses a cache variable to store various elements such as the sqrtPricex96
, which checks the spot price of the univ3 pool.
Vulnerability Detail
The slot0
or spot price has been historically proven to be a bad practice since it is easily manipulatable and core operations regarding liquidity in this codebase rely on that spot price. Large intentional trades can cause unfair price movement via front-running to shift the spot price of underlying pair assets and mess up the token amounts calculation
Impact
Wrong liquidity calculation, potential use losses
Code Snippet
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331-L342
https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L475-L514
Tool used
Manual Review
Recommendation
To make any calculation use a TWAP instead of slot0.
Duplicate of #109
p-tsanev - LiquidityBorrowingManager.sol#takeOverDebt() - wrong key used when pushing loans to the new borrow
p-tsanev
high
LiquidityBorrowingManager.sol#takeOverDebt() - wrong key used when pushing loans to the new borrow
Summary
The takeOverDebt()
function is supposed, upon met criteria, to transfer a borrowing to a different owner, by creating a new borrowing key for the mapping and passing the old loans to the new borrow struct. There is a critical flaw in this functionality, that would not allow for correct overtaking.
Vulnerability Detail
The function updates the old borrow's fees up to the moment of taking over and then generates a new key and a new borrow struct. However, when the function _addKeysAndLoansInfo
is invoked to transfer the old loans to the new key, instead of the newBorrowingKey
, the old borrowingKey
variable is passed instead.
This would lead to old loans being pushed again in the array for the old key, potentially reverting on exceeding the maximum allowed loans per positions.
Taking over would not be possible in such cases.
Impact
Impossible to take over loans, which is a core functionality of the protocol
Code Snippet
Tool used
Manual Review
Recommendation
Pass the correct value to the function as such:
_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans)
Duplicate of #53
Bauer - `_patchAmountsAndCallSwap() will not operate correctly
Bauer
high
`_patchAmountsAndCallSwap() will not operate correctly
Summary
The amountOutMin
parameter is set to 0 when passed to the _patchAmountsAndCallSwap()
function. Consequently, the subsequent check to ensure that amountOut is greater than amountOutMin will always fail.
Vulnerability Detail
The function ApproveSwapAndPay._patchAmountsAndCallSwap()
is used for executing token swaps within the protocol, ensuring that the external swap targets are appropriately approved and that the swaps meet the specified criteria.
The function verifies whether the received amount meets the minimum requirement specified by amountOutMin. If the received amount is zero or falls short of the minimum requirement, the function will revert the transaction.
amountOut = _getBalance(tokenOut) - balanceOutBefore;
// Checking if the received amount satisfies the minimum requirement
if (amountOut == 0 || amountOut < amountOutMin) {
revert SwapSlippageCheckError(amountOutMin, amountOut);
}
In the LiquidityBorrowingManager._precalculateBorrowing()
function, the amountOutMin
parameter is being set to 0 when it's passed to the _patchAmountsAndCallSwap()
function. This configuration means that the subsequent check, if (amountOut == 0 || amountOut < amountOutMin), will always fail.
if (saleTokenBalance > 0) {
if (params.externalSwap.swapTarget != address(0)) {
// Call the external swap function and update the hold token balance in the cache
cache.holdTokenBalance += _patchAmountsAndCallSwap(
params.saleToken,
params.holdToken,
params.externalSwap,
saleTokenBalance,
0
);
Impact
The subsequent check to ensure that amountOut
is greater than amountOutMin
will always fail
Code Snippet
Tool used
Manual Review
Recommendation
To address this issue, it's important to correctly set the amountOutMin parameter based on the specific requirements of the swap and the expected output.
Soltho - No slippage protection for Uniswap V3 swaps
Soltho
medium
No slippage protection for Uniswap V3 swaps
Summary
The _v3SwapExactInput
function in ApproveSwapAndPay
has fixed a fixed slipagge tolerance values(MIN_SQRT_RATIO
and MAX_SQRT_RATIO
) set. User can get a poor result from his swap and the transaction doesn't revert.
Vulnerability Detail
Contract doesn't protect users from a high slipagge when using Uniswap V3 swaps, so it can result in undesired operations being successfull , while _patchAmountsAndCallSwap
function, that allows for arbitrary calls, allows user to customize every single parameter.
(int256 amount0Delta, int256 amount1Delta) = IUniswapV3Pool(
computePoolAddress(params.tokenIn, params.tokenOut, params.fee)
).swap(
address(this), //recipient
zeroForTokenIn,
params.amountIn.toInt256(),
(zeroForTokenIn ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1),
abi.encode(params.fee, params.tokenIn, params.tokenOut)
);
Impact
Medium, since it can make undesired operations for users successfully.
Code Snippet
Tool used
Manual Review
Recommendation
Add extra slippage protection in the function, preferrably allow to introduce arbitrary sqrtPriceLimitX96
values for it, just like in _patchAmountsAndCallSwap
.
feelereth - The loop counter i is incremented without checks on the length of the tokens array. This can cause an overflow and potentially overwrite memory.
feelereth
high
The loop counter i is incremented without checks on the length of the tokens array. This can cause an overflow and potentially overwrite memory.
Summary
• Incrementing a loop counter without checks can cause overflow and memory corruption
• Should validate the counter against array length before incrementing
• Bounded increment prevents out of bounds access
Vulnerability Detail
The loop counter i
is incremented without checking the length of the tokens array. This can cause an overflow and potentially overwrite memory.
Here is an example to illustrate the issue:
function getBalances(address[] calldata tokens) external view returns (uint256[] memory balances) {
uint256 length = tokens.length;
balances = new uint256[](length);
for (uint256 i; i < length; ) {
// i is incremented without bounds checking
unchecked {
++i;
}
}
}
If tokens has a length of 0, then length will be 0. But the loop will still increment i, causing it to become 1, then 2, etc. This will overwrite memory outside the bounds of the balances array.
Impact
- Incrementing the loop counter i without checking the length of the loans array is dangerous and can lead to memory corruption.
- the unchecked loop counter increment can lead to unauthorized modifications of critical contract storage variables. This has a huge potential impact - from simple errors and bugs at best, to exploitability and stolen funds at worst.
Code Snippet
Tool used
Manual Review
Recommendation
To fix this, the increment should be bounded by the length:
for (uint256 i = 0; i < length; ) {
// do stuff
if (i < length - 1) {
++i;
}
}
Now i
can only increment up to length - 1
, preventing the overflow.
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.