diff --git a/contracts/src/interfaces/IHyperdrive.sol b/contracts/src/interfaces/IHyperdrive.sol index cf9d1be1a..df84bf8fa 100644 --- a/contracts/src/interfaces/IHyperdrive.sol +++ b/contracts/src/interfaces/IHyperdrive.sol @@ -332,6 +332,10 @@ interface IHyperdrive is /// int128 scale. error UnsafeCastToInt128(); + /// @notice Thrown when casting a value to a int256 that is outside of the + /// int256 scale. + error UnsafeCastToInt256(); + /// @notice Thrown when an unsupported option is passed to a function or /// a user attempts to sweep an invalid token. The options and sweep /// targets that are supported vary between instances. diff --git a/contracts/src/libraries/HyperdriveMath.sol b/contracts/src/libraries/HyperdriveMath.sol index 9ef193fff..bef057d83 100644 --- a/contracts/src/libraries/HyperdriveMath.sol +++ b/contracts/src/libraries/HyperdriveMath.sol @@ -38,40 +38,31 @@ library HyperdriveMath { ); timeStretch = ONE.divDown(timeStretch); - // If the position duration is 1 year, we can return the benchmark. - if (_positionDuration == 365 days) { - return timeStretch; - } - - // Otherwise, we need to adjust the time stretch to account for the - // position duration. We do this by holding the reserve ratio constant - // and solving for the new time stretch directly. + // We know that the following simultaneous equations hold: // - // We can calculate the spot price at the target apr and position - // duration as: + // (1 + apr) * A ** timeStretch = 1 // - // p = 1 / (1 + apr * (positionDuration / 365 days)) + // and // - // We then calculate the benchmark reserve ratio, `ratio`, implied by - // the benchmark time stretch using the `calculateInitialBondReserves` - // function. + // (1 + apr * (positionDuration / 365 days)) * A ** targetTimeStretch = 1 // - // We can then derive the adjusted time stretch using the spot price - // calculation: + // where A is the reserve ratio. We can solve these equations for the + // target time stretch as follows: // - // p = ratio ** timeStretch - // => - // timeStretch = ln(p) / ln(ratio) - uint256 targetSpotPrice = ONE.divDown( - ONE + _apr.mulDivDown(_positionDuration, 365 days) - ); - uint256 benchmarkReserveRatio = ONE.divDown( - calculateInitialBondReserves(ONE, ONE, _apr, 365 days, timeStretch) - ); + // targetTimeStretch = ( + // ln(1 + apr * (positionDuration / 365 days)) / + // ln(1 + apr) + // ) * timeStretch + // + // NOTE: Round down so that the output is an underestimate. return - uint256(-int256(targetSpotPrice).ln()).divDown( - uint256(-int256(benchmarkReserveRatio).ln()) - ); + ( + uint256( + (ONE + _apr.mulDivDown(_positionDuration, 365 days)) + .toInt256() + .ln() + ).divDown(uint256((ONE + _apr).toInt256().ln())) + ).mulDown(timeStretch); } /// @dev Calculates the spot price of bonds in terms of base. This diff --git a/contracts/src/libraries/SafeCast.sol b/contracts/src/libraries/SafeCast.sol index e70737ddf..23f63b08b 100644 --- a/contracts/src/libraries/SafeCast.sol +++ b/contracts/src/libraries/SafeCast.sol @@ -35,4 +35,14 @@ library SafeCast { } y = int128(x); } + + /// @notice This function safely casts an uint256 to an int256. + /// @param x The uint256 to cast to int256. + /// @return y The int256 casted from x. + function toInt256(uint256 x) internal pure returns (int256 y) { + if (!(x <= uint256(type(int256).max))) { + revert IHyperdrive.UnsafeCastToInt256(); + } + y = int256(x); + } } diff --git a/crates/hyperdrive-math/src/utils.rs b/crates/hyperdrive-math/src/utils.rs index af2304d4d..95077dbc8 100644 --- a/crates/hyperdrive-math/src/utils.rs +++ b/crates/hyperdrive-math/src/utils.rs @@ -9,46 +9,28 @@ pub fn get_time_stretch(rate: FixedPoint, position_duration: FixedPoint) -> Fixe let time_stretch = fixed!(5.24592e18) / (fixed!(0.04665e18) * FixedPoint::from(U256::from(rate) * uint256!(100))); let time_stretch = fixed!(1e18) / time_stretch; - // if the position duration is 1 year, we can return the benchmark - if position_duration == seconds_in_a_year { - return time_stretch; - } - // Otherwise, we need to adjust the time stretch to account for the - // position duration. We do this by holding the reserve ratio constant - // and solving for the new time stretch directly. + // We know that the following simultaneous equations hold: + // + // (1 + apr) * A ** timeStretch = 1 // - // We can calculate the spot price at the target apr and position - // duration as: + // and // - // p = 1 / (1 + apr * (positionDuration / 365 days)) + // (1 + apr * (positionDuration / 365 days)) * A ** targetTimeStretch = 1 // - // We then calculate the benchmark reserve ratio, `ratio`, implied by - // the benchmark time stretch using the `calculateInitialBondReserves` - // function. + // where A is the reserve ratio. We can solve these equations for the + // target time stretch as follows: // - // We can then derive the adjusted time stretch using the spot price - // calculation: + // targetTimeStretch = ( + // ln(1 + apr * (positionDuration / 365 days)) / + // ln(1 + apr) + // ) * timeStretch // - // p = ratio ** timeStretch - // => - // timeStretch = ln(p) / ln(ratio) - let target_spot_price = - fixed!(1e18) / (fixed!(1e18) + rate.mul_div_down(position_duration, seconds_in_a_year)); - let benchmark_reserve_ratio = fixed!(1e18) - / calculate_initial_bond_reserves( - fixed!(1e18), - fixed!(1e18), - rate, - seconds_in_a_year, - time_stretch, - ); - // target spot price and benchmark reserve ratio will have negative ln, - // but since we are dividing them we can cast to positive before converting types - // TODO: implement FixedPoint `neg` pub fn to support "-" - let new_time_stretch = FixedPoint::from(-FixedPoint::ln(I256::from(target_spot_price))) - / FixedPoint::from(-FixedPoint::ln(I256::from(benchmark_reserve_ratio))); - new_time_stretch + // NOTE: Round down so that the output is an underestimate. + (FixedPoint::from(FixedPoint::ln(I256::from( + fixed!(1e18) + rate.mul_div_down(position_duration, seconds_in_a_year), + ))) / FixedPoint::from(FixedPoint::ln(I256::from(fixed!(1e18) + rate)))) + * time_stretch } pub fn get_effective_share_reserves(