delvtech / hyperdrive

An automated market maker for fixed and variable yield with on-demand terms.
Apache License 2.0
33 stars 4 forks source link

Inconsistency between checks on the effective share reserves and `calculateMaxSell` #677

Closed jalextowle closed 10 months ago

jalextowle commented 1 year ago

Overview

It's currently possible for the effective share reserves to drop below $z_{min}$. Here's a test based on 6a4edcf4 that successfully drops the effective share reserves below the minimum share reserves:

// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.19;

import { console2 as console } from "forge-std/console2.sol";
import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol";
import { HyperdriveTest } from "test/utils/HyperdriveTest.sol";
import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol";
import { Lib } from "test/utils/Lib.sol";

contract ExampleTest is HyperdriveTest {
    using HyperdriveUtils for *;
    using Lib for *;

    function test_example() external {
        initialize(alice, 0.02e18, 100e18);

        // Log the minimum share reserves.
        console.log(
            "Minimum share reserves: %s",
            hyperdrive.getPoolConfig().minimumShareReserves.toString(18)
        );

        // Bob opens a max short.
        openShort(bob, hyperdrive.calculateMaxShort());

        // The term passes.
        advanceTime(POSITION_DURATION, 0);

        // Log the effective share reserves.
        console.log(
            "Effective share reserves: %s",
            HyperdriveMath.calculateEffectiveShareReserves(
                hyperdrive.getPoolInfo().shareReserves,
                hyperdrive.getPoolInfo().shareAdjustment
            ).toString(18)
        );

        // Alice opens a small short.
        openShort(alice, 1e18);

        // Log the effective share reserves.
        console.log(
            "Effective share reserves: %s",
            HyperdriveMath.calculateEffectiveShareReserves(
                hyperdrive.getPoolInfo().shareReserves,
                hyperdrive.getPoolInfo().shareAdjustment
            ).toString(18)
        );
    }
}

This produces the following logs:

  Minimum share reserves: 1.000000000000000000
  Effective share reserves: 1.000000000000000108
  Effective share reserves: 0.238507056401543382

Allowing $z - \zeta$ to fall below $z_{min}$ is what we want in cases when $\zeta > 0$ because it ensures that LP's can remove all of the pool's liquidity above the minimum share reserves. On the other hand, this logic conflicts with the implementation of calculateMaxSell in YieldSpaceMath, and it results in the trading curve looking different when $\zeta > 0$ versus when $\zeta \leq 0$ because some of the curve is accessible.

jalextowle commented 1 year ago

After some golfing, I was able to get the effective share reserves down to 2 wei. This is the test:

// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.19;

import { console2 as console } from "forge-std/console2.sol";
import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol";
import { HyperdriveTest } from "test/utils/HyperdriveTest.sol";
import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol";
import { Lib } from "test/utils/Lib.sol";

contract ExampleTest is HyperdriveTest {
    using HyperdriveUtils for *;
    using Lib for *;

    function test_example() external {
        // Alice initializes the pool.
        initialize(alice, 0.02e18, 10e18);

        // Bob adds liquidity.
        uint256 lpShares = addLiquidity(bob, 1_000_000_000e18);

        // Log the minimum share reserves.
        console.log(
            "Minimum share reserves: %s",
            hyperdrive.getPoolConfig().minimumShareReserves.toString(18)
        );

        // Bob opens a max short.
        openShort(bob, hyperdrive.calculateMaxShort());

        // The term passes.
        advanceTime(POSITION_DURATION, 0);

        // Bob opens a max short.
        openShort(bob, 2.746469942e18);

        // Log the effective share reserves.
        console.log(
            "Effective share reserves: %s",
            HyperdriveMath.calculateEffectiveShareReserves(
                hyperdrive.getPoolInfo().shareReserves,
                hyperdrive.getPoolInfo().shareAdjustment
            ).toString(18)
        );

        // Bob removes his liquidity.
        removeLiquidity(bob, lpShares);

        // Log the effective share reserves.
        console.log(
            "Effective share reserves: %s",
            HyperdriveMath.calculateEffectiveShareReserves(
                hyperdrive.getPoolInfo().shareReserves,
                hyperdrive.getPoolInfo().shareAdjustment
            ).toString(18)
        );
    }
}

and it produces the following output:

  Minimum share reserves: 1.000000000000000000
  Effective share reserves: 0.000000000118733682
  Effective share reserves: 0.000000000000000002

Now that I know the value can get quite close to 0 with only a few trades, it makes me wonder if this is really okay.

jalextowle commented 1 year ago

After playing around with the more severe example, I can't open a long or add liquidity. All of the other operations decrease the effective share reserves, so the pool is locked up.

jalextowle commented 10 months ago

Revisiting this we have the following tension:

  1. Allowing traders to decrease the effective share reserves to a small value is dangerous because it can lock up the pool.
  2. Enforcing the invariant that $z - \zeta \geq z_{min}$ can lead to LPs not being able to withdraw all of their capital and seriously complicates the logic in LPMath.sol.

With this in mind, I'd like to come up with a simple "middle path" that prevents pathological behavior without enforcing the $z - \zeta \geq z_{min}$ check in the LP flow. Before I can do this, I need to play around with the system in its new state to see how bad the situation is when the effective share reserves are small.

jalextowle commented 10 months ago

This was addressed by #726.