diff --git a/contracts/src/interfaces/IHyperdrive.sol b/contracts/src/interfaces/IHyperdrive.sol index 8c13180ef..c9e897d8d 100644 --- a/contracts/src/interfaces/IHyperdrive.sol +++ b/contracts/src/interfaces/IHyperdrive.sol @@ -200,6 +200,21 @@ interface IHyperdrive is bytes extraData; } + struct PairOptions { + /// @dev The address that receives the long proceeds from a pair action. + address longDestination; + /// @dev The address that receives the short proceeds from a pair action. + address shortDestination; + /// @dev A boolean indicating that the trade or LP action should be + /// settled in base if true and in the yield source shares if false. + bool asBase; + /// @dev Additional data that can be used to implement custom logic in + /// implementation contracts. By convention, the last 32 bytes of + /// extra data are ignored by instances and "passed through" to the + /// event. This can be used to pass metadata through transactions. + bytes extraData; + } + /// Errors /// /// @notice Thrown when the inputs to a batch transfer don't match in diff --git a/contracts/src/interfaces/IHyperdriveEvents.sol b/contracts/src/interfaces/IHyperdriveEvents.sol index 4a8b8ca13..f563b4b01 100644 --- a/contracts/src/interfaces/IHyperdriveEvents.sol +++ b/contracts/src/interfaces/IHyperdriveEvents.sol @@ -102,6 +102,20 @@ interface IHyperdriveEvents is IMultiTokenEvents { bytes extraData ); + /// @notice Emitted when a pair of long and short positions are opened. + event OpenPair( + address indexed longTrader, + address indexed shortTrader, + uint256 indexed maturityTime, + uint256 longAssetId, + uint256 shortAssetId, + uint256 amount, + uint256 vaultSharePrice, + bool asBase, + uint256 bondAmount, + bytes extraData + ); + /// @notice Emitted when a checkpoint is created. event CreateCheckpoint( uint256 indexed checkpointTime, diff --git a/contracts/src/internal/HyperdriveBase.sol b/contracts/src/internal/HyperdriveBase.sol index b6ea75e4a..67a813c2b 100644 --- a/contracts/src/internal/HyperdriveBase.sol +++ b/contracts/src/internal/HyperdriveBase.sol @@ -29,16 +29,20 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { /// @dev Process a deposit in either base or vault shares. /// @param _amount The amount of capital to deposit. The units of this /// quantity are either base or vault shares, depending on the value - /// of `_options.asBase`. - /// @param _options The options that configure how the deposit is - /// settled. In particular, the currency used in the deposit is - /// specified here. Aside from those options, yield sources can - /// choose to implement additional options. + /// of `_asBase`. + /// @param _asBase A flag indicating if the deposit should be made in base + /// or in vault shares. + /// @param _extraData Additional data that can be used to implement custom + /// logic in implementation contracts. By convention, the last 32 + /// bytes of extra data are ignored by instances and "passed through" + /// to the event. This can be used to pass metadata through + /// transactions. /// @return sharesMinted The shares created by this deposit. /// @return vaultSharePrice The vault share price. function _deposit( uint256 _amount, - IHyperdrive.Options calldata _options + bool _asBase, + bytes calldata _extraData ) internal returns (uint256 sharesMinted, uint256 vaultSharePrice) { // WARN: This logic doesn't account for slippage in the conversion // from base to shares. If deposits to the yield source incur @@ -50,19 +54,16 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { // Deposit with either base or shares depending on the provided options. uint256 refund; - if (_options.asBase) { + if (_asBase) { // Process the deposit in base. - (sharesMinted, refund) = _depositWithBase( - _amount, - _options.extraData - ); + (sharesMinted, refund) = _depositWithBase(_amount, _extraData); } else { // The refund is equal to the full message value since ETH will // never be a shares asset. refund = msg.value; // Process the deposit in shares. - _depositWithShares(_amount, _options.extraData); + _depositWithShares(_amount, _extraData); } // Calculate the vault share price. @@ -198,6 +199,23 @@ abstract contract HyperdriveBase is IHyperdriveEvents, HyperdriveStorage { } } + /// @dev A yield source dependent check that verifies that the provided + /// pair options are valid. The default check is that the destinations + /// are non-zero to prevent users from accidentally transferring funds + /// to the zero address. Custom integrations can override this to + /// implement additional checks. + /// @param _options The provided options for the transaction. + function _checkPairOptions( + IHyperdrive.PairOptions calldata _options + ) internal pure virtual { + if ( + _options.longDestination == address(0) || + _options.shortDestination == address(0) + ) { + revert IHyperdrive.RestrictedZeroAddress(); + } + } + /// @dev Convert an amount of vault shares to an amount of base. /// @param _shareAmount The vault shares amount. /// @return baseAmount The base amount. diff --git a/contracts/src/internal/HyperdriveLP.sol b/contracts/src/internal/HyperdriveLP.sol index d549ff200..4985ebf23 100644 --- a/contracts/src/internal/HyperdriveLP.sol +++ b/contracts/src/internal/HyperdriveLP.sol @@ -55,7 +55,8 @@ abstract contract HyperdriveLP is // their contribution was worth. (uint256 shareContribution, uint256 vaultSharePrice) = _deposit( _contribution, - _options + _options.asBase, + _options.extraData ); // Ensure that the contribution is large enough to set aside the minimum @@ -210,7 +211,8 @@ abstract contract HyperdriveLP is // Deposit for the user, this call also transfers from them (uint256 shareContribution, uint256 vaultSharePrice) = _deposit( _contribution, - _options + _options.asBase, + _options.extraData ); // Perform a checkpoint. diff --git a/contracts/src/internal/HyperdriveLong.sol b/contracts/src/internal/HyperdriveLong.sol index c5a50fef5..0c4ac6f1b 100644 --- a/contracts/src/internal/HyperdriveLong.sol +++ b/contracts/src/internal/HyperdriveLong.sol @@ -55,7 +55,8 @@ abstract contract HyperdriveLong is IHyperdriveEvents, HyperdriveLP { // Deposit the user's input amount. (uint256 sharesDeposited, uint256 vaultSharePrice) = _deposit( _amount, - _options + _options.asBase, + _options.extraData ); // Enforce the minimum user outputs and the minimum vault share price. diff --git a/contracts/src/internal/HyperdrivePair.sol b/contracts/src/internal/HyperdrivePair.sol new file mode 100644 index 000000000..460fad798 --- /dev/null +++ b/contracts/src/internal/HyperdrivePair.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.22; + +import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; +import { IHyperdriveEvents } from "../interfaces/IHyperdriveEvents.sol"; +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 { SafeCast } from "../libraries/SafeCast.sol"; +import { HyperdriveBase } from "./HyperdriveLP.sol"; +import { HyperdriveMultiToken } from "./HyperdriveMultiToken.sol"; + +/// @author DELV +/// @title HyperdriveLong +/// @notice Implements the long accounting for Hyperdrive. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +abstract contract HyperdrivePair is + IHyperdriveEvents, + HyperdriveBase, + HyperdriveMultiToken +{ + using FixedPointMath for uint256; + using FixedPointMath for int256; + using SafeCast for uint256; + using SafeCast for int256; + + // FIXME: Add in a governance fee that is taken from the deposit amount and + // reduces the bond amount earned. + // + /// @dev Opens a pair of long and short positions that directly match each + /// other. The amount of long and short positions that are created is + /// equal to the base value of the deposit. These positions are sent to + /// the provided destinations. + /// @param _amount The amount of capital provided to open the long. The + /// units of this quantity are either base or vault shares, depending + /// on the value of `_options.asBase`. + /// @param _options The pair options that configure how the trade is settled. + /// @return maturityTime The maturity time of the new long and short positions. + /// @return bondAmount The bond amount of the new long and short positoins. + function _openPair( + uint256 _amount, + uint256 _minVaultSharePrice, + IHyperdrive.PairOptions calldata _options + ) + internal + nonReentrant + isNotPaused + returns (uint256 maturityTime, uint256 bondAmount) + { + // Check that the message value is valid. + _checkMessageValue(); + + // Check that the provided options are valid. + _checkPairOptions(_options); + + // Deposit the user's input amount. The amount of base deposited is + // equal to the amount of bonds that will be minted. + (uint256 sharesDeposited, uint256 vaultSharePrice) = _deposit( + _amount, + _options.asBase, + _options.extraData + ); + bondAmount = sharesDeposited.mulDown(vaultSharePrice); + + // Enforce the minimum vault share price. + if (vaultSharePrice < _minVaultSharePrice) { + revert IHyperdrive.MinimumSharePrice(); + } + + // Perform a checkpoint. + uint256 latestCheckpoint = _latestCheckpoint(); + _applyCheckpoint( + latestCheckpoint, + vaultSharePrice, + LPMath.SHARE_PROCEEDS_MAX_ITERATIONS, + true + ); + + // Apply the state changes caused by creating the pair. + maturityTime = latestCheckpoint + _positionDuration; + _applyCreatePair(maturityTime, sharesDeposited, bondAmount); + + // Mint bonds equal in value to the base deposited. + uint256 longAssetId = AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + maturityTime + ); + uint256 shortAssetId = AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + maturityTime + ); + _mint(longAssetId, _options.longDestination, bondAmount); + _mint(shortAssetId, _options.shortDestination, bondAmount); + + // Emit an OpenPair event. + emit OpenPair( + _options.longDestination, + _options.shortDestination, + maturityTime, + longAssetId, + shortAssetId, + _amount, + vaultSharePrice, + _options.asBase, + bondAmount, + _options.extraData + ); + + return (maturityTime, bondAmount); + } + + // FIXME + // function _redeemPair( + // uint256 _amount, + // IHyperdrive.Options calldata _options + // ) internal returns (uint256 maturityTime, uint256 longAmount, uint256 shortAmount) { + // // FIXME + // } + + /// @dev Applies state changes to create a pair of matched long and short + /// positions. This operation leaves the pool's solvency and idle + /// capital unchanged because the positions fully net out. Specifically: + /// + /// - Share reserves, share adjustments, and bond reserves remain + /// constant since the provided capital backs the positions directly. + /// - Solvency remains constant because the net effect of matching long + /// and short positions is neutral. + /// - Idle capital is unaffected since no excess funds are added or + /// removed during this process. + /// + /// Therefore: + /// + /// - Solvency checks are unnecessary. + /// - Idle capital does not need to be redistributed to LPs. + /// @param _maturityTime The maturity time of the pair of long and short + /// positions + /// @param _sharesDeposited The amount of shares deposited. + /// @param _bondAmount The amount of bonds created. + function _applyCreatePair( + uint256 _maturityTime, + uint256 _sharesDeposited, + uint256 _bondAmount + ) internal { + // Update the average maturity time of longs and short positions and the + // amount of long and short positions outstanding. Everything else + // remains constant. + _marketState.longAverageMaturityTime = uint256( + _marketState.longAverageMaturityTime + ) + .updateWeightedAverage( + _marketState.longsOutstanding, + _maturityTime * ONE, // scale up to fixed point scale + _bondAmount, + true + ) + .toUint128(); + _marketState.shortAverageMaturityTime = uint256( + _marketState.shortAverageMaturityTime + ) + .updateWeightedAverage( + _marketState.shortsOutstanding, + _maturityTime * ONE, // scale up to fixed point scale + _bondAmount, + true + ) + .toUint128(); + _marketState.longsOutstanding += _bondAmount.toUint128(); + _marketState.shortsOutstanding += _bondAmount.toUint128(); + } +} diff --git a/contracts/src/internal/HyperdriveShort.sol b/contracts/src/internal/HyperdriveShort.sol index 3fa726fc0..6152b480a 100644 --- a/contracts/src/internal/HyperdriveShort.sol +++ b/contracts/src/internal/HyperdriveShort.sol @@ -129,7 +129,7 @@ abstract contract HyperdriveShort is IHyperdriveEvents, HyperdriveLP { if (_maxDeposit < deposit) { revert IHyperdrive.OutputLimit(); } - _deposit(deposit, _options); + _deposit(deposit, _options.asBase, _options.extraData); // Apply the state updates caused by opening the short. // Note: Updating the state using the result using the diff --git a/contracts/test/MockERC4626Hyperdrive.sol b/contracts/test/MockERC4626Hyperdrive.sol index 7864479f5..abc63b7e7 100644 --- a/contracts/test/MockERC4626Hyperdrive.sol +++ b/contracts/test/MockERC4626Hyperdrive.sol @@ -34,7 +34,7 @@ contract MockERC4626Hyperdrive is ERC4626Hyperdrive { uint256 _amount, IHyperdrive.Options calldata _options ) public returns (uint256 sharesMinted, uint256 vaultSharePrice) { - return _deposit(_amount, _options); + return _deposit(_amount, _options.asBase, _options.extraData); } function withdraw(