Skip to content

Checkpoint liveness issue with small net curve trade  #762

@jalextowle

Description

@jalextowle

There was an intermittent failure while running the benchmark: https://github.com/delvtech/hyperdrive/actions/runs/7978957722/job/21785238086. After looking into it, it became apparent that small net curve trades can actually result in liveness issues.

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

// FIXME
import { console2 as console } from "forge-std/console2.sol";

import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol";
import { FixedPointMath, ONE } from "contracts/src/libraries/FixedPointMath.sol";
import { HyperdriveMath } from "contracts/src/libraries/HyperdriveMath.sol";
import { YieldSpaceMath } from "contracts/src/libraries/HyperdriveMath.sol";
import { ERC20ForwarderFactory } from "contracts/src/token/ERC20ForwarderFactory.sol";
import { IMockHyperdrive } from "contracts/test/MockHyperdrive.sol";
import { MockHyperdriveMath } from "contracts/test/MockHyperdriveMath.sol";
import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol";
import { HyperdriveTest } from "test/utils/HyperdriveTest.sol";
import { Lib } from "test/utils/Lib.sol";

contract ExampleTest is HyperdriveTest {
    using FixedPointMath for uint256;
    using HyperdriveUtils for IHyperdrive;
    using Lib for *;

    function test_checkpoint_liveness(
        uint256 fixedRate,
        uint256 contribution,
        uint256 initialLongAmount,
        uint256 initialShortAmount,
        uint256 finalLongAmount
    ) external {
        // FIXME
        fixedRate = 3988;
        contribution = 370950184595018764582435593;
        initialLongAmount = 10660;
        initialShortAmount = 999000409571;
        finalLongAmount = 1000000000012659;

        _test__calculateMaxLong(
            fixedRate,
            contribution,
            initialLongAmount,
            initialShortAmount,
            finalLongAmount
        );
    }

    function _test__calculateMaxLong(
        uint256 fixedRate,
        uint256 contribution,
        uint256 initialLongAmount,
        uint256 initialShortAmount,
        uint256 finalLongAmount
    ) internal {
        // Deploy Hyperdrive.
        fixedRate = fixedRate.normalizeToRange(0.001e18, 0.5e18);
        deploy(alice, fixedRate, 0, 0, 0, 0);

        // Initialize the Hyperdrive pool.
        contribution = contribution.normalizeToRange(1_000e18, 500_000_000e18);
        initialize(alice, fixedRate, contribution);

        // Ensure that the max long is actually the max long.
        _verifyMaxLong(
            fixedRate,
            initialLongAmount,
            initialShortAmount,
            finalLongAmount
        );
    }

    function _verifyMaxLong(
        uint256 fixedRate,
        uint256 initialLongAmount,
        uint256 initialShortAmount,
        uint256 finalLongAmount
    ) internal {
        // Open a long and a short. This sets the long buffer to a non-trivial
        // value which stress tests the max long function.
        initialLongAmount = initialLongAmount.normalizeToRange(
            MINIMUM_TRANSACTION_AMOUNT,
            hyperdrive.calculateMaxLong() / 2
        );
        openLong(bob, initialLongAmount);
        initialShortAmount = initialShortAmount.normalizeToRange(
            MINIMUM_TRANSACTION_AMOUNT,
            hyperdrive.calculateMaxShort() / 2
        );
        openShort(bob, initialShortAmount);

        advanceTime(CHECKPOINT_DURATION, int256(0));
        console.log("should get here");
        hyperdrive.checkpoint(hyperdrive.latestCheckpoint());
        console.log("shouldn't get here");

        // TODO: The fact that we need such a large amount of iterations could
        // indicate a bug in the max long function.
        //
        // Open the maximum long on Hyperdrive.
        IHyperdrive.PoolConfig memory config = hyperdrive.getPoolConfig();
        IHyperdrive.PoolInfo memory info = hyperdrive.getPoolInfo();
        uint256 maxIterations = 10;
        if (fixedRate > 0.15e18) {
            maxIterations += 5;
        }
        if (fixedRate > 0.35e18) {
            maxIterations += 5;
        }
        (uint256 maxLong, ) = HyperdriveUtils.calculateMaxLong(
            HyperdriveUtils.MaxTradeParams({
                shareReserves: info.shareReserves,
                shareAdjustment: info.shareAdjustment,
                bondReserves: info.bondReserves,
                longsOutstanding: info.longsOutstanding,
                longExposure: info.longExposure,
                timeStretch: config.timeStretch,
                vaultSharePrice: info.vaultSharePrice,
                initialVaultSharePrice: config.initialVaultSharePrice,
                minimumShareReserves: config.minimumShareReserves,
                curveFee: config.fees.curve,
                flatFee: config.fees.flat,
                governanceLPFee: config.fees.governanceLP
            }),
            hyperdrive.getCheckpointExposure(hyperdrive.latestCheckpoint()),
            maxIterations
        );
        (uint256 maturityTime, uint256 longAmount) = openLong(bob, maxLong);

        // TODO: Re-visit this after fixing `calculateMaxLong` to work with
        // matured positions.
        //
        // Ensure that opening another long fails. We fuzz in the range of
        // 10% to 1000x the max long.
        //
        // NOTE: The max spot price increases after we open the first long
        // because the spot price increases. In some cases, this could cause
        // a small trade to suceed after the large trade, so we use relatively
        // large amounts for the second trade.
        vm.stopPrank();
        vm.startPrank(bob);
        finalLongAmount = finalLongAmount.normalizeToRange(
            maxLong.mulDown(0.1e18).max(MINIMUM_TRANSACTION_AMOUNT),
            maxLong.mulDown(1000e18).max(
                MINIMUM_TRANSACTION_AMOUNT.mulDown(10e18)
            )
        );
        baseToken.mint(bob, finalLongAmount);
        baseToken.approve(address(hyperdrive), finalLongAmount);
        vm.expectRevert();
        hyperdrive.openLong(
            finalLongAmount,
            0,
            0,
            IHyperdrive.Options({
                destination: bob,
                asBase: true,
                extraData: new bytes(0)
            })
        );

        // Ensure that the long can be closed.
        closeLong(bob, maturityTime, longAmount);
    }
}

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions