Skip to content

Commit

Permalink
Merge pull request #105 from hifi-finance/feat/flash-swap-uni-v3-mult…
Browse files Browse the repository at this point in the history
…i-hop

* feat(flash-swap): add "Path.sol" library
feat(flash-swap): add "BytesLib.sol" library
refactor(flash-swap): move "NoDelegateCall.sol" to "libraries/" dir

* feat(flash-swap): add support for n-hop swaps in "flashLiquidate" function
feat(flash-swap): add "swapExactOutputInternal" function
feat(flash-swap): replace "poolFee" param in "FlashLiquidateParams" struct with "path"
feat(flash-swap): add "FlashUniswapV3__InsufficientSwapOutputAmount" error
refactor(flash-swap): use specific imports
refactor(flash-swap): rename "FlashSwapAndLiquidateBorrow" event to "FlashLiquidate"
refactor(flash-swap): rename "UniswapV3SwapCallback" structs to "FlashLiquidateCallback"
test(flash-swap): update "getSwapCallbackData" function to mirror contract changes
chore(flash-swap): re-generate types

* fix(flash-swap): bug in "uniswapV3SwapCallback" function regarding "repayAmount" recalculation
refactor(flash-swap): merge "getPoolKey" and "poolFor" functions into a single "getPool" function
refactor(flash-swap): rename "FlashLiquidateCallbackParams" struct to "UniswapV3SwapCallbackParams"
refactor(flash-swap): rename "benificiary" param in "swapExactOutputInternal" function to "to"
chore(flash-swap): regenerate types

* chore(flash-swap): skip test coverage for "uniswap-v3/libraries" dir
chore(flash-swap): re-generate types

* chore(flash-swap): disable solhint for Uniswap V3 libraries

* test(flash-swap): update integration tests to mirror FlashUniswapV3 contract changes
test(flash-swap): remove "uniswapV3Pool" contract type to "Contracts" interface
test(flash-swap): add "dai" contract type to "Contracts" interface
feat(constants): add "DAI_{DECIMALS,NAME,SYMBOL}" constants to tokens.ts
feat(constants): add "DEFAULT_FEE" constant to oracles.ts

* chore(flash-swap): fix lint issues

* docs(flash-swap): add dev NatSpec comments to libraries
  • Loading branch information
scorpion9979 committed Dec 12, 2023
1 parent 913b982 commit 44bec5f
Show file tree
Hide file tree
Showing 20 changed files with 532 additions and 214 deletions.
1 change: 1 addition & 0 deletions packages/constants/src/oracles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BigNumber } from "@ethersproject/bignumber";

export const DEFAULT_CARDINALITY: number = 144;
export const DEFAULT_FEE: number = 500;
export const DEFAULT_TWAP_INTERVAL: number = 1800;
export const Q192: BigNumber = BigNumber.from("6277101735386680763835789423207666416102355444464034512896");
export const TICKS = {
Expand Down
4 changes: 4 additions & 0 deletions packages/constants/src/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { BigNumber } from "@ethersproject/bignumber";

export const DAI_DECIMALS: BigNumber = BigNumber.from(18);
export const DAI_NAME: string = "Dai Stablecoin";
export const DAI_SYMBOL: string = "DAI";

export const USDC_DECIMALS: BigNumber = BigNumber.from(6);
export const USDC_NAME: string = "USD Coin";
export const USDC_PRICE_PRECISION_SCALAR: BigNumber = BigNumber.from(1_000_000_000_000);
Expand Down
2 changes: 1 addition & 1 deletion packages/flash-swap/.solcover.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module.exports = {
"uniswap-v2/IUniswapV2Pair.sol",
"uniswap-v2/UniswapV2Pair.sol",
"uniswap-v2/test",
"uniswap-v3/NoDelegateCall.sol",
"uniswap-v3/libraries",
"uniswap-v3/UniswapV3Pool",
"uniswap-v3/test",
],
Expand Down
224 changes: 135 additions & 89 deletions packages/flash-swap/contracts/uniswap-v3/FlashUniswapV3.sol
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
pragma solidity ^0.8.4;

import "@prb/contracts/token/erc20/IErc20.sol";
import "@prb/contracts/token/erc20/SafeErc20.sol";
import "@hifi/protocol/contracts/core/balance-sheet/IBalanceSheetV2.sol";
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";

import "./IFlashUniswapV3.sol";
import { IErc20 } from "@prb/contracts/token/erc20/IErc20.sol";
import { SafeErc20 } from "@prb/contracts/token/erc20/SafeErc20.sol";
import { IBalanceSheetV2 } from "@hifi/protocol/contracts/core/balance-sheet/IBalanceSheetV2.sol";
import { IHToken } from "@hifi/protocol/contracts/core/h-token/IHToken.sol";
import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import { IUniswapV3SwapCallback } from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol";
import { Path } from "./libraries/Path.sol";
import { IFlashUniswapV3 } from "./IFlashUniswapV3.sol";

/// @title FlashUniswapV3
/// @author Hifi
contract FlashUniswapV3 is IFlashUniswapV3 {
using Path for bytes;
using SafeErc20 for IErc20;

/// PUBLIC STORAGE ///
Expand All @@ -35,16 +38,14 @@ contract FlashUniswapV3 is IFlashUniswapV3 {
}

struct FlashLiquidateLocalVars {
PoolKey poolKey;
IErc20 underlying;
bool zeroForOne;
}

struct UniswapV3SwapCallbackParams {
IHToken bond;
address borrower;
IErc20 collateral;
PoolKey poolKey;
bytes path;
address sender;
int256 turnout;
uint256 underlyingAmount;
Expand All @@ -65,32 +66,18 @@ contract FlashUniswapV3 is IFlashUniswapV3 {
});
}

// Compute the flash pool key and address.
vars.poolKey = getPoolKey({
tokenA: address(params.collateral),
tokenB: address(vars.underlying),
fee: params.poolFee
});

// The direction of the swap, true for token0 to token1, false for token1 to token0.
vars.zeroForOne = address(vars.underlying) == vars.poolKey.token1;

IUniswapV3Pool(poolFor(vars.poolKey)).swap({
recipient: address(this),
zeroForOne: vars.zeroForOne,
amountSpecified: int256(params.underlyingAmount) * -1,
sqrtPriceLimitX96: vars.zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,
data: abi.encode(
UniswapV3SwapCallbackParams({
bond: params.bond,
borrower: params.borrower,
collateral: params.collateral,
poolKey: vars.poolKey,
sender: msg.sender,
turnout: params.turnout,
underlyingAmount: params.underlyingAmount
})
)
swapExactOutputInternal({
amountOut: params.underlyingAmount,
to: address(this),
params: UniswapV3SwapCallbackParams({
bond: params.bond,
borrower: params.borrower,
collateral: params.collateral,
path: params.path,
sender: msg.sender,
turnout: params.turnout,
underlyingAmount: params.underlyingAmount
})
});
}

Expand All @@ -100,6 +87,9 @@ contract FlashUniswapV3 is IFlashUniswapV3 {
uint256 repayAmount;
uint256 seizeAmount;
uint256 subsidyAmount;
address tokenOut;
uint24 fee;
address tokenIn;
}

/// @inheritdoc IUniswapV3SwapCallback
Expand All @@ -113,79 +103,87 @@ contract FlashUniswapV3 is IFlashUniswapV3 {
// Unpack the ABI encoded data passed by the UniswapV3Pool contract.
UniswapV3SwapCallbackParams memory params = abi.decode(data, (UniswapV3SwapCallbackParams));

(vars.tokenOut, vars.tokenIn, vars.fee) = params.path.decodeFirstPool();

// Check that the caller is the Uniswap V3 flash pool contract.
if (msg.sender != poolFor(params.poolKey)) {
if (msg.sender != getPool({ tokenA: vars.tokenIn, tokenB: vars.tokenOut, fee: vars.fee })) {
revert FlashUniswapV3__CallNotAuthorized(msg.sender);
}

// Mint hTokens and liquidate the borrower.
vars.mintedHTokenAmount = mintHTokens({ bond: params.bond, underlyingAmount: params.underlyingAmount });
vars.seizeAmount = liquidateBorrow({
borrower: params.borrower,
bond: params.bond,
collateral: params.collateral,
mintedHTokenAmount: vars.mintedHTokenAmount
});

// Calculate the amount of collateral required to repay.
vars.repayAmount = uint256(amount0Delta > 0 ? amount0Delta : amount1Delta);
// Calculate the amount of input tokens required to receive the exact output amount.
vars.repayAmount = amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);

// Note that "turnout" is a signed int. When it is negative, it acts as a maximum subsidy amount.
// When its value is positive, it acts as a minimum profit.
if (int256(vars.seizeAmount) < int256(vars.repayAmount) + params.turnout) {
revert FlashUniswapV3__TurnoutNotSatisfied({
seizeAmount: vars.seizeAmount,
repayAmount: vars.repayAmount,
turnout: params.turnout
});
// Initiate the next swap.
if (params.path.hasMultiplePools()) {
params.path = params.path.skipToken();
swapExactOutputInternal({ amountOut: vars.repayAmount, to: msg.sender, params: params });
}
// Or liquidate the underwater vault.
else {
// Mint hTokens and liquidate the borrower.
vars.mintedHTokenAmount = mintHTokens({ bond: params.bond, underlyingAmount: params.underlyingAmount });
vars.seizeAmount = liquidateBorrow({
borrower: params.borrower,
bond: params.bond,
collateral: params.collateral,
mintedHTokenAmount: vars.mintedHTokenAmount
});

// Transfer the subsidy amount.
if (vars.repayAmount > vars.seizeAmount) {
unchecked {
vars.subsidyAmount = vars.repayAmount - vars.seizeAmount;
// Note that "turnout" is a signed int. When it is negative, it acts as a maximum subsidy amount.
// When its value is positive, it acts as a minimum profit.
if (int256(vars.seizeAmount) < int256(vars.repayAmount) + params.turnout) {
revert FlashUniswapV3__TurnoutNotSatisfied({
seizeAmount: vars.seizeAmount,
repayAmount: vars.repayAmount,
turnout: params.turnout
});
}
params.collateral.safeTransferFrom(params.sender, address(this), vars.subsidyAmount);
}
// Or reap the profit.
else if (vars.seizeAmount > vars.repayAmount) {
unchecked {
vars.profitAmount = vars.seizeAmount - vars.repayAmount;

// Transfer the subsidy amount.
if (vars.repayAmount > vars.seizeAmount) {
unchecked {
vars.subsidyAmount = vars.repayAmount - vars.seizeAmount;
}
params.collateral.safeTransferFrom(params.sender, address(this), vars.subsidyAmount);
}
// Or reap the profit.
else if (vars.seizeAmount > vars.repayAmount) {
unchecked {
vars.profitAmount = vars.seizeAmount - vars.repayAmount;
}
params.collateral.safeTransfer(params.sender, vars.profitAmount);
}
params.collateral.safeTransfer(params.sender, vars.profitAmount);
}

// Pay back the loan.
params.collateral.safeTransfer(msg.sender, vars.repayAmount);

// Emit an event.
emit FlashSwapAndLiquidateBorrow({
liquidator: params.sender,
borrower: params.borrower,
bond: address(params.bond),
collateral: address(params.collateral),
underlyingAmount: params.underlyingAmount,
seizeAmount: vars.seizeAmount,
repayAmount: vars.repayAmount,
subsidyAmount: vars.subsidyAmount,
profitAmount: vars.profitAmount
});
// Pay back the loan.
params.collateral.safeTransfer(msg.sender, vars.repayAmount);

// Emit an event.
emit FlashLiquidate({
liquidator: params.sender,
borrower: params.borrower,
bond: address(params.bond),
collateral: address(params.collateral),
underlyingAmount: params.underlyingAmount,
seizeAmount: vars.seizeAmount,
repayAmount: vars.repayAmount,
subsidyAmount: vars.subsidyAmount,
profitAmount: vars.profitAmount
});
}
}

/// INTERNAL CONSTANT FUNCTIONS ///

/// @dev Returns the Uniswap V3 pool key for a given token pair and fee level.
function getPoolKey(
/// @dev Calculates the CREATE2 address for a Uniswap V3 pool for a given token pair and fee level without
/// making any external calls.
function getPool(
address tokenA,
address tokenB,
uint24 fee
) internal pure returns (PoolKey memory) {
) internal view returns (address pool) {
if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA);
return PoolKey({ token0: tokenA, token1: tokenB, fee: fee });
}
PoolKey memory key = PoolKey({ token0: tokenA, token1: tokenB, fee: fee });

/// @dev Calculates the CREATE2 address for a Uniswap V3 pool without making any external calls.
function poolFor(PoolKey memory key) internal view returns (address pool) {
// solhint-disable-next-line reason-string
require(key.token0 < key.token1);
pool = address(
Expand Down Expand Up @@ -253,4 +251,52 @@ contract FlashUniswapV3 is IFlashUniswapV3 {
mintedHTokenAmount = newHTokenBalance - oldHTokenBalance;
}
}

struct SwapExactOutputLocalVars {
uint256 amountOutReceived;
uint24 fee;
address tokenIn;
address tokenOut;
bool zeroForOne;
}

/// @dev Performs a Uniswap V3 swap, receiving an exact amount of output.
function swapExactOutputInternal(
uint256 amountOut,
address to,
UniswapV3SwapCallbackParams memory params
) private returns (uint256 amountIn) {
SwapExactOutputLocalVars memory vars;

// Decode the first pool from the path.
(vars.tokenOut, vars.tokenIn, vars.fee) = params.path.decodeFirstPool();

// Compute the direction of the swap.
vars.zeroForOne = vars.tokenIn < vars.tokenOut;

// Swap the exact output amount.
(int256 amount0Delta, int256 amount1Delta) = IUniswapV3Pool(
getPool({ tokenA: vars.tokenIn, tokenB: vars.tokenOut, fee: vars.fee })
).swap({
recipient: to,
zeroForOne: vars.zeroForOne,
amountSpecified: -int256(amountOut),
sqrtPriceLimitX96: vars.zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,
data: abi.encode(params)
});

// Compute the amount of input required to receive the exact output amount and the actual amount
// of output received.
(amountIn, vars.amountOutReceived) = vars.zeroForOne
? (uint256(amount0Delta), uint256(-amount1Delta))
: (uint256(amount1Delta), uint256(-amount0Delta));

// It's technically possible to not receive the full output amount when no price limit has been specified.
if (vars.amountOutReceived != amountOut) {
revert FlashUniswapV3__InsufficientSwapOutputAmount({
amountOutExpected: amountOut,
amountOutReceived: vars.amountOutReceived
});
}
}
}
18 changes: 12 additions & 6 deletions packages/flash-swap/contracts/uniswap-v3/IFlashUniswapV3.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
pragma solidity >=0.8.4;

import "@hifi/protocol/contracts/core/balance-sheet/IBalanceSheetV2.sol";
import "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol";
import { IErc20 } from "@prb/contracts/token/erc20/IErc20.sol";
import { IBalanceSheetV2 } from "@hifi/protocol/contracts/core/balance-sheet/IBalanceSheetV2.sol";
import { IHToken } from "@hifi/protocol/contracts/core/h-token/IHToken.sol";
import { IUniswapV3SwapCallback } from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol";

/// @title IFlashUniswapV3
/// @author Hifi
Expand All @@ -13,6 +15,9 @@ interface IFlashUniswapV3 is IUniswapV3SwapCallback {
/// @notice Emitted when the caller is not the Uniswap V3 pool contract.
error FlashUniswapV3__CallNotAuthorized(address caller);

/// @notice Emitted when the amount of tokens received from the swap is less than the amount expected.
error FlashUniswapV3__InsufficientSwapOutputAmount(uint256 amountOutExpected, uint256 amountOutReceived);

/// @notice Emitted when liquidating a vault backed by underlying.
error FlashUniswapV3__LiquidateUnderlyingBackedVault(address borrower, address underlying);

Expand All @@ -32,7 +37,7 @@ interface IFlashUniswapV3 is IUniswapV3SwapCallback {
/// @param repayAmount The amount of collateral that had to be repaid by the liquidator.
/// @param subsidyAmount The amount of collateral subsidized by the liquidator.
/// @param profitAmount The amount of collateral pocketed as profit by the liquidator.
event FlashSwapAndLiquidateBorrow(
event FlashLiquidate(
address indexed liquidator,
address indexed borrower,
address indexed bond,
Expand All @@ -46,12 +51,12 @@ interface IFlashUniswapV3 is IUniswapV3SwapCallback {

/// STRUCTS ///

/// @dev The parameters for the flash liquidation.
/// @dev The parameters for the n-hop flash liquidation.
struct FlashLiquidateParams {
address borrower;
IHToken bond;
IErc20 collateral;
uint24 poolFee;
bytes path;
int256 turnout;
uint256 underlyingAmount;
}
Expand All @@ -73,7 +78,8 @@ interface IFlashUniswapV3 is IUniswapV3SwapCallback {

/// NON-CONSTANT FUNCTIONS ///

/// @notice Flash borrows underlying from Uniswap V3, liquidates the underwater account, and repays the flash loan.
/// @notice Flash borrows underlying from Uniswap V3 via an n-hop swap, liquidates the underwater account, and
/// repays the flash loan.
/// @param params The parameters for the liquidation.
function flashLiquidate(FlashLiquidateParams memory params) external;
}
2 changes: 1 addition & 1 deletion packages/flash-swap/contracts/uniswap-v3/UniswapV3Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity =0.7.6;

import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";

import "./NoDelegateCall.sol";
import "./libraries/NoDelegateCall.sol";

import "@uniswap/v3-core/contracts/libraries/LowGasSafeMath.sol";
import "@uniswap/v3-core/contracts/libraries/SafeCast.sol";
Expand Down
Loading

0 comments on commit 44bec5f

Please sign in to comment.