diff --git a/contracts/Liquidator/FlashSwapLiquidationOperator/FlashSwapLiquidationOperator.sol b/contracts/Liquidator/FlashSwapLiquidationOperator/FlashSwapLiquidationOperator.sol new file mode 100644 index 000000000..40e009f0d --- /dev/null +++ b/contracts/Liquidator/FlashSwapLiquidationOperator/FlashSwapLiquidationOperator.sol @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import { IPancakeV3FlashCallback } from "@pancakeswap/v3-core/contracts/interfaces/callback/IPancakeV3FlashCallback.sol"; +import { IPancakeV3SwapCallback } from "@pancakeswap/v3-core/contracts/interfaces/callback/IPancakeV3SwapCallback.sol"; +import { IPancakeV3Pool } from "@pancakeswap/v3-core/contracts/interfaces/IPancakeV3Pool.sol"; +import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import { IWBNB } from "../../Swap/interfaces/IWBNB.sol"; +import { ensureNonzeroAddress } from "../zeroAddress.sol"; +import { BUSDLiquidator } from "../BUSDLiquidator.sol"; +import { IVToken, IVBep20, IVBNB } from "../Interfaces.sol"; +import { UnifiedVTokenHandler } from "./UnifiedVTokenHandler.sol"; +import { PathExt } from "./PathExt.sol"; +import { ISmartRouter } from "./pancakeswap-v8/ISmartRouter.sol"; +import { Path } from "./pancakeswap-v8/Path.sol"; +import { PoolAddress } from "./pancakeswap-v8/PoolAddress.sol"; +import { approveOrRevert } from "../approveOrRevert.sol"; +import { MIN_SQRT_RATIO, MAX_SQRT_RATIO } from "./pancakeswap-v8/constants.sol"; + +contract FlashSwapLiquidationOperator is IPancakeV3FlashCallback, IPancakeV3SwapCallback, UnifiedVTokenHandler { + using SafeERC20Upgradeable for IERC20Upgradeable; + using Path for bytes; + using PathExt for bytes; + + /// @notice Liquidation parameters + struct FlashLiquidationParameters { + /// @notice The receiver of the liquidated collateral + address beneficiary; + /// @notice Borrower whose position is being liquidated + address borrower; + /// @notice Amount of borrowed tokens to repay + uint256 repayAmount; + /// @notice Collateral vToken to seize + IVToken vTokenCollateral; + /// @notice Reversed (!) swap path to use for liquidation. For regular (not in-kind) + /// liquidations it should start with the borrowed token and end with the collateral + /// token. For in-kind liquidations, must consist of a single PancakeSwap pool to + /// source the liquidity from. + bytes path; + /// @notice Deadline for the transaction execution + uint256 deadline; + } + + /// @notice Callback data passed to the flash or swap callback + struct CallbackData { + /// @notice Liquidation parameters + FlashLiquidationParameters params; + /// @notice Pool key of the pool that called the callback + PoolAddress.PoolKey poolKey; + } + + /// @notice The PancakeSwap SmartRouter contract + ISmartRouter public immutable swapRouter; + + /// @notice The BUSD liquidator contract + BUSDLiquidator public immutable busdLiquidator; + + /// @notice The PancakeSwap deployer contract + address public immutable deployer; + + /// @notice Thrown if the provided swap path start does not correspond to the borrowed token + /// @param expected Expected swap path start (borrowed token) + /// @param actual Provided swap path start + error InvalidSwapStart(address expected, address actual); + + /// @notice Thrown if the provided swap path end does not correspond to the collateral token + /// @param expected Expected swap path end (collateral token) + /// @param actual Provided swap path end + error InvalidSwapEnd(address expected, address actual); + + /// @notice Thrown if flash callback or swap callback is called by a non-PancakeSwap contract + /// @param expected Expected callback sender (pool address computed based on the pool key) + /// @param actual Actual callback sender + error InvalidCallbackSender(address expected, address actual); + + /// @notice Thrown if received a native asset from any account except vNative + /// @param expected Expected asset sender, vNative + /// @param actual Actual asset sender + error InvalidNativeAssetSender(address expected, address actual); + + /// @notice Thrown if the flash callback is unexpectedly called when performing a cross-token liquidation + error FlashNotInKind(); + + /// @notice Thrown if the swap callback is called with unexpected or zero amount of tokens + error EmptySwap(); + + /// @notice Thrown if the deadline has passed + error DeadlinePassed(uint256 currentTimestamp, uint256 deadline); + + /// @param vNative_ vToken that wraps the native asset + /// @param swapRouter_ PancakeSwap SmartRouter contract + /// @param busdLiquidator_ BUSD liquidator contract + constructor( + IVBNB vNative_, + ISmartRouter swapRouter_, + BUSDLiquidator busdLiquidator_ + ) UnifiedVTokenHandler(vNative_, IWBNB(swapRouter_.WETH9())) { + ensureNonzeroAddress(address(swapRouter_)); + ensureNonzeroAddress(address(busdLiquidator_)); + + swapRouter = swapRouter_; + busdLiquidator = busdLiquidator_; + deployer = swapRouter_.deployer(); + } + + /// @notice A function that receives native assets from vNative + receive() external payable { + if (msg.sender != address(vNative)) { + revert InvalidNativeAssetSender(address(vNative), msg.sender); + } + } + + /// @notice Liquidates a borrower's position using flash swap + /// @param params Liquidation parameters + function liquidate(FlashLiquidationParameters calldata params) external { + if (params.deadline < block.timestamp) { + revert DeadlinePassed(block.timestamp, params.deadline); + } + + address borrowedTokenAddress = address(borrowedToken()); + address collateralTokenAddress = address(_underlying(params.vTokenCollateral)); + + (address startTokenA, address startTokenB, uint24 fee) = params.path.decodeFirstPool(); + if (startTokenA != borrowedTokenAddress) { + revert InvalidSwapStart(borrowedTokenAddress, startTokenA); + } + + PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey(startTokenA, startTokenB, fee); + + if (collateralTokenAddress == borrowedTokenAddress) { + _flashLiquidateInKind(poolKey, params); + } else { + (, address endToken, ) = params.path.decodeLastPool(); + if (endToken != collateralTokenAddress) { + revert InvalidSwapEnd(collateralTokenAddress, endToken); + } + _flashLiquidateCross(poolKey, params); + } + } + + /// @notice Callback called by PancakeSwap pool during in-kind liquidation. Liquidates the + /// borrow, seizing vTokens with the same underlying as the borrowed asset, redeems these + /// vTokens and repays the flash swap. + /// @param fee0 Fee amount in pool's token0 + /// @param fee1 Fee amount in pool's token1 + /// @param data Callback data, passed during _flashLiquidateInKind + function pancakeV3FlashCallback(uint256 fee0, uint256 fee1, bytes memory data) external { + CallbackData memory decoded = abi.decode(data, (CallbackData)); + _verifyCallback(decoded.poolKey); + + FlashLiquidationParameters memory params = decoded.params; + IERC20Upgradeable collateralToken = _underlying(params.vTokenCollateral); + if (address(collateralToken) != address(borrowedToken())) { + revert FlashNotInKind(); + } + + uint256 receivedAmount = _liquidateAndRedeem(params.borrower, params.repayAmount, params.vTokenCollateral); + + uint256 fee = (fee0 == 0 ? fee1 : fee0); + approveOrRevert(collateralToken, address(swapRouter), receivedAmount); + collateralToken.safeTransfer(msg.sender, params.repayAmount + fee); + collateralToken.safeTransfer(params.beneficiary, collateralToken.balanceOf(address(this))); + } + + /// @notice Callback called by PancakeSwap pool during regular (cross-token) liquidations. + /// Liquidates the borrow, seizing vTokenCollateral, redeems these vTokens and repays the flash swap. + /// If necessary, swaps the collateral tokens to the pool tokens to repay the flash swap. + /// @param amount0Delta Amount of pool's token0 to repay (negative if token0 is the borrowed token) + /// @param amount1Delta Amount of pool's token1 to repay (negative if token1 is the borrowed token) + /// @param data Callback data, passed during _flashLiquidateCross + function pancakeV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external { + CallbackData memory decoded = abi.decode(data, (CallbackData)); + _verifyCallback(decoded.poolKey); + if (amount0Delta <= 0 && amount1Delta <= 0) { + revert EmptySwap(); + } + FlashLiquidationParameters memory params = decoded.params; + + uint256 receivedAmount = _liquidateAndRedeem(params.borrower, params.repayAmount, params.vTokenCollateral); + IERC20Upgradeable collateralToken = _underlying(params.vTokenCollateral); + + uint256 amountToPay; + IERC20Upgradeable tokenToPay; + if (amount0Delta > 0) { + tokenToPay = IERC20Upgradeable(decoded.poolKey.token0); + amountToPay = uint256(amount0Delta); + } else if (amount1Delta > 0) { + tokenToPay = IERC20Upgradeable(decoded.poolKey.token1); + amountToPay = uint256(amount1Delta); + } + + if (params.path.hasMultiplePools()) { + // Swap collateral token to the pool token and pay for the current swap + bytes memory remainingPath = params.path.skipToken(); + approveOrRevert(collateralToken, address(swapRouter), receivedAmount); + swapRouter.exactOutput( + ISmartRouter.ExactOutputParams({ + path: remainingPath, + recipient: msg.sender, // repaying to the pool + amountOut: amountToPay, + amountInMaximum: receivedAmount + }) + ); + approveOrRevert(collateralToken, address(swapRouter), 0); + } else { + // Pay for the swap directly with collateral tokens + collateralToken.safeTransfer(msg.sender, amountToPay); + } + collateralToken.safeTransfer(params.beneficiary, collateralToken.balanceOf(address(this))); + } + + /// @notice Returns the BUSD vToken + function vTokenBorrowed() public view returns (IVBep20) { + return busdLiquidator.vBUSD(); + } + + /// @notice Returns the BUSD token + function borrowedToken() public view returns (IERC20Upgradeable) { + return IERC20Upgradeable(vTokenBorrowed().underlying()); + } + + /// @dev Flash-borrows the borrowed token and starts the in-kind liquidation process + /// @param poolKey The pool key of the pool to flash-borrow from + /// @param params Liquidation parameters + function _flashLiquidateInKind( + PoolAddress.PoolKey memory poolKey, + FlashLiquidationParameters calldata params + ) internal { + IPancakeV3Pool pool = IPancakeV3Pool(PoolAddress.computeAddress(deployer, poolKey)); + address borrowedTokenAddress = address(borrowedToken()); + pool.flash( + address(this), + poolKey.token0 == borrowedTokenAddress ? params.repayAmount : 0, + poolKey.token1 == borrowedTokenAddress ? params.repayAmount : 0, + abi.encode(CallbackData(params, poolKey)) + ); + } + + /// @dev Flash-swaps the pool's tokenB to the borrowed token and starts the cross-token + /// liquidation process. Regardless of what tokenB is, we will later swap the seized + /// collateral tokens to the pool's tokenB along the path to repay the flash swap if needed. + /// @param poolKey The pool key of the pool to flash-swap in + /// @param params Liquidation parameters + function _flashLiquidateCross( + PoolAddress.PoolKey memory poolKey, + FlashLiquidationParameters calldata params + ) internal { + IPancakeV3Pool pool = IPancakeV3Pool(PoolAddress.computeAddress(deployer, poolKey)); + + bool zeroForOne = poolKey.token1 == address(borrowedToken()); + uint160 sqrtPriceLimitX96 = (zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1); + pool.swap( + address(this), + zeroForOne, + -int256(params.repayAmount), + sqrtPriceLimitX96, + abi.encode(CallbackData(params, poolKey)) + ); + } + + /// @dev Liquidates the borrow repaying the borrowed tokens, seizes vTokenCollateral, + /// redeems these vTokens and returns the received amount + /// @param borrower Borrower whose position to liquidate + /// @param repayAmount Amount of borrowed tokens to repay + /// @param vTokenCollateral Collateral vToken to seize + /// @return Amount of collateral tokens received + function _liquidateAndRedeem( + address borrower, + uint256 repayAmount, + IVToken vTokenCollateral + ) internal returns (uint256) { + uint256 vTokenBalanceBefore = vTokenCollateral.balanceOf(address(this)); + approveOrRevert(borrowedToken(), address(busdLiquidator), repayAmount); + busdLiquidator.liquidateBorrow(borrower, repayAmount, vTokenCollateral); + approveOrRevert(borrowedToken(), address(busdLiquidator), 0); + uint256 vTokensReceived = vTokenCollateral.balanceOf(address(this)) - vTokenBalanceBefore; + return _redeem(vTokenCollateral, vTokensReceived); + } + + /// @dev Ensures that the caller of a callback is a legitimate PancakeSwap pool + /// @param poolKey The pool key of the pool to verify + function _verifyCallback(PoolAddress.PoolKey memory poolKey) internal view { + address pool = PoolAddress.computeAddress(deployer, poolKey); + if (msg.sender != pool) { + revert InvalidCallbackSender(pool, msg.sender); + } + } +} diff --git a/contracts/Liquidator/FlashSwapLiquidationOperator/PathExt.sol b/contracts/Liquidator/FlashSwapLiquidationOperator/PathExt.sol new file mode 100644 index 000000000..53add1685 --- /dev/null +++ b/contracts/Liquidator/FlashSwapLiquidationOperator/PathExt.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import { BytesLib } from "./pancakeswap-v8/BytesLib.sol"; +import { Path } from "./pancakeswap-v8/Path.sol"; + +/** + * @title PathExt + * @author Venus + * @notice An extension to the Path library that provides additional functionality – decoding + * the last pool in a path and skipping several tokens in a path. This is useful to validate + * the path before attempting to execute a multihop swap. + */ +library PathExt { + using BytesLib for bytes; + using Path for bytes; + + /// @dev The offset of a single token address (20 bytes) and pool fee (3 bytes) + uint256 private constant NEXT_OFFSET = 23; + + /// @dev Skips several token + fee elements from the buffer and returns the remainder + /// @param path The swap path + /// @param num The number of token + fee elements to skip + /// @return The remaining token + fee elements in the path + function skipTokens(bytes memory path, uint256 num) internal pure returns (bytes memory) { + if (num == 0) { + return path; + } + return path.slice(NEXT_OFFSET * num, path.length - NEXT_OFFSET * num); + } + + /// @dev Decodes the last pool in the path + /// @param path The swap path + /// @return tokenA The first token of the given pool + /// @return tokenB The second token of the given pool + /// @return fee The fee level of the pool + function decodeLastPool(bytes memory path) internal pure returns (address tokenA, address tokenB, uint24 fee) { + return skipTokens(path, path.numPools() - 1).decodeFirstPool(); + } +} diff --git a/contracts/Liquidator/FlashSwapLiquidationOperator/UnifiedVTokenHandler.sol b/contracts/Liquidator/FlashSwapLiquidationOperator/UnifiedVTokenHandler.sol new file mode 100644 index 000000000..bea8bea90 --- /dev/null +++ b/contracts/Liquidator/FlashSwapLiquidationOperator/UnifiedVTokenHandler.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import { IWBNB } from "../../Swap/interfaces/IWBNB.sol"; +import { IVToken, IVBep20, IVBNB } from "../Interfaces.sol"; + +/** + * @title UnifiedVTokenHandler + * @author Venus + * @notice Helper contract to handle vTokens, encapsulating the differences between a vToken + * working with a native asset and vTokens handling ERC-20 underlyings. Automatically wraps + * native asset into a wrapped token. The inheriting contract MUST implement the receive() + * function. + */ +contract UnifiedVTokenHandler { + IVBNB public immutable vNative; + IWBNB public immutable wrappedNative; + + error RedeemFailed(uint256 errorCode); + + /// @param vNative_ The vToken representing the native asset + /// @param wrappedNative_ The wrapped native asset + constructor(IVBNB vNative_, IWBNB wrappedNative_) { + // Zero addresses are allowed intentionally. In case the params are zero, + // the contract will only work with token-based vTokens, which may be desired + // since we're sunsetting vNative. + vNative = vNative_; + wrappedNative = wrappedNative_; + } + + /// @dev Redeems the given amount of vTokens and wraps the native asssets into wrapped + /// tokens if necessary. Returns the actually received amount. + /// @param vToken The vToken to redeem + /// @param vTokenAmount The amount of vTokens to redeem + /// @return The amount of underlying tokens received + function _redeem(IVToken vToken, uint256 vTokenAmount) internal returns (uint256) { + if (address(vToken) == address(vNative)) { + return _wrap(_redeemNative(vTokenAmount)); + } else { + return _redeemTokens(IVBep20(address(vToken)), vTokenAmount); + } + } + + /// @dev Wraps the given amount of native asset into wrapped tokens + /// @param amount The amount of native asset to wrap + /// @return The amount of wrapped tokens received + function _wrap(uint256 amount) internal returns (uint256) { + uint256 wrappedNativeBalanceBefore = wrappedNative.balanceOf(address(this)); + wrappedNative.deposit{ value: amount }(); + return wrappedNative.balanceOf(address(this)) - wrappedNativeBalanceBefore; + } + + /// @dev Returns the either the underlying token or the wrapped native token if + /// the vToken works with native asset + /// @param vToken The vToken to get the underlying token for + /// @return The underlying token + function _underlying(IVToken vToken) internal view returns (IERC20Upgradeable) { + if (address(vToken) == address(vNative)) { + return IERC20Upgradeable(address(wrappedNative)); + } else { + return IERC20Upgradeable(IVBep20(address(vToken)).underlying()); + } + } + + /// @dev Redeems ERC-20 tokens from the given vToken + /// @param vToken The vToken to redeem tokens from + /// @param vTokenAmount The amount of vTokens to redeem + /// @return The amount of underlying tokens received + function _redeemTokens(IVBep20 vToken, uint256 vTokenAmount) private returns (uint256) { + IERC20Upgradeable underlying = IERC20Upgradeable(vToken.underlying()); + uint256 underlyingBalanceBefore = underlying.balanceOf(address(this)); + uint256 errorCode = vToken.redeem(vTokenAmount); + if (errorCode != 0) { + revert RedeemFailed(errorCode); + } + return underlying.balanceOf(address(this)) - underlyingBalanceBefore; + } + + /// @dev Redeems native asset from the given vToken. The inheriting contract MUST + /// implement the receive() function for this to work. + /// @param vTokenAmount The amount of vTokens to redeem + /// @return The amount of native asset received + function _redeemNative(uint256 vTokenAmount) private returns (uint256) { + uint256 underlyingBalanceBefore = address(this).balance; + vNative.redeem(vTokenAmount); + return address(this).balance - underlyingBalanceBefore; + } +} diff --git a/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/BytesLib.sol b/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/BytesLib.sol new file mode 100644 index 000000000..899e20dd5 --- /dev/null +++ b/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/BytesLib.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * @title Solidity Bytes Arrays Utils + * @author Gonçalo Sá + * + * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity. + * The library lets you concatenate, slice and type cast bytes arrays both in memory and storage. + */ +pragma solidity >=0.8.0 <0.9.0; + +library BytesLib { + function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) { + require(_length + 31 >= _length, "slice_overflow"); + require(_bytes.length >= _start + _length, "slice_outOfBounds"); + + bytes memory tempBytes; + + assembly { + switch iszero(_length) + case 0 { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(_length, 31) + + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) + let end := add(mc, _length) + + for { + // The multiplication in the next line has the same exact purpose + // as the one above. + let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + mstore(tempBytes, _length) + + //update free-memory pointer + //allocating the array padded to 32 bytes like the compiler does now + mstore(0x40, and(add(mc, 31), not(31))) + } + //if we want a zero-length slice let's just return a zero-length array + default { + tempBytes := mload(0x40) + //zero out the 32 bytes slice we are about to return + //we need to do it because Solidity does not garbage collect + mstore(tempBytes, 0) + + mstore(0x40, add(tempBytes, 0x20)) + } + } + + return tempBytes; + } + + function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { + require(_bytes.length >= _start + 20, "toAddress_outOfBounds"); + address tempAddress; + + assembly { + tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) + } + + return tempAddress; + } + + function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) { + require(_start + 3 >= _start, "toUint24_overflow"); + require(_bytes.length >= _start + 3, "toUint24_outOfBounds"); + uint24 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x3), _start)) + } + + return tempUint; + } +} diff --git a/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/ISmartRouter.sol b/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/ISmartRouter.sol new file mode 100644 index 000000000..f0d32ab9a --- /dev/null +++ b/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/ISmartRouter.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.0; + +interface ISmartRouter { + struct ExactOutputParams { + bytes path; + address recipient; + uint256 amountOut; + uint256 amountInMaximum; + } + + function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); + + function deployer() external view returns (address); + + function WETH9() external view returns (address); +} diff --git a/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/Path.sol b/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/Path.sol new file mode 100644 index 000000000..5e58e8f43 --- /dev/null +++ b/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/Path.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.6.0; + +import "./BytesLib.sol"; + +/// @title Functions for manipulating path data for multihop swaps +library Path { + using BytesLib for bytes; + + /// @dev The length of the bytes encoded address + uint256 private constant ADDR_SIZE = 20; + /// @dev The length of the bytes encoded fee + uint256 private constant FEE_SIZE = 3; + + /// @dev The offset of a single token address and pool fee + uint256 private constant NEXT_OFFSET = ADDR_SIZE + FEE_SIZE; + /// @dev The offset of an encoded pool key + uint256 private constant POP_OFFSET = NEXT_OFFSET + ADDR_SIZE; + /// @dev The minimum length of an encoding that contains 2 or more pools + uint256 private constant MULTIPLE_POOLS_MIN_LENGTH = POP_OFFSET + NEXT_OFFSET; + + /// @notice Returns true iff the path contains two or more pools + /// @param path The encoded swap path + /// @return True if path contains two or more pools, otherwise false + function hasMultiplePools(bytes memory path) internal pure returns (bool) { + return path.length >= MULTIPLE_POOLS_MIN_LENGTH; + } + + /// @notice Returns the number of pools in the path + /// @param path The encoded swap path + /// @return The number of pools in the path + function numPools(bytes memory path) internal pure returns (uint256) { + // Ignore the first token address. From then on every fee and token offset indicates a pool. + return ((path.length - ADDR_SIZE) / NEXT_OFFSET); + } + + /// @notice Decodes the first pool in path + /// @param path The bytes encoded swap path + /// @return tokenA The first token of the given pool + /// @return tokenB The second token of the given pool + /// @return fee The fee level of the pool + function decodeFirstPool(bytes memory path) internal pure returns (address tokenA, address tokenB, uint24 fee) { + tokenA = path.toAddress(0); + fee = path.toUint24(ADDR_SIZE); + tokenB = path.toAddress(NEXT_OFFSET); + } + + /// @notice Gets the segment corresponding to the first pool in the path + /// @param path The bytes encoded swap path + /// @return The segment containing all data necessary to target the first pool in the path + function getFirstPool(bytes memory path) internal pure returns (bytes memory) { + return path.slice(0, POP_OFFSET); + } + + /// @notice Skips a token + fee element from the buffer and returns the remainder + /// @param path The swap path + /// @return The remaining token + fee elements in the path + function skipToken(bytes memory path) internal pure returns (bytes memory) { + return path.slice(NEXT_OFFSET, path.length - NEXT_OFFSET); + } +} diff --git a/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/PoolAddress.sol b/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/PoolAddress.sol new file mode 100644 index 000000000..16097f98d --- /dev/null +++ b/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/PoolAddress.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +/// @title Provides functions for deriving a pool address from the factory, tokens, and the fee +library PoolAddress { + // The following line was modified by Venus so that the init hash corresponds to the one of PancakeSwap + bytes32 internal constant POOL_INIT_CODE_HASH = 0x6ce8eb472fa82df5469c6ab6d485f17c3ad13c8cd7af59b3d4a8026c5ce0f7e2; + + /// @notice The identifying key of the pool + struct PoolKey { + address token0; + address token1; + uint24 fee; + } + + /// @notice Returns PoolKey: the ordered tokens with the matched fee levels + /// @param tokenA The first token of a pool, unsorted + /// @param tokenB The second token of a pool, unsorted + /// @param fee The fee level of the pool + /// @return Poolkey The pool details with ordered token0 and token1 assignments + function getPoolKey(address tokenA, address tokenB, uint24 fee) internal pure returns (PoolKey memory) { + if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); + return PoolKey({ token0: tokenA, token1: tokenB, fee: fee }); + } + + /// @notice Deterministically computes the pool address given the factory and PoolKey + /// @param factory The Uniswap V3 factory contract address + /// @param key The PoolKey + /// @return pool The contract address of the V3 pool + function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) { + require(key.token0 < key.token1); + pool = address( + uint160( + uint256( + keccak256( + abi.encodePacked( + hex"ff", + factory, + keccak256(abi.encode(key.token0, key.token1, key.fee)), + POOL_INIT_CODE_HASH + ) + ) + ) + ) + ); + } +} diff --git a/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/constants.sol b/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/constants.sol new file mode 100644 index 000000000..c09809580 --- /dev/null +++ b/contracts/Liquidator/FlashSwapLiquidationOperator/pancakeswap-v8/constants.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity >=0.5.0; + +/// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) +uint160 constant MIN_SQRT_RATIO = 4295128739; +/// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) +uint160 constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; diff --git a/contracts/Liquidator/Interfaces.sol b/contracts/Liquidator/Interfaces.sol index 3470f76c9..e5cd7dcdc 100644 --- a/contracts/Liquidator/Interfaces.sol +++ b/contracts/Liquidator/Interfaces.sol @@ -7,6 +7,8 @@ interface IVToken is IERC20Upgradeable { function borrowBalanceCurrent(address borrower) external returns (uint256); function transfer(address dst, uint256 amount) external returns (bool); + + function redeem(uint256 redeemTokens) external returns (uint256); } interface IVBep20 is IVToken { diff --git a/package.json b/package.json index 56413ebbf..5c782fa95 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "dependencies": { "@openzeppelin/contracts": "^4.8.3", "@openzeppelin/contracts-upgradeable": "^4.8.0", + "@pancakeswap/v3-core": "^1.0.2", + "@pancakeswap/v3-periphery": "^1.0.2", "dotenv": "^16.0.1", "module-alias": "^2.2.2" }, diff --git a/tests/hardhat/Fork/FlashSwapLiquidationOperator.ts b/tests/hardhat/Fork/FlashSwapLiquidationOperator.ts new file mode 100644 index 000000000..f2fb7462a --- /dev/null +++ b/tests/hardhat/Fork/FlashSwapLiquidationOperator.ts @@ -0,0 +1,227 @@ +import { smock } from "@defi-wonderland/smock"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import chai from "chai"; +import { BigNumberish } from "ethers"; +import { BytesLike, parseEther, parseUnits } from "ethers/lib/utils"; +import { ethers, upgrades } from "hardhat"; + +import { + BUSDLiquidator, + ComptrollerMock, + FaucetToken, + FlashSwapLiquidationOperator, + FlashSwapLiquidationOperator__factory, + VBep20, +} from "../../../typechain"; +import { deployJumpRateModel } from "../fixtures/ComptrollerWithMarkets"; +import { FORK_MAINNET, forking, initMainnetUser } from "./utils"; + +const { expect } = chai; +chai.use(smock.matchers); + +const LIQUIDATOR_PERCENT = parseUnits("1.01", 18); + +const addresses = { + bscmainnet: { + COMPTROLLER: "0xfD36E2c2a6789Db23113685031d7F16329158384", + VBUSD: "0x95c78222B3D6e262426483D42CfA53685A67Ab9D", + VBNB: "0xA07c5b74C9B40447a954e1466938b865b6BBea36", + TIMELOCK: "0x939bD8d64c0A9583A7Dcea9933f7b21697ab6396", + ACCESS_CONTROL_MANAGER: "0x4788629ABc6cFCA10F9f969efdEAa1cF70c23555", + PCS_SWAP_ROUTER_V3: "0x13f4EA83D0bd40E75C8222255bc855a974568Dd4", + PCS_V3_DEPOLYER: "0x41ff9AA7e16B8B1a8a8dc4f0eFacd93D02d071c9", + BUSD_HOLDER: "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56", + }, +}; + +const deployBUSDLiquidator = async ({ + comptroller, + vBUSD, + treasuryAddress, + liquidatorShareMantissa, +}: { + comptroller: ComptrollerMock; + vBUSD: VBep20; + treasuryAddress: string; + liquidatorShareMantissa: BigNumberish; +}) => { + const busdLiquidatorFactory = await ethers.getContractFactory("BUSDLiquidator"); + const busdLiquidator = (await upgrades.deployProxy(busdLiquidatorFactory, [liquidatorShareMantissa], { + constructorArgs: [comptroller.address, vBUSD.address, treasuryAddress], + })) as BUSDLiquidator; + await busdLiquidator.deployed(); + return busdLiquidator; +}; + +interface BorrowerPosition { + borrowerAddress: string; + vTokenCollateral: string; +} + +interface BUSDLiquidatorFixture { + comptroller: ComptrollerMock; + busdLiquidator: BUSDLiquidator; + vBUSD: VBep20; + busd: FaucetToken; + treasuryAddress: string; + flashSwapper: FlashSwapLiquidationOperator; + borrowerPositions: BorrowerPosition[]; +} + +const setupFork = async (): Promise => { + const zeroRateModel = await deployJumpRateModel({ + baseRatePerYear: 0, + multiplierPerYear: 0, + jumpMultiplierPerYear: 0, + }); + + const comptroller = await ethers.getContractAt("ComptrollerMock", addresses.bscmainnet.COMPTROLLER); + const vBUSD = await ethers.getContractAt("VBep20", addresses.bscmainnet.VBUSD); + const busd = await ethers.getContractAt("contracts/Utils/IBEP20.sol:IBEP20", await vBUSD.underlying()); + const treasuryAddress = await comptroller.treasuryAddress(); + const acm = await ethers.getContractAt("IAccessControlManager", addresses.bscmainnet.ACCESS_CONTROL_MANAGER); + + const busdLiquidator = await deployBUSDLiquidator({ + comptroller, + vBUSD, + treasuryAddress: await comptroller.treasuryAddress(), + liquidatorShareMantissa: LIQUIDATOR_PERCENT, + }); + + const timelock = await initMainnetUser(addresses.bscmainnet.TIMELOCK, parseEther("1")); + await acm + .connect(timelock) + .giveCallPermission(comptroller.address, "_setActionsPaused(address[],uint8[],bool)", busdLiquidator.address); + await comptroller.connect(timelock)._setForcedLiquidation(vBUSD.address, true); + await vBUSD.connect(timelock)._setInterestRateModel(zeroRateModel.address); + const MINT_ACTION = 0; + await comptroller.connect(timelock)._setActionsPaused([vBUSD.address], [MINT_ACTION], false); + await comptroller.connect(timelock)._setMarketSupplyCaps([vBUSD.address], [ethers.constants.MaxUint256]); + + const flashSwapperFactory: FlashSwapLiquidationOperator__factory = + await ethers.getContractFactory("FlashSwapLiquidationOperator"); + const flashSwapper = await flashSwapperFactory.deploy( + addresses.bscmainnet.VBNB, + addresses.bscmainnet.PCS_SWAP_ROUTER_V3, + busdLiquidator.address, + ); + + const borrowerPositions = [ + { + borrowerAddress: "", + vTokenCollateral: "", + }, + ]; + + return { comptroller, busdLiquidator, vBUSD, busd, treasuryAddress, flashSwapper, borrowerPositions }; +}; + +const injectBUSDLiquidity = async () => { + const vBUSD = await ethers.getContractAt("VBep20", addresses.bscmainnet.VBUSD); + const busd = await ethers.getContractAt("contracts/Utils/IBEP20.sol:IBEP20", await vBUSD.underlying()); + const busdHolder = await initMainnetUser(addresses.bscmainnet.BUSD_HOLDER, parseEther("1")); + await busd.connect(busdHolder).approve(vBUSD.address, parseUnits("10000000", 18)); + await vBUSD.connect(busdHolder).mint(parseUnits("10000000", 18)); +}; + +interface Pool { + tokenA: string; + tokenB: string; + fee: BigNumberish; +} + +const pool = (tokenA: string, tokenB: string, fee: BigNumberish) => ({ + tokenA, + tokenB, + fee, +}); + +const encodeFee = (fee: BigNumberish): string => ethers.utils.hexZeroPad(ethers.BigNumber.from(fee).toHexString(), 3); + +const makePath = (pools: Pool[]) => { + const path: BytesLike[] = []; + for (let i = 0; i < pools.length; i++) { + const pool = pools[i]; + if (i === 0) { + path.push(pool.tokenA); + } else { + if (path[path.length - 1] != pool.tokenA) { + throw new Error("Invalid path"); + } + } + path.push(encodeFee(pool.fee)); + path.push(pool.tokenB); + } + return ethers.utils.hexConcat(path); +}; + +const test = (setup: () => Promise) => () => { + describe("FlashSwapLiquidationOperator", () => { + let owner: SignerWithAddress; + let flashSwapper: FlashSwapLiquidationOperator; + + const WBNB = "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c"; + const BUSD = "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56"; + const ADA = "0x3EE2200Efb3400fAbB9AacF31297cBdD1d435D47"; + const VADA = "0x9A0AF7FDb2065Ce470D72664DE73cAE409dA28Ec"; + const VBNB = addresses.bscmainnet.VBNB; + const VBUSD = addresses.bscmainnet.VBUSD; + + beforeEach(async () => { + ({ flashSwapper } = await loadFixture(setup)); + [owner] = await ethers.getSigners(); + }); + + it("sets the address parameters correctly upon deployment", async () => { + expect(await flashSwapper.vTokenBorrowed()).to.equal(VBUSD); + expect(await flashSwapper.borrowedToken()).to.equal(BUSD); + expect(await flashSwapper.vNative()).to.equal(VBNB); + expect(await flashSwapper.swapRouter()).to.equal(addresses.bscmainnet.PCS_SWAP_ROUTER_V3); + expect(await flashSwapper.deployer()).to.equal(addresses.bscmainnet.PCS_V3_DEPOLYER); + }); + + it("executes an in-kind liquidation", async () => { + await injectBUSDLiquidity(); + // Using WBNB-BUSD pool as a source of BUSD liquidity + const REVERSED_WBNB_BUSD_PATH = makePath([pool(BUSD, WBNB, 500)]); + await flashSwapper.liquidate({ + beneficiary: owner.address, + borrower: "0xDF3df3EE9Fb6D5c9B4fdcF80A92D25d2285A859C", + repayAmount: parseUnits("1", 18), + vTokenCollateral: VBUSD, // Notice in-kind liquidation, repaying BUSD and seizing BUSD + path: REVERSED_WBNB_BUSD_PATH, + deadline: 1698457358, + }); + }); + + it("executes a single-hop flash liquidation", async () => { + const REVERSED_WBNB_BUSD_PATH = makePath([pool(BUSD, WBNB, 500)]); + await flashSwapper.liquidate({ + beneficiary: owner.address, + borrower: "0x3D5A1FB54234Da332f85881575E9216b3bB2D83d", + repayAmount: parseUnits("1", 18), + vTokenCollateral: VBNB, + path: REVERSED_WBNB_BUSD_PATH, + deadline: 1698457358, + }); + }); + + it("executes a multihop flash liquidation", async () => { + const REVERSED_ADA_BUSD_PATH = makePath([pool(BUSD, WBNB, 500), pool(WBNB, ADA, 2500)]); + await flashSwapper.liquidate({ + beneficiary: owner.address, + borrower: "0x3D5A1FB54234Da332f85881575E9216b3bB2D83d", + repayAmount: parseUnits("1", 18), + vTokenCollateral: VADA, + path: REVERSED_ADA_BUSD_PATH, + deadline: 1698457358, + }); + }); + }); +}; + +if (FORK_MAINNET) { + const blockNumber = 32652750; + forking(blockNumber, test(setupFork)); +} diff --git a/yarn.lock b/yarn.lock index 2bf5e1c89..b53403567 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2156,6 +2156,13 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts-upgradeable@npm:3.4.2-solc-0.7": + version: 3.4.2-solc-0.7 + resolution: "@openzeppelin/contracts-upgradeable@npm:3.4.2-solc-0.7" + checksum: 662d168ea1763faa5e168751043e4e041ccd810c3d4c781aa5aee0e9947e9f95d51edaeb1daaa4cc2860463beb961b576c6a3e60e3fbb6fa27188a611c8522e4 + languageName: node + linkType: hard + "@openzeppelin/contracts-upgradeable@npm:^4.6.0, @openzeppelin/contracts-upgradeable@npm:^4.7.3, @openzeppelin/contracts-upgradeable@npm:^4.8.0": version: 4.9.3 resolution: "@openzeppelin/contracts-upgradeable@npm:4.9.3" @@ -2170,6 +2177,13 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts@npm:3.4.2-solc-0.7": + version: 3.4.2-solc-0.7 + resolution: "@openzeppelin/contracts@npm:3.4.2-solc-0.7" + checksum: 1a6048f31ed560c34429a05e534102c51124ecaf113aca7ebeb7897cfaaf61007cdd7952374c282adaeb79b04ee86ee80b16eed28b62fc6d60e3ffcd7a696895 + languageName: node + linkType: hard + "@openzeppelin/contracts@npm:^4.3.3, @openzeppelin/contracts@npm:^4.6.0, @openzeppelin/contracts@npm:^4.8.3": version: 4.9.3 resolution: "@openzeppelin/contracts@npm:4.9.3" @@ -2245,6 +2259,26 @@ __metadata: languageName: node linkType: hard +"@pancakeswap/v3-core@npm:^1.0.2": + version: 1.0.2 + resolution: "@pancakeswap/v3-core@npm:1.0.2" + checksum: 3fddad82a0a4bfdbe99ee10e40b538dc9f83547b547a16c9ec02a145a4f2337e1eeca7525f628dde237554a0bd0266e231de0410a156eb53273a736d29a99609 + languageName: node + linkType: hard + +"@pancakeswap/v3-periphery@npm:^1.0.2": + version: 1.0.2 + resolution: "@pancakeswap/v3-periphery@npm:1.0.2" + dependencies: + "@openzeppelin/contracts": 3.4.2-solc-0.7 + "@openzeppelin/contracts-upgradeable": 3.4.2-solc-0.7 + "@uniswap/lib": ^4.0.1-alpha + "@uniswap/v2-core": 1.0.1 + base64-sol: 1.0.1 + checksum: 32624c66f1d4d366bd13a0430367f7a666af7478faff513cbe4544612cbf6b502f55b6f3e6ca755e1c262f13cfaefec6cd296a21206ae39050763cc0a94a56b6 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -2980,6 +3014,20 @@ __metadata: languageName: node linkType: hard +"@uniswap/lib@npm:^4.0.1-alpha": + version: 4.0.1-alpha + resolution: "@uniswap/lib@npm:4.0.1-alpha" + checksum: d7bbacccef40966af16c7e215ab085f575686d316b2802c9e1cfd03f7ad351970e547535670a28b2279c3cfcc4fb02888614c46f94efe2987af2309f3ec86127 + languageName: node + linkType: hard + +"@uniswap/v2-core@npm:1.0.1": + version: 1.0.1 + resolution: "@uniswap/v2-core@npm:1.0.1" + checksum: eaa118fe45eac2e80b7468547ce2cde12bd3c8157555d2e40e0462a788c9506c6295247b511382da85e44a89ad92aff7bb3433b23bfbd2eeea23942ecd46e979 + languageName: node + linkType: hard + "@venusprotocol/governance-contracts@npm:^0.0.2": version: 0.0.2 resolution: "@venusprotocol/governance-contracts@npm:0.0.2" @@ -3052,6 +3100,8 @@ __metadata: "@openzeppelin/contracts": ^4.8.3 "@openzeppelin/contracts-upgradeable": ^4.8.0 "@openzeppelin/hardhat-upgrades": ^1.21.0 + "@pancakeswap/v3-core": ^1.0.2 + "@pancakeswap/v3-periphery": ^1.0.2 "@semantic-release/changelog": ^6.0.1 "@semantic-release/git": ^10.0.1 "@semantic-release/npm": ^9.0.1 @@ -3760,6 +3810,13 @@ __metadata: languageName: node linkType: hard +"base64-sol@npm:1.0.1": + version: 1.0.1 + resolution: "base64-sol@npm:1.0.1" + checksum: be0f9e8cf3c744256913223fbae8187773f530cc096e98a77f49ef0bd6cedeb294d15a784e439419f7cb99f07bf85b08999169feafafa1a9e29c3affc0bc6d0a + languageName: node + linkType: hard + "bcrypt-pbkdf@npm:^1.0.0": version: 1.0.2 resolution: "bcrypt-pbkdf@npm:1.0.2"