Skip to content

Commit

Permalink
Zombie Interest (#685)
Browse files Browse the repository at this point in the history
add logic for collecting interest from zombie positions
  • Loading branch information
jrhea authored Dec 12, 2023
1 parent 844fc0e commit f3905d8
Show file tree
Hide file tree
Showing 14 changed files with 699 additions and 25 deletions.
1 change: 1 addition & 0 deletions contracts/src/external/HyperdriveTarget0.sol
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ abstract contract HyperdriveTarget0 is
IHyperdrive.PoolInfo memory poolInfo = IHyperdrive.PoolInfo({
shareReserves: _marketState.shareReserves,
shareAdjustment: _marketState.shareAdjustment,
zombieShareReserves: _marketState.zombieShareReserves,
bondReserves: _marketState.bondReserves,
sharePrice: sharePrice,
longsOutstanding: _marketState.longsOutstanding,
Expand Down
6 changes: 6 additions & 0 deletions contracts/src/interfaces/IHyperdrive.sol
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ interface IHyperdrive is IHyperdriveRead, IHyperdriveCore, IMultiToken {
/// @dev The net amount of shares that have been added and removed from
/// the share reserves due to flat updates.
int128 shareAdjustment;
/// @dev The amount shares that associated with positions that are matured,
/// but not yet redeemed.
uint128 zombieShareReserves;
/// @dev The global exposure of the pool due to open longs
uint128 longExposure;
/// @dev The amount of longs that are still open.
Expand Down Expand Up @@ -177,6 +180,9 @@ interface IHyperdrive is IHyperdriveRead, IHyperdriveCore, IMultiToken {
/// bonds. This is used to ensure that the pricing mechanism is
/// held invariant under flat updates for security reasons.
int256 shareAdjustment;
// @dev The amount shares that associated with positions that are matured,
/// but not yet redeemed.
uint256 zombieShareReserves;
/// @dev The reserves of bonds held by the pool.
uint256 bondReserves;
/// @dev The total supply of LP shares.
Expand Down
22 changes: 22 additions & 0 deletions contracts/src/internal/HyperdriveBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,28 @@ abstract contract HyperdriveBase is HyperdriveStorage {
}
}

/// @dev Collect the interest earned by closed positions
/// that haven't been redeemed.
/// @param _amount The amount in shares that earned the zombie interest.
/// @param _oldSharePrice The share price at the time of the last checkpoint.
/// @param _newSharePrice The current share price.
function _collectZombieInterest(
uint256 _amount,
uint256 _oldSharePrice,
uint256 _newSharePrice
) internal {
if (_newSharePrice > _oldSharePrice && _oldSharePrice > 0) {
// dz * (c1 - c0)/c1
uint256 zombieInterest = _amount.mulDivDown(
_newSharePrice - _oldSharePrice,
_newSharePrice
);
_marketState.zombieShareReserves -= zombieInterest.toUint128();
_marketState.shareReserves += zombieInterest.toUint128();
_marketState.shareAdjustment += int128(zombieInterest.toUint128());
}
}

/// @dev Calculates the number of share reserves that are not reserved by
/// open positions.
/// @param _sharePrice The current share price.
Expand Down
27 changes: 23 additions & 4 deletions contracts/src/internal/HyperdriveCheckpoint.sol
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ abstract contract HyperdriveCheckpoint is
// Create the share price checkpoint.
checkpoint_.sharePrice = _sharePrice.toUint128();

// Collect the interest that has accrued since the last checkpoint.
_collectZombieInterest(
_marketState.zombieShareReserves,
_checkpoints[_checkpointTime - _checkpointDuration].sharePrice,
_sharePrice
);

// Close out all of the short positions that matured at the beginning of
// this checkpoint. This ensures that shorts don't continue to collect
// free variable interest and that LP's can withdraw the proceeds of
Expand Down Expand Up @@ -118,6 +125,18 @@ abstract contract HyperdriveCheckpoint is
int256(shareProceeds), // keep the effective share reserves constant
_checkpointTime
);
uint256 shareReservesDelta = maturedShortsAmount.divDown(
_sharePrice
);
shareProceeds = HyperdriveMath.calculateShortProceeds(
maturedShortsAmount,
shareReservesDelta,
openSharePrice,
_sharePrice,
_sharePrice,
_flatFee
);
_marketState.zombieShareReserves += shareProceeds.toUint128();
positionsClosed = true;
}

Expand Down Expand Up @@ -147,6 +166,7 @@ abstract contract HyperdriveCheckpoint is
int256(shareProceeds), // keep the effective share reserves constant
checkpointTime
);
_marketState.zombieShareReserves += shareProceeds.toUint128();
positionsClosed = true;
}

Expand Down Expand Up @@ -189,15 +209,14 @@ abstract contract HyperdriveCheckpoint is
return _sharePrice;
}

/// @dev Calculates the proceeds of the long holders of a given position at
/// maturity. The long holders will be the LPs if the position is a
/// short.
/// @dev Calculates the proceeds of the holders of a given position at
/// maturity.
/// @param _bondAmount The bond amount of the position.
/// @param _sharePrice The current share price.
/// @param _openSharePrice The share price at the beginning of the
/// position's checkpoint.
/// @param _isLong A flag indicating whether or not the position is a long.
/// @return shareProceeds The proceeds of the long holders in shares.
/// @return shareProceeds The proceeds of the holders in shares.
/// @return governanceFee The fee paid to governance in shares.
function _calculateMaturedProceeds(
uint256 _bondAmount,
Expand Down
17 changes: 17 additions & 0 deletions contracts/src/internal/HyperdriveLong.sol
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,23 @@ abstract contract HyperdriveLong is HyperdriveLP {

// Distribute the excess idle to the withdrawal pool.
_distributeExcessIdle(sharePrice);
} else {
// The user is redeeming a long that has already matured. So we
// collect the interest that has accrued since the last checkpoint.
// NOTE: We only collect the interest on the position that is being closed.
uint256 checkpointTime = _latestCheckpoint();
_collectZombieInterest(
shareProceeds,
_checkpoints[checkpointTime].sharePrice,
sharePrice
);
uint256 zombieShareReserves = _marketState.zombieShareReserves;
if (shareProceeds < zombieShareReserves) {
zombieShareReserves -= shareProceeds;
} else {
zombieShareReserves = 0;
}
_marketState.zombieShareReserves = zombieShareReserves.toUint128();
}

// Withdraw the profit to the trader.
Expand Down
17 changes: 17 additions & 0 deletions contracts/src/internal/HyperdriveShort.sol
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,23 @@ abstract contract HyperdriveShort is HyperdriveLP {

// Distribute the excess idle to the withdrawal pool.
_distributeExcessIdle(sharePrice);
} else {
// The user is redeeming a short that has already matured. So we
// collect the interest that has accrued since the last checkpoint.
// NOTE: We only collect the interest on the position that is being closed.
uint256 checkpointTime = _latestCheckpoint();
_collectZombieInterest(
shareProceeds,
_checkpoints[checkpointTime].sharePrice,
sharePrice
);
uint256 zombieShareReserves = _marketState.zombieShareReserves;
if (shareProceeds < zombieShareReserves) {
zombieShareReserves -= shareProceeds;
} else {
zombieShareReserves = 0;
}
_marketState.zombieShareReserves = zombieShareReserves.toUint128();
}

// Withdraw the profit to the trader. This includes the proceeds from
Expand Down
10 changes: 4 additions & 6 deletions contracts/src/libraries/HyperdriveMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -659,14 +659,12 @@ library HyperdriveMath {
uint256 _flatFee
) internal pure returns (uint256 shareProceeds) {
// If the interest is more negative than the trading profits and margin
// released, than the short proceeds are marked to zero. Otherwise, we
// released, then the short proceeds are marked to zero. Otherwise, we
// calculate the proceeds as the sum of the trading proceeds, the
// interest proceeds, and the margin released.
uint256 bondFactor = _bondAmount.mulDivDown(
_closeSharePrice,
// We round up here do avoid overestimating the share proceeds.
_openSharePrice.mulUp(_sharePrice)
);
uint256 bondFactor = _bondAmount
.mulDivDown(_closeSharePrice, _openSharePrice)
.divDown(_sharePrice);

// We increase the bondFactor by the flat fee amount, because the trader
// has provided the flat fee as margin, and so it must be returned to
Expand Down
1 change: 1 addition & 0 deletions crates/hyperdrive-math/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ impl Distribution<State> for Standard {
let share_reserves = rng.gen_range(fixed!(1_000e18)..=fixed!(100_000_000e18));
let info = PoolInfo {
share_reserves: share_reserves.into(),
zombie_share_reserves: fixed!(0).into(),
bond_reserves: rng
.gen_range(
share_reserves * FixedPoint::from(config.initial_share_price)
Expand Down
12 changes: 7 additions & 5 deletions crates/hyperdrive-math/src/short/close.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ impl State {
share_price: FixedPoint,
flat_fee: FixedPoint,
) -> FixedPoint {
let mut bond_factor = bond_amount.mul_div_down(
close_share_price,
// We round up here do avoid overestimating the share proceeds.
open_share_price.mul_up(share_price),
);
let mut bond_factor = bond_amount
.mul_div_down(
close_share_price,
// We round up here do avoid overestimating the share proceeds.
open_share_price,
)
.div_down(share_price);
bond_factor += bond_amount.mul_div_down(flat_fee, share_price);

if bond_factor > share_amount {
Expand Down
2 changes: 2 additions & 0 deletions crates/test-utils/src/crash_reports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ mod tests {
"raw_pool_info": {
"shareReserves": 100000000000000000000000000,
"shareAdjustment": 0,
"zombieShareReserves": 0,
"bondReserves": 102178995195337961200000000,
"lpTotalSupply": 99999990000000000000000000,
"sharePrice": 1000000006341958396,
Expand Down Expand Up @@ -257,6 +258,7 @@ mod tests {
"pool_info": {
"shareReserves": "100000000.0",
"shareAdjustment": "0.0",
"zombieShareReserves": "0.0",
"bondReserves": "102178995.1953379612",
"lpTotalSupply": "99999990.0",
"sharePrice": "1.000000006341958396",
Expand Down
14 changes: 13 additions & 1 deletion test/integrations/hyperdrive/IntraCheckpointNettingTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@ contract IntraCheckpointNettingTest is HyperdriveTest {
// longExposure should be 0
IHyperdrive.PoolInfo memory poolInfo = hyperdrive.getPoolInfo();
assertApproxEqAbs(poolInfo.longExposure, 0, 1);

// idle should be equal to shareReserves
uint256 expectedShareReserves = MockHyperdrive(address(hyperdrive))
.calculateIdleShareReserves(hyperdrive.getPoolInfo().sharePrice) +
Expand Down Expand Up @@ -732,6 +733,7 @@ contract IntraCheckpointNettingTest is HyperdriveTest {

// fast forward time and accrue interest
advanceTime(POSITION_DURATION, variableInterest);
hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive));

// open positions
uint256[] memory longMaturityTimes = new uint256[](numTrades);
Expand Down Expand Up @@ -760,7 +762,7 @@ contract IntraCheckpointNettingTest is HyperdriveTest {
advanceTimeWithCheckpoints(timeElapsed, variableInterest);

// remove liquidity
removeLiquidity(alice, aliceLpShares);
(, uint256 withdrawalShares) = removeLiquidity(alice, aliceLpShares);

// Ensure all the positions have matured before trying to close them.
IHyperdrive.PoolInfo memory poolInfo = hyperdrive.getPoolInfo();
Expand All @@ -779,6 +781,7 @@ contract IntraCheckpointNettingTest is HyperdriveTest {
// close the long positions
closeLong(bob, longMaturityTimes[i], bondAmounts[i]);
}
redeemWithdrawalShares(alice, withdrawalShares);

// longExposure should be 0
poolInfo = hyperdrive.getPoolInfo();
Expand All @@ -803,11 +806,13 @@ contract IntraCheckpointNettingTest is HyperdriveTest {
{
uint256 apr = 0.05e18;
deploy(alice, apr, initialSharePrice, 0, 0, 0);
// JR TODO: we should add this as a parameter to fuzz to ensure that we are solvent with withdrawal shares
uint256 contribution = 500_000_000e18;
aliceLpShares = initialize(alice, apr, contribution);

// fast forward time and accrue interest
advanceTime(POSITION_DURATION, variableInterest);
hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive));
}

// open positions
Expand Down Expand Up @@ -848,6 +853,13 @@ contract IntraCheckpointNettingTest is HyperdriveTest {
// close the long positions
closeLong(bob, longMaturityTimes[i], bondAmounts[i]);
}
poolInfo = hyperdrive.getPoolInfo();

// TODO: Enable this. It fails for test_netting_extreme_negative_interest_time_elapsed
// (uint256 withdrawalProceeds, ) = redeemWithdrawalShares(
// alice,
// withdrawalShares
// );

// longExposure should be 0
poolInfo = hyperdrive.getPoolInfo();
Expand Down
Loading

0 comments on commit f3905d8

Please sign in to comment.