Skip to content

Commit

Permalink
Merge pull request #98 from hifi-finance/feat/flash-swap-uni-v3
Browse files Browse the repository at this point in the history
Implement Flash Swap for Uniswap V3
  • Loading branch information
scorpion9979 committed Mar 31, 2023
2 parents f1b338b + 1dd6131 commit edf8e94
Show file tree
Hide file tree
Showing 164 changed files with 32,374 additions and 48 deletions.
6 changes: 6 additions & 0 deletions packages/errors/src/flashSwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ export enum FlashUniswapV2Errors {
TURNOUT_NOT_SATISFIED = "FlashUniswapV2__TurnoutNotSatisfied",
UNDERLYING_NOT_IN_POOL = "FlashUniswapV2__UnderlyingNotInPool",
}

export enum FlashUniswapV3Errors {
CALL_NOT_AUTHORIZED = "FlashUniswapV3__CallNotAuthorized",
LIQUIDATE_UNDERLYING_BACKED_VAULT = "FlashUniswapV3__LiquidateUnderlyingBackedVault",
TURNOUT_NOT_SATISFIED = "FlashUniswapV3__TurnoutNotSatisfied",
}
2 changes: 1 addition & 1 deletion packages/errors/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export { HifiPoolErrors, HifiPoolRegistryErrors, YieldSpaceErrors } from "./amm"
export { OwnableErrors } from "./external";

// flashSwap.ts
export { FlashUniswapV2Errors } from "./flashSwap";
export { FlashUniswapV2Errors, FlashUniswapV3Errors } from "./flashSwap";

// protocol.ts
export {
Expand Down
10 changes: 9 additions & 1 deletion packages/flash-swap/.solcover.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,13 @@ const rootSolCover = require("../../.solcover");

module.exports = {
...rootSolCover,
skipFiles: ["test", "uniswap-v2/IUniswapV2Pair.sol", "uniswap-v2/UniswapV2Pair.sol", "uniswap-v2/test"],
skipFiles: [
"test",
"uniswap-v2/IUniswapV2Pair.sol",
"uniswap-v2/UniswapV2Pair.sol",
"uniswap-v2/test",
"uniswap-v3/NoDelegateCall.sol",
"uniswap-v3/UniswapV3Pool",
"uniswap-v3/test",
],
};
54 changes: 48 additions & 6 deletions packages/flash-swap/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Hifi Flash Swap ![npm (scoped)](https://img.shields.io/npm/v/@hifi/flash-swap)

Flash swap implementations for liquidating underwater accounts. We can currently source liquidity only from Uniswap V2
and its forks.
Flash swap implementations for liquidating underwater accounts.

The build artifacts can be browsed via [unpkg.com](https://unpkg.com/browse/@hifi/flash-swap@latest/).

Expand All @@ -24,15 +23,17 @@ $ npm install @hifi/flash-swap
The node package that you just installed contains both Solidity and JavaScript code. The former is the smart contracts
themselves; the latter, the smart contract ABIs and the TypeChain bindings.

### Solidity
### FlashUniswapV2

#### Solidity

You are not supposed to import the smart contracts. Instead, you should interact with the Uniswap pool
directly. For example, with the [UniswapV2Pair](https://github.com/Uniswap/v2-core/blob/v1.0.1/contracts/UniswapV2Pair.sol)
contract you would call the `swap` function, and then Uniswap will forward the call to the `FlashUniswapV2`
contract. You can read more about flash swaps work in Uniswap on
contract. You can read more about flash swaps work in Uniswap V2 on
[docs.uniswap.org](https://docs.uniswap.org/protocol/V2/concepts/core-concepts/flash-swaps).

### JavaScript
#### JavaScript

Example for Uniswap V2:

Expand All @@ -52,13 +53,54 @@ async function flashSwap() {

const borrower = "0x...";
const hToken = "0x...";
const collateral = "0x...";
const turnout = parseUnits("1", 18);
const data = defaultAbiCoder.encode(["address", "address", "uint256"], [borrower, hToken, turnout]);
const data = defaultAbiCoder.encode(
["address", "address", "address", "uint256"],
[borrower, hToken, collateral, turnout],
);

await pair.swap(token0Amount, token1Amount, to, data);
}
```

### FlashUniswapV3

#### Solidity

To interact with the `FlashUniswapV3` contract, you will call the `flashLiquidate` function directly. This function performs the flash swap internally and requires you to pass the necessary liquidation parameters as a `FlashLiquidateParams` object.

#### JavaScript

Example for Uniswap V3:

```javascript
import { getDefaultProvider } from "@ethersproject/providers";
import { parseUnits } from "@ethersproject/units";
import { FlashUniswapV3__factory } from "@hifi/flash-swap/dist/types/factories/FlashUniswapV3__factory";

async function flashLiquidate() {
const defaultProvider = getDefaultProvider();
const flashUniswapV3 = new FlashUniswapV3__factory("0x...", defaultProvider);

const borrower = "0x...";
const hToken = "0x...";
const collateral = "0x...";
const poolFee = 3000;
const turnout = parseUnits("1", 18);
const underlyingAmount = parseUnits("100", 18);

await flashUniswapV3.flashLiquidate({
borrower: borrower,
bond: hToken,
collateral: collateral,
poolFee: poolFee,
turnout: turnout,
underlyingAmount: underlyingAmount,
});
}
```

## License

[LGPL v3](./LICENSE.md) © Mainframe Group Inc.
3 changes: 2 additions & 1 deletion packages/flash-swap/contracts/uniswap-v2/IFlashUniswapV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ interface IFlashUniswapV2 is IUniswapV2Callee {
/// @param liquidator The address of the liquidator account.
/// @param borrower The address of the borrower account being liquidated.
/// @param bond The address of the hToken contract.
/// @param underlyingAmount The amount of underlying flash borrowed
/// @param underlyingAmount The amount of underlying flash borrowed.
/// @param seizeAmount The amount of collateral seized.
/// @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.
Expand Down
256 changes: 256 additions & 0 deletions packages/flash-swap/contracts/uniswap-v3/FlashUniswapV3.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
// 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";

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

/// PUBLIC STORAGE ///

/// @inheritdoc IFlashUniswapV3
IBalanceSheetV2 public immutable override balanceSheet;

/// @inheritdoc IFlashUniswapV3
address public immutable override uniV3Factory;

/// @dev TickMath constants for computing the sqrt price limit.
uint160 internal constant MIN_SQRT_RATIO = 4295128739;
uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;

/// @dev The Uniswap V3 pool init code hash.
bytes32 internal constant POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54;

/// CONSTRUCTOR ///
constructor(IBalanceSheetV2 balanceSheet_, address uniV3Factory_) {
balanceSheet = balanceSheet_;
uniV3Factory = uniV3Factory_;
}

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

struct UniswapV3SwapCallbackParams {
IHToken bond;
address borrower;
IErc20 collateral;
PoolKey poolKey;
address sender;
int256 turnout;
uint256 underlyingAmount;
}

/// PUBLIC NON-CONSTANT FUNCTIONS ///

/// @inheritdoc IFlashUniswapV3
function flashLiquidate(FlashLiquidateParams memory params) external override {
FlashLiquidateLocalVars memory vars;

// This flash swap contract does not support liquidating vaults backed by underlying.
vars.underlying = params.bond.underlying();
if (params.collateral == vars.underlying) {
revert FlashUniswapV3__LiquidateUnderlyingBackedVault({
borrower: params.borrower,
underlying: address(vars.underlying)
});
}

// 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
})
)
});
}

struct UniswapV3SwapCallbackLocalVars {
uint256 mintedHTokenAmount;
uint256 profitAmount;
uint256 repayAmount;
uint256 seizeAmount;
uint256 subsidyAmount;
}

/// @inheritdoc IUniswapV3SwapCallback
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes calldata data
) external override {
UniswapV3SwapCallbackLocalVars memory vars;

// Unpack the ABI encoded data passed by the UniswapV3Pool contract.
UniswapV3SwapCallbackParams memory params = abi.decode(data, (UniswapV3SwapCallbackParams));

// Check that the caller is the Uniswap V3 flash pool contract.
if (msg.sender != poolFor(params.poolKey)) {
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);

// 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
});
}

// 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);
}

// 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
});
}

/// INTERNAL CONSTANT FUNCTIONS ///

/// @dev Returns the Uniswap V3 pool key for a given token pair and fee level.
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 });
}

/// @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(
uint160(
uint256(
keccak256(
abi.encodePacked(
hex"ff",
uniV3Factory,
keccak256(abi.encode(key.token0, key.token1, key.fee)),
POOL_INIT_CODE_HASH
)
)
)
)
);
}

/// INTERNAL NON-CONSTANT FUNCTIONS ///

/// @dev Liquidates the borrower, receiving collateral at a discount.
function liquidateBorrow(
address borrower,
IHToken bond,
IErc20 collateral,
uint256 mintedHTokenAmount
) internal returns (uint256 seizeCollateralAmount) {
uint256 collateralAmount = balanceSheet.getCollateralAmount(borrower, collateral);
uint256 hypotheticalRepayAmount = balanceSheet.getRepayAmount(collateral, collateralAmount, bond);

// If the hypothetical repay amount is bigger than the debt amount, this could be a single-collateral multi-bond
// vault. Otherwise, it could be a multi-collateral single-bond vault. However, it is difficult to generalize
// for the multi-collateral and multi-bond situation. The repay amount could be greater, smaller, or equal
// to the debt amount depending on the collateral and debt amount distribution.
uint256 debtAmount = balanceSheet.getDebtAmount(borrower, bond);
uint256 repayAmount = hypotheticalRepayAmount > debtAmount ? debtAmount : hypotheticalRepayAmount;

// Truncate the repay amount such that we keep the dust in this contract rather than the BalanceSheet.
uint256 truncatedRepayAmount = mintedHTokenAmount > repayAmount ? repayAmount : mintedHTokenAmount;

// Liquidate borrow.
uint256 oldCollateralBalance = collateral.balanceOf(address(this));
balanceSheet.liquidateBorrow(borrower, bond, truncatedRepayAmount, collateral);
uint256 newCollateralBalance = collateral.balanceOf(address(this));
unchecked {
seizeCollateralAmount = newCollateralBalance - oldCollateralBalance;
}
}

/// @dev Deposits the underlying in the HToken contract to mint hTokens on a one-to-one basis.
function mintHTokens(IHToken bond, uint256 underlyingAmount) internal returns (uint256 mintedHTokenAmount) {
IErc20 underlying = bond.underlying();

// Allow the HToken contract to spend underlying if allowance not enough.
uint256 allowance = underlying.allowance(address(this), address(bond));
if (allowance < underlyingAmount) {
underlying.approve(address(bond), type(uint256).max);
}

// Deposit underlying to mint hTokens.
uint256 oldHTokenBalance = bond.balanceOf(address(this));
bond.depositUnderlying(underlyingAmount);
uint256 newHTokenBalance = bond.balanceOf(address(this));
unchecked {
mintedHTokenAmount = newHTokenBalance - oldHTokenBalance;
}
}
}
Loading

0 comments on commit edf8e94

Please sign in to comment.