This file can be dropped into perp-lushan at commit 56ab2962b36ae924b7755e12a172cc0cd37e84ed (Sept 15) and run as a suite in test/clearingHouse
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");
});
});
})