Skip to content

Instantly share code, notes, and snippets.

@cgewecke
Last active September 21, 2021 16:10
Show Gist options
  • Save cgewecke/24d60f628bebe9c998dffc62763030fc to your computer and use it in GitHub Desktop.
Save cgewecke/24d60f628bebe9c998dffc62763030fc to your computer and use it in GitHub Desktop.

This file can be dropped into perp-lushan at commit 56ab2962b36ae924b7755e12a172cc0cd37e84ed (Sept 15) and run as a suite in test/clearingHouse

⚠️ Cases are meant to be run with .only. Files does not snapshot and revert between scenarios.

Price calculation requires the JSBI package (npm install --save-dev jsbi)

import { MockContract } from "@eth-optimism/smock"
import { parseEther } from "@ethersproject/units"
import { expect } from "chai"
import { parseUnits } from "ethers/lib/utils"
import { ethers, waffle } from "hardhat"
import {
  BaseToken,
  ClearingHouse,
  Exchange,
  MarketRegistry,
  OrderBook,
  TestERC20,
  UniswapV3Pool,
  Vault,
} from "../../typechain"
import { QuoteToken } from "../../typechain/QuoteToken"
import { deposit } from "../helper/token"
import { forward } from "../shared/time"
import { encodePriceSqrt } from "../shared/utilities"
import { BaseQuoteOrdering, createClearingHouseFixture } from "./fixtures"
import JSBI from 'jsbi';

describe("Funding", () => {
  const [admin, alice, bob, carol] = waffle.provider.getWallets()
  const loadFixture: ReturnType<typeof waffle.createFixtureLoader> = waffle.createFixtureLoader([admin])
  let clearingHouse: ClearingHouse
  let marketRegistry: MarketRegistry
  let exchange: Exchange
  let orderBook: OrderBook
  let vault: Vault
  let collateral: TestERC20
  let baseToken: BaseToken
  let quoteToken: QuoteToken
  let mockedBaseAggregator: MockContract
  let pool: UniswapV3Pool
  let collateralDecimals: number

  beforeEach(async () => {
    const _clearingHouseFixture = await loadFixture(
      createClearingHouseFixture(BaseQuoteOrdering.BASE_0_QUOTE_1, false),
    )
    clearingHouse = _clearingHouseFixture.clearingHouse as ClearingHouse
    orderBook = _clearingHouseFixture.orderBook
    exchange = _clearingHouseFixture.exchange
    marketRegistry = _clearingHouseFixture.marketRegistry
    vault = _clearingHouseFixture.vault
    collateral = _clearingHouseFixture.USDC
    baseToken = _clearingHouseFixture.baseToken
    quoteToken = _clearingHouseFixture.quoteToken
    mockedBaseAggregator = _clearingHouseFixture.mockedBaseAggregator
    pool = _clearingHouseFixture.pool
    collateralDecimals = await collateral.decimals()

    // price at 50400 == 151.373306858723226652
    // tick = 50200 (1.0001^50200 = 151.373306858723226652)
    await pool.initialize(encodePriceSqrt("151.373306858723226652", "1"))
    // the initial number of oracle can be recorded is 1; thus, have to expand it
    await pool.increaseObservationCardinalityNext((2 ^ 16) - 1)

    // add pool after it's initialized
    await marketRegistry.addPool(baseToken.address, 10000)

    // alice add long limit order
    await collateral.mint(alice.address, parseUnits("10000", collateralDecimals))
    await deposit(alice, vault, 10000, collateral)

    await collateral.mint(bob.address, parseUnits("1000", collateralDecimals))
    await deposit(bob, vault, 100, collateral)

    await collateral.mint(carol.address, parseUnits("1000", collateralDecimals))
    await deposit(carol, vault, 1000, collateral)
  })

  async function logValues(_taker: any, description: string) {
    const sqrtPriceX96 = (await pool.slot0()).sqrtPriceX96;

    const priceX86 = JSBI.BigInt(sqrtPriceX96.toString())
    const squaredPrice = JSBI.multiply(priceX86, priceX86);
    const decimalsRatio = 1e18;
    const denominator = JSBI.exponentiate(JSBI.BigInt(2), JSBI.BigInt(192));
    const scaledPrice = JSBI.multiply(squaredPrice, JSBI.BigInt(decimalsRatio));
    const price = (JSBI.divide(scaledPrice, denominator)).toString();
    const accountValue = await clearingHouse.getAccountValue(_taker.address);
    const freeCollateral = await vault.getFreeCollateral(_taker.address)
    const basePositionSize = await clearingHouse.getPositionSize(_taker.address, baseToken.address)
    const positionValue = await clearingHouse.getPositionValue(_taker.address, baseToken.address)
    const totalUnrealizedPNL = await clearingHouse.getTotalUnrealizedPnl(_taker.address)
    const owedRealizedPnl = await clearingHouse.getOwedRealizedPnl(_taker.address);
    const openNotional = await clearingHouse.getOpenNotional(_taker.address, baseToken.address)
    const totalInitialMarginReq = await clearingHouse.getTotalInitialMarginRequirement(_taker.address)
    const netQuoteBalance = await clearingHouse.getNetQuoteBalance(_taker.address)
    const pendingFundPayment = await clearingHouse.getPendingFundingPayment(_taker.address, baseToken.address)
    //const [baseBalance, quoteBalance] = await clearingHouse.getTokenBalance(_taker.address, baseToken.address)
    const vaultBalanceOf = await vault.balanceOf(_taker.address);
    const collatBalanceOf = await collateral.balanceOf(_taker.address);

    console.log()
    console.log(description)
    console.log('==========');
    console.log('accountValue:          ' + accountValue);
    console.log('freeCollateral:        ' + freeCollateral);
    console.log('basePositionSize:      ' + basePositionSize);
    console.log('positionValue:         ' + positionValue);
    console.log('totalUnrealizedPNL:    ' + totalUnrealizedPNL);
    console.log('owedRealizedPnl:       ' + owedRealizedPnl);
    console.log('openNotional:          ' + openNotional);
    console.log('vault.balanceOf:       ' + vaultBalanceOf);
    console.log('collat.balanceOf:      ' + collatBalanceOf);
    console.log('totalInitialMarginReq: ' + totalInitialMarginReq);
    console.log('netQuoteBalance:       ' + netQuoteBalance);
    console.log('pendingFundPayment:    ' + pendingFundPayment);
    console.log('price:                 ' + price);
  }

  describe("Funding", () => {

    beforeEach(async () => {
      const lowerTick: number = 0
      const upperTick: number = 100000

      await clearingHouse.connect(alice).addLiquidity({
        baseToken: baseToken.address,
        base: parseEther("65.943787"),
        quote: parseEther("10000"),
        lowerTick,
        upperTick,
        minBase: 0,
        minQuote: 0,
        deadline: ethers.constants.MaxUint256,
      });
    })

    it("long 100 USD, AMM price $3 > Oracle price for 1 day, close position", async () => {
      // set oracle price near market price
      // price at 50400 == 151.373306858723226652
      // tick = 50200 (1.0001^50200 = 151.373306858723226652)
      mockedBaseAggregator.smocked.latestRoundData.will.return.with(async () => {
        return [0, parseUnits("151.373307", 6), 0, 0, 0]
      })

      await logValues(bob, 'pre open-position');

      // Buys .658 Base
      await clearingHouse.connect(bob).openPosition({
        baseToken: baseToken.address,
        isBaseToQuote: false,
        isExactInput: true,
        oppositeAmountBound: 0,
        amount: parseEther("100"),
        sqrtPriceLimitX96: 0,
        deadline: ethers.constants.MaxUint256,
        referralCode: ethers.constants.HashZero,
      })

      // AMM price ~$154.139414
      await logValues(bob, "openPosition initial")

      await forward(86400);

      // Funding = $3 * base position (.658) * 1 day
      await logValues(bob, '1 day elapses');

      // Closes:
      await clearingHouse.connect(bob).closePosition({
        baseToken: baseToken.address,
        sqrtPriceLimitX96: 0,
        oppositeAmountBound: 0,
        deadline: ethers.constants.MaxUint256,
        referralCode: ethers.constants.HashZero,
      });

      await logValues(bob, "close position");
    });

    it("long 100 USD, AMM price $3 < Oracle price for 1 day, close position", async () => {
      // set oracle price near market price
      mockedBaseAggregator.smocked.latestRoundData.will.return.with(async () => {
        return [0, parseUnits("151.373307", 6), 0, 0, 0]
      })

      await logValues(bob, 'pre open-position');

      // Buys .658 Base
      await clearingHouse.connect(bob).openPosition({
        baseToken: baseToken.address,
        isBaseToQuote: false,
        isExactInput: true,
        oppositeAmountBound: 0,
        amount: parseEther("100"),
        sqrtPriceLimitX96: 0,
        deadline: ethers.constants.MaxUint256,
        referralCode: ethers.constants.HashZero,
      })

      // AMM price ~$154.139414
      await logValues(bob, "openPosition initial")

      // Carol shorts 1.3 base
      // Moves AMM price below oracle price to: 148.665565
      await clearingHouse.connect(carol).openPosition({
        baseToken: baseToken.address,
        isBaseToQuote: true,
        isExactInput: true,
        oppositeAmountBound: 0,
        amount: parseEther("1.3"),
        sqrtPriceLimitX96: 0,
        deadline: ethers.constants.MaxUint256,
        referralCode: ethers.constants.HashZero,
      })

      await logValues(bob, "after carol shorts, moving price down")

      await forward(86400);

      await logValues(bob, '1 day elapses');

      // Closes:
      await clearingHouse.connect(bob).closePosition({
        baseToken: baseToken.address,
        sqrtPriceLimitX96: 0,
        oppositeAmountBound: 0,
        deadline: ethers.constants.MaxUint256,
        referralCode: ethers.constants.HashZero,
      });

      await logValues(bob, "close position");
    });

    it("long 100 USD, Oracle price moves above AMM price for 1 day, close position", async () => {
      // set oracle price near market price
      mockedBaseAggregator.smocked.latestRoundData.will.return.with(async () => {
        return [0, parseUnits("151.373307", 6), 0, 0, 0]
      })

      await logValues(bob, 'pre open-position');

      // Buys .658 Base
      await clearingHouse.connect(bob).openPosition({
        baseToken: baseToken.address,
        isBaseToQuote: false,
        isExactInput: true,
        oppositeAmountBound: 0,
        amount: parseEther("100"),
        sqrtPriceLimitX96: 0,
        deadline: ethers.constants.MaxUint256,
        referralCode: ethers.constants.HashZero,
      })

      // AMM price ~$154.139414
      await logValues(bob, "openPosition initial")

      mockedBaseAggregator.smocked.latestRoundData.will.return.with(async () => {
        return [0, parseUnits("157.373307", 6), 0, 0, 0]
      })

      await logValues(bob, "oracle price to 157.373")

      await forward(86400);

      await logValues(bob, '1 day elapses');

      // Closes:
      await clearingHouse.connect(bob).closePosition({
        baseToken: baseToken.address,
        sqrtPriceLimitX96: 0,
        oppositeAmountBound: 0,
        deadline: ethers.constants.MaxUint256,
        referralCode: ethers.constants.HashZero,
      });

      await logValues(bob, "close position");
    });

    it("long 200 USD (2X), Oracle price moves above AMM price for 1 day, close position", async () => {
      mockedBaseAggregator.smocked.latestRoundData.will.return.with(async () => {
        return [0, parseUnits("151.373307", 6), 0, 0, 0]
      })

      await logValues(bob, 'pre open-position');

      // Buys .658 Base
      await clearingHouse.connect(bob).openPosition({
        baseToken: baseToken.address,
        isBaseToQuote: false,
        isExactInput: true,
        oppositeAmountBound: 0,
        amount: parseEther("200"),
        sqrtPriceLimitX96: 0,
        deadline: ethers.constants.MaxUint256,
        referralCode: ethers.constants.HashZero,
      })

      // AMM price ~$156.93056
      await logValues(bob, "openPosition initial")

      mockedBaseAggregator.smocked.latestRoundData.will.return.with(async () => {
        return [0, parseUnits("160.24", 6), 0, 0, 0]
      })

      await logValues(bob, "oracle price to 160.24")

      await forward(86400);

      await logValues(bob, '1 day elapses');

      // Closes:
      await clearingHouse.connect(bob).closePosition({
        baseToken: baseToken.address,
        sqrtPriceLimitX96: 0,
        oppositeAmountBound: 0,
        deadline: ethers.constants.MaxUint256,
        referralCode: ethers.constants.HashZero,
      });

      await logValues(bob, "close position");
    });

    it.only("long 100 USD, Oracle above AMM price for 1 day, then drops, then close position", async () => {
      // set oracle price near market price
      mockedBaseAggregator.smocked.latestRoundData.will.return.with(async () => {
        return [0, parseUnits("151.373307", 6), 0, 0, 0]
      })

      await logValues(bob, 'pre open-position');

      // Buys .658 Base
      await clearingHouse.connect(bob).openPosition({
        baseToken: baseToken.address,
        isBaseToQuote: false,
        isExactInput: true,
        oppositeAmountBound: 0,
        amount: parseEther("100"),
        sqrtPriceLimitX96: 0,
        deadline: ethers.constants.MaxUint256,
        referralCode: ethers.constants.HashZero,
      })

      // AMM price ~$154.139414
      await logValues(bob, "openPosition initial")

      mockedBaseAggregator.smocked.latestRoundData.will.return.with(async () => {
        return [0, parseUnits("157.373307", 6), 0, 0, 0]
      })

      await logValues(bob, "oracle price to 157.373")

      await forward(86400);

      await logValues(bob, '1 day elapses');

      mockedBaseAggregator.smocked.latestRoundData.will.return.with(async () => {
        return [0, parseUnits("154.139414", 6), 0, 0, 0]
      })

      await logValues(bob, 'Oracle price syncs back down to AMM price');

      // Closes:
      await clearingHouse.connect(bob).closePosition({
        baseToken: baseToken.address,
        sqrtPriceLimitX96: 0,
        oppositeAmountBound: 0,
        deadline: ethers.constants.MaxUint256,
        referralCode: ethers.constants.HashZero,
      });

      await logValues(bob, "close position");
    });
  });
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment