From 664ba71a13c9d36dc6bb8d02a176fd24c094c81f Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 31 Jan 2023 18:29:05 -0600 Subject: [PATCH 01/12] Added a function that closes longs --- contracts/Hyperdrive.sol | 77 +++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 91749f29b..ca3d3e885 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -94,8 +94,7 @@ contract Hyperdrive is ERC20 { _mint(msg.sender, _contribution); } - /// @notice Opens a long position that whose with a term length starting in - /// the current block. + /// @notice Opens a long position. /// @param _amount The amount of base to use when trading. function openLong(uint256 _amount) external { if (_amount == 0) { @@ -116,7 +115,7 @@ contract Hyperdrive is ERC20 { ( uint256 poolBaseDelta, uint256 poolBondDelta, - uint256 bondsPurchased + uint256 bondProceeds ) = HyperdriveMath.calculateOutGivenIn( shareReserves, bondReserves, @@ -129,20 +128,21 @@ contract Hyperdrive is ERC20 { true ); - // Apply the trading deltas to the reserves. + // Apply the trading deltas to the reserves and increase the base buffer + // by the number of bonds purchased to ensure that the pool can fully + // redeem the newly purchased bonds. shareReserves += poolBaseDelta; bondReserves -= poolBondDelta; - - // Increase the base buffer by the number of bonds purchased to ensure - // that the pool can fully redeem the newly purchased bonds. - baseBuffer += bondsPurchased; + baseBuffer += bondProceeds; // TODO: We should fuzz test this and other trading functions to ensure // that the APR never goes below zero. If it does, we may need to // enforce additional invariants. // - // Ensure that the base reserves are greater than the base buffer and - // that the bond reserves are greater than the bond buffer. + // Since the base buffer may have increased relative to the base + // reserves and the bond reserves decreased, we must ensure that the + // base reserves are greater than the base buffer and that the bond + // reserves are greater than the bond buffer. if (sharePrice * shareReserves >= baseBuffer) { revert ElementError.BaseBufferExceedsShareReserves(); } @@ -150,12 +150,55 @@ contract Hyperdrive is ERC20 { revert ElementError.BondBufferExceedsBondReserves(); } - // Mint the bonds to the trader. - longToken.mint( - msg.sender, - block.timestamp, - bondsPurchased, - new bytes(0) - ); + // Mint the bonds to the trader with a mint time set to the current block. + longToken.mint(msg.sender, block.timestamp, bondProceeds, new bytes(0)); + } + + /// @notice Closes a long position with a specified mint time. + /// @param _mintTime The mint time of the longs to close. + /// @param _amount The amount of longs to close. + function closeLong(uint256 _mintTime, uint256 _amount) external { + if (_amount == 0) { + revert ElementError.ZeroAmount(); + } + + // Burn the trader's long tokens. + longToken.burn(msg.sender, _mintTime, _amount); + + // Calculate the pool and user deltas using the trading function. + uint256 timeElapsed = block.timestamp - _mintTime; + uint256 timeRemaining = timeElapsed < termLength + ? (termLength - timeElapsed) * FixedPointMath.ONE_18 + : 0; + ( + uint256 poolBaseDelta, + uint256 poolBondDelta, + uint256 baseProceeds + ) = HyperdriveMath.calculateOutGivenIn( + shareReserves, + bondReserves, + totalSupply(), + _amount, + timeRemaining, + timeStretch, + sharePrice, + initialSharePrice, + false + ); + + // Apply the trading deltas to the reserves and decrease the base buffer + // by the amount of bonds sold. Since the difference between the base + // reserves and the base buffer stays the same or gets larger and the + // difference between the bond reserves and the bond buffer increases, + // we don't need to check that the reserves are larger than the buffers. + shareReserves -= poolBaseDelta; + bondReserves += poolBondDelta; + baseBuffer -= _amount; + + // Transfer the base returned to the trader. + bool success = baseToken.transfer(msg.sender, baseProceeds); + if (!success) { + revert ElementError.TransferFailed(); + } } } From 6f9f09333dea8c52078571302f1408321d696f21 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 31 Jan 2023 19:05:57 -0600 Subject: [PATCH 02/12] Polishing and addressing oversights --- contracts/Hyperdrive.sol | 19 ++-- contracts/libraries/HyperdriveMath.sol | 125 +++++++++++++++---------- 2 files changed, 85 insertions(+), 59 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index ca3d3e885..fb682ad54 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -9,6 +9,8 @@ import { HyperdriveMath } from "contracts/libraries/HyperdriveMath.sol"; import { IERC1155Mintable } from "contracts/interfaces/IERC1155Mintable.sol"; contract Hyperdrive is ERC20 { + using FixedPointMath for uint256; + /// Tokens /// IERC20 public immutable baseToken; @@ -113,14 +115,14 @@ contract Hyperdrive is ERC20 { // Calculate the pool and user deltas using the trading function. ( - uint256 poolBaseDelta, + uint256 poolShareDelta, uint256 poolBondDelta, uint256 bondProceeds ) = HyperdriveMath.calculateOutGivenIn( shareReserves, bondReserves, totalSupply(), - _amount, + _amount.divDown(sharePrice), FixedPointMath.ONE_18, timeStretch, sharePrice, @@ -131,7 +133,7 @@ contract Hyperdrive is ERC20 { // Apply the trading deltas to the reserves and increase the base buffer // by the number of bonds purchased to ensure that the pool can fully // redeem the newly purchased bonds. - shareReserves += poolBaseDelta; + shareReserves += poolShareDelta; bondReserves -= poolBondDelta; baseBuffer += bondProceeds; @@ -171,9 +173,9 @@ contract Hyperdrive is ERC20 { ? (termLength - timeElapsed) * FixedPointMath.ONE_18 : 0; ( - uint256 poolBaseDelta, + uint256 poolShareDelta, uint256 poolBondDelta, - uint256 baseProceeds + uint256 shareProceeds ) = HyperdriveMath.calculateOutGivenIn( shareReserves, bondReserves, @@ -191,12 +193,15 @@ contract Hyperdrive is ERC20 { // reserves and the base buffer stays the same or gets larger and the // difference between the bond reserves and the bond buffer increases, // we don't need to check that the reserves are larger than the buffers. - shareReserves -= poolBaseDelta; + shareReserves -= poolShareDelta; bondReserves += poolBondDelta; baseBuffer -= _amount; // Transfer the base returned to the trader. - bool success = baseToken.transfer(msg.sender, baseProceeds); + bool success = baseToken.transfer( + msg.sender, + shareProceeds.mulDown(sharePrice) + ); if (!success) { revert ElementError.TransferFailed(); } diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index 1c5069ba6..c049cebb2 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -6,7 +6,7 @@ import { FixedPointMath } from "contracts/libraries/FixedPointMath.sol"; import { YieldSpaceMath } from "contracts/libraries/YieldSpaceMath.sol"; // FIXME: The matrix of uses of flat+curve includes cases that should never -// occur. In particular, if isBondOut && t > 0 or isBondIn && t > 0, then the +// occur. In particular, if isBondOut && t < 1 or isBondIn && t < 1, then the // flat part refers to base tokens and the model doesn't make sense. // /// @notice Math for the Hyperdrive pricing model. @@ -50,35 +50,41 @@ library HyperdriveMath { /// when share_reserves = bond_reserves, which would ensure that half /// of the pool reserves couldn't be used to provide liquidity. /// @param amountIn The amount of the asset that is provided. - /// @param t The amount of time until maturity in seconds. - /// @param s The time stretch parameter. - /// @param c The share price. - /// @param mu The initial share price. + /// @param timeRemaining The amount of time until maturity in seconds. + /// @param timeStretch The time stretch parameter. + /// @param sharePrice The share price. + /// @param initialSharePrice The initial share price. /// @param isBondOut A flag that specifies whether bonds are the asset being /// received or the asset being provided. + /// @return poolShareDelta The delta that should be applied to the pool's + /// share reserves. + /// @return poolBondDelta The delta that should be applied to the pool's + /// bond reserves. + /// @return userDelta The amount of assets the user should receive. function calculateOutGivenIn( uint256 shareReserves, uint256 bondReserves, uint256 bondReserveAdjustment, uint256 amountIn, - uint256 t, - uint256 s, - uint256 c, - uint256 mu, + uint256 timeRemaining, + uint256 timeStretch, + uint256 sharePrice, + uint256 initialSharePrice, bool isBondOut ) internal pure returns ( - uint256 poolBaseDelta, + uint256 poolShareDelta, uint256 poolBondDelta, uint256 userDelta ) { // TODO: See if this is actually true. // - // This pricing model only supports the purchasing of bonds when t = 1. - if (isBondOut && t < 1) { + // This pricing model only supports the purchasing of bonds when + // timeRemaining = 1. + if (isBondOut && timeRemaining < 1) { revert ElementError.HyperdriveMath_BaseWithNonzeroTime(); } if (isBondOut) { @@ -90,31 +96,35 @@ library HyperdriveMath { bondReserveAdjustment, amountIn, FixedPointMath.ONE_18, - s, - c, - mu, + timeStretch, + sharePrice, + initialSharePrice, isBondOut ); return (amountIn, amountOut, amountOut); } else { - // Since we are trading bonds, it's possible that t < 1. We consider - // (1-t)*amountIn of the bonds to be fully matured and t*amountIn of - // the bonds to be newly minted. The fully matured bonds are redeemed - // one-to-one (on the "flat" part of the curve) and the newly minted - // bonds are traded on a YieldSpace curve configured to t = 1. - uint256 flat = amountIn.mulDown(FixedPointMath.ONE_18.sub(t)); - uint256 curveIn = amountIn.mulDown(t); + // Since we are trading bonds, it's possible that timeRemaining < 1. + // We consider (1-timeRemaining)*amountIn of the bonds to be fully + // matured and timeRemaining*amountIn of the bonds to be newly + // minted. The fully matured bonds are redeemed one-to-one to base + // (our result is given in shares, so we divide the one-to-one + // redemption by the share price) and the newly minted bonds are + // traded on a YieldSpace curve configured to timeRemaining = 1. + uint256 flat = amountIn + .mulDown(FixedPointMath.ONE_18.sub(timeRemaining)) + .divDown(sharePrice); + uint256 curveIn = amountIn.mulDown(timeRemaining); uint256 curveOut = YieldSpaceMath.calculateOutGivenIn( // Debit the share reserves by the flat trade. - shareReserves.sub(flat.divDown(c)), + shareReserves.sub(flat.divDown(initialSharePrice)), // Credit the bond reserves by the flat trade. bondReserves.add(flat), bondReserveAdjustment, curveIn, FixedPointMath.ONE_18, - s, - c, - mu, + timeStretch, + sharePrice, + initialSharePrice, isBondOut ); return (flat.add(curveOut), curveIn, flat.add(curveOut)); @@ -130,71 +140,82 @@ library HyperdriveMath { /// when share_reserves = bond_reserves, which would ensure that half /// of the pool reserves couldn't be used to provide liquidity. /// @param amountOut The amount of the asset that is received. - /// @param t The amount of time until maturity in seconds. - /// @param s The time stretch parameter. - /// @param c The share price. - /// @param mu The initial share price. + /// @param timeRemaining The amount of time until maturity in seconds. + /// @param timeStretch The time stretch parameter. + /// @param sharePrice The share price. + /// @param initialSharePrice The initial share price. /// @param isBondIn A flag that specifies whether bonds are the asset being /// provided or the asset being received. + /// @return poolShareDelta The delta that should be applied to the pool's + /// share reserves. + /// @return poolBondDelta The delta that should be applied to the pool's + /// bond reserves. + /// @return userDelta The amount of assets the user should receive. function calculateInGivenOut( uint256 shareReserves, uint256 bondReserves, uint256 bondReserveAdjustment, uint256 amountOut, - uint256 t, - uint256 s, - uint256 c, - uint256 mu, + uint256 timeRemaining, + uint256 timeStretch, + uint256 sharePrice, + uint256 initialSharePrice, bool isBondIn ) internal pure returns ( - uint256 poolBaseDelta, + uint256 poolShareDelta, uint256 poolBondDelta, uint256 userDelta ) { // TODO: See if this is actually true. // - // This pricing model only supports the selling of bonds when t = 1. - if (isBondIn && t < 1) { + // This pricing model only supports the selling of bonds when + // timeRemaining = 1. + if (isBondIn && timeRemaining < 1) { revert ElementError.HyperdriveMath_BaseWithNonzeroTime(); } if (isBondIn) { // If bonds are being sold, then the entire trade occurs on the - // curved portion since t = 1. + // curved portion since timeRemaining = 1. uint256 amountIn = YieldSpaceMath.calculateInGivenOut( shareReserves, bondReserves, bondReserveAdjustment, amountOut, FixedPointMath.ONE_18, - s, - c, - mu, + timeStretch, + sharePrice, + initialSharePrice, isBondIn ); return (amountOut, amountIn, amountIn); } else { - // Since we are trading bonds, it's possible that t < 1. We consider - // (1-t)*amountIn of the bonds to be fully matured and t*amountIn of - // the bonds to be newly minted. The fully matured bonds are redeemed - // one-to-one (on the "flat" part of the curve) and the newly minted - // bonds are traded on a YieldSpace curve configured to t = 1. - uint256 flat = amountOut.mulDown(FixedPointMath.ONE_18.sub(t)); - uint256 curveOut = amountOut.mulDown(t); + // Since we are buying bonds, it's possible that timeRemaining < 1. + // We consider (1-timeRemaining)*amountOut of the bonds being + // purchased to be fully matured and timeRemaining*amountOut of the + // bonds to be newly minted. The fully matured bonds are redeemed + // one-to-one to base (our result is given in shares, so we divide + // the one-to-one redemption by the share price) and the newly + // minted bonds are traded on a YieldSpace curve configured to + // timeRemaining = 1. + uint256 flat = amountOut + .mulDown(FixedPointMath.ONE_18.sub(timeRemaining)) + .divDown(sharePrice); + uint256 curveOut = amountOut.mulDown(timeRemaining); uint256 curveIn = YieldSpaceMath.calculateInGivenOut( // Credit the share reserves by the flat trade. - shareReserves.add(flat.divDown(c)), + shareReserves.add(flat.divDown(sharePrice)), // Debit the bond reserves by the flat trade. bondReserves.sub(flat), bondReserveAdjustment, curveOut, FixedPointMath.ONE_18, - s, - c, - mu, + timeStretch, + sharePrice, + initialSharePrice, isBondIn ); return (flat.add(curveIn), curveIn, flat.add(curveIn)); From bbcb040a477231cce06961a6b27c822a4c46c90c Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 31 Jan 2023 19:09:45 -0600 Subject: [PATCH 03/12] Changed parameter name to include units --- contracts/Hyperdrive.sol | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index fb682ad54..4a50e953a 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -97,9 +97,9 @@ contract Hyperdrive is ERC20 { } /// @notice Opens a long position. - /// @param _amount The amount of base to use when trading. - function openLong(uint256 _amount) external { - if (_amount == 0) { + /// @param _baseAmount The amount of base to use when trading. + function openLong(uint256 _baseAmount) external { + if (_baseAmount == 0) { revert ElementError.ZeroAmount(); } @@ -107,7 +107,7 @@ contract Hyperdrive is ERC20 { bool success = baseToken.transferFrom( msg.sender, address(this), - _amount + _baseAmount ); if (!success) { revert ElementError.TransferFailed(); @@ -122,7 +122,7 @@ contract Hyperdrive is ERC20 { shareReserves, bondReserves, totalSupply(), - _amount.divDown(sharePrice), + _baseAmount.divDown(sharePrice), FixedPointMath.ONE_18, timeStretch, sharePrice, @@ -158,14 +158,14 @@ contract Hyperdrive is ERC20 { /// @notice Closes a long position with a specified mint time. /// @param _mintTime The mint time of the longs to close. - /// @param _amount The amount of longs to close. - function closeLong(uint256 _mintTime, uint256 _amount) external { - if (_amount == 0) { + /// @param _bondAmount The amount of longs to close. + function closeLong(uint256 _mintTime, uint256 _bondAmount) external { + if (_bondAmount == 0) { revert ElementError.ZeroAmount(); } - // Burn the trader's long tokens. - longToken.burn(msg.sender, _mintTime, _amount); + // Burn the bonds that are being closed. + longToken.burn(msg.sender, _mintTime, _bondAmount); // Calculate the pool and user deltas using the trading function. uint256 timeElapsed = block.timestamp - _mintTime; @@ -180,7 +180,7 @@ contract Hyperdrive is ERC20 { shareReserves, bondReserves, totalSupply(), - _amount, + _bondAmount, timeRemaining, timeStretch, sharePrice, @@ -195,7 +195,7 @@ contract Hyperdrive is ERC20 { // we don't need to check that the reserves are larger than the buffers. shareReserves -= poolShareDelta; bondReserves += poolBondDelta; - baseBuffer -= _amount; + baseBuffer -= _bondAmount; // Transfer the base returned to the trader. bool success = baseToken.transfer( From f318227d1b9409404295e12476acee675aff4dae Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 31 Jan 2023 21:04:05 -0600 Subject: [PATCH 04/12] Adds a function that opens a short --- contracts/Hyperdrive.sol | 69 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 4a50e953a..6e44f25dd 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -96,6 +96,8 @@ contract Hyperdrive is ERC20 { _mint(msg.sender, _contribution); } + /// Long /// + /// @notice Opens a long position. /// @param _baseAmount The amount of base to use when trading. function openLong(uint256 _baseAmount) external { @@ -114,15 +116,16 @@ contract Hyperdrive is ERC20 { } // Calculate the pool and user deltas using the trading function. + uint256 shareAmount = _baseAmount.divDown(sharePrice); ( - uint256 poolShareDelta, + , uint256 poolBondDelta, uint256 bondProceeds ) = HyperdriveMath.calculateOutGivenIn( shareReserves, bondReserves, totalSupply(), - _baseAmount.divDown(sharePrice), + shareAmount, FixedPointMath.ONE_18, timeStretch, sharePrice, @@ -133,7 +136,7 @@ contract Hyperdrive is ERC20 { // Apply the trading deltas to the reserves and increase the base buffer // by the number of bonds purchased to ensure that the pool can fully // redeem the newly purchased bonds. - shareReserves += poolShareDelta; + shareReserves += shareAmount; bondReserves -= poolBondDelta; baseBuffer += bondProceeds; @@ -157,8 +160,8 @@ contract Hyperdrive is ERC20 { } /// @notice Closes a long position with a specified mint time. - /// @param _mintTime The mint time of the longs to close. - /// @param _bondAmount The amount of longs to close. + /// @param _mintTime The mint time of the bonds to close. + /// @param _bondAmount The amount of bonds to close. function closeLong(uint256 _mintTime, uint256 _bondAmount) external { if (_bondAmount == 0) { revert ElementError.ZeroAmount(); @@ -206,4 +209,60 @@ contract Hyperdrive is ERC20 { revert ElementError.TransferFailed(); } } + + /// Short /// + + /// @notice Opens a short position. + /// @param _bondAmount The amount of bonds to short. + function openShort(uint256 _bondAmount) external { + if (_bondAmount == 0) { + revert ElementError.ZeroAmount(); + } + + // Calculate the pool and user deltas using the trading function. + ( + uint256 poolShareDelta, + , + uint256 shareProceeds + ) = HyperdriveMath.calculateOutGivenIn( + shareReserves, + bondReserves, + totalSupply(), + _bondAmount, + FixedPointMath.ONE_18, + timeStretch, + sharePrice, + initialSharePrice, + false + ); + + // Take custody of the maximum amount the trader can lose on the short. + bool success = baseToken.transferFrom(msg.sender, address(this), _bondAmount - shareProceeds); + if (!success) { + revert ElementError.TransferFailed(); + } + + // Apply the trading deltas to the reserves and increase the bond buffer + // by the amount of bonds that were shorted. + shareReserves -= poolShareDelta; + bondReserves += _bondAmount; + bondBuffer += _bondAmount; + + // The bond buffer is increased by the same amount as the bond buffer, + // so there is no need to check that the bond reserves is greater than + // or equal to the bond buffer. Since the share reserves are reduced, + // we need to verify that the base reserves are greater than or equal + // to the base buffer. + if (sharePrice * shareReserves >= baseBuffer) { + revert ElementError.BaseBufferExceedsShareReserves(); + } + + // Mint the short tokens to the trader. + shortToken.mint( + msg.sender, + block.timestamp + termLength, + _bondAmount, + new bytes(0) + ); + } } From 70e3580a31d49b95f7f8a8abac4ecdc510cf4204 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 31 Jan 2023 21:06:52 -0600 Subject: [PATCH 05/12] Fixed point math fix --- contracts/Hyperdrive.sol | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 6e44f25dd..5baab96dd 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -117,11 +117,8 @@ contract Hyperdrive is ERC20 { // Calculate the pool and user deltas using the trading function. uint256 shareAmount = _baseAmount.divDown(sharePrice); - ( - , - uint256 poolBondDelta, - uint256 bondProceeds - ) = HyperdriveMath.calculateOutGivenIn( + (, uint256 poolBondDelta, uint256 bondProceeds) = HyperdriveMath + .calculateOutGivenIn( shareReserves, bondReserves, totalSupply(), @@ -220,11 +217,8 @@ contract Hyperdrive is ERC20 { } // Calculate the pool and user deltas using the trading function. - ( - uint256 poolShareDelta, - , - uint256 shareProceeds - ) = HyperdriveMath.calculateOutGivenIn( + (uint256 poolShareDelta, , uint256 shareProceeds) = HyperdriveMath + .calculateOutGivenIn( shareReserves, bondReserves, totalSupply(), @@ -234,10 +228,15 @@ contract Hyperdrive is ERC20 { sharePrice, initialSharePrice, false - ); + ); // Take custody of the maximum amount the trader can lose on the short. - bool success = baseToken.transferFrom(msg.sender, address(this), _bondAmount - shareProceeds); + uint256 baseProceeds = shareProceeds.mulDown(sharePrice); + bool success = baseToken.transferFrom( + msg.sender, + address(this), + _bondAmount - baseProceeds + ); if (!success) { revert ElementError.TransferFailed(); } From 9c4a68af6bb6acf677368e9fa440cf90a24fd2c8 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Tue, 31 Jan 2023 21:25:21 -0600 Subject: [PATCH 06/12] Started work on the close short function --- contracts/Hyperdrive.sol | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 588994b28..16cfd4d76 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -162,14 +162,14 @@ contract Hyperdrive is ERC20 { } /// @notice Closes a long position with a specified maturity time. - /// @param _maturityTime The maturity time of the bonds to close. - /// @param _bondAmount The amount of bonds to close. + /// @param _maturityTime The maturity time of the longs to close. + /// @param _bondAmount The amount of longs to close. function closeLong(uint256 _maturityTime, uint256 _bondAmount) external { if (_bondAmount == 0) { revert ElementError.ZeroAmount(); } - // Burn the bonds that are being closed. + // Burn the longs that are being closed. longToken.burn(msg.sender, _maturityTime, _bondAmount); // Calculate the pool and user deltas using the trading function. @@ -268,4 +268,38 @@ contract Hyperdrive is ERC20 { new bytes(0) ); } + + // TODO: Make sure that the correct amount of variable interest is given to + // the shorter. + // + /// @notice Closes a short position with a specified maturity time. + /// @param _maturityTime The maturity time of the shorts to close. + /// @param _bondAmount The amount of shorts to close. + function closeShort(uint256 _maturityTime, uint256 _bondAmount) external { + if (_bondAmount == 0) { + revert ElementError.ZeroAmount(); + } + + // Burn the shorts that are being closed. + shortToken.burn(msg.sender, _maturityTime, _bondAmount); + + // Calculate the pool and user deltas using the trading function. + uint256 timeRemaining = block.timestamp < _maturityTime + ? (_maturityTime - block.timestamp) * FixedPointMath.ONE_18 + : 0; + (uint256 poolShareDelta, uint256 poolBondDelta, uint256 shareObligation) = HyperdriveMath + .calculateInGivenOut( + shareReserves, + bondReserves, + totalSupply(), + _bondAmount, + timeRemaining, + timeStretch, + sharePrice, + initialSharePrice, + false + ); + + // FIXME: Finish the shorting function. + } } From fcad4cc2f387dab4a36d70adf917b4277c619e86 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 1 Feb 2023 17:06:53 -0600 Subject: [PATCH 07/12] Implemented a method for closing shorts --- contracts/Hyperdrive.sol | 62 +++++++++++++++++------ contracts/interfaces/IERC1155Mintable.sol | 4 +- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 16cfd4d76..a7cd4b699 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -8,6 +8,8 @@ import { FixedPointMath } from "contracts/libraries/FixedPointMath.sol"; import { HyperdriveMath } from "contracts/libraries/HyperdriveMath.sol"; import { IERC1155Mintable } from "contracts/interfaces/IERC1155Mintable.sol"; +/// @notice A fixed-rate AMM that mints bonds on demand for longs and shorts. +/// @author Element Finance contract Hyperdrive is ERC20 { using FixedPointMath for uint256; @@ -24,7 +26,7 @@ contract Hyperdrive is ERC20 { /// Market state /// - // TODO: These should both be uint128 and share a slot. + // TODO: Can we make these uint128? uint256 public shareReserves; uint256 public bondReserves; @@ -57,7 +59,7 @@ contract Hyperdrive is ERC20 { timeStretch = _timeStretch; // TODO: This isn't correct. This will need to be updated when asset - // delegation is implemented. + // delgation is implemented. initialSharePrice = FixedPointMath.ONE_18; sharePrice = FixedPointMath.ONE_18; } @@ -260,35 +262,43 @@ contract Hyperdrive is ERC20 { revert ElementError.BaseBufferExceedsShareReserves(); } - // Mint the short tokens to the trader. + // Mint the short tokens to the trader. The ID is a concatenation of the + // current share price and the maturity time of the shorts. shortToken.mint( msg.sender, - block.timestamp + termLength, + (sharePrice << 32) | (block.timestamp + termLength), _bondAmount, new bytes(0) ); } - // TODO: Make sure that the correct amount of variable interest is given to - // the shorter. - // /// @notice Closes a short position with a specified maturity time. - /// @param _maturityTime The maturity time of the shorts to close. + /// @param _key The key of the shorts to close. The short key is a + /// concatenation of the share price when the short was opened and + /// the maturity time of the short. /// @param _bondAmount The amount of shorts to close. - function closeShort(uint256 _maturityTime, uint256 _bondAmount) external { + function closeShort(uint256 _key, uint256 _bondAmount) external { if (_bondAmount == 0) { revert ElementError.ZeroAmount(); } // Burn the shorts that are being closed. - shortToken.burn(msg.sender, _maturityTime, _bondAmount); + shortToken.burn(msg.sender, _key, _bondAmount); + + // Deserialize the key into the share price at the time the short was + // opened and the maturity time of the short. + uint256 openSharePrice = _key >> 32; + uint256 maturityTime = _key & ((1 << 32) - 1); // Calculate the pool and user deltas using the trading function. - uint256 timeRemaining = block.timestamp < _maturityTime - ? (_maturityTime - block.timestamp) * FixedPointMath.ONE_18 + uint256 timeRemaining = block.timestamp < maturityTime + ? (maturityTime - block.timestamp) * FixedPointMath.ONE_18 : 0; - (uint256 poolShareDelta, uint256 poolBondDelta, uint256 shareObligation) = HyperdriveMath - .calculateInGivenOut( + ( + uint256 poolShareDelta, + uint256 poolBondDelta, + uint256 sharePayment + ) = HyperdriveMath.calculateInGivenOut( shareReserves, bondReserves, totalSupply(), @@ -300,6 +310,28 @@ contract Hyperdrive is ERC20 { false ); - // FIXME: Finish the shorting function. + // Apply the trading deltas to the reserves. Since the share reserves + // are increased and the base buffer doesn't change, we don't need to + // check that the base reserves are greater than the base buffer. + shareReserves += poolShareDelta; + bondReserves -= poolBondDelta; + + // Transfer the profit to the shorter. This includes the proceeds from + // the short sale as well as the variable interest that was collected + // on the face value of the bonds. + uint256 saleProceeds = _bondAmount.sub( + sharePrice.mulDown(sharePayment) + ); + uint256 interestProceeds = sharePrice + .divDown(openSharePrice) + .sub(FixedPointMath.ONE_18) + .mulDown(_bondAmount); + bool success = baseToken.transfer( + msg.sender, + saleProceeds.add(interestProceeds) + ); + if (!success) { + revert ElementError.TransferFailed(); + } } } diff --git a/contracts/interfaces/IERC1155Mintable.sol b/contracts/interfaces/IERC1155Mintable.sol index f6429bb7a..5944e9163 100644 --- a/contracts/interfaces/IERC1155Mintable.sol +++ b/contracts/interfaces/IERC1155Mintable.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.13; -interface IERC1155Mintable { +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +interface IERC1155Mintable is IERC1155 { /// @notice Allows the admin to mint tokens to a specified address. /// @param _target The target of the tokens. /// @param _id The ID of the token to mint. From f9e1f2c57268917c8cdd03723ce25f609de3d42d Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Wed, 1 Feb 2023 17:18:09 -0600 Subject: [PATCH 08/12] Removed the bond buffer and general polishing --- contracts/Hyperdrive.sol | 51 +++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index a7cd4b699..f158a76cf 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -15,38 +15,61 @@ contract Hyperdrive is ERC20 { /// Tokens /// + // @dev The base asset. IERC20 public immutable baseToken; + + // @dev A mintable ERC1155 token that is used to record long balances. + // Hyperdrive must be able to mint short tokens for trading to occur. IERC1155Mintable public immutable longToken; + + // @dev A mintable ERC1155 token that is used to record short balances. + // Hyperdrive must be able to mint short tokens for trading to occur. IERC1155Mintable public immutable shortToken; /// Time /// - uint256 public immutable termLength; + // @dev The amount of seconds that elapse before a bond can be redeemed. + uint256 public immutable positionDuration; + + // @dev A parameter that decreases slippage around a target rate. uint256 public immutable timeStretch; /// Market state /// - // TODO: Can we make these uint128? + // @dev The share price at the time the pool was created. + uint256 public immutable initialSharePrice; + + // @dev The current share price. + uint256 public sharePrice; + + // @dev The share reserves. The share reserves multiplied by the share price + // give the base reserves, so shares are a mechanism of ensuring that + // interest is properly awarded over time. uint256 public shareReserves; + + // @dev The bond reserves. In Hyperdrive, the bond reserves aren't backed by + // pre-minted bonds and are instead used as a virtual value that + // ensures that the spot rate changes according to the laws of supply + // and demand. uint256 public bondReserves; + // @dev The base buffer stores the amount of outstanding obligations to bond + // holders. This is required to maintain solvency since the bond + // reserves are virtual and bonds are minted on demand. uint256 public baseBuffer; - uint256 public bondBuffer; - - uint256 public sharePrice; - uint256 public immutable initialSharePrice; /// @notice Initializes a Hyperdrive pool. /// @param _baseToken The base token contract. /// @param _longToken The long token contract. /// @param _shortToken The short token contract. - /// @param _termLength The length of the terms supported by this Hyperdrive in seconds. + /// @param _positionDuration The time in seconds that elaspes before bonds + /// can be redeemed one-to-one for base. /// @param _timeStretch The time stretch of the pool. constructor( IERC20 _baseToken, IERC1155Mintable _longToken, IERC1155Mintable _shortToken, - uint256 _termLength, + uint256 _positionDuration, uint256 _timeStretch ) ERC20("Hyperdrive LP", "hLP") { // Initialize the token addresses. @@ -55,7 +78,7 @@ contract Hyperdrive is ERC20 { shortToken = _shortToken; // Initialize the time configurations. - termLength = _termLength; + positionDuration = _positionDuration; timeStretch = _timeStretch; // TODO: This isn't correct. This will need to be updated when asset @@ -90,7 +113,7 @@ contract Hyperdrive is ERC20 { initialSharePrice, sharePrice, _apr, - termLength, + positionDuration, timeStretch ); @@ -150,14 +173,11 @@ contract Hyperdrive is ERC20 { if (sharePrice * shareReserves >= baseBuffer) { revert ElementError.BaseBufferExceedsShareReserves(); } - if (bondReserves >= bondBuffer) { - revert ElementError.BondBufferExceedsBondReserves(); - } // Mint the bonds to the trader with an ID of the maturity time. longToken.mint( msg.sender, - block.timestamp + termLength, + block.timestamp + positionDuration, bondProceeds, new bytes(0) ); @@ -251,7 +271,6 @@ contract Hyperdrive is ERC20 { // by the amount of bonds that were shorted. shareReserves -= poolShareDelta; bondReserves += _bondAmount; - bondBuffer += _bondAmount; // The bond buffer is increased by the same amount as the bond buffer, // so there is no need to check that the bond reserves is greater than @@ -266,7 +285,7 @@ contract Hyperdrive is ERC20 { // current share price and the maturity time of the shorts. shortToken.mint( msg.sender, - (sharePrice << 32) | (block.timestamp + termLength), + (sharePrice << 32) | (block.timestamp + positionDuration), _bondAmount, new bytes(0) ); From 26202805896b5d3e8c011f6b6147c7282d40c157 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 2 Feb 2023 09:57:03 -0600 Subject: [PATCH 09/12] Addressed some review feedback from @jrhea --- contracts/Hyperdrive.sol | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index f158a76cf..591c3fae4 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -9,7 +9,7 @@ import { HyperdriveMath } from "contracts/libraries/HyperdriveMath.sol"; import { IERC1155Mintable } from "contracts/interfaces/IERC1155Mintable.sol"; /// @notice A fixed-rate AMM that mints bonds on demand for longs and shorts. -/// @author Element Finance +/// @author Delve Labs contract Hyperdrive is ERC20 { using FixedPointMath for uint256; @@ -272,11 +272,8 @@ contract Hyperdrive is ERC20 { shareReserves -= poolShareDelta; bondReserves += _bondAmount; - // The bond buffer is increased by the same amount as the bond buffer, - // so there is no need to check that the bond reserves is greater than - // or equal to the bond buffer. Since the share reserves are reduced, - // we need to verify that the base reserves are greater than or equal - // to the base buffer. + // Since the share reserves are reduced, we need to verify that the base + // reserves are greater than or equal to the base buffer. if (sharePrice * shareReserves >= baseBuffer) { revert ElementError.BaseBufferExceedsShareReserves(); } @@ -329,22 +326,32 @@ contract Hyperdrive is ERC20 { false ); - // Apply the trading deltas to the reserves. Since the share reserves - // are increased and the base buffer doesn't change, we don't need to - // check that the base reserves are greater than the base buffer. - shareReserves += poolShareDelta; - bondReserves -= poolBondDelta; - - // Transfer the profit to the shorter. This includes the proceeds from - // the short sale as well as the variable interest that was collected - // on the face value of the bonds. - uint256 saleProceeds = _bondAmount.sub( + // Calculate the proceeds of closing the shorts. This includes the + // trading proceeds as well as the variable interest that accrued on + // the face value of the bonds shorted while the short was open. + uint256 tradingProceeds = _bondAmount.sub( sharePrice.mulDown(sharePayment) ); uint256 interestProceeds = sharePrice .divDown(openSharePrice) .sub(FixedPointMath.ONE_18) .mulDown(_bondAmount); + + // TODO: Add a comment about the interest that was generated. + // Apply the trading deltas to the reserves. + shareReserves += poolShareDelta; + shareReserves -= interestProceeds.divDown(sharePrice); + bondReserves -= poolBondDelta; + + // Since the share reserves are reduced, we need to verify that the base + // reserves are greater than or equal to the base buffer. + if (sharePrice * shareReserves >= baseBuffer) { + revert ElementError.BaseBufferExceedsShareReserves(); + } + + // Transfer the profit to the shorter. This includes the proceeds from + // the short sale as well as the variable interest that was collected + // on the face value of the bonds. bool success = baseToken.transfer( msg.sender, saleProceeds.add(interestProceeds) From 084533a030af83e37eafbb34cf54c468dd5abcfc Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 2 Feb 2023 12:58:32 -0600 Subject: [PATCH 10/12] Addressed review feedback from @jrhea --- contracts/Hyperdrive.sol | 7 ++- contracts/libraries/HyperdriveMath.sol | 83 +++++++++----------------- 2 files changed, 32 insertions(+), 58 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 591c3fae4..65aa44fb5 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -9,7 +9,7 @@ import { HyperdriveMath } from "contracts/libraries/HyperdriveMath.sol"; import { IERC1155Mintable } from "contracts/interfaces/IERC1155Mintable.sol"; /// @notice A fixed-rate AMM that mints bonds on demand for longs and shorts. -/// @author Delve Labs +/// @author Delve contract Hyperdrive is ERC20 { using FixedPointMath for uint256; @@ -87,6 +87,8 @@ contract Hyperdrive is ERC20 { sharePrice = FixedPointMath.ONE_18; } + /// LP /// + /// @notice Allows the first LP to initialize the market with a target APR. /// @param _contribution The amount of base asset to contribute. /// @param _apr The target APR. @@ -322,8 +324,7 @@ contract Hyperdrive is ERC20 { timeRemaining, timeStretch, sharePrice, - initialSharePrice, - false + initialSharePrice ); // Calculate the proceeds of closing the shorts. This includes the diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index c049cebb2..4034f5093 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -131,8 +131,8 @@ library HyperdriveMath { } } - /// @dev Calculates the amount of an asset that will be received given a - /// specified amount of the other asset given the current AMM reserves. + /// @dev Calculates the amount of base that must be provided to receive a + /// specified amount of bonds. /// @param shareReserves The share reserves of the AMM. /// @param bondReserves The bonds reserves of the AMM. /// @param bondReserveAdjustment The bond reserves are adjusted to improve @@ -144,8 +144,6 @@ library HyperdriveMath { /// @param timeStretch The time stretch parameter. /// @param sharePrice The share price. /// @param initialSharePrice The initial share price. - /// @param isBondIn A flag that specifies whether bonds are the asset being - /// provided or the asset being received. /// @return poolShareDelta The delta that should be applied to the pool's /// share reserves. /// @return poolBondDelta The delta that should be applied to the pool's @@ -159,8 +157,7 @@ library HyperdriveMath { uint256 timeRemaining, uint256 timeStretch, uint256 sharePrice, - uint256 initialSharePrice, - bool isBondIn + uint256 initialSharePrice ) internal pure @@ -170,55 +167,31 @@ library HyperdriveMath { uint256 userDelta ) { - // TODO: See if this is actually true. - // - // This pricing model only supports the selling of bonds when + // Since we are buying bonds, it's possible that timeRemaining < 1. + // We consider (1-timeRemaining)*amountOut of the bonds being + // purchased to be fully matured and timeRemaining*amountOut of the + // bonds to be newly minted. The fully matured bonds are redeemed + // one-to-one to base (our result is given in shares, so we divide + // the one-to-one redemption by the share price) and the newly + // minted bonds are traded on a YieldSpace curve configured to // timeRemaining = 1. - if (isBondIn && timeRemaining < 1) { - revert ElementError.HyperdriveMath_BaseWithNonzeroTime(); - } - if (isBondIn) { - // If bonds are being sold, then the entire trade occurs on the - // curved portion since timeRemaining = 1. - uint256 amountIn = YieldSpaceMath.calculateInGivenOut( - shareReserves, - bondReserves, - bondReserveAdjustment, - amountOut, - FixedPointMath.ONE_18, - timeStretch, - sharePrice, - initialSharePrice, - isBondIn - ); - return (amountOut, amountIn, amountIn); - } else { - // Since we are buying bonds, it's possible that timeRemaining < 1. - // We consider (1-timeRemaining)*amountOut of the bonds being - // purchased to be fully matured and timeRemaining*amountOut of the - // bonds to be newly minted. The fully matured bonds are redeemed - // one-to-one to base (our result is given in shares, so we divide - // the one-to-one redemption by the share price) and the newly - // minted bonds are traded on a YieldSpace curve configured to - // timeRemaining = 1. - uint256 flat = amountOut - .mulDown(FixedPointMath.ONE_18.sub(timeRemaining)) - .divDown(sharePrice); - uint256 curveOut = amountOut.mulDown(timeRemaining); - uint256 curveIn = YieldSpaceMath.calculateInGivenOut( - // Credit the share reserves by the flat trade. - shareReserves.add(flat.divDown(sharePrice)), - // Debit the bond reserves by the flat trade. - bondReserves.sub(flat), - bondReserveAdjustment, - curveOut, - FixedPointMath.ONE_18, - timeStretch, - sharePrice, - initialSharePrice, - isBondIn - ); - return (flat.add(curveIn), curveIn, flat.add(curveIn)); - } + uint256 flat = amountOut + .mulDown(FixedPointMath.ONE_18.sub(timeRemaining)) + .divDown(sharePrice); + uint256 curveOut = amountOut.mulDown(timeRemaining); + uint256 curveIn = YieldSpaceMath.calculateInGivenOut( + // Credit the share reserves by the flat trade. + shareReserves.add(flat.divDown(sharePrice)), + // Debit the bond reserves by the flat trade. + bondReserves.sub(flat), + bondReserveAdjustment, + curveOut, + FixedPointMath.ONE_18, + timeStretch, + sharePrice, + initialSharePrice, + isBondIn + ); + return (flat.add(curveIn), curveIn, flat.add(curveIn)); } } From ca49d5c4081f6df8f773567c598f7b29e2593c00 Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 2 Feb 2023 13:01:56 -0600 Subject: [PATCH 11/12] Addressed remaining issues --- contracts/Hyperdrive.sol | 30 +++++++++----------------- contracts/libraries/HyperdriveMath.sol | 2 +- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 65aa44fb5..c1224963c 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -327,9 +327,15 @@ contract Hyperdrive is ERC20 { initialSharePrice ); - // Calculate the proceeds of closing the shorts. This includes the - // trading proceeds as well as the variable interest that accrued on - // the face value of the bonds shorted while the short was open. + // Apply the trading deltas to the reserves. Since the share reserves + // increase or stay the same, there is no need to check that the share + // reserves are greater than or equal to the base buffer. + shareReserves += poolShareDelta; + bondReserves -= poolBondDelta; + + // Transfer the profit to the shorter. This includes the proceeds from + // the short sale as well as the variable interest that was collected + // on the face value of the bonds. uint256 tradingProceeds = _bondAmount.sub( sharePrice.mulDown(sharePayment) ); @@ -337,25 +343,9 @@ contract Hyperdrive is ERC20 { .divDown(openSharePrice) .sub(FixedPointMath.ONE_18) .mulDown(_bondAmount); - - // TODO: Add a comment about the interest that was generated. - // Apply the trading deltas to the reserves. - shareReserves += poolShareDelta; - shareReserves -= interestProceeds.divDown(sharePrice); - bondReserves -= poolBondDelta; - - // Since the share reserves are reduced, we need to verify that the base - // reserves are greater than or equal to the base buffer. - if (sharePrice * shareReserves >= baseBuffer) { - revert ElementError.BaseBufferExceedsShareReserves(); - } - - // Transfer the profit to the shorter. This includes the proceeds from - // the short sale as well as the variable interest that was collected - // on the face value of the bonds. bool success = baseToken.transfer( msg.sender, - saleProceeds.add(interestProceeds) + tradingProceeds.add(interestProceeds) ); if (!success) { revert ElementError.TransferFailed(); diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index 4034f5093..4af2369bf 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -190,7 +190,7 @@ library HyperdriveMath { timeStretch, sharePrice, initialSharePrice, - isBondIn + false ); return (flat.add(curveIn), curveIn, flat.add(curveIn)); } From 1b937dd9f7f5f135ba82fba590824e98e696fa9c Mon Sep 17 00:00:00 2001 From: Alex Towle Date: Thu, 2 Feb 2023 13:11:25 -0600 Subject: [PATCH 12/12] Finishing touches --- contracts/Hyperdrive.sol | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index c1224963c..dfa21a533 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -284,7 +284,7 @@ contract Hyperdrive is ERC20 { // current share price and the maturity time of the shorts. shortToken.mint( msg.sender, - (sharePrice << 32) | (block.timestamp + positionDuration), + encodeShortKey(sharePrice, block.timestamp + positionDuration), _bondAmount, new bytes(0) ); @@ -303,10 +303,8 @@ contract Hyperdrive is ERC20 { // Burn the shorts that are being closed. shortToken.burn(msg.sender, _key, _bondAmount); - // Deserialize the key into the share price at the time the short was - // opened and the maturity time of the short. - uint256 openSharePrice = _key >> 32; - uint256 maturityTime = _key & ((1 << 32) - 1); + // Get the open share price and maturity time from the short key. + (uint256 openSharePrice, uint256 maturityTime) = decodeShortKey(_key); // Calculate the pool and user deltas using the trading function. uint256 timeRemaining = block.timestamp < maturityTime @@ -351,4 +349,30 @@ contract Hyperdrive is ERC20 { revert ElementError.TransferFailed(); } } + + /// Utilities /// + + /// @notice Serializes a share price and a maturity time into a short key. + /// @param _openSharePrice The share price when the short was opened. + /// @param _maturityTime The maturity time of the bond that was shorted. + /// @return key The serialized short key. + function encodeShortKey( + uint256 _openSharePrice, + uint256 _maturityTime + ) public pure returns (uint256 key) { + return (_openSharePrice << 32) | _maturityTime; + } + + /// @notice Deserializes a short key into the opening share price and + /// maturity time. + /// @param _key The serialized short key. + /// @return openSharePrice The share price when the short was opened. + /// @return maturityTime The maturity time of the bond that was shorted. + function decodeShortKey( + uint256 _key + ) public pure returns (uint256 openSharePrice, uint256 maturityTime) { + openSharePrice = _key >> 32; // most significant 224 bits + maturityTime = _key & 0xffffffff; // least significant 32 bits + return (openSharePrice, maturityTime); + } }