Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(flash-swap): support liquidating vaults with underlying as collateral #64

Merged
merged 16 commits into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f222278
feat(flash-swap): implement liquidating underlying as collateral
scorpion9979 Nov 2, 2021
ef7a6d3
feat(flash-swap): add tests for underlying as collateral
scorpion9979 Nov 3, 2021
254bafb
refactor(flash-swap): expect no profit + better error naming
scorpion9979 Nov 3, 2021
4e3f8cb
feat(tasks): add deploy task for underlying as collaeral flash-swap
scorpion9979 Nov 3, 2021
59db094
feat(flash-swap): repay USDC liquidation 0.3% Uniswap fee from bot wa…
scorpion9979 Nov 3, 2021
c238f40
feat(flash-swap): add test to increase coverage
scorpion9979 Nov 3, 2021
ffda179
refactor: move common logic in "HifiFlashUniswapV2.sol"
PaulRBerg Nov 4, 2021
b02aac3
refactor(flash-swap): "HifiFlashUniswapV2" into "CollateralFlashUnisw…
PaulRBerg Nov 4, 2021
5a2e0dc
refactor(flash-swap): use only "underlying" in var names in "Underlyi…
PaulRBerg Nov 4, 2021
e1ed460
test(flash-swap): refactor variable names in "UnderlyingFlashUniswapV…
PaulRBerg Nov 4, 2021
601f296
ci: fix task names in flash swap deployers
PaulRBerg Nov 4, 2021
4da7d20
chore(flash-swap): order variables alphabetically
PaulRBerg Nov 4, 2021
613df07
fix(flash-swap): properly compare "repayUnderlyingAmount" to "seizedU…
PaulRBerg Nov 4, 2021
d86fc14
fix(flash-swap): transfer profit instead of subsidized amount
PaulRBerg Nov 5, 2021
190e0ec
test(flash-swap): repay underlying equal to seized underlying
scorpion9979 Nov 5, 2021
5ced780
test(flash-swap): flash borrow other token when token order is changed
PaulRBerg Nov 5, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/flash-swap/contracts/test/GodModeHToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ contract GodModeHToken is HToken {
function __godMode_setUnderlying(IErc20 newUnderlying) external {
underlying = newUnderlying;
}

function __godMode_setMaturity(uint256 newMaturity) external {
maturity = newMaturity;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ interface IUnderlyingFlashUniswapV2 is IUniswapV2Callee {
/// repayUnderlyingAmount = ---------------------
/// 997
///
/// @dev See "getAmountIn" and "getAmountOut" in UniswapV2Library.sol. Flash swaps are repaid via the
/// same borrowed pair token and the 0.3% LP fee applies.
/// @dev See "getAmountIn" and "getAmountOut" in UniswapV2Library.sol. Flash swaps can be repaid via the
/// same borrowed pair token but the 0.3% LP fee still applies.
/// @param underlyingAmount The amount of underlying flash borrowed.
/// @return repayUnderlyingAmount The minimum amount of underlying that must be repaid.
function getRepayUnderlyingAmount(uint256 underlyingAmount) external view returns (uint256 repayUnderlyingAmount);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ contract UnderlyingFlashUniswapV2 is IUnderlyingFlashUniswapV2 {
uint256 seizedUnderlyingAmount;
uint256 shortfallUnderlyingAmount;
address subsidizer;
uint256 surplusUnderlyingAmount;
IErc20 underlying;
uint256 underlyingAmount;
}
Expand Down Expand Up @@ -153,13 +154,19 @@ contract UnderlyingFlashUniswapV2 is IUnderlyingFlashUniswapV2 {
// Calculate the amount of underlying required to repay.
vars.repayUnderlyingAmount = getRepayUnderlyingAmount(vars.underlyingAmount);

// There is no incentive to liquidate underlying-backed vaults after the bond maturation. Thus the flash swap
// fee must be subsidized when the repay underlying amount is greater than the seized underlying amount.
// When the liquidation incentive is zero, there is no incentive to liquidate underlying-backed vaults post
// bond maturation. The flash swap fee must be subsidized when the repay underlying amount is greater than
// the seized underlying amount.
if (vars.repayUnderlyingAmount > vars.seizedUnderlyingAmount) {
unchecked {
vars.shortfallUnderlyingAmount = vars.repayUnderlyingAmount - vars.seizedUnderlyingAmount;
}
vars.underlying.safeTransferFrom(vars.subsidizer, address(this), vars.shortfallUnderlyingAmount);
} else if (vars.seizedUnderlyingAmount > vars.repayUnderlyingAmount) {
unchecked {
vars.surplusUnderlyingAmount = vars.seizedUnderlyingAmount - vars.repayUnderlyingAmount;
}
vars.underlying.safeTransfer(sender, vars.shortfallUnderlyingAmount);
PaulRBerg marked this conversation as resolved.
Show resolved Hide resolved
}

// Pay back the loan.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { defaultAbiCoder } from "@ethersproject/abi";
import { BigNumber } from "@ethersproject/bignumber";
import { Zero } from "@ethersproject/constants";
import { LIQUIDATION_INCENTIVES } from "@hifi/constants";
import { BalanceSheetErrors, CollateralFlashUniswapV2Errors } from "@hifi/errors";
import { USDC, WBTC, hUSDC, price } from "@hifi/helpers";
import { expect } from "chai";
import { toBn } from "evm-bn";

import type { GodModeErc20 } from "../../../../src/types/GodModeErc20";
import { deployGodModeErc20 } from "../../../shared/deployers";

async function bumpPoolReserves(this: Mocha.Context, wbtcAmount: BigNumber, usdcAmount: BigNumber): Promise<void> {
async function increasePoolReserves(this: Mocha.Context, wbtcAmount: BigNumber, usdcAmount: BigNumber): Promise<void> {
// Mint WBTC to the pair contract.
if (!wbtcAmount.isZero()) {
await this.contracts.wbtc.__godMode_mint(this.contracts.uniswapV2Pair.address, wbtcAmount);
Expand All @@ -34,18 +34,18 @@ function encodeCallData(this: Mocha.Context): string {

async function getSeizableAndProfitCollateralAmounts(
this: Mocha.Context,
repayHUsdcAmount: BigNumber,
underlyingAmount: BigNumber,
repayAmount: BigNumber,
swapUnderlyingAmount: BigNumber,
): Promise<{ expectedProfitWbtcAmount: BigNumber; seizableWbtcAmount: BigNumber }> {
const seizableWbtcAmount = await this.contracts.balanceSheet.getSeizableCollateralAmount(
this.contracts.hToken.address,
repayHUsdcAmount,
repayAmount,
this.contracts.wbtc.address,
);
const repayWbtcAmount = await this.contracts.collateralFlashUniswapV2.getRepayCollateralAmount(
this.contracts.uniswapV2Pair.address,
this.contracts.usdc.address,
underlyingAmount,
swapUnderlyingAmount,
);
const expectedProfitWbtcAmount = seizableWbtcAmount.sub(repayWbtcAmount);
return { expectedProfitWbtcAmount, seizableWbtcAmount };
Expand Down Expand Up @@ -136,7 +136,7 @@ export function shouldBehaveLikeUniswapV2Call(): void {
});
});

context("when the caller is the pair contract", function () {
context("when the caller is the UniswapV2Pair contract", function () {
beforeEach(async function () {
// Set the oracle price to 1 WBTC = $20k.
await this.contracts.wbtcPriceFeed.setPrice(price("20000"));
Expand All @@ -145,10 +145,10 @@ export function shouldBehaveLikeUniswapV2Call(): void {
await this.contracts.usdcPriceFeed.setPrice(price("1"));

// Mint 100 WBTC and 2m USDC to the pair contract. This makes the price 1 WBTC ~ 20k USDC.
await bumpPoolReserves.call(this, WBTC("100"), USDC("2e6"));
await increasePoolReserves.call(this, WBTC("100"), USDC("2e6"));
});

context("when the underlying is not in the pair contract", function () {
context("when the underlying is not part of the UniswapV2Pair contract", function () {
it("reverts", async function () {
const { token0Amount, token1Amount } = await getTokenAmounts.call(this, Zero, USDC("10000"));
const foo: GodModeErc20 = await deployGodModeErc20(this.signers.admin, "Foo", "FOO", BigNumber.from(18));
Expand All @@ -160,7 +160,7 @@ export function shouldBehaveLikeUniswapV2Call(): void {
});
});

context("when the underlying is in the pair contract", function () {
context("when the underlying is part of the UniswapV2Pair contract", function () {
context("when collateral is flash borrowed", function () {
it("reverts", async function () {
const { token0Amount, token1Amount } = await getTokenAmounts.call(this, WBTC("1"), Zero);
Expand All @@ -173,18 +173,17 @@ export function shouldBehaveLikeUniswapV2Call(): void {

context("when underlying is flash borrowed", function () {
const borrowAmount: BigNumber = hUSDC("10000");
const collateralAmount: BigNumber = Zero;
const collateralCeiling: BigNumber = WBTC("100");
const debtCeiling: BigNumber = hUSDC("1e6");
const liquidationIncentive: BigNumber = toBn("1.10");
const underlyingAmount: BigNumber = USDC("10000");
const swapCollateralAmount: BigNumber = Zero;
const swapUnderlyingAmount: BigNumber = USDC("10000");
const wbtcDepositAmount: BigNumber = WBTC("1");

let token0Amount: BigNumber;
let token1Amount: BigNumber;

beforeEach(async function () {
const tokenAmounts = await getTokenAmounts.call(this, collateralAmount, underlyingAmount);
const tokenAmounts = await getTokenAmounts.call(this, swapCollateralAmount, swapUnderlyingAmount);
token0Amount = tokenAmounts.token0Amount;
token1Amount = tokenAmounts.token1Amount;

Expand All @@ -197,7 +196,7 @@ export function shouldBehaveLikeUniswapV2Call(): void {
// Set the liquidation incentive.
await this.contracts.fintroller
.connect(this.signers.admin)
.setLiquidationIncentive(this.contracts.wbtc.address, liquidationIncentive);
.setLiquidationIncentive(this.contracts.wbtc.address, LIQUIDATION_INCENTIVES.default);

// Set the collateral ceiling.
await this.contracts.fintroller
Expand Down Expand Up @@ -238,44 +237,47 @@ export function shouldBehaveLikeUniswapV2Call(): void {
});

context("when the borrower has a liquidity shortfall", function () {
context("when the price given by the pair contract price is better than the oracle price", function () {
beforeEach(async function () {
// Set the WBTC price to $12.5k to make borrower's collateral ratio 125%.
await this.contracts.wbtcPriceFeed.setPrice(price("12500"));
context(
"when the price given by the UniswapV2Pair contract price is better than the oracle price",
function () {
beforeEach(async function () {
// Set the WBTC price to $12.5k to make borrower's collateral ratio 125%.
await this.contracts.wbtcPriceFeed.setPrice(price("12500"));

// Burn 1m USDC from the pair contract. This makes the pair contract price 1 WBTC ~ 10k USDC.
await reducePoolReserves.call(this, Zero, USDC("1e6"));
});
// Burn 1m USDC from the pair contract. This makes the pair contract price 1 WBTC ~ 10k USDC.
await reducePoolReserves.call(this, Zero, USDC("1e6"));
});

it("reverts", async function () {
const to: string = this.contracts.collateralFlashUniswapV2.address;
await expect(
this.contracts.uniswapV2Pair
.connect(this.signers.liquidator)
.swap(token0Amount, token1Amount, to, data),
).to.be.revertedWith(CollateralFlashUniswapV2Errors.InsufficientProfit);
});
});
it("reverts", async function () {
const to: string = this.contracts.collateralFlashUniswapV2.address;
await expect(
this.contracts.uniswapV2Pair
.connect(this.signers.liquidator)
.swap(token0Amount, token1Amount, to, data),
).to.be.revertedWith(CollateralFlashUniswapV2Errors.InsufficientProfit);
});
},
);

context("when the price given by the pair contract is the same as the oracle price", function () {
context("when the price given by the UniswapV2Pair contract is the same as the oracle price", function () {
let expectedProfitWbtcAmount: BigNumber;
let seizableWbtcAmount: BigNumber;

context("when the collateral ratio is lower than 110%", function () {
const repayHUsdcAmount: BigNumber = hUSDC("9090.909090909090909090");
const repayAmount: BigNumber = hUSDC("9090.909090909090909090");

beforeEach(async function () {
// Set the WBTC price to $10k to make the borrower's collateral ratio 100%.
await this.contracts.wbtcPriceFeed.setPrice(price("10000"));

// Calculate the amounts necessary for running the tests.
const calculatesAmounts = await getSeizableAndProfitCollateralAmounts.call(
const calculatedAmounts = await getSeizableAndProfitCollateralAmounts.call(
this,
repayHUsdcAmount,
underlyingAmount,
repayAmount,
swapUnderlyingAmount,
);
expectedProfitWbtcAmount = calculatesAmounts.expectedProfitWbtcAmount;
seizableWbtcAmount = calculatesAmounts.seizableWbtcAmount;
expectedProfitWbtcAmount = calculatedAmounts.expectedProfitWbtcAmount;
seizableWbtcAmount = calculatedAmounts.seizableWbtcAmount;
});

it("flash swaps USDC via and makes a WBTC profit", async function () {
Expand All @@ -290,7 +292,7 @@ export function shouldBehaveLikeUniswapV2Call(): void {
});

context("when the collateral ratio is lower than 150% but higher than 110%", function () {
const repayHUsdcAmount: BigNumber = hUSDC("10000");
const repayAmount: BigNumber = hUSDC("10000");

beforeEach(async function () {
// Set the WBTC price to $12.5k to make borrower's collateral ratio 125%.
Expand All @@ -300,16 +302,16 @@ export function shouldBehaveLikeUniswapV2Call(): void {
await reducePoolReserves.call(this, Zero, USDC("75e4"));

// Calculate the amounts necessary for running the tests.
const calculatesAmounts = await getSeizableAndProfitCollateralAmounts.call(
const calculatedAmounts = await getSeizableAndProfitCollateralAmounts.call(
this,
repayHUsdcAmount,
underlyingAmount,
repayAmount,
swapUnderlyingAmount,
);
expectedProfitWbtcAmount = calculatesAmounts.expectedProfitWbtcAmount;
seizableWbtcAmount = calculatesAmounts.seizableWbtcAmount;
expectedProfitWbtcAmount = calculatedAmounts.expectedProfitWbtcAmount;
seizableWbtcAmount = calculatedAmounts.seizableWbtcAmount;
});

context("new order of tokens in the pair", function () {
context("new order of tokens in the UniswapV2Pair contract", function () {
let localToken0Amount: BigNumber;
let localToken1Amount: BigNumber;

Expand All @@ -318,13 +320,13 @@ export function shouldBehaveLikeUniswapV2Call(): void {
if (token0 == this.contracts.wbtc.address) {
await this.contracts.uniswapV2Pair.__godMode_setToken0(this.contracts.usdc.address);
await this.contracts.uniswapV2Pair.__godMode_setToken1(this.contracts.wbtc.address);
localToken0Amount = underlyingAmount;
localToken1Amount = collateralAmount;
localToken0Amount = swapUnderlyingAmount;
localToken1Amount = swapCollateralAmount;
} else {
await this.contracts.uniswapV2Pair.__godMode_setToken0(this.contracts.wbtc.address);
await this.contracts.uniswapV2Pair.__godMode_setToken1(this.contracts.usdc.address);
localToken0Amount = collateralAmount;
localToken1Amount = underlyingAmount;
localToken0Amount = swapCollateralAmount;
localToken1Amount = swapUnderlyingAmount;
}
await this.contracts.uniswapV2Pair.sync();
});
Expand All @@ -340,7 +342,7 @@ export function shouldBehaveLikeUniswapV2Call(): void {
});
});

context("initial order of tokens in the pair", function () {
context("initial order of tokens in the UniswapV2Pair contract", function () {
it("flash swaps USDC via and makes a WBTC profit", async function () {
const to: string = this.contracts.collateralFlashUniswapV2.address;
const preWbtcBalance = await this.contracts.wbtc.balanceOf(this.signers.liquidator.address);
Expand All @@ -362,7 +364,7 @@ export function shouldBehaveLikeUniswapV2Call(): void {
this.signers.liquidator.address,
this.signers.borrower.address,
this.contracts.hToken.address,
underlyingAmount,
swapUnderlyingAmount,
seizableWbtcAmount,
expectedProfitWbtcAmount,
);
Expand Down
Loading