Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions contracts/src/interfaces/IHyperdrive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ interface IHyperdrive is
/// price of the underlying yield source on deployment.
error InvalidInitialVaultSharePrice();

/// @notice Thrown when the LP share price couldn't be calculated in a
/// critical situation.
error InvalidLPSharePrice();

/// @notice Thrown when the present value calculation fails.
error InvalidPresentValue();

Expand Down Expand Up @@ -307,6 +311,10 @@ interface IHyperdrive is
/// pool's depository assets changes.
error SweepFailed();

/// @notice Thrown when the distribute excess idle calculation fails due
/// to the starting present value calculation failing.
error DistributeExcessIdleFailed();

/// @notice Thrown when an ether transfer fails.
error TransferFailed();

Expand Down
63 changes: 49 additions & 14 deletions contracts/src/internal/HyperdriveBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -180,18 +180,30 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage {
/// @dev Gets the distribute excess idle parameters from the current state.
/// @param _vaultSharePrice The current vault share price.
/// @return params The distribute excess idle parameters.
function _getDistributeExcessIdleParams(
/// @return success A failure flag indicating if the calculation succeeded.
function _getDistributeExcessIdleParamsSafe(
uint256 _idle,
uint256 _withdrawalSharesTotalSupply,
uint256 _vaultSharePrice
) internal view returns (LPMath.DistributeExcessIdleParams memory params) {
)
internal
view
returns (LPMath.DistributeExcessIdleParams memory params, bool success)
{
// Calculate the starting present value. If this fails, we return a
// failure flag and proceed to avoid impacting checkpointing liveness.
LPMath.PresentValueParams
memory presentValueParams = _getPresentValueParams(
_vaultSharePrice
);
uint256 startingPresentValue = LPMath.calculatePresentValue(
uint256 startingPresentValue;
(startingPresentValue, success) = LPMath.calculatePresentValueSafe(
presentValueParams
);
if (!success) {
return (params, false);
}

// NOTE: For consistency with the present value calculation, we round
// up the long side and round down the short side.
int256 netCurveTrade = int256(
Expand All @@ -215,6 +227,7 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage {
originalShareAdjustment: presentValueParams.shareAdjustment,
originalBondReserves: presentValueParams.bondReserves
});
success = true;
}

/// @dev Gets the present value parameters from the current state.
Expand All @@ -230,6 +243,7 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage {
vaultSharePrice: _vaultSharePrice,
initialVaultSharePrice: _initialVaultSharePrice,
minimumShareReserves: _minimumShareReserves,
minimumTransactionAmount: _minimumTransactionAmount,
timeStretch: _timeStretch,
longsOutstanding: _marketState.longsOutstanding,
longAverageTimeRemaining: _calculateTimeRemainingScaled(
Expand Down Expand Up @@ -431,25 +445,46 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage {
return idleShares;
}

/// @dev Calculates the LP share price.
/// @dev Calculates the LP share price. If the LP share price can't be
/// calculated, this function returns a failure flag.
/// @param _vaultSharePrice The current vault share price.
/// @return lpSharePrice The LP share price in units of (base / lp shares).
function _calculateLPSharePrice(
/// @return The LP share price in units of (base / lp shares).
/// @return A flag indicating if the calculation succeeded.
function _calculateLPSharePriceSafe(
uint256 _vaultSharePrice
) internal view returns (uint256 lpSharePrice) {
) internal view returns (uint256, bool) {
// Calculate the present value safely to prevent liveness problems. If
// the calculation fails, we return 0.
(uint256 presentValueShares, bool success) = LPMath
.calculatePresentValueSafe(
_getPresentValueParams(_vaultSharePrice)
);
if (!success) {
return (0, false);
}

// NOTE: Round down to underestimate the LP share price.
//
// Calculate the present value in base and the LP total supply.
uint256 presentValue = _vaultSharePrice > 0
? LPMath
.calculatePresentValue(_getPresentValueParams(_vaultSharePrice))
.mulDown(_vaultSharePrice)
? presentValueShares.mulDown(_vaultSharePrice)
: 0;
uint256 lpTotalSupply = _totalSupply[AssetId._LP_ASSET_ID] +
_totalSupply[AssetId._WITHDRAWAL_SHARE_ASSET_ID] -
_withdrawPool.readyToWithdraw;
lpSharePrice = lpTotalSupply == 0
? 0 // NOTE: Round down to underestimate the LP share price.
: presentValue.divDown(lpTotalSupply);
return lpSharePrice;

// If the LP total supply is zero, the LP share price can't be computed
// due to a divide-by-zero error.
if (lpTotalSupply == 0) {
return (0, false);
}

// NOTE: Round down to underestimate the LP share price.
//
// Calculate the LP share price.
uint256 lpSharePrice = presentValue.divDown(lpTotalSupply);

return (lpSharePrice, true);
}

/// @dev Calculates the fees that go to the LPs and governance.
Expand Down
15 changes: 11 additions & 4 deletions contracts/src/internal/HyperdriveCheckpoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -223,18 +223,25 @@ abstract contract HyperdriveCheckpoint is
0
);

// Distribute the excess idle to the withdrawal pool.
_distributeExcessIdle(_vaultSharePrice);
// Distribute the excess idle to the withdrawal pool. If the
// distribute excess idle calculation fails, we proceed with the
// calculation since checkpoints should be minted regardless of
// whether idle could be distributed.
_distributeExcessIdleSafe(_vaultSharePrice);
}

// Emit an event about the checkpoint creation that includes the LP
// share price.
// share price. If the LP share price calculation fails, we proceed in
// minting the checkpoint and just emit the LP share price as zero. This
// ensures that the system's liveness isn't impacted by temporarily
// being unable to calculate the present value.
(uint256 lpSharePrice, ) = _calculateLPSharePriceSafe(_vaultSharePrice);
emit CreateCheckpoint(
_checkpointTime,
_vaultSharePrice,
maturedShortsAmount,
maturedLongsAmount,
_calculateLPSharePrice(_vaultSharePrice)
lpSharePrice
);

return _vaultSharePrice;
Expand Down
79 changes: 52 additions & 27 deletions contracts/src/internal/HyperdriveLP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -229,24 +229,32 @@ abstract contract HyperdriveLP is
// Mint LP shares to the supplier.
_mint(AssetId._LP_ASSET_ID, _options.destination, lpShares);

// Distribute the excess idle to the withdrawal pool.
_distributeExcessIdle(vaultSharePrice);
// Distribute the excess idle to the withdrawal pool. If the distribute
// 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);
if (!success) {
revert IHyperdrive.DistributeExcessIdleFailed();
}

// Emit an AddLiquidity event.
uint256 lpSharePrice = lpTotalSupply == 0
? 0 // NOTE: We always round the LP share price down for consistency.
: startingPresentValue.divDown(lpTotalSupply);
uint256 contribution = _contribution; // avoid stack-too-deep
uint256 baseContribution = _convertToBaseFromOption(
_contribution,
contribution,
vaultSharePrice,
_options
);
IHyperdrive.Options calldata options = _options; // avoid stack-too-deep
emit AddLiquidity(
_options.destination,
options.destination,
lpShares,
baseContribution, // base contribution
shareContribution, // vault shares contribution
_options.asBase,
options.asBase,
lpSharePrice
);
}
Expand Down Expand Up @@ -293,9 +301,6 @@ abstract contract HyperdriveLP is
_lpShares
);

// Distribute excess idle to the withdrawal pool.
_distributeExcessIdle(vaultSharePrice);

// Redeem as many of the withdrawal shares as possible.
uint256 withdrawalSharesRedeemed;
(proceeds, withdrawalSharesRedeemed) = _redeemWithdrawalSharesInternal(
Expand All @@ -307,7 +312,11 @@ abstract contract HyperdriveLP is
);
withdrawalShares = _lpShares - withdrawalSharesRedeemed;

// Emit a RemoveLiquidity event.
// Emit a RemoveLiquidity event. If the LP share price calculation
// fails, we proceed in removing liquidity and just emit the LP share
// price as zero. This ensures that the system's liveness isn't impacted
// by temporarily being unable to calculate the present value.
(uint256 lpSharePrice, ) = _calculateLPSharePriceSafe(vaultSharePrice);
emit RemoveLiquidity(
_options.destination,
_lpShares,
Expand All @@ -319,7 +328,7 @@ abstract contract HyperdriveLP is
), // vault shares proceeds
_options.asBase,
uint256(withdrawalShares),
_calculateLPSharePrice(vaultSharePrice)
lpSharePrice
);

return (proceeds, withdrawalShares);
Expand Down Expand Up @@ -353,10 +362,6 @@ abstract contract HyperdriveLP is
uint256 vaultSharePrice = _pricePerVaultShare();
_applyCheckpoint(_latestCheckpoint(), vaultSharePrice);

// Distribute the excess idle to the withdrawal pool prior to redeeming
// the withdrawal shares.
_distributeExcessIdle(vaultSharePrice);

// Redeem as many of the withdrawal shares as possible.
(proceeds, withdrawalSharesRedeemed) = _redeemWithdrawalSharesInternal(
msg.sender,
Expand Down Expand Up @@ -388,7 +393,7 @@ abstract contract HyperdriveLP is
/// withdrawal shares ready to withdraw.
/// @param _source The address that owns the withdrawal shares to redeem.
/// @param _withdrawalShares The withdrawal shares to redeem.
/// @param _sharePrice The share price.
/// @param _vaultSharePrice The vault share price.
/// @param _minOutputPerShare The minimum amount the LP expects to
/// receive for each withdrawal share that is burned. The units of
/// this quantity are either base or vault shares, depending on the
Expand All @@ -402,10 +407,17 @@ abstract contract HyperdriveLP is
function _redeemWithdrawalSharesInternal(
address _source,
uint256 _withdrawalShares,
uint256 _sharePrice,
uint256 _vaultSharePrice,
uint256 _minOutputPerShare,
IHyperdrive.Options calldata _options
) internal returns (uint256 proceeds, uint256 withdrawalSharesRedeemed) {
// Distribute the excess idle to the withdrawal pool. If the distribute
// excess idle calculation fails, we proceed with the calculation since
// LPs should be able to redeem their withdrawal shares for existing
// withdrawal proceeds regardless of whether or not idle could be
// distributed.
_distributeExcessIdleSafe(_vaultSharePrice);

// Clamp the shares to the total amount of shares ready for withdrawal
// to avoid unnecessary reverts. We exit early if the user has no shares
// available to redeem.
Expand Down Expand Up @@ -438,7 +450,7 @@ abstract contract HyperdriveLP is
_withdrawPool.proceeds -= shareProceeds.toUint128();

// Withdraw the share proceeds to the user.
proceeds = _withdraw(shareProceeds, _sharePrice, _options);
proceeds = _withdraw(shareProceeds, _vaultSharePrice, _options);

// NOTE: Round up to make the check more conservative.
//
Expand All @@ -453,39 +465,52 @@ abstract contract HyperdriveLP is
/// @dev Distribute as much of the excess idle as possible to the withdrawal
/// pool while holding the LP share price constant.
/// @param _vaultSharePrice The current vault share price.
function _distributeExcessIdle(uint256 _vaultSharePrice) internal {
/// @return A failure flag indicating if the calculation succeeded.
function _distributeExcessIdleSafe(
uint256 _vaultSharePrice
) internal returns (bool) {
// If there are no withdrawal shares, then there is nothing to
// distribute.
uint256 withdrawalSharesTotalSupply = _totalSupply[
AssetId._WITHDRAWAL_SHARE_ASSET_ID
] - _withdrawPool.readyToWithdraw;
if (withdrawalSharesTotalSupply == 0) {
return;
return true;
}

// If there is no excess idle, then there is nothing to distribute.
uint256 idle = _calculateIdleShareReserves(_vaultSharePrice);
if (idle == 0) {
return;
return true;
}

// Get the distribute excess idle parameters. If this fails for some
// we return a failure flag so that the caller can handle the failure.
(
LPMath.DistributeExcessIdleParams memory params,
bool success
) = _getDistributeExcessIdleParamsSafe(
idle,
withdrawalSharesTotalSupply,
_vaultSharePrice
);
if (!success) {
return false;
}

// Calculate the amount of withdrawal shares that should be redeemed
// and their share proceeds.
(uint256 withdrawalSharesRedeemed, uint256 shareProceeds) = LPMath
.calculateDistributeExcessIdle(
_getDistributeExcessIdleParams(
idle,
withdrawalSharesTotalSupply,
_vaultSharePrice
)
);
.calculateDistributeExcessIdle(params);

// Update the withdrawal pool's state.
_withdrawPool.readyToWithdraw += withdrawalSharesRedeemed.toUint128();
_withdrawPool.proceeds += shareProceeds.toUint128();

// Remove the withdrawal pool proceeds from the reserves.
_updateLiquidity(-int256(shareProceeds));

return true;
}

/// @dev Updates the pool's liquidity and holds the pool's spot price constant.
Expand Down
27 changes: 22 additions & 5 deletions contracts/src/internal/HyperdriveLong.sol
Original file line number Diff line number Diff line change
Expand Up @@ -195,15 +195,27 @@ abstract contract HyperdriveLong is IHyperdriveEvents, HyperdriveLP {
nonNettedLongs + int256(_bondAmount),
nonNettedLongs
);

// Distribute the excess idle to the withdrawal pool. If the
// distribute excess idle calculation fails, we revert to avoid
// putting the system in an unhealthy state after the trade is
// processed.
bool success = _distributeExcessIdleSafe(vaultSharePrice);
if (!success) {
revert IHyperdrive.DistributeExcessIdleFailed();
}
} else {
// Apply the zombie close to the state and adjust the share proceeds
// to account for negative interest that might have accrued to the
// zombie share reserves.
shareProceeds = _applyZombieClose(shareProceeds, vaultSharePrice);
}

// Distribute the excess idle to the withdrawal pool.
_distributeExcessIdle(vaultSharePrice);
// Distribute the excess idle to the withdrawal pool. If the
// distribute excess idle calculation fails, we proceed with the
// calculation since traders should be able to close their positions
// at maturity regardless of whether idle could be distributed.
_distributeExcessIdleSafe(vaultSharePrice);
}

// Withdraw the profit to the trader.
uint256 proceeds = _withdraw(shareProceeds, vaultSharePrice, _options);
Expand Down Expand Up @@ -287,8 +299,13 @@ abstract contract HyperdriveLong is IHyperdriveEvents, HyperdriveLP {
);
}

// Distribute the excess idle to the withdrawal pool.
_distributeExcessIdle(_vaultSharePrice);
// Distribute the excess idle to the withdrawal pool. If the distribute
// excess idle calculation fails, we revert to avoid putting the system
// in an unhealthy state after the trade is processed.
bool success = _distributeExcessIdleSafe(_vaultSharePrice);
if (!success) {
revert IHyperdrive.DistributeExcessIdleFailed();
}
}

/// @dev Applies the trading deltas from a closed long to the reserves and
Expand Down
Loading