Skip to content

Commit

Permalink
Fix oiShares calculation (#70)
Browse files Browse the repository at this point in the history
* Fix oiShares calculation on build

* Add oi to position info for oiInitial calc, fix multiple build test

* Fix gh actions to clear cache and recompile all contracts

* Change Position.Info to use oiToSharesRatio for initial oi calc

* Update Position.Info for frac remaining, store ticks, mulDiv w oiShares

* Fix oiShares calculation on build

* Add oi to position info for oiInitial calc, fix multiple build test

* Fix gh actions to clear cache and recompile all contracts

* Change Position.Info to use oiToSharesRatio for initial oi calc

* Update Position.Info for frac remaining, store ticks, mulDiv w oiShares

* Fix gh actions

* Update README for Position.Info changes

* Add balancer github ref for FixedPoint, LogExpMath.sol

* Update docs README

* Fix pos lib getter tests

* Fix TODO in market unwind

* Fix pos lib value tests

* Fix pos lib trading fee tests

* Fix pos lib liquidatable tests

* Fix market build tests

* Fix market slippage tests

* Fix market liquidate tests

* Fix market unwind tests: update_pos, removes_oi, multiple_pos

* Fix market unwind tests: reg_vol, reg_mint, exec_transfer

* Fix remaining market unwind tests

* Pay funding when set risk param

* Fix market liquidate tests

* Fix market interface for positions(), fix comment for midPriceAtEntry

* Fix calcOiShares

* Add round trip test for fixed cast

* Add checks of pos.oiShares vs oiShares to market tests

* Make pos.oiShares mutable to fix oiShares rounding issues

* Fix README

* Remove subfloor for aggregate oi shares on unwind, liquidate

* Fix market interface returns
  • Loading branch information
mikeyrf authored Jun 7, 2022
1 parent 24dffd5 commit e165eb1
Show file tree
Hide file tree
Showing 45 changed files with 3,137 additions and 1,456 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/test-python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches:
- main
pull_request_target:
pull_request:
branches:
- main

Expand All @@ -30,7 +30,6 @@ jobs:
echo WEB3_INFURA_PROJECT_ID=${{ secrets.WEB3_INFURA_PROJECT_ID }} >> .env
echo ETHERSCAN_TOKEN=${{ secrets.ETHERSCAN_TOKEN }} >> .env
cat .env
- name: Cache Compiler Installations
uses: actions/cache@v2
with:
Expand Down
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,22 @@ The market contract tracks the current open interest for all outstanding positio

```
library Position {
struct Info {
uint96 notional; // initial notional = collateral * leverage
uint96 debt; // debt
uint48 entryToMidRatio; // ratio of entryPrice / _midFromFeed() at build
bool isLong; // whether long or short
bool liquidated; // whether has been liquidated
uint256 oiShares; // shares of aggregate open interest on side
}
/// @dev immutables: notionalInitial, debtInitial, midTick, entryTick, isLong
/// @dev mutables: liquidated, oiShares, fractionRemaining
struct Info {
uint96 notionalInitial; // initial notional = collateral * leverage
uint96 debtInitial; // initial debt = notional - collateral
int24 midTick; // midPrice = 1.0001 ** midTick at build
int24 entryTick; // entryPrice = 1.0001 ** entryTick at build
bool isLong; // whether long or short
bool liquidated; // whether has been liquidated (mutable)
uint240 oiShares; // current shares of aggregate open interest on side (mutable)
uint16 fractionRemaining; // fraction of initial position remaining (mutable)
}
}
```

For each market contract, there is an associated feed contract that delivers the data from the data stream. The market contract stores a pointer to the `feed` contract that it retrieves new data from, and the market uses its `update()` function to retrieve the most recent price and liquidity data from the feed through a call to `IOverlayV1Feed(feed).latest()`. This call occurs every time a user interacts with the market.
For each market contract, there is an associated feed contract that delivers the data from the data stream. The market contract stores a pointer to the `feed` contract that it retrieves new data from, and the market uses the feed's `update()` function to retrieve the most recent price and liquidity data from the feed through a call to `IOverlayV1Feed(feed).latest()`. This call occurs every time a user interacts with the market.

All markets are implemented by the contract `OverlayV1Market.sol`, regardless of the underlying feed type.

Expand Down Expand Up @@ -97,14 +101,16 @@ library Oracle {
}
}
```
from the [`Oracle.sol`](./contracts/libraries/Oracle.sol) library. `Oracle.Data` data is consumed by each deployment of `OverlayV1Market.sol` for traders to take positions on the market of interest.
from the [`Oracle.sol`](./contracts/libraries/Oracle.sol) library. `Oracle.Data` is consumed by each deployment of `OverlayV1Market.sol` for traders to take positions on the market of interest.

For each oracle provider supported, there should be a specific implementation of a feed contract that inherits from `OverlayV1Feed.sol` (e.g. [`OverlayV1UniswapV3Feed.sol`](./contracts/feeds/uniswapv3/OverlayV1UniswapV3Feed.sol) for Uniswap V3 pools).


### OVL Module

OVL module consists of an ERC20 token with permissioned mint and burn functions. Upon initialization, markets must be given permission to mint and burn OVL to compensate traders for their PnL on positions.
The OVL module consists of an ERC20 token with permissioned mint and burn functions. Upon initialization, markets must be given permission to mint and burn OVL to compensate traders for their PnL on positions.

[`OverlayV1Factory.sol`](./contracts/OverlayV1Factory.sol) grants these mint and burn permissions on a call to `deployMarket()`. Because of this, the factory contract must have admin privileges on the OVL token prior to deploying markets.


## Deployment Process
Expand Down
159 changes: 98 additions & 61 deletions contracts/OverlayV1Market.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import "./interfaces/IOverlayV1Market.sol";
import "./interfaces/IOverlayV1Token.sol";
import "./interfaces/feeds/IOverlayV1Feed.sol";

import "./libraries/FixedCast.sol";
import "./libraries/FixedPoint.sol";
import "./libraries/Oracle.sol";
import "./libraries/Position.sol";
import "./libraries/Risk.sol";
import "./libraries/Roller.sol";
import "./libraries/Tick.sol";

contract OverlayV1Market is IOverlayV1Market {
using FixedCast for uint16;
using FixedCast for uint256;
using FixedPoint for uint256;
using Oracle for Oracle.Data;
using Position for mapping(bytes32 => Position.Info);
Expand Down Expand Up @@ -187,40 +191,25 @@ contract OverlayV1Market is IOverlayV1Market {
require(isLong ? price <= priceLimit : price >= priceLimit, "OVLV1:slippage>max");

// add new position's open interest to the side's aggregate oi value
// and increase number of oi shares issued. assemble position for storage

// cache for gas savings
uint256 oiTotalOnSide = isLong ? oiLong : oiShort;
uint256 oiTotalSharesOnSide = isLong ? oiLongShares : oiShortShares;

// check new total oi on side does not exceed capOi
oiTotalOnSide += oi;
oiTotalSharesOnSide += oi;
require(oiTotalOnSide <= capOi, "OVLV1:oi>cap");

// update total aggregate oi and oi shares
if (isLong) {
oiLong = oiTotalOnSide;
oiLongShares = oiTotalSharesOnSide;
} else {
oiShort = oiTotalOnSide;
oiShortShares = oiTotalSharesOnSide;
}
// and increase number of oi shares issued
uint256 oiShares = _addToOiAggregates(oi, capOi, isLong);

// assemble position info data
// check position is not immediately liquidatable prior to storing
Position.Info memory pos = Position.Info({
notional: uint96(notional), // won't overflow as capNotional max is 8e24
debt: uint96(debt),
notionalInitial: uint96(notional), // won't overflow as capNotional max is 8e24
debtInitial: uint96(debt),
midTick: Tick.priceToTick(midPrice),
entryTick: Tick.priceToTick(price),
isLong: isLong,
entryToMidRatio: Position.calcEntryToMidRatio(price, midPrice),
liquidated: false,
oiShares: oi
oiShares: uint240(oiShares), // won't overflow as oiShares ~ notional/mid
fractionRemaining: ONE.toUint16Fixed()
});
require(
!pos.liquidatable(
oiTotalOnSide,
oiTotalSharesOnSide,
isLong ? oiLong : oiShort,
isLong ? oiLongShares : oiShortShares,
midPrice, // mid price used on liquidations
params.get(Risk.Parameters.CapPayoff),
params.get(Risk.Parameters.MaintenanceMarginFraction),
Expand Down Expand Up @@ -252,8 +241,11 @@ contract OverlayV1Market is IOverlayV1Market {
uint256 fraction,
uint256 priceLimit
) external {
require(fraction > 0, "OVLV1:fraction<min");
require(fraction <= ONE, "OVLV1:fraction>max");
// only keep 4 decimal precision (1 bps) for fraction given
// pos.fractionRemaining only to 4 decimals
fraction = fraction.toUint16Fixed().toUint256Fixed();
require(fraction > 0, "OVLV1:fraction<min");

uint256 value;
uint256 cost;
Expand Down Expand Up @@ -334,28 +326,27 @@ contract OverlayV1Market is IOverlayV1Market {

// subtract unwound open interest from the side's aggregate oi value
// and decrease number of oi shares issued
// use subFloor to avoid reverts with rounding issues
// NOTE: use subFloor to avoid reverts with oi rounding issues
if (pos.isLong) {
oiLong = oiLong.subFloor(
pos.oiCurrent(fraction, oiTotalOnSide, oiTotalSharesOnSide)
);
oiLongShares = oiLongShares.subFloor(pos.oiSharesCurrent(fraction));
oiLongShares -= pos.oiSharesCurrent(fraction);
} else {
oiShort = oiShort.subFloor(
pos.oiCurrent(fraction, oiTotalOnSide, oiTotalSharesOnSide)
);
oiShortShares = oiShortShares.subFloor(pos.oiSharesCurrent(fraction));
oiShortShares -= pos.oiSharesCurrent(fraction);
}

// register the amount to be minted/burned
// capPayoff prevents overflow reverts with int256 cast
_registerMintOrBurn(int256(value) - int256(cost));

// store the updated position info data
// use subFloor to avoid reverts with rounding issues
pos.notional = uint96(uint256(pos.notional).subFloor(pos.notionalInitial(fraction)));
pos.debt = uint96(uint256(pos.debt).subFloor(pos.debtCurrent(fraction)));
pos.oiShares = pos.oiShares.subFloor(pos.oiSharesCurrent(fraction));
// store the updated position info data by reducing the
// oiShares and fraction remaining of initial position
pos.oiShares -= uint240(pos.oiSharesCurrent(fraction));
pos.fractionRemaining = pos.updatedFractionRemaining(fraction);
positions.set(msg.sender, positionId, pos);
}

Expand Down Expand Up @@ -437,28 +428,26 @@ contract OverlayV1Market is IOverlayV1Market {

// subtract liquidated open interest from the side's aggregate oi value
// and decrease number of oi shares issued
// use subFloor to avoid reverts with rounding issues
// NOTE: use subFloor to avoid reverts with oi rounding issues
if (pos.isLong) {
oiLong = oiLong.subFloor(
pos.oiCurrent(fraction, oiTotalOnSide, oiTotalSharesOnSide)
);
oiLongShares = oiLongShares.subFloor(pos.oiSharesCurrent(fraction));
oiLongShares -= pos.oiSharesCurrent(fraction);
} else {
oiShort = oiShort.subFloor(
pos.oiCurrent(fraction, oiTotalOnSide, oiTotalSharesOnSide)
);
oiShortShares = oiShortShares.subFloor(pos.oiSharesCurrent(fraction));
oiShortShares -= pos.oiSharesCurrent(fraction);
}

// register the amount to be burned
_registerMintOrBurn(int256(value) - int256(cost) - int256(marginToBurn));

// store the updated position info data. mark as liquidated
pos.notional = 0;
pos.debt = 0;
pos.oiShares = 0;
pos.liquidated = true;
pos.entryToMidRatio = 0;
pos.oiShares = 0;
pos.fractionRemaining = 0;
positions.set(owner, positionId, pos);
}

Expand All @@ -484,26 +473,8 @@ contract OverlayV1Market is IOverlayV1Market {
/// @dev updates market: pays funding and fetches freshest data from feed
/// @dev update is called every time market is interacted with
function update() public returns (Oracle.Data memory) {
// apply funding if at least one block has passed
uint256 timeElapsed = block.timestamp - timestampUpdateLast;
if (timeElapsed > 0) {
// calculate adjustments to oi due to funding
bool isLongOverweight = oiLong > oiShort;
uint256 oiOverweight = isLongOverweight ? oiLong : oiShort;
uint256 oiUnderweight = isLongOverweight ? oiShort : oiLong;
(oiOverweight, oiUnderweight) = oiAfterFunding(
oiOverweight,
oiUnderweight,
timeElapsed
);

// pay funding
oiLong = isLongOverweight ? oiOverweight : oiUnderweight;
oiShort = isLongOverweight ? oiUnderweight : oiOverweight;

// refresh last update data
timestampUpdateLast = block.timestamp;
}
// pay funding for time elasped since last interaction w market
_payFunding();

// fetch new oracle data from feed
// applies sanity check in case of data manipulation
Expand Down Expand Up @@ -761,8 +732,74 @@ contract OverlayV1Market is IOverlayV1Market {
return minted;
}

/// @notice Updates the market for funding changes to open interest
/// @notice since last time market was interacted with
function _payFunding() private {
// apply funding if at least one block has passed
uint256 timeElapsed = block.timestamp - timestampUpdateLast;
if (timeElapsed > 0) {
// calculate adjustments to oi due to funding
bool isLongOverweight = oiLong > oiShort;
uint256 oiOverweight = isLongOverweight ? oiLong : oiShort;
uint256 oiUnderweight = isLongOverweight ? oiShort : oiLong;
(oiOverweight, oiUnderweight) = oiAfterFunding(
oiOverweight,
oiUnderweight,
timeElapsed
);

// pay funding
oiLong = isLongOverweight ? oiOverweight : oiUnderweight;
oiShort = isLongOverweight ? oiUnderweight : oiOverweight;

// set last time market was updated
timestampUpdateLast = block.timestamp;
}
}

/// @notice Adds open interest and open interest shares to aggregate storage
/// @notice pairs (oiLong, oiLongShares) or (oiShort, oiShortShares)
/// @return oiShares_ as the new position's shares of aggregate open interest
function _addToOiAggregates(
uint256 oi,
uint256 capOi,
bool isLong
) private returns (uint256 oiShares_) {
// cache for gas savings
uint256 oiTotalOnSide = isLong ? oiLong : oiShort;
uint256 oiTotalSharesOnSide = isLong ? oiLongShares : oiShortShares;

// calculate oi shares
uint256 oiShares = Position.calcOiShares(oi, oiTotalOnSide, oiTotalSharesOnSide);

// add oi and oi shares to temp aggregate values
oiTotalOnSide += oi;
oiTotalSharesOnSide += oiShares;

// check new total oi on side does not exceed capOi
require(oiTotalOnSide <= capOi, "OVLV1:oi>cap");

// update total aggregate oi and oi shares storage vars
if (isLong) {
oiLong = oiTotalOnSide;
oiLongShares = oiTotalSharesOnSide;
} else {
oiShort = oiTotalOnSide;
oiShortShares = oiTotalSharesOnSide;
}

// return new position's oi shares
oiShares_ = oiShares;
}

/// @notice Sets the governance per-market risk parameter
/// @dev updates funding state of market but does not fetch from oracle
/// @dev to avoid edge cases when dataIsValid is false
function setRiskParam(Risk.Parameters name, uint256 value) external onlyFactory {
// pay funding to update state of market since last interaction
_payFunding();

// check then set risk param
_checkRiskParam(name, value);
_cacheRiskCalc(name, value);
params.set(name, value);
Expand Down
10 changes: 6 additions & 4 deletions contracts/interfaces/IOverlayV1Market.sol
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,14 @@ interface IOverlayV1Market {
external
view
returns (
uint96 notional_,
uint96 debt_,
uint48 entryToMidRatio_,
uint96 notionalInitial_,
uint96 debtInitial_,
int24 midTick_,
int24 entryTick_,
bool isLong_,
bool liquidated_,
uint256 oiShares_
uint240 oiShares_,
uint16 fractionRemaining_
);

// update related quantities
Expand Down
21 changes: 21 additions & 0 deletions contracts/libraries/FixedCast.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

library FixedCast {
uint256 internal constant ONE_256 = 1e18; // 18 decimal places
uint256 internal constant ONE_16 = 1e4; // 4 decimal places

/// @dev casts a uint16 to a FixedPoint uint256 with 18 decimals
function toUint256Fixed(uint16 value) internal pure returns (uint256) {
uint256 multiplier = ONE_256 / ONE_16;
return (uint256(value) * multiplier);
}

/// @dev casts a FixedPoint uint256 to a uint16 with 4 decimals
function toUint16Fixed(uint256 value) internal pure returns (uint16) {
uint256 divisor = ONE_256 / ONE_16;
uint256 ret256 = value / divisor;
require(ret256 <= type(uint16).max, "OVLV1: FixedCast out of bounds");
return uint16(ret256);
}
}
Loading

0 comments on commit e165eb1

Please sign in to comment.