Skip to content

Commit

Permalink
Implemented a price discovery circuit breaker on addLiquidity and `…
Browse files Browse the repository at this point in the history
…initialize` (#1067)

* Implemented a basic version of the `addLiquidity` circuit breaker

* Fixed `test_lp_withdrawal_long_and_short_maturity`

* Fixed the remaining tests

* add priceDiscoveryCheck to LPMath and also check it in initialize

* use initial price

* add tests and fix placement of check

* remove lib from LPMath

* remove comment

* remove console import

* remove console import

* fix price discovery tests

* fixed tests

* commit test to investigate

* add test_solvency_at_0_apr

* add test_solvency_cross_checkpoint_long_short

* address review feedback

* Update test/integrations/hyperdrive/PriceDiscovery.t.sol

* Added some testing examples

* Minor updates

* Updated `verifyPriceDiscovery` to `calculateSolvencyAfterMaxLong`

* Cleaned up the tests

* Increased the efficiency of the solvency check

* Fixed the code size issue

* Addressed Saw Mon's comment

* Improved one of the price discovery tests

* Updated the price discovery tests

* Fixed the remaining tests

* Addressed review feedback from @mcclurejt

* Fixed the deployment scripts

---------

Co-authored-by: jonny rhea <jonathan.rhea@gmail.com>
Co-authored-by: Jonny Rhea <5555162+jrhea@users.noreply.github.com>
  • Loading branch information
3 people authored Jul 8, 2024
1 parent 5154a7a commit d838493
Show file tree
Hide file tree
Showing 12 changed files with 1,101 additions and 89 deletions.
2 changes: 1 addition & 1 deletion contracts/src/external/Hyperdrive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ abstract contract Hyperdrive is
uint256,
IHyperdrive.Options calldata
) external payable returns (uint256) {
_delegate(target3);
_delegate(target2);
}

/// @inheritdoc IHyperdriveCore
Expand Down
17 changes: 17 additions & 0 deletions contracts/src/external/HyperdriveTarget2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,23 @@ abstract contract HyperdriveTarget2 is
IHyperdrive.PoolConfig memory _config
) HyperdriveStorage(_config) {}

/// LPs ///

/// @notice Allows the first LP to initialize the market with a target APR.
/// @param _contribution The amount of capital to supply. The units of this
/// quantity are either base or vault shares, depending on the value
/// of `_options.asBase`.
/// @param _apr The target APR.
/// @param _options The options that configure how the operation is settled.
/// @return The initial number of LP shares created.
function initialize(
uint256 _contribution,
uint256 _apr,
IHyperdrive.Options calldata _options
) external payable returns (uint256) {
return _initialize(_contribution, _apr, _options);
}

/// Shorts ///

/// @notice Opens a short position.
Expand Down
15 changes: 0 additions & 15 deletions contracts/src/external/HyperdriveTarget3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,6 @@ abstract contract HyperdriveTarget3 is

/// LPs ///

/// @notice Allows the first LP to initialize the market with a target APR.
/// @param _contribution The amount of capital to supply. The units of this
/// quantity are either base or vault shares, depending on the value
/// of `_options.asBase`.
/// @param _apr The target APR.
/// @param _options The options that configure how the operation is settled.
/// @return The initial number of LP shares created.
function initialize(
uint256 _contribution,
uint256 _apr,
IHyperdrive.Options calldata _options
) external payable returns (uint256) {
return _initialize(_contribution, _apr, _options);
}

/// @notice Allows LPs to supply liquidity for LP shares.
/// @param _contribution The amount of capital to supply. The units of this
/// quantity are either base or vault shares, depending on the value
Expand Down
164 changes: 153 additions & 11 deletions contracts/src/internal/HyperdriveBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AssetId } from "../libraries/AssetId.sol";
import { FixedPointMath, ONE } from "../libraries/FixedPointMath.sol";
import { HyperdriveMath } from "../libraries/HyperdriveMath.sol";
import { LPMath } from "../libraries/LPMath.sol";
import { YieldSpaceMath } from "../libraries/YieldSpaceMath.sol";
import { SafeCast } from "../libraries/SafeCast.sol";
import { HyperdriveStorage } from "./HyperdriveStorage.sol";

Expand Down Expand Up @@ -440,18 +441,12 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage {
}

/// @dev Updates the global long exposure.
/// @param _before The long exposure before the update.
/// @param _after The long exposure after the update.
/// @param _before The checkpoint long exposure before the update.
/// @param _after The checkpoint long exposure after the update.
function _updateLongExposure(int256 _before, int256 _after) internal {
// The global long exposure is the sum of the non-netted longs in each
// checkpoint. To update this value, we subtract the current value
// (`_before.max(0)`) and add the new value (`_after.max(0)`).
int128 delta = (_after.max(0) - _before.max(0)).toInt128();
if (delta > 0) {
_marketState.longExposure += uint128(delta);
} else if (delta < 0) {
_marketState.longExposure -= uint128(-delta);
}
_marketState.longExposure = LPMath
.calculateLongExposure(_marketState.longExposure, _before, _after)
.toUint128();
}

/// @dev Update the weighted spot price from a specified checkpoint. The
Expand Down Expand Up @@ -682,6 +677,153 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage {
return (lpSharePrice, true);
}

/// @dev Calculates the pool's solvency if a long is opened that brings the
/// rate to 0%. This is the maximum possible long that can be opened on
/// the YieldSpace curve.
/// @param _shareReserves The pool's share reserves.
/// @param _shareAdjustment The pool's share adjustment.
/// @param _bondReserves The pool's bond reserves.
/// @param _vaultSharePrice The vault share price.
/// @param _longExposure The pool's long exposure.
/// @param _checkpointExposure The pool's checkpoint exposure.
/// @return The solvency after opening the max long.
/// @return A flag indicating whether or not the calculation succeeded.
function _calculateSolvencyAfterMaxLongSafe(
uint256 _shareReserves,
int256 _shareAdjustment,
uint256 _bondReserves,
uint256 _vaultSharePrice,
uint256 _longExposure,
int256 _checkpointExposure
) internal view returns (int256, bool) {
// Calculate the share payment and bond proceeds of opening the largest
// possible long on the YieldSpace curve. This does not include fees.
// These calculations fail when the max long is close to zero, and we
// ignore these failures since we can proceed with the calculation in
// this case.
(uint256 effectiveShareReserves, bool success) = HyperdriveMath
.calculateEffectiveShareReservesSafe(
_shareReserves,
_shareAdjustment
);
if (!success) {
return (0, false);
}
(uint256 maxSharePayment, ) = YieldSpaceMath
.calculateMaxBuySharesInSafe(
effectiveShareReserves,
_bondReserves,
ONE - _timeStretch,
_vaultSharePrice,
_initialVaultSharePrice
);
(uint256 maxBondProceeds, ) = YieldSpaceMath
.calculateBondsOutGivenSharesInDownSafe(
effectiveShareReserves,
_bondReserves,
maxSharePayment,
ONE - _timeStretch,
_vaultSharePrice,
_initialVaultSharePrice
);

// If one of the max share payment or max bond proceeds calculations
// fail or return zero, the max long amount is zero plus or minus a few
// wei.
if (maxSharePayment == 0 || maxBondProceeds == 0) {
maxSharePayment = 0;
maxBondProceeds = 0;
}

// Apply the fees from opening a long to the max share payment and bond
// proceeds. Fees applied to the share payment hurt solvency and fees
// applied to the bond proceeds make the pool more solvent. To be
// conservative, we only apply the fee to the share payment.
uint256 spotPrice = HyperdriveMath.calculateSpotPrice(
effectiveShareReserves,
_bondReserves,
_initialVaultSharePrice,
_timeStretch
);
(maxSharePayment, , ) = _calculateOpenLongFees(
maxSharePayment,
maxBondProceeds,
_vaultSharePrice,
spotPrice
);

// Calculate the pool's solvency after opening the max long.
uint256 shareReserves = _shareReserves + maxSharePayment;
uint256 longExposure = LPMath.calculateLongExposure(
_longExposure,
_checkpointExposure,
_checkpointExposure + maxBondProceeds.toInt256()
);
uint256 vaultSharePrice = _vaultSharePrice;
return (
shareReserves.mulDown(vaultSharePrice).toInt256() -
longExposure.toInt256() -
_minimumShareReserves.mulUp(vaultSharePrice).toInt256(),
true
);
}

/// @dev Calculates the share reserves delta, the bond reserves delta, and
/// the total governance fee after opening a long.
/// @param _shareReservesDelta The change in the share reserves without fees.
/// @param _bondReservesDelta The change in the bond reserves without fees.
/// @param _vaultSharePrice The current vault share price.
/// @param _spotPrice The current spot price.
/// @return The change in the share reserves with fees.
/// @return The change in the bond reserves with fees.
/// @return The governance fee in shares.
function _calculateOpenLongFees(
uint256 _shareReservesDelta,
uint256 _bondReservesDelta,
uint256 _vaultSharePrice,
uint256 _spotPrice
) internal view returns (uint256, uint256, uint256) {
// Calculate the fees charged to the user (curveFee) and the portion
// of those fees that are paid to governance (governanceCurveFee).
(
uint256 curveFee, // bonds
uint256 governanceCurveFee // bonds
) = _calculateFeesGivenShares(
_shareReservesDelta,
_spotPrice,
_vaultSharePrice
);

// Calculate the impact of the curve fee on the bond reserves. The curve
// fee benefits the LPs by causing less bonds to be deducted from the
// bond reserves.
_bondReservesDelta -= curveFee;

// NOTE: Round down to underestimate the governance fee.
//
// Calculate the fees owed to governance in shares. Open longs are
// calculated entirely on the curve so the curve fee is the total
// governance fee. In order to convert it to shares we need to multiply
// it by the spot price and divide it by the vault share price:
//
// shares = (bonds * base/bonds) / (base/shares)
// shares = bonds * shares/bonds
// shares = shares
uint256 totalGovernanceFee = governanceCurveFee.mulDivDown(
_spotPrice,
_vaultSharePrice
);

// Calculate the number of shares to add to the shareReserves.
// shareReservesDelta, _shareAmount and totalGovernanceFee
// are all denominated in shares:
//
// shares = shares - shares
_shareReservesDelta -= totalGovernanceFee;

return (_shareReservesDelta, _bondReservesDelta, totalGovernanceFee);
}

/// @dev Calculates the fees that go to the LPs and governance.
/// @param _shareAmount The amount of shares exchanged for bonds.
/// @param _spotPrice The price without slippage of bonds in terms of base
Expand Down
89 changes: 78 additions & 11 deletions contracts/src/internal/HyperdriveLP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ abstract contract HyperdriveLP is
HyperdriveMultiToken
{
using FixedPointMath for uint256;
using FixedPointMath for int256;
using LPMath for LPMath.PresentValueParams;
using SafeCast for int256;
using SafeCast for uint256;
Expand Down Expand Up @@ -103,6 +104,24 @@ abstract contract HyperdriveLP is
revert IHyperdrive.InvalidEffectiveShareReserves();
}

// Check to see whether or not the initial liquidity will result in
// invalid price discovery. If the spot price can't be brought to one,
// we revert to avoid dangerous pool states.
(
int256 solvencyAfterMaxLong,
bool success
) = _calculateSolvencyAfterMaxLongSafe(
shareReserves,
shareAdjustment,
bondReserves,
vaultSharePrice,
0,
0
);
if (!success || solvencyAfterMaxLong < 0) {
revert IHyperdrive.CircuitBreakerTriggered();
}

// Initialize the reserves.
_marketState.shareReserves = shareReserves.toUint128();
_marketState.shareAdjustment = shareAdjustment.toInt128();
Expand All @@ -127,14 +146,17 @@ abstract contract HyperdriveLP is
);

// Emit an Initialize event.
uint256 contribution = _contribution; // avoid stack-too-deep
uint256 apr = _apr; // avoid stack-too-deep
IHyperdrive.Options calldata options = _options; // avoid stack-too-deep
emit Initialize(
_options.destination,
options.destination,
lpShares,
_contribution,
contribution,
vaultSharePrice,
_options.asBase,
_apr,
_options.extraData
options.asBase,
apr,
options.extraData
);

return lpShares;
Expand Down Expand Up @@ -200,8 +222,28 @@ abstract contract HyperdriveLP is
true
);

// Calculate the solvency after opening a max long before applying the
// add liquidity updates. This is a benchmark for the pool's current
// price discovery. Adding liquidity should not negatively impact price
// discovery.
(
int256 solvencyAfterMaxLongBefore,
bool success
) = _calculateSolvencyAfterMaxLongSafe(
_marketState.shareReserves,
_marketState.shareAdjustment,
_marketState.bondReserves,
vaultSharePrice,
_marketState.longExposure,
_nonNettedLongs(latestCheckpoint + _positionDuration)
);
if (!success) {
revert IHyperdrive.CircuitBreakerTriggered();
}

// Ensure that the spot APR is close enough to the previous weighted
// spot price to fall within the tolerance.
uint256 contribution = _contribution; // avoid stack-too-deep
{
uint256 previousWeightedSpotAPR = HyperdriveMath
.calculateAPRFromPrice(
Expand Down Expand Up @@ -275,7 +317,6 @@ abstract contract HyperdriveLP is
// NOTE: Round down to make the check more conservative.
//
// Enforce the minimum LP share price slippage guard.
uint256 contribution = _contribution; // avoid stack-too-deep
if (contribution.divDown(lpShares) < _minLpSharePrice) {
revert IHyperdrive.OutputLimit();
}
Expand All @@ -287,21 +328,47 @@ abstract contract HyperdriveLP is
// excess idle calculation fails, we revert to avoid allowing the system
// to enter an unhealthy state. A failure indicates that the present
// value can't be calculated.
bool success = _distributeExcessIdleSafe(vaultSharePrice);
success = _distributeExcessIdleSafe(vaultSharePrice);
if (!success) {
revert IHyperdrive.DistributeExcessIdleFailed();
}

// Check to see whether or not adding this liquidity will result in
// worsened price discovery. If the spot price can't be brought to one
// and price discovery worsened after adding liquidity, we revert to
// avoid dangerous pool states.
uint256 latestCheckpoint_ = latestCheckpoint; // avoid stack-too-deep
uint256 lpShares_ = lpShares; // avoid stack-too-deep
IHyperdrive.Options calldata options = _options; // avoid stack-too-deep
uint256 vaultSharePrice_ = vaultSharePrice; // avoid stack-too-deep
int256 solvencyAfterMaxLongAfter;
(
solvencyAfterMaxLongAfter,
success
) = _calculateSolvencyAfterMaxLongSafe(
_marketState.shareReserves,
_marketState.shareAdjustment,
_marketState.bondReserves,
vaultSharePrice_,
_marketState.longExposure,
_nonNettedLongs(latestCheckpoint_ + _positionDuration)
);
if (
!success ||
solvencyAfterMaxLongAfter < solvencyAfterMaxLongBefore.min(0)
) {
revert IHyperdrive.CircuitBreakerTriggered();
}

// Emit an AddLiquidity event.
uint256 lpSharePrice = lpTotalSupply == 0
? 0 // NOTE: We always round the LP share price down for consistency.
: startingPresentValue.mulDivDown(vaultSharePrice, lpTotalSupply);
IHyperdrive.Options calldata options = _options; // avoid stack-too-deep
: startingPresentValue.mulDivDown(vaultSharePrice_, lpTotalSupply);
emit AddLiquidity(
options.destination,
lpShares,
lpShares_,
contribution,
vaultSharePrice,
vaultSharePrice_,
options.asBase,
lpSharePrice,
options.extraData
Expand Down
Loading

0 comments on commit d838493

Please sign in to comment.