Skip to content

Commit af5be0c

Browse files
DannyDelottMazyGio
andauthored
Add calcShortMarketValue to hyperdrive-wasm (#1167)
Co-authored-by: MazyGio <[email protected]>
1 parent 307f671 commit af5be0c

File tree

7 files changed

+635
-291
lines changed

7 files changed

+635
-291
lines changed

apps/hyperdrive-trading/src/routeTree.gen.ts

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,64 +10,64 @@
1010

1111
// Import Routes
1212

13-
import { Route as rootRoute } from "./ui/routes/__root";
14-
import { Route as BridgeImport } from "./ui/routes/bridge";
15-
import { Route as IndexImport } from "./ui/routes/index";
16-
import { Route as MarketAddressImport } from "./ui/routes/market.$address";
17-
import { Route as MarketsImport } from "./ui/routes/markets";
18-
import { Route as VoidImport } from "./ui/routes/void";
13+
import { Route as rootRoute } from './ui/routes/__root'
14+
import { Route as VoidImport } from './ui/routes/void'
15+
import { Route as MarketsImport } from './ui/routes/markets'
16+
import { Route as BridgeImport } from './ui/routes/bridge'
17+
import { Route as IndexImport } from './ui/routes/index'
18+
import { Route as MarketAddressImport } from './ui/routes/market.$address'
1919

2020
// Create/Update Routes
2121

2222
const VoidRoute = VoidImport.update({
23-
path: "/void",
23+
path: '/void',
2424
getParentRoute: () => rootRoute,
25-
} as any);
25+
} as any)
2626

2727
const MarketsRoute = MarketsImport.update({
28-
path: "/markets",
28+
path: '/markets',
2929
getParentRoute: () => rootRoute,
30-
} as any);
30+
} as any)
3131

3232
const BridgeRoute = BridgeImport.update({
33-
path: "/bridge",
33+
path: '/bridge',
3434
getParentRoute: () => rootRoute,
35-
} as any);
35+
} as any)
3636

3737
const IndexRoute = IndexImport.update({
38-
path: "/",
38+
path: '/',
3939
getParentRoute: () => rootRoute,
40-
} as any);
40+
} as any)
4141

4242
const MarketAddressRoute = MarketAddressImport.update({
43-
path: "/market/$address",
43+
path: '/market/$address',
4444
getParentRoute: () => rootRoute,
45-
} as any);
45+
} as any)
4646

4747
// Populate the FileRoutesByPath interface
4848

49-
declare module "@tanstack/react-router" {
49+
declare module '@tanstack/react-router' {
5050
interface FileRoutesByPath {
51-
"/": {
52-
preLoaderRoute: typeof IndexImport;
53-
parentRoute: typeof rootRoute;
54-
};
55-
"/bridge": {
56-
preLoaderRoute: typeof BridgeImport;
57-
parentRoute: typeof rootRoute;
58-
};
59-
"/markets": {
60-
preLoaderRoute: typeof MarketsImport;
61-
parentRoute: typeof rootRoute;
62-
};
63-
"/void": {
64-
preLoaderRoute: typeof VoidImport;
65-
parentRoute: typeof rootRoute;
66-
};
67-
"/market/$address": {
68-
preLoaderRoute: typeof MarketAddressImport;
69-
parentRoute: typeof rootRoute;
70-
};
51+
'/': {
52+
preLoaderRoute: typeof IndexImport
53+
parentRoute: typeof rootRoute
54+
}
55+
'/bridge': {
56+
preLoaderRoute: typeof BridgeImport
57+
parentRoute: typeof rootRoute
58+
}
59+
'/markets': {
60+
preLoaderRoute: typeof MarketsImport
61+
parentRoute: typeof rootRoute
62+
}
63+
'/void': {
64+
preLoaderRoute: typeof VoidImport
65+
parentRoute: typeof rootRoute
66+
}
67+
'/market/$address': {
68+
preLoaderRoute: typeof MarketAddressImport
69+
parentRoute: typeof rootRoute
70+
}
7171
}
7272
}
7373

@@ -79,6 +79,6 @@ export const routeTree = rootRoute.addChildren([
7979
MarketsRoute,
8080
VoidRoute,
8181
MarketAddressRoute,
82-
]);
82+
])
8383

8484
/* prettier-ignore-end */

apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/CurrentValueCell.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Skeleton from "react-loading-skeleton";
77
import { useAppConfig } from "src/ui/appconfig/useAppConfig";
88
import { formatBalance } from "src/ui/base/formatting/formatBalance";
99
import { useIsTailwindSmallScreen } from "src/ui/base/mediaBreakpoints";
10+
import { useEstimateShortMarketValue } from "src/ui/hyperdrive/shorts/hooks/useEstimateShortMarketValue";
1011
import { usePreviewCloseShort } from "src/ui/hyperdrive/shorts/hooks/usePreviewCloseShort";
1112

1213
export function CurrentValueCell({
@@ -33,8 +34,14 @@ export function CurrentValueCell({
3334
shortAmountIn: openShort.bondAmount,
3435
});
3536

37+
const { marketEstimate } = useEstimateShortMarketValue({
38+
hyperdriveAddress: hyperdrive.address,
39+
maturityTime: openShort.maturity,
40+
shortAmountIn: openShort.bondAmount,
41+
});
42+
3643
const currentValueLabel = formatBalance({
37-
balance: currentValueInBase || 0n,
44+
balance: currentValueInBase || marketEstimate || 0n,
3845
decimals: baseToken.decimals,
3946
places: baseToken.places,
4047
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { makeQueryKey } from "src/base/makeQueryKey";
3+
import { QueryStatusWithIdle, getStatus } from "src/base/queryStatus";
4+
import { useAppConfig } from "src/ui/appconfig/useAppConfig";
5+
import { prepareSharesOut } from "src/ui/hyperdrive/hooks/usePrepareSharesOut";
6+
import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive";
7+
import { Address } from "viem";
8+
9+
interface UseEstimateShortMarketValue {
10+
hyperdriveAddress: Address;
11+
maturityTime: bigint | undefined;
12+
shortAmountIn: bigint | undefined;
13+
asBase?: boolean;
14+
enabled?: boolean;
15+
}
16+
17+
interface UseEstimateShortMarketValueResult {
18+
estimateShortMarketValueStatus: QueryStatusWithIdle;
19+
marketEstimate: bigint | undefined;
20+
}
21+
22+
export function useEstimateShortMarketValue({
23+
hyperdriveAddress,
24+
maturityTime,
25+
shortAmountIn,
26+
asBase = true,
27+
enabled = true,
28+
}: UseEstimateShortMarketValue): UseEstimateShortMarketValueResult {
29+
const readHyperdrive = useReadHyperdrive(hyperdriveAddress);
30+
const appConfig = useAppConfig();
31+
const queryEnabled =
32+
!!readHyperdrive && !!maturityTime && !!shortAmountIn && enabled;
33+
34+
const { data, status, fetchStatus } = useQuery({
35+
queryKey: makeQueryKey("estimateShortMarketValue", {
36+
hyperdriveAddress,
37+
maturityTime: maturityTime?.toString(),
38+
shortAmountIn: shortAmountIn?.toString(),
39+
asBase,
40+
}),
41+
42+
enabled: queryEnabled,
43+
queryFn: queryEnabled
44+
? async () => {
45+
const result = await readHyperdrive.estimateShortMarketValue({
46+
maturityTime,
47+
shortAmountIn,
48+
asBase,
49+
});
50+
51+
// All shares from the sdk need to be prepared for the UI
52+
return asBase
53+
? result
54+
: await prepareSharesOut({
55+
appConfig,
56+
hyperdriveAddress,
57+
readHyperdrive,
58+
sharesAmount: result,
59+
});
60+
}
61+
: undefined,
62+
});
63+
64+
return {
65+
marketEstimate: data,
66+
estimateShortMarketValueStatus: getStatus(status, fetchStatus),
67+
};
68+
}

crates/hyperdrive-wasm/src/short/close.rs

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
use std::ops::{Div, Mul, Sub};
2+
3+
use fixed_point::fixed;
14
use hyperdrive_math::State;
25
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
36

47
use crate::{
58
error::ToJsResult,
69
types::{JsPoolConfig, JsPoolInfo},
7-
utils::ToU256,
10+
utils::{ToFixedPoint, ToU256},
811
};
912

1013
/// Calculates the amount of shares the trader will receive after fees for
@@ -57,3 +60,100 @@ pub fn calcCloseShort(
5760

5861
Ok(result_fp.to_u256()?.to_string())
5962
}
63+
64+
/// Calculates the market value of a short position using the equation:
65+
/// market_estimate = yield_accrued + trading_proceeds - curve_fees_paid + flat_fees_returned
66+
///
67+
/// yield_accrued = dy * (c-c0)/c0
68+
/// trading_proceeds = dy * (1 - p) * t
69+
/// curve_fees_paid = trading_proceeds * curve_fee
70+
/// flat_fees_returned = dy * t * flat_fee
71+
///
72+
/// dy = bond amount
73+
/// c = closeVaultSharePrice (current if non-matured, or checkpoint's if matured)
74+
/// c0 = openVaultSharePrice
75+
/// p = spotPrice
76+
/// t = timeRemaining
77+
///
78+
/// @param poolInfo - The current state of the pool
79+
///
80+
/// @param poolConfig - The pool's configuration
81+
///
82+
/// @param bondAmount - The number of short bonds to close
83+
///
84+
/// @param openVaultSharePrice - The vault share price at the checkpoint when
85+
/// the position was opened
86+
///
87+
/// @param closeVaultSharePrice - The current vault share price, or if the
88+
/// position has matured, the vault share price from the closing checkpoint
89+
///
90+
/// @param maturityTime - The maturity timestamp of the short (in seconds)
91+
///
92+
/// @param currentTime - The current timestamp (in seconds)
93+
#[wasm_bindgen(skip_jsdoc)]
94+
pub fn calcShortMarketValue(
95+
poolInfo: &JsPoolInfo,
96+
poolConfig: &JsPoolConfig,
97+
bondAmount: &str,
98+
openVaultSharePrice: &str,
99+
closeVaultSharePrice: &str,
100+
maturityTime: &str,
101+
currentTime: &str,
102+
) -> Result<String, JsValue> {
103+
let state = State {
104+
info: poolInfo.try_into()?,
105+
config: poolConfig.try_into()?,
106+
};
107+
// dy is the bonds shorted
108+
let bond_amount = bondAmount.to_fixed_point()?;
109+
110+
// c is the closing vault share price
111+
let close_vault_share_price = closeVaultSharePrice.to_fixed_point()?;
112+
113+
// c0 is the open vault share price
114+
let open_vault_share_price = openVaultSharePrice.to_fixed_point()?;
115+
116+
let maturity_time = maturityTime.to_fixed_point()?;
117+
let current_time = currentTime.to_fixed_point()?;
118+
119+
// p is the current pool spot price
120+
let spot_price = state
121+
.calculate_spot_price()
122+
.to_js_result()?
123+
.to_fixed_point()?;
124+
125+
// t is the time remaining
126+
// (maturity_time - latest_checkpoint) / position_duration
127+
let latest_checkpoint = state.to_checkpoint(current_time.into()).to_fixed_point()?;
128+
let time_remaining = if maturity_time > latest_checkpoint {
129+
// NOTE: Round down to underestimate the time remaining.
130+
(maturity_time.sub(latest_checkpoint))
131+
.div_down(state.config.position_duration.to_fixed_point()?)
132+
} else {
133+
fixed!(0)
134+
};
135+
136+
// yield accrued
137+
// dy * (c-c0)/c0
138+
let yield_accrued = bond_amount
139+
.mul(close_vault_share_price.sub(open_vault_share_price))
140+
.div(open_vault_share_price);
141+
142+
// trading_proceeds
143+
// dy * (1 - p) * t
144+
let trading_proceeds = bond_amount
145+
.mul(fixed!(1e18).sub(spot_price))
146+
.mul(time_remaining);
147+
148+
// curve_fees_paid = trading_proceeds * curve_fee
149+
let curve_fees_paid = trading_proceeds.mul(state.config.fees.curve.to_fixed_point()?);
150+
151+
// flat_fees_returned = dy * t * flat_fee
152+
let flat_fees_returned = bond_amount
153+
.mul(time_remaining)
154+
.mul(state.config.fees.flat.to_fixed_point()?);
155+
156+
let result = yield_accrued + trading_proceeds - curve_fees_paid + flat_fees_returned;
157+
158+
Ok(result.to_u256()?.to_string())
159+
}

packages/hyperdrive-js-core/src/hyperdrive/ReadHyperdrive/ReadHyperdrive.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1816,9 +1816,72 @@ export class ReadHyperdrive extends ReadModel {
18161816
}),
18171817
};
18181818
}
1819+
/**
1820+
* Get a rough estimate of the market value of a short. This can be used to
1821+
* value a position that cannot be fully closed.
1822+
*/
1823+
async estimateShortMarketValue({
1824+
maturityTime,
1825+
asBase,
1826+
shortAmountIn,
1827+
options,
1828+
}: {
1829+
maturityTime: bigint;
1830+
shortAmountIn: bigint;
1831+
asBase: boolean;
1832+
extraData?: `0x${string}`;
1833+
options?: ContractReadOptions;
1834+
}): Promise<bigint> {
1835+
const poolConfig = await this.getPoolConfig(options);
1836+
const poolInfo = await this.getPoolInfo(options);
1837+
1838+
// The checkpoint in which this position was opened.
1839+
// This is always maturity time - position duration thanks to mint on demand
1840+
const openCheckpointTimestamp = maturityTime - poolConfig.positionDuration;
1841+
const { vaultSharePrice: openSharePrice } = await this.getCheckpoint({
1842+
timestamp: openCheckpointTimestamp,
1843+
options,
1844+
});
1845+
1846+
const currentTime = Math.floor(Date.now() / 1000);
1847+
1848+
// If the position is mature, we use the closing vault share price otherwise
1849+
// use the current vault share price
1850+
let closeSharePrice = poolInfo.vaultSharePrice;
1851+
if (maturityTime <= currentTime) {
1852+
const closingCheckpoint = await this.getCheckpoint({
1853+
timestamp: maturityTime,
1854+
options,
1855+
});
1856+
closeSharePrice = closingCheckpoint.vaultSharePrice;
1857+
}
1858+
1859+
const marketEstimateInShares = BigInt(
1860+
hyperwasm.calcShortMarketValue(
1861+
convertBigIntsToStrings(poolInfo),
1862+
convertBigIntsToStrings(poolConfig),
1863+
shortAmountIn.toString(),
1864+
openSharePrice.toString(),
1865+
closeSharePrice.toString(),
1866+
maturityTime.toString(),
1867+
currentTime.toString(),
1868+
),
1869+
);
1870+
1871+
if (!asBase) {
1872+
return marketEstimateInShares;
1873+
}
1874+
1875+
return this.convertToBase({
1876+
sharesAmount: marketEstimateInShares,
1877+
options,
1878+
});
1879+
}
18191880

18201881
/**
18211882
* Predicts the amount of base asset a user will receive when closing a short.
1883+
* If closing the short would result in negative interest, an error will be
1884+
* thrown.
18221885
*/
18231886
async previewCloseShort({
18241887
maturityTime,
@@ -2082,3 +2145,7 @@ function calculateLpApy({
20822145

20832146
return lpApy;
20842147
}
2148+
2149+
function isNegativeInterestError(error: string) {
2150+
return error.includes("InsufficientLiquidity: Negative Interest");
2151+
}

0 commit comments

Comments
 (0)