Skip to content

Commit

Permalink
fix: issue-109
Browse files Browse the repository at this point in the history
  • Loading branch information
fann95 committed Nov 2, 2023
1 parent cb4d91f commit bf84623
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 92 deletions.
11 changes: 6 additions & 5 deletions contracts/LiquidityBorrowingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ contract LiquidityBorrowingManager is
SwapParams externalSwap;
/// @notice The unique borrowing key associated with the loan
bytes32 borrowingKey;
/// @notice The slippage allowance for the swap in basis points (1/10th of a percent)
uint256 swapSlippageBP1000;
/// @dev sqrtPriceLimitX96 The Q64.96 sqrt price limit. when liquidity is restored, a hold token is sold therefore,
/// If zeroForSaleToken==false, the price cannot be less than this value after the swap.
/// If zeroForSaleToken==true, the price cannot be greater than this value after the swap
uint160 sqrtPriceLimitX96;
}
/// borrowingKey=>LoanInfo
mapping(bytes32 => LoanInfo[]) public loansInfo;
Expand Down Expand Up @@ -681,7 +683,7 @@ contract LiquidityBorrowingManager is
RestoreLiquidityParams({
zeroForSaleToken: zeroForSaleToken,
fee: params.internalSwapPoolfee,
slippageBP1000: params.swapSlippageBP1000,
sqrtPriceLimitX96: params.sqrtPriceLimitX96,
totalfeesOwed: borrowing.feesOwed,
totalBorrowedAmount: borrowing.borrowedAmount
}),
Expand Down Expand Up @@ -941,8 +943,7 @@ contract LiquidityBorrowingManager is
fee: params.internalSwapPoolfee,
tokenIn: params.saleToken,
tokenOut: params.holdToken,
amountIn: saleTokenBalance,
amountOutMinimum: 0
amountIn: saleTokenBalance
})
);
}
Expand Down
10 changes: 1 addition & 9 deletions contracts/abstract/ApproveSwapAndPay.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ abstract contract ApproveSwapAndPay {
address tokenOut;
/// @dev The amount of `tokenIn` to be swapped.
uint256 amountIn;
/// @dev The minimum amount of `tokenOut` expected to receive from the swap.
uint256 amountOutMinimum;
}

/// @notice Struct to hold parameters for swapping tokens
Expand Down Expand Up @@ -201,8 +199,6 @@ abstract contract ApproveSwapAndPay {
* @return amountOut The amount of tokens received as output from the swap.
* @notice This internal function swaps the exact amount of `params.amountIn` tokens from `params.tokenIn` to `params.tokenOut`.
* The swapped amount is calculated based on the current pool ratio between `params.tokenIn` and `params.tokenOut`.
* If the resulting `amountOut` is less than `params.amountOutMinimum`, the function will revert with a `SwapSlippageCheckError`
* indicating the minimum expected amount was not met.
*/
function _v3SwapExactInput(
v3SwapExactInputParams memory params
Expand All @@ -217,15 +213,11 @@ abstract contract ApproveSwapAndPay {
address(this), //recipient
zeroForTokenIn,
params.amountIn.toInt256(),
(zeroForTokenIn ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1),
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);
}
}

/**
Expand Down
130 changes: 104 additions & 26 deletions contracts/abstract/LiquidityManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import "../interfaces/IQuoterV2.sol";
import "./ApproveSwapAndPay.sol";
import "../Vault.sol";
import { Constants } from "../libraries/Constants.sol";
import { ErrLib } from "../libraries/ErrLib.sol";

abstract contract LiquidityManager is ApproveSwapAndPay {
using { ErrLib.revertError } for bool;
/**
* @notice Represents information about a loan.
* @dev This struct is used to store liquidity and tokenId for a loan.
Expand Down Expand Up @@ -36,7 +38,7 @@ abstract contract LiquidityManager is ApproveSwapAndPay {
struct RestoreLiquidityParams {
bool zeroForSaleToken;
uint24 fee;
uint256 slippageBP1000;
uint160 sqrtPriceLimitX96;
uint256 totalfeesOwed;
uint256 totalBorrowedAmount;
}
Expand Down Expand Up @@ -219,6 +221,65 @@ abstract contract LiquidityManager is ApproveSwapAndPay {
}
}

/**
* @dev This function is used to simulate a swap operation.
*
* It quotes the exact input single for the swap using the `underlyingQuoterV2` contract.
*
* @param fee The pool's fee in hundredths of a bip, i.e. 1e-6
* @param tokenIn The address of the token being used as input for the swap.
* @param tokenOut The address of the token being received as output from the swap.
* @param amountIn The amount of tokenIn to be used as input for the swap.
*
* @return sqrtPriceX96After The square root price after the swap.
* @return amountOut The amount of tokenOut received as output from the swap.
*/
function _simulateSwap(
uint24 fee,
address tokenIn,
address tokenOut,
uint256 amountIn
) private returns (uint160 sqrtPriceX96After, uint256 amountOut) {
// Quote exact input single for swap
(amountOut, sqrtPriceX96After, , ) = underlyingQuoterV2.quoteExactInputSingle(
IQuoterV2.QuoteExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
amountIn: amountIn,
fee: fee,
sqrtPriceLimitX96: 0
})
);
}

/**
* @dev This function is used to prevent front-running during a swap.
*
* We do not check slippage during a swap as we need to restore liquidity anyway despite the losses,
* so we only check the initial price state in the pool to prevent price manipulation.
*
* When liquidity is restored, a hold token is sold therefore,
* - If `zeroForSaleToken` is `false`, the current `sqrtPrice` cannot be less than `sqrtPriceLimitX96`.
* - If `zeroForSaleToken` is `true`, the current `sqrtPrice` cannot be greater than `sqrtPriceLimitX96`.
*
* @param zeroForSaleToken A boolean indicating whether the sale token is zero or not.
* @param fee The fee for the swap.
* @param sqrtPriceLimitX96 The square root price limit for the swap.
* @param saleToken The address of the token being sold.
* @param holdToken The address of the token being held.
*/
function _frontRunningAttackPrevent(
bool zeroForSaleToken,
uint24 fee,
uint160 sqrtPriceLimitX96,
address saleToken,
address holdToken
) internal view {
uint160 sqrtPriceX96 = _getCurrentSqrtPriceX96(zeroForSaleToken, saleToken, holdToken, fee);
(zeroForSaleToken ? sqrtPriceX96 > sqrtPriceLimitX96 : sqrtPriceX96 < sqrtPriceLimitX96)
.revertError(ErrLib.ErrorCode.UNACCEPTABLE_SQRT_PRICE);
}

/**
* @dev Restores liquidity from loans.
* @param params The RestoreLiquidityParams struct containing restoration parameters.
Expand Down Expand Up @@ -251,48 +312,63 @@ abstract contract LiquidityManager is ApproveSwapAndPay {
);

if (holdTokenAmountIn > 0) {
// Quote exact input single for swap
uint256 saleTokenAmountOut;
(saleTokenAmountOut, cache.sqrtPriceX96, , ) = underlyingQuoterV2
.quoteExactInputSingle(
IQuoterV2.QuoteExactInputSingleParams({
tokenIn: cache.holdToken,
tokenOut: cache.saleToken,
amountIn: holdTokenAmountIn,
fee: params.fee,
sqrtPriceLimitX96: 0
})
if (params.sqrtPriceLimitX96 != 0) {
_frontRunningAttackPrevent(
params.zeroForSaleToken,
params.fee,
params.sqrtPriceLimitX96,
cache.saleToken,
cache.holdToken
);

}
// Perform external swap if external swap target is provided
if (externalSwap.swapTarget != address(0)) {
uint256 saleTokenAmountOut;
if (params.sqrtPriceLimitX96 != 0) {
(, saleTokenAmountOut) = _simulateSwap(
params.fee,
cache.holdToken,
cache.saleToken,
holdTokenAmountIn
);
}
_patchAmountsAndCallSwap(
cache.holdToken,
cache.saleToken,
externalSwap,
holdTokenAmountIn,
(saleTokenAmountOut * params.slippageBP1000) / Constants.BPS
// The minimum amount out should not be less than with an internal pool swap.
// checking only once during the first swap when params.sqrtPriceLimitX96 != 0
saleTokenAmountOut
);
} else {
// Calculate hold token amount in again for new sqrtPriceX96
(holdTokenAmountIn, ) = _getHoldTokenAmountIn(
params.zeroForSaleToken,
cache.tickLower,
cache.tickUpper,
cache.sqrtPriceX96,
loan.liquidity,
cache.holdTokenDebt
);
// The internal swap in the same pool in which liquidity is restored.
if (params.fee == cache.fee) {
(cache.sqrtPriceX96, ) = _simulateSwap(
params.fee,
cache.holdToken,
cache.saleToken,
holdTokenAmountIn
);

// recalculate the hold token amount again for the new sqrtPriceX96
(holdTokenAmountIn, ) = _getHoldTokenAmountIn(
params.zeroForSaleToken,
cache.tickLower,
cache.tickUpper,
cache.sqrtPriceX96, // updated by IQuoterV2.QuoteExactInputSingleParams
loan.liquidity,
cache.holdTokenDebt
);
}

// Perform v3 swap exact input and update sqrtPriceX96
_v3SwapExactInput(
v3SwapExactInputParams({
fee: params.fee,
tokenIn: cache.holdToken,
tokenOut: cache.saleToken,
amountIn: holdTokenAmountIn,
amountOutMinimum: (saleTokenAmountOut * params.slippageBP1000) /
Constants.BPS
amountIn: holdTokenAmountIn
})
);
// Update the value of sqrtPriceX96 in the cache using the _getCurrentSqrtPriceX96 function
Expand All @@ -311,6 +387,8 @@ abstract contract LiquidityManager is ApproveSwapAndPay {
loan.liquidity
);
}
// the price manipulation check is carried out only once
params.sqrtPriceLimitX96 = 0;
}

// Increase liquidity and transfer liquidity owner reward
Expand Down
3 changes: 2 additions & 1 deletion contracts/libraries/ErrLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ library ErrLib {
INVALID_SWAP, //11
INVALID_CALLER, //12
UNEXPECTED_CHANGES, //13
TOO_BIG_DAILY_RATE //14
TOO_BIG_DAILY_RATE, //14
UNACCEPTABLE_SQRT_PRICE //15
}

error RevertErrorCode(ErrorCode code);
Expand Down
12 changes: 8 additions & 4 deletions contracts/mock/AggregatorMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ contract AggregatorMock {
}

function nonWhitelistedSwap(bytes calldata wrappedCallData) external {
_swap(wrappedCallData);
_swap(wrappedCallData, false);
}

function swap(bytes calldata wrappedCallData) external {
_swap(wrappedCallData);
_swap(wrappedCallData, false);
}

function _swap(bytes calldata wrappedCallData) internal {
function badswap(bytes calldata wrappedCallData) external {
_swap(wrappedCallData, true);
}

function _swap(bytes calldata wrappedCallData, bool slip) internal {
(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMin) = abi.decode(
wrappedCallData,
(address, address, uint256, uint256)
Expand All @@ -50,6 +54,6 @@ contract AggregatorMock {

require(amountOut >= amountOutMin, "AggregatorMock: price slippage check");
_safeTransferFrom(tokenIn, msg.sender, address(this), amountIn);
_safeTransfer(tokenOut, msg.sender, amountOut);
_safeTransfer(tokenOut, msg.sender, slip ? (amountOut * 100) / 10000 : amountOut);
}
}

0 comments on commit bf84623

Please sign in to comment.