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

[CRASH REPORT] fuzz invariant -- close long after maturity result is not as expected #697

Closed dpaiton closed 10 months ago

dpaiton commented 11 months ago

Crash Report

https://app.rollbar.com/a/delv/fix/item/FirstProject/38

Description

Test: fuzz_long_short_maturity_values.py

Expected Behavior

The trades should have matured from advancing time and an expected amount should have been returned.

expected value: expected_base_amount_from_trade = open_trade_event.bond_amount - open_trade_event.bond_amount * flat_fee_percent

actual value: actual_base_amount = close_trade_event.base_amount

Actual Behavior

The expected and actual returned amounts do not match.

count       12.000000
mean      4111.166667
std       5659.114898
min          2.000000
25%        281.500000
50%       1724.000000
75%       5791.000000
max      17986.000000
Name: additional_info.invariance_check:base_amount_from_trade_difference_in_wei, dtype: float64

Steps to Reproduce

Run the fuzz test

Screenshots or Error Messages

image

Logs or Stack Traces

fuzz_long_short_maturity_values2023_12_12_18_49_15_Z.json

slundqui commented 10 months ago

Relevant crash report after v0.4.0 update: fuzz_long_short_maturity_values2024_01_10_20_11_05_Z.json

jrhea commented 10 months ago

I created this test to try to repro:

   function test_foo(
    ) external {

        uint256 fixedRate = 0.05e18;
        uint256 initialLiquidity = 100_000_000e18;

        uint256 zombieShareReserves1;
        uint256 shareReserves1;
        {
            // Initialize the pool with capital.
            deploy(bob, fixedRate, 1e18, 0, 0, 0, 0);
            initialize(bob, fixedRate, 2 * MINIMUM_SHARE_RESERVES);

            // Alice adds liquidity.
            addLiquidity(alice, initialLiquidity);

            // Limit the fuzz testing to variableRate's less than or equal to 200%.
            int256 variableRate = 0.05e18;

            // Ensure a feasible trade size.
            uint256 longTradeSize = 20_000e18;

            // Celine opens a long.
            (uint256 maturityTime, uint256 bonds) = openLong(celine, longTradeSize);

            // One term passes and longs mature.
            advanceTime(POSITION_DURATION, variableRate);
            hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive));

            closeLong(celine, maturityTime, bonds);
        }
    }

results are:

  baseProceeds 19999.999999999999999999
  sharePrice 1.051271096376024039
  bondAmount 20000.000000000000000000
  shareProceeds x sharePrice-flat fee 19999.999999999999999999

i would expect baseProceeds = bondAmount, but we are off by 1 wei.

jrhea commented 10 months ago

To reproduce the config above:

100 milly in liquidity, 5% variable, 5% fixed, zero fees 1) open a long with 20k base 2) exactly one term passes 3) close long

jalextowle commented 10 months ago

I wrote the following test to try to reproduce the issues with the ERC4626Hyperdrive deployment. This may be helpful for the continued investigation, so I figured I'd post it here:

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

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

import { ERC4626Hyperdrive } from "contracts/src/instances/erc4626/ERC4626Hyperdrive.sol";
import { ERC4626Target0 } from "contracts/src/instances/erc4626/ERC4626Target0.sol";
import { ERC4626Target1 } from "contracts/src/instances/erc4626/ERC4626Target1.sol";
import { ERC4626Target2 } from "contracts/src/instances/erc4626/ERC4626Target2.sol";
import { ERC4626Target3 } from "contracts/src/instances/erc4626/ERC4626Target3.sol";
import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol";
import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol";
import { FixedPointMath } from "contracts/src/libraries/FixedPointMath.sol";
import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol";
import { MockERC4626 } from "contracts/test/MockERC4626.sol";
import { HyperdriveTest } from "test/utils/HyperdriveTest.sol";
import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol";

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

    function setUp() public override {
        super.setUp();

        // Deploy the base token and vault.
        baseToken = new ERC20Mintable(
            "Base Token",
            "BASE",
            18,
            address(0),
            false
        );
        IERC4626 vault = IERC4626(address(new MockERC4626(
            baseToken,
            "Vault Token",
            "VAULT",
            2e18,
            address(0),
            false
        )));

        // Deploy the hyperdrive instance.
        IHyperdrive.PoolConfig memory config = testConfig(0.05e18, POSITION_DURATION);
        hyperdrive = IHyperdrive(address(new ERC4626Hyperdrive(
            config,
            address(new ERC4626Target0(config, vault)),
            address(new ERC4626Target1(config, vault)),
            address(new ERC4626Target2(config, vault)),
            address(new ERC4626Target3(config, vault)),
            vault
        )));
    }

    function test_example() external {
        // Initialize the Hyperdrive pool.
        initialize(alice, 0.05e18, 100_000_000e18);

        // Alice opens a long.
        console.log("test_example: 0");
        (uint256 maturityTime, uint256 longAmount) = openLong(alice, 100_000e18);
        console.log("test_example: 0.1");

        // A small amount of time passes.
        vm.warp(block.timestamp + POSITION_DURATION.mulDown(0.2e18));
        hyperdrive.checkpoint(hyperdrive.latestCheckpoint());
        console.log("test_example: 0.2");

        // Alice opens a large short.
        uint256 basePaid = hyperdrive.calculateMaxLong().mulDown(0.2e18);
        (, uint256 bondAmount) = openLong(alice, basePaid);
        openShort(alice, bondAmount);
        openLong(alice, basePaid);
        openShort(alice, bondAmount);
        openLong(alice, basePaid);
        openShort(alice, bondAmount);
        openLong(alice, basePaid);
        openShort(alice, bondAmount);

        openLong(alice, hyperdrive.calculateMaxLong().mulDown(0.55e18));
        openLong(alice, hyperdrive.calculateMaxShort().mulDown(0.1034e18));

        // The term passes and interest accrues.
        vm.warp(maturityTime);

        // Alice closes her long.
        uint256 baseProceeds = closeLong(alice, maturityTime, longAmount);
        console.log("bondAmount = %s", longAmount.toString(18));
        console.log("baseProceeds = %s", baseProceeds.toString(18));
        console.log("difference = %s", (longAmount - baseProceeds).toString(18));
    }
}
jrhea commented 10 months ago

Okay, we can recreate the issue in Solidity with this test:

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

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

import { ERC4626Hyperdrive } from "contracts/src/instances/erc4626/ERC4626Hyperdrive.sol";
import { ERC4626Target0 } from "contracts/src/instances/erc4626/ERC4626Target0.sol";
import { ERC4626Target1 } from "contracts/src/instances/erc4626/ERC4626Target1.sol";
import { ERC4626Target2 } from "contracts/src/instances/erc4626/ERC4626Target2.sol";
import { ERC4626Target3 } from "contracts/src/instances/erc4626/ERC4626Target3.sol";
import { IERC4626 } from "contracts/src/interfaces/IERC4626.sol";
import { IHyperdrive } from "contracts/src/interfaces/IHyperdrive.sol";
import { ERC20Mintable } from "contracts/test/ERC20Mintable.sol";
import { MockERC4626 } from "contracts/test/MockERC4626.sol";
import { HyperdriveTest } from "test/utils/HyperdriveTest.sol";
import { HyperdriveUtils } from "test/utils/HyperdriveUtils.sol";

contract ExampleTest is HyperdriveTest {
    using Lib for *;

    function setUp() public override {
        super.setUp();

        // Deploy the base token and vault.
        baseToken = new ERC20Mintable(
            "Base Token",
            "BASE",
            18,
            address(0),
            false
        );
        IERC4626 vault = IERC4626(address(new MockERC4626(
            baseToken,
            "Vault Token",
            "VAULT",
            0.05e18,
            address(0),
            false
        )));

        // Deploy the hyperdrive instance.
        IHyperdrive.PoolConfig memory config = testConfig(0.05e18, 7 days);
        hyperdrive = IHyperdrive(address(new ERC4626Hyperdrive(
            config,
            address(new ERC4626Target0(config, vault)),
            address(new ERC4626Target1(config, vault)),
            address(new ERC4626Target2(config, vault)),
            address(new ERC4626Target3(config, vault)),
            vault
        )));
    }

    function test_example() external {
        // Initialize the Hyperdrive pool.
        initialize(alice, 0.05e18, 100_000_000e18);
        console.log("sharePrice = %s", hyperdrive.getPoolInfo().sharePrice.toString(18));
        vm.warp(block.timestamp + 36 seconds);
        //hyperdrive.checkpoint(HyperdriveUtils.latestCheckpoint(hyperdrive));
        console.log("sharePrice = %s", hyperdrive.getPoolInfo().sharePrice.toString(18));

        // Alice opens a long.
        (uint256 maturityTime, uint256 longAmount) = openLong(alice, 20_000e18);
        console.log("longAmount = %s", longAmount.toString(18));

        // The term passes and interest accrues.
        vm.warp(maturityTime);

        // Alice closes her long.
        uint256 baseProceeds = closeLong(alice, maturityTime, longAmount);
        console.log("longAmount = %s", longAmount.toString(18));
        console.log("baseProceeds = %s", baseProceeds.toString(18));
        console.log("delta = %s", baseProceeds - longAmount);

    }
}
Running 1 test for test/units/derp.t.sol:ExampleTest
[PASS] test_example() (gas: 699843)
Logs:
  sharePrice = 1.000000000000000000
  sharePrice = 1.000000057077625570
  longAmount = 20019.175749563363684514
  longAmount = 20019.175749563363684514
  baseProceeds = 20019.175749563363701900
  delta = 17386

Test result: ok. 1 passed; 0 failed; finished in 10.64ms