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

trader view precompile #181

Open
wants to merge 14 commits into
base: isolated-margin
Choose a base branch
from
26 changes: 26 additions & 0 deletions contracts/contracts/hubble-v2/interfaces/ITraderViewer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import { IClearingHouse } from "./IClearingHouse.sol";
import { ILimitOrderBook } from "./IJuror.sol";
import { IOrderHandler } from "./IOrderHandler.sol";

interface ITraderViewer {
function getNotionalPositionAndMargin(address trader, bool includeFundingPayments, IClearingHouse.Mode mode) external view returns(uint256 notionalPosition, int256 margin, uint256 requiredMargin);

function getTraderDataForMarket(address trader, uint256 ammIndex, IClearingHouse.Mode mode) external view returns(
bool isIsolated, uint256 notionalPosition, int256 unrealizedPnl, uint256 requiredMargin, int256 pendingFunding
);

function getCrossMarginAccountData(address trader, IClearingHouse.Mode mode)
external
view
returns(uint256 notionalPosition, uint256 requiredMargin, int256 unrealizedPnl, int256 pendingFunding);

function getTotalFundingForCrossMarginPositions(address trader) external view returns(int256 totalFunding);

function validateCancelLimitOrderV2(ILimitOrderBook.Order memory order, address sender, bool assertLowMargin, bool assertOverPositionCap) external view returns (string memory err, bytes32 orderHash, IOrderHandler.CancelOrderRes memory res);

function getRequiredMargin(int256 baseAssetQuantity, uint256 price, uint ammIndex, address trader) external view returns(uint256 requiredMargin);
}
12 changes: 12 additions & 0 deletions plugin/evm/orderbook/hubbleutils/data_structures.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ const (
Min_Allowable_Margin
)

type MarginType = uint8
0xshinobii marked this conversation as resolved.
Show resolved Hide resolved

const (
Cross_Margin MarginType = iota
Isolated_Margin
)
0xshinobii marked this conversation as resolved.
Show resolved Hide resolved

type AccountPreferences struct {
MarginType MarginType
MarginFraction *big.Int
}
0xshinobii marked this conversation as resolved.
Show resolved Hide resolved

type Collateral struct {
Price *big.Int // scaled by 1e6
Weight *big.Int // scaled by 1e6
Expand Down
41 changes: 41 additions & 0 deletions plugin/evm/orderbook/hubbleutils/margin_math.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type UserState struct {
Margins []*big.Int
PendingFunding *big.Int
ReservedMargin *big.Int
AccountPreferences map[Market]*AccountPreferences
}

func UpgradeVersionV0orV1(blockTimestamp uint64) UpgradeVersion {
Expand All @@ -40,6 +41,7 @@ func UpgradeVersionV0orV1(blockTimestamp uint64) UpgradeVersion {
return V0
}

// @todo update to newer version wherever it is used in the evm
func GetAvailableMargin(hState *HubbleState, userState *UserState) *big.Int {
notionalPosition, margin := GetNotionalPositionAndMargin(hState, userState, Min_Allowable_Margin)
return GetAvailableMargin_(notionalPosition, margin, userState.ReservedMargin, hState.MinAllowableMargin)
Expand Down Expand Up @@ -118,6 +120,45 @@ func getOptimalPnl(hState *HubbleState, position *Position, margin *big.Int, mar
return notionalPosition, unrealizedPnl
}

func GetNotionalPositionAndRequiredMargin(hState *HubbleState, userState *UserState) (*big.Int, *big.Int, *big.Int) {
margin := Sub(GetNormalizedMargin(hState.Assets, userState.Margins), userState.PendingFunding)
notionalPosition, requiredMargin, unrealizedPnl := GetCrossMarginAccountData(hState, userState)
return notionalPosition, Add(margin, unrealizedPnl), requiredMargin
}

func GetCrossMarginAccountData(hState *HubbleState, userState *UserState) (*big.Int, *big.Int, *big.Int) {
notionalPosition := big.NewInt(0)
unrealizedPnl := big.NewInt(0)
requiredMargin := big.NewInt(0)

for _, market := range hState.ActiveMarkets {
if userState.AccountPreferences[market].MarginType == Cross_Margin {
_notionalPosition, _unrealizedPnl, _requiredMargin := GetTraderPositionDetails(userState.Positions[market], hState.OraclePrices[market], userState.AccountPreferences[market].MarginFraction)
notionalPosition.Add(notionalPosition, _notionalPosition)
unrealizedPnl.Add(unrealizedPnl, _unrealizedPnl)
requiredMargin.Add(requiredMargin, _requiredMargin)
}
}
return notionalPosition, requiredMargin, unrealizedPnl
}

func GetTraderPositionDetails(position *Position, oraclePrice *big.Int, marginFraction *big.Int) (notionalPosition *big.Int, uPnL *big.Int, requiredMargin *big.Int) {
if position == nil || position.Size.Sign() == 0 {
return big.NewInt(0), big.NewInt(0), big.NewInt(0)
}

// based on oracle price,
notionalPosition, unrealizedPnl, _ := GetPositionMetadata(
oraclePrice,
position.OpenNotional,
position.Size,
big.NewInt(0), // margin is not used here
)
requiredMargin = Div1e6(Mul(notionalPosition, marginFraction))

return notionalPosition, unrealizedPnl, requiredMargin
}

func GetPositionMetadata(price *big.Int, openNotional *big.Int, size *big.Int, margin *big.Int) (notionalPosition *big.Int, unrealisedPnl *big.Int, marginFraction *big.Int) {
notionalPosition = GetNotionalPosition(price, size)
uPnL := new(big.Int)
Expand Down
56 changes: 52 additions & 4 deletions plugin/evm/orderbook/hubbleutils/margin_math_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ var _hState = &HubbleState{
var userState = &UserState{
Positions: map[Market]*Position{
0: {
Size: big.NewInt(0.582 * 1e18), // 0.0582
OpenNotional: big.NewInt(875 * 1e6), // 87.5, openPrice = 1503.43
Size: big.NewInt(0.582 * 1e18), // 0.582
OpenNotional: big.NewInt(875 * 1e6), // 875, openPrice = 1503.43
},
1: {
Size: Scale(big.NewInt(-101), 18), // -101
Expand All @@ -52,8 +52,18 @@ var userState = &UserState{
big.NewInt(30.5 * 1e6), // 30.5
big.NewInt(14 * 1e6), // 14
},
PendingFunding: big.NewInt(0),
ReservedMargin: big.NewInt(0),
PendingFunding: big.NewInt(-50 * 1e6), // +50
ReservedMargin: big.NewInt(60 * 1e6), // 60
AccountPreferences: map[Market]*AccountPreferences{
0: {
MarginType: Cross_Margin,
MarginFraction: big.NewInt(0.2 * 1e6), // 0.2
},
1: {
MarginType: Isolated_Margin,
MarginFraction: big.NewInt(0.1 * 1e6), // 0.1
},
},
}

func TestWeightedAndSpotCollateral(t *testing.T) {
Expand Down Expand Up @@ -238,3 +248,41 @@ func TestGetTotalNotionalPositionAndUnrealizedPnl(t *testing.T) {
assert.Equal(t, expectedNotionalPosition, notionalPosition)
assert.Equal(t, expectedUPnL, uPnL)
}

func TestGetNotionalPositionAndRequiredMargin(t *testing.T) {
t.Run("one market in cross and other in isolated mode", func(t *testing.T) {
expectedMargin := GetNormalizedMargin(_hState.Assets, userState.Margins)
fmt.Println(expectedMargin)
notionalPosition, margin, requiredMargin := GetNotionalPositionAndRequiredMargin(_hState, userState)
expectedNotionalPosition := Unscale(Mul(userState.Positions[0].Size, _hState.OraclePrices[0]), 18)
expectedUPnL := Sub(expectedNotionalPosition, userState.Positions[0].OpenNotional)
expectedMargin = Sub(Add(expectedMargin, expectedUPnL), userState.PendingFunding)
expectedRequiredMargin := Unscale(Mul(expectedNotionalPosition, userState.AccountPreferences[0].MarginFraction), 6)
assert.Equal(t, expectedNotionalPosition, notionalPosition)
assert.Equal(t, expectedMargin, margin)
assert.Equal(t, expectedRequiredMargin, requiredMargin)
})

t.Run("both markets in cross mode", func(t *testing.T) {
userState.AccountPreferences[1].MarginType = Cross_Margin
notionalPosition, margin, requiredMargin := GetNotionalPositionAndRequiredMargin(_hState, userState)
expectedNotionalPosition := big.NewInt(0)
expectedRequiredMargin := big.NewInt(0)
expectedMargin := GetNormalizedMargin(_hState.Assets, userState.Margins)
for _, market := range _hState.ActiveMarkets {
notional := Abs(Unscale(Mul(userState.Positions[market].Size, _hState.OraclePrices[market]), 18))
expectedNotionalPosition = Add(expectedNotionalPosition, notional)
multiplier := big.NewInt(1)
if userState.Positions[market].Size.Sign() == -1 {
multiplier = big.NewInt(-1)
}
expectedUPnL := Mul(Sub(notional, userState.Positions[market].OpenNotional), multiplier)
expectedMargin = Add(expectedMargin, expectedUPnL)
expectedRequiredMargin = Add(expectedRequiredMargin, Unscale(Mul(notional, userState.AccountPreferences[market].MarginFraction), 6))
}
expectedMargin = Sub(expectedMargin, userState.PendingFunding)
assert.Equal(t, expectedNotionalPosition, notionalPosition)
assert.Equal(t, expectedMargin, margin)
assert.Equal(t, expectedRequiredMargin, requiredMargin)
})
}
79 changes: 79 additions & 0 deletions precompile/contracts/bibliophile/amm.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ const (
ASKS_SLOT int64 = 22
BIDS_HEAD_SLOT int64 = 23
ASKS_HEAD_SLOT int64 = 24
TRADE_MARGIN_FRACTION_SLOT int64 = 28
LIQUIDATION_MARGIN_FRACTION_SLOT int64 = 29
ISOLATED_TRADE_MARGIN_FRACTION_SLOT int64 = 30
ISOLATED_LIQUIDATION_MARGIN_FRACTION_SLOT int64 = 31
ACCOUNT_PREFERENCES_SLOT int64 = 33
MAX_POSITION_CAP_SLOT int64 = 34
)

// AMM State
Expand Down Expand Up @@ -146,6 +152,79 @@ func getPosition(stateDB contract.StateDB, market common.Address, trader *common
}
}

func getTradeMarginFraction(stateDB contract.StateDB, market common.Address) *big.Int {
return stateDB.GetState(market, common.BigToHash(big.NewInt(TRADE_MARGIN_FRACTION_SLOT))).Big()
}

func getLiquidationMarginFraction(stateDB contract.StateDB, market common.Address) *big.Int {
return stateDB.GetState(market, common.BigToHash(big.NewInt(LIQUIDATION_MARGIN_FRACTION_SLOT))).Big()
}

func getIsolatedTradeMarginFraction(stateDB contract.StateDB, market common.Address) *big.Int {
return stateDB.GetState(market, common.BigToHash(big.NewInt(ISOLATED_TRADE_MARGIN_FRACTION_SLOT))).Big()
}

func getIsolatedLiquidationMarginFraction(stateDB contract.StateDB, market common.Address) *big.Int {
return stateDB.GetState(market, common.BigToHash(big.NewInt(ISOLATED_LIQUIDATION_MARGIN_FRACTION_SLOT))).Big()
}

func accountPreferencesSlot(trader *common.Address) *big.Int {
return new(big.Int).SetBytes(crypto.Keccak256(append(common.LeftPadBytes(trader.Bytes(), 32), common.LeftPadBytes(big.NewInt(ACCOUNT_PREFERENCES_SLOT).Bytes(), 32)...)))
}

func getMarginType(stateDB contract.StateDB, market common.Address, trader *common.Address) uint8 {
return uint8(stateDB.GetState(market, common.BigToHash(accountPreferencesSlot(trader))).Big().Uint64())
}

func traderMarginFraction(stateDB contract.StateDB, market common.Address, trader *common.Address) *big.Int {
return stateDB.GetState(market, common.BigToHash(new(big.Int).Add(accountPreferencesSlot(trader), big.NewInt(1)))).Big()
}

func getMaxPositionCap(stateDB contract.StateDB, market common.Address) *big.Int {
return stateDB.GetState(market, common.BigToHash(big.NewInt(MAX_POSITION_CAP_SLOT))).Big()
}

func getMarginFractionByMode(stateDB contract.StateDB, market common.Address, trader *common.Address, mode uint8) *big.Int {
if mode == hu.Maintenance_Margin {
if (getMarginType(stateDB, market, trader) == hu.Isolated_Margin) {
return getIsolatedLiquidationMarginFraction(stateDB, market)
} else {
return getLiquidationMarginFraction(stateDB, market)
}
}
// retuns trade margin fraction by default
// @todo check if can be reverted in case of invalid mode
return getTraderMarginFraction(stateDB, market, trader)
}

func getTraderMarginFraction(stateDB contract.StateDB, market common.Address, trader *common.Address) *big.Int {
traderMarginFraction_ := traderMarginFraction(stateDB, market, trader)
if (traderMarginFraction_.Cmp(big.NewInt(0)) != 0) {
0xshinobii marked this conversation as resolved.
Show resolved Hide resolved
return traderMarginFraction_
} else if (getMarginType(stateDB, market, trader) == hu.Isolated_Margin) {
return getIsolatedTradeMarginFraction(stateDB, market)
} else {
return getTradeMarginFraction(stateDB, market)
}
}

func getRequiredMargin(stateDB contract.StateDB, baseAsset *big.Int, price *big.Int, marketId int64, trader *common.Address) *big.Int {
quoteAsset := hu.Div1e18(hu.Mul(hu.Abs(baseAsset), price))
return getRequiredMarginForQuote(stateDB, GetMarketAddressFromMarketID(marketId, stateDB), trader, quoteAsset)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yo keep it consistent with the structure in contracts, how about moving this function to traderViewer

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah kept it here to avoid code duplication in juror and traderView but I see your point. moved it to traderViewer


func getRequiredMarginForQuote(stateDB contract.StateDB, market common.Address, trader *common.Address, quote *big.Int) *big.Int {
0xshinobii marked this conversation as resolved.
Show resolved Hide resolved
marginFraction := getTraderMarginFraction(stateDB, market, trader)
return hu.Div1e6(hu.Mul(quote, marginFraction))
}

func getPositionCap(stateDB contract.StateDB, market int64, trader *common.Address) *big.Int {
marketAddress := GetMarketAddressFromMarketID(market, stateDB)
maxPositionCap := getMaxPositionCap(stateDB, marketAddress)
traderMarginFraction := getTraderMarginFraction(stateDB, marketAddress, trader)
return hu.Div1e6(hu.Mul(maxPositionCap, traderMarginFraction))
}

// Utils

func getPendingFundingPayment(stateDB contract.StateDB, market common.Address, trader *common.Address) *big.Int {
Expand Down
102 changes: 102 additions & 0 deletions precompile/contracts/bibliophile/clearing_house.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ type GetNotionalPositionAndMarginInput struct {
type GetNotionalPositionAndMarginOutput struct {
NotionalPosition *big.Int
Margin *big.Int
RequiredMargin *big.Int
}

type GetTraderDataForMarketOutput struct {
NotionalPosition *big.Int
RequiredMargin *big.Int
UnrealizedPnl *big.Int
PendingFunding *big.Int
IsIsolated bool
}

func getNotionalPositionAndMargin(stateDB contract.StateDB, input *GetNotionalPositionAndMarginInput, upgradeVersion hu.UpgradeVersion) GetNotionalPositionAndMarginOutput {
Expand Down Expand Up @@ -104,6 +113,99 @@ func getNotionalPositionAndMargin(stateDB contract.StateDB, input *GetNotionalPo
}
}

func getNotionalPositionAndRequiredMargin(stateDB contract.StateDB, input *GetNotionalPositionAndMarginInput) GetNotionalPositionAndMarginOutput {
positions, underlyingPrices, accountPreferences, activeMarketIds := getMarketsDataFromDB(stateDB, &input.Trader, input.Mode)
pendingFunding := big.NewInt(0)
if input.IncludeFundingPayments {
pendingFunding = getTotalFundingForCrossMarginPositions(stateDB, &input.Trader)
}
notionalPosition, margin, requiredMargin := hu.GetNotionalPositionAndRequiredMargin(
&hu.HubbleState{
Assets: GetCollaterals(stateDB),
OraclePrices: underlyingPrices,
ActiveMarkets: activeMarketIds,
},
&hu.UserState{
Positions: positions,
Margins: getMargins(stateDB, input.Trader),
PendingFunding: pendingFunding,
AccountPreferences: accountPreferences,
},
)
return GetNotionalPositionAndMarginOutput{
NotionalPosition: notionalPosition,
Margin: margin,
RequiredMargin: requiredMargin,
}
}
0xshinobii marked this conversation as resolved.
Show resolved Hide resolved

func getCrossMarginAccountData(stateDB contract.StateDB, trader *common.Address, mode uint8, upgradeVersion hu.UpgradeVersion) GetTraderDataForMarketOutput {
positions, underlyingPrices, accountPreferences, activeMarketIds := getMarketsDataFromDB(stateDB, trader, mode)
notionalPosition, requiredMargin, unrealizedPnl := hu.GetCrossMarginAccountData(
&hu.HubbleState{
ActiveMarkets: activeMarketIds,
OraclePrices: underlyingPrices,
UpgradeVersion: upgradeVersion,
0xshinobii marked this conversation as resolved.
Show resolved Hide resolved
},
&hu.UserState{
Positions: positions,
AccountPreferences: accountPreferences,
},
)
pendingFunding := getTotalFundingForCrossMarginPositions(stateDB, trader)
return GetTraderDataForMarketOutput {
NotionalPosition: notionalPosition,
RequiredMargin: requiredMargin,
UnrealizedPnl: unrealizedPnl,
PendingFunding: pendingFunding,
}
}

func getMarketsDataFromDB(stateDB contract.StateDB, trader *common.Address, mode uint8) (positions map[int]*hu.Position, underlyingPrices map[int]*big.Int, accountPreferences map[int]*hu.AccountPreferences, activeMarketIds []int) {
markets := GetMarkets(stateDB)
numMarkets := len(markets)
positions = make(map[int]*hu.Position, numMarkets)
underlyingPrices = make(map[int]*big.Int, numMarkets)
accountPreferences = make(map[int]*hu.AccountPreferences, numMarkets)
activeMarketIds = make([]int, numMarkets)
for i, market := range markets {
// @todo can use `market` instead of `GetMarketAddressFromMarketID`?
0xshinobii marked this conversation as resolved.
Show resolved Hide resolved
positions[i] = getPosition(stateDB, GetMarketAddressFromMarketID(int64(i), stateDB), trader)
underlyingPrices[i] = getUnderlyingPrice(stateDB, market)
activeMarketIds[i] = i
accountPreferences[i].MarginType = getMarginType(stateDB, market, trader)
accountPreferences[i].MarginFraction = getMarginFractionByMode(stateDB, market, trader, mode)
}
return positions, underlyingPrices, accountPreferences, activeMarketIds
}

func getTotalFundingForCrossMarginPositions(stateDB contract.StateDB, trader *common.Address) *big.Int {
totalFunding := big.NewInt(0)
for _, market := range GetMarkets(stateDB) {
if getMarginType(stateDB, market, trader) == hu.Cross_Margin {
totalFunding.Add(totalFunding, getPendingFundingPayment(stateDB, market, trader))
}
}
return totalFunding
}

func getTraderDataForMarket(stateDB contract.StateDB, trader *common.Address, marketId int64, mode uint8) GetTraderDataForMarketOutput {
market := GetMarketAddressFromMarketID(marketId, stateDB)
position := getPosition(stateDB, market, trader)
marginFraction := getMarginFractionByMode(stateDB, market, trader, mode)
underlyingPrice := getUnderlyingPrice(stateDB, market)
notionalPosition, unrealizedPnl, requiredMargin := hu.GetTraderPositionDetails(position, underlyingPrice, marginFraction)
pendingFunding := getPendingFundingPayment(stateDB, market, trader)
isIsolated := getMarginType(stateDB, market, trader) == hu.Isolated_Margin
return GetTraderDataForMarketOutput{
IsIsolated: isIsolated,
NotionalPosition: notionalPosition,
RequiredMargin: requiredMargin,
UnrealizedPnl: unrealizedPnl,
PendingFunding: pendingFunding,
}
}

func GetTotalFunding(stateDB contract.StateDB, trader *common.Address) *big.Int {
totalFunding := big.NewInt(0)
for _, market := range GetMarkets(stateDB) {
Expand Down
Loading
Loading