diff --git a/contracts/Hyperdrive.sol b/contracts/Hyperdrive.sol index 5290b180e..5bd8d59e5 100644 --- a/contracts/Hyperdrive.sol +++ b/contracts/Hyperdrive.sol @@ -1149,13 +1149,13 @@ abstract contract Hyperdrive is MultiToken, IHyperdrive { return (_positionsOutstanding.mulDown(_averageMaturityTime)) .add(_positionAmount.mulDown(_positionMaturityTime)) - .divDown(_positionsOutstanding.add(_positionAmount)); + .divUp(_positionsOutstanding.add(_positionAmount)); } else { if (_positionsOutstanding == _positionAmount) return 0; return (_positionsOutstanding.mulDown(_averageMaturityTime)) .sub(_positionAmount.mulDown(_positionMaturityTime)) - .divDown(_positionsOutstanding.sub(_positionAmount)); + .divUp(_positionsOutstanding.sub(_positionAmount)); } } diff --git a/contracts/libraries/ERC20Permit.sol b/contracts/libraries/ERC20Permit.sol index 7d48371d9..2fd14e7e8 100644 --- a/contracts/libraries/ERC20Permit.sol +++ b/contracts/libraries/ERC20Permit.sol @@ -69,7 +69,7 @@ abstract contract ERC20Permit is IERC20Permit { } /// @notice An optional override function to execute and change state before immutable assignment - function _extraConstruction() internal virtual {} + function _extraConstruction() internal virtual {} // solhint-disable-line no-empty-blocks // --- Token --- /// @notice Allows a token owner to send tokens to another address diff --git a/contracts/libraries/FixedPointMath.sol b/contracts/libraries/FixedPointMath.sol index 49dfc1240..1fb70a917 100644 --- a/contracts/libraries/FixedPointMath.sol +++ b/contracts/libraries/FixedPointMath.sol @@ -4,12 +4,15 @@ pragma solidity ^0.8.18; import "./Errors.sol"; /// @notice A fixed-point math library. -/// @author Element Finance +/// @author Delve library FixedPointMath { int256 internal constant _ONE_18 = 1e18; uint256 public constant ONE_18 = 1e18; /// @dev Credit to Balancer (https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/solidity-utils/contracts/math/FixedPoint.sol) + /// @param a Fixed point number in 1e18 format. + /// @param b Fixed point number in 1e18 format. + /// @return Result of a + b. function add(uint256 a, uint256 b) internal pure returns (uint256) { // Fixed Point addition is the same as regular checked addition @@ -19,6 +22,9 @@ library FixedPointMath { } /// @dev Credit to Balancer (https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/solidity-utils/contracts/math/FixedPoint.sol) + /// @param a Fixed point number in 1e18 format. + /// @param b Fixed point number in 1e18 format. + /// @return Result of a - b. function sub(uint256 a, uint256 b) internal pure returns (uint256) { // Fixed Point addition is the same as regular checked addition @@ -27,7 +33,11 @@ library FixedPointMath { return c; } - /// @dev Credit to Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/FixedPointMathLib.sol) + /// @dev Credit to Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) + /// @param x Fixed point number in 1e18 format. + /// @param y Fixed point number in 1e18 format. + /// @param d Fixed point number in 1e18 format. + /// @return z The result of x * y / d rounded down. function mulDivDown( uint256 x, uint256 y, @@ -47,11 +57,27 @@ library FixedPointMath { } } + /// @dev Credit to Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) + /// @param a Fixed point number in 1e18 format. + /// @param b Fixed point number in 1e18 format. + /// @return Result of a * b rounded down. function mulDown(uint256 a, uint256 b) internal pure returns (uint256) { return (mulDivDown(a, b, 1e18)); } - /// @dev Credit to Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/FixedPointMathLib.sol) + /// @dev Credit to Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) + /// @param a Fixed point number in 1e18 format. + /// @param b Fixed point number in 1e18 format. + /// @return Result of a / b rounded down. + function divDown(uint256 a, uint256 b) internal pure returns (uint256) { + return (mulDivDown(a, 1e18, b)); // Equivalent to (a * 1e18) / b rounded down. + } + + /// @dev Credit to Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) + /// @param x Fixed point number in 1e18 format. + /// @param y Fixed point number in 1e18 format. + /// @param d Fixed point number in 1e18 format. + /// @return z The result of x * y / d rounded up. function mulDivUp( uint256 x, uint256 y, @@ -73,12 +99,27 @@ library FixedPointMath { } } - function divDown(uint256 a, uint256 b) internal pure returns (uint256) { - return (mulDivDown(a, 1e18, b)); // Equivalent to (a * 1e18) / b rounded down. + /// @dev Credit to Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) + /// @param a Fixed point number in 1e18 format. + /// @param b Fixed point number in 1e18 format. + /// @return The result of a * b rounded up. + function mulUp(uint256 a, uint256 b) internal pure returns (uint256) { + return (mulDivUp(a, b, 1e18)); + } + + /// @dev Credit to Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) + /// @param a Fixed point number in 1e18 format. + /// @param b Fixed point number in 1e18 format. + /// @return The result of a / b rounded up. + function divUp(uint256 a, uint256 b) internal pure returns (uint256) { + return (mulDivUp(a, 1e18, b)); } /// @dev Exponentiation (x^y) with unsigned 18 decimal fixed point base and exponent. /// @dev Partially inspired by Balancer LogExpMath library (https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/solidity-utils/contracts/math/LogExpMath.sol) + /// @param x Fixed point number in 1e18 format. + /// @param y Fixed point number in 1e18 format. + /// @return The result of x^y. function pow(uint256 x, uint256 y) internal pure returns (uint256) { // Using properties of logarithms we calculate x^y: // -> ln(x^y) = y * ln(x) @@ -99,14 +140,16 @@ library FixedPointMath { return uint256(exp(ylnx)); } - // Computes e^x in 1e18 fixed point. - // Credit to Remco (https://github.com/recmo/experiment-solexp/blob/main/src/FixedPointMathLib.sol) + /// @dev Computes e^x in 1e18 fixed point. + /// @dev Credit to Remco (https://github.com/recmo/experiment-solexp/blob/main/src/FixedPointMathLib.sol) + /// @param x Fixed point number in 1e18 format. + /// @return r The result of e^x. function exp(int256 x) internal pure returns (int256 r) { unchecked { // Input x is in fixed point format, with scale factor 1/1e18. // When the result is < 0.5 we return zero. This happens when - // x <= floor(log(0.5e18) * 1e18) ~ -42e18 + // x <= floor(log(0.5e-18) * 1e18) ~ -42e18 if (x <= -42139678854452767551) { return 0; } @@ -169,6 +212,8 @@ library FixedPointMath { /// @dev Computes ln(x) in 1e18 fixed point. /// @dev Reverts if x is negative /// @dev Credit to Remco (https://github.com/recmo/experiment-solexp/blob/main/src/FixedPointMathLib.sol) + /// @param x Fixed point number in 1e18 format. + /// @return Result of ln(x). function ln(int256 x) internal pure returns (int256) { if (x <= 0) revert Errors.FixedPointMath_NegativeOrZeroInput(); return _ln(x); @@ -238,10 +283,11 @@ library FixedPointMath { } } - // Integer log2 - // @returns floor(log2(x)) if x is nonzero, otherwise 0. This is the same - // as the location of the highest set bit. - // Credit to Remco (https://github.com/recmo/experiment-solexp/blob/main/src/FixedPointMathLib.sol) + /// @dev Integer log2 + /// @dev Credit to Remco (https://github.com/recmo/experiment-solexp/blob/main/src/FixedPointMathLib.sol) + /// @param x Integer + /// @return r The floor(log2(x)) if x is nonzero, otherwise 0. This is the same + /// as the location of the highest set bit. function _ilog2(uint256 x) private pure returns (uint256 r) { assembly { r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) diff --git a/contracts/libraries/HyperdriveMath.sol b/contracts/libraries/HyperdriveMath.sol index c6aae32e0..95567e028 100644 --- a/contracts/libraries/HyperdriveMath.sol +++ b/contracts/libraries/HyperdriveMath.sol @@ -175,7 +175,6 @@ library HyperdriveMath { uint256 flat = _amountIn.mulDown( FixedPointMath.ONE_18.sub(_normalizedTimeRemaining) ); - _shareReserves = _shareReserves.add(flat); _bondReserves = _bondReserves.sub(flat.mulDown(_sharePrice)); uint256 curveIn = _amountIn.mulDown(_normalizedTimeRemaining); @@ -211,16 +210,19 @@ library HyperdriveMath { uint256 curveIn = _amountIn .mulDown(_normalizedTimeRemaining) .divDown(_sharePrice); - uint256 curveOut = YieldSpaceMath.calculateOutGivenIn( - _shareReserves, - _bondReserves, - _bondReserveAdjustment, - curveIn, - FixedPointMath.ONE_18.sub(_timeStretch), - _sharePrice, - _initialSharePrice, - _isBaseIn - ); + uint256 curveOut = 0; + if (curveIn > 0) { + curveOut = YieldSpaceMath.calculateOutGivenIn( + _shareReserves, + _bondReserves, + _bondReserveAdjustment, + curveIn, + FixedPointMath.ONE_18.sub(_timeStretch), + _sharePrice, + _initialSharePrice, + _isBaseIn + ); + } uint256 shareDelta = flat.add(curveOut); return (shareDelta, curveIn, shareDelta); } diff --git a/contracts/libraries/YieldSpaceMath.sol b/contracts/libraries/YieldSpaceMath.sol index fce9c1c09..fdf6534e1 100644 --- a/contracts/libraries/YieldSpaceMath.sol +++ b/contracts/libraries/YieldSpaceMath.sol @@ -58,7 +58,7 @@ library YieldSpaceMath { // NOTE: k - shareReserves >= 0 to avoid a complex number // ((c / mu) * (mu * shareReserves)^(1 - tau) + bondReserves^(1 - tau) - (c / mu) * (mu * (shareReserves + amountIn))^(1 - tau))^(1 / (1 - tau))) uint256 newBondReserves = k.sub(_shareReserves).pow( - FixedPointMath.ONE_18.divDown(_stretchedTimeElapsed) + FixedPointMath.ONE_18.divUp(_stretchedTimeElapsed) ); // NOTE: bondReserves - newBondReserves >= 0, but I think avoiding a complex number in the step above ensures this never happens // bondsOut = bondReserves - ( (c / mu) * (mu * shareReserves)^(1 - tau) + bondReserves^(1 - tau) - (c / mu) * (mu * (shareReserves + shareIn))^(1 - tau))^(1 / (1 - tau))) diff --git a/test/Hyperdrive.t.sol b/test/Hyperdrive.t.sol index 16e37a156..254f76839 100644 --- a/test/Hyperdrive.t.sol +++ b/test/Hyperdrive.t.sol @@ -170,11 +170,6 @@ contract HyperdriveTest is Test { // Get the reserves before opening the long. PoolInfo memory poolInfoBefore = getPoolInfo(); - // TODO: Small base amounts result in higher than quoted APRs. We should - // first investigate the math to see if there are obvious simplifications - // to be made, and then consider increasing the amount of precision in - // used in our fixed rate format. - // // Purchase a small amount of bonds. vm.stopPrank(); vm.startPrank(bob); @@ -205,8 +200,83 @@ contract HyperdriveTest is Test { maturityTime - block.timestamp, 365 days ); - // TODO: This tolerance seems too high. - assertApproxEqAbs(realizedApr, apr, 1e10); + + // Verify that opening a long doesn't make the APR go up + assertGt(apr, realizedApr); + + // Verify that the reserves were updated correctly. + PoolInfo memory poolInfoAfter = getPoolInfo(); + assertEq( + poolInfoAfter.shareReserves, + poolInfoBefore.shareReserves + + baseAmount.divDown(poolInfoBefore.sharePrice) + ); + assertEq( + poolInfoAfter.bondReserves, + poolInfoBefore.bondReserves - bondAmount + ); + assertEq(poolInfoAfter.lpTotalSupply, poolInfoBefore.lpTotalSupply); + assertEq(poolInfoAfter.sharePrice, poolInfoBefore.sharePrice); + assertEq( + poolInfoAfter.longsOutstanding, + poolInfoBefore.longsOutstanding + bondAmount + ); + assertApproxEqAbs( + poolInfoAfter.longAverageMaturityTime, + maturityTime, + 1 + ); + assertEq( + poolInfoAfter.shortsOutstanding, + poolInfoBefore.shortsOutstanding + ); + assertEq(poolInfoAfter.shortAverageMaturityTime, 0); + } + + function test_open_long_with_small_amount() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Get the reserves before opening the long. + PoolInfo memory poolInfoBefore = getPoolInfo(); + + // Purchase a small amount of bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 baseAmount = .01e18; + + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + hyperdrive.openLong(baseAmount, 0, bob); + + // Verify the base transfers. + assertEq(baseToken.balanceOf(bob), 0); + assertEq( + baseToken.balanceOf(address(hyperdrive)), + contribution + baseAmount + ); + + // Verify that Bob received an acceptable amount of bonds. Since the + // base amount is very low relative to the pool's liquidity, the implied + // APR should be approximately equal to the pool's APR. + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + uint256 bondAmount = hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ); + uint256 realizedApr = calculateAPRFromRealizedPrice( + baseAmount, + bondAmount, + maturityTime - block.timestamp, + 365 days + ); + + // Verify that opening a long doesn't make the APR go up + assertGt(apr, realizedApr); // Verify that the reserves were updated correctly. PoolInfo memory poolInfoAfter = getPoolInfo(); @@ -342,9 +412,6 @@ contract HyperdriveTest is Test { ); hyperdrive.closeLong(maturityTime, bondAmount, 0, bob); - // TODO: Bob receives more base than he started with. Fees should take - // care of this, but this should be investigating nonetheless. - // // Verify that all of Bob's bonds were burned and that he has // approximately as much base as he started with. uint256 baseProceeds = baseToken.balanceOf(bob); @@ -355,7 +422,77 @@ contract HyperdriveTest is Test { ), 0 ); - assertApproxEqAbs(baseProceeds, baseAmount, 1e10); + // Verify that bob doesn't end up with more than he started with + assertGe(baseAmount, baseProceeds); + + // Verify that the reserves were updated correctly. Since this trade + // happens at the beginning of the term, the bond reserves should be + // increased by the full amount. + PoolInfo memory poolInfoAfter = getPoolInfo(); + assertEq( + poolInfoAfter.shareReserves, + poolInfoBefore.shareReserves - + baseProceeds.divDown(poolInfoBefore.sharePrice) + ); + assertEq( + poolInfoAfter.bondReserves, + poolInfoBefore.bondReserves + bondAmount + ); + assertEq(poolInfoAfter.lpTotalSupply, poolInfoBefore.lpTotalSupply); + assertEq(poolInfoAfter.sharePrice, poolInfoBefore.sharePrice); + assertEq( + poolInfoAfter.longsOutstanding, + poolInfoBefore.longsOutstanding - bondAmount + ); + assertEq(poolInfoAfter.longAverageMaturityTime, 0); + assertEq( + poolInfoAfter.shortsOutstanding, + poolInfoBefore.shortsOutstanding + ); + assertEq(poolInfoAfter.shortAverageMaturityTime, 0); + } + + function test_close_long_immediately_with_small_amount() external { + uint256 apr = 0.05e18; + + // Initialize the pool with a large amount of capital. + uint256 contribution = 500_000_000e18; + initialize(alice, apr, contribution); + + // Purchase some bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 baseAmount = .01e18; + baseToken.mint(baseAmount); + baseToken.approve(address(hyperdrive), baseAmount); + hyperdrive.openLong(baseAmount, 0, bob); + + // Get the reserves before closing the long. + PoolInfo memory poolInfoBefore = getPoolInfo(); + + // Immediately close the bonds. + vm.stopPrank(); + vm.startPrank(bob); + uint256 maturityTime = (block.timestamp - (block.timestamp % 1 days)) + + 365 days; + uint256 bondAmount = hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ); + hyperdrive.closeLong(maturityTime, bondAmount, 0, bob); + + // Verify that all of Bob's bonds were burned and that he has + // approximately as much base as he started with. + uint256 baseProceeds = baseToken.balanceOf(bob); + assertEq( + hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + bob + ), + 0 + ); + // Verify that bob doesn't end up with more than he started with + assertGe(baseAmount, baseProceeds); // Verify that the reserves were updated correctly. Since this trade // happens at the beginning of the term, the bond reserves should be @@ -417,10 +554,6 @@ contract HyperdriveTest is Test { ); hyperdrive.closeLong(maturityTime, bondAmount, 0, bob); - // TODO: Bob receives more base than the bond amount. It appears that - // the yield space implementation returns a positive value even when - // the input is zero. - // // Verify that all of Bob's bonds were burned and that he has // approximately as much base as he started with. uint256 baseProceeds = baseToken.balanceOf(bob); @@ -431,7 +564,7 @@ contract HyperdriveTest is Test { ), 0 ); - assertApproxEqAbs(baseProceeds, bondAmount, 1e10); + assertEq(baseProceeds, bondAmount); // Verify that the reserves were updated correctly. Since this trade // is a redemption, there should be no changes to the bond reserves. diff --git a/test/YieldSpaceMath.t.sol b/test/YieldSpaceMath.t.sol index 5a49d0726..9b92e207b 100644 --- a/test/YieldSpaceMath.t.sol +++ b/test/YieldSpaceMath.t.sol @@ -6,32 +6,28 @@ import { YieldSpaceMath } from "contracts/libraries/YieldSpaceMath.sol"; contract YieldSpaceMathTest is Test { function test__calculateOutGivenIn() public { - assertEq( - YieldSpaceMath.calculateOutGivenIn( - 56.79314253e18, // shareReserves - 62.38101813e18, // bondReserves - 119.1741606776616e18, // bondReserveAdjustment - 5.03176076e18, // amountOut - 1e18 - 0.08065076081220067e18, // stretchedTimeElapsed - 1e18, // c - 1e18, // mu - true // isBondIn - ), - 5.500250311701939082e18 + uint256 result1 = YieldSpaceMath.calculateOutGivenIn( + 61.824903300361854e18, // shareReserves + 56.92761678068477e18, // bondReserves + 119.1741606776616e18, // bondReserveAdjustment + 5.500250311701939e18, // amountIn + 1e18 - 0.08065076081220067e18, // stretchedTimeElapsed + 1e18, // c + 1e18, // mu + true // isBondIn ); + assertEq(result1, 5.955718322566968926e18); - assertEq( - YieldSpaceMath.calculateOutGivenIn( - 61.824903300361854e18, // shareReserves - 56.92761678068477e18, // bondReserves - 119.1741606776616e18, // bondReserveAdjustment - 5.500250311701939e18, // amountOut - 1e18 - 0.08065076081220067e18, // stretchedTimeElapsed - 1e18, // c - 1e18, // mu - false // isBondIn - ), - 5.031654806080805188e18 + uint256 result2 = YieldSpaceMath.calculateOutGivenIn( + 61.824903300361854e18, // shareReserves + 56.92761678068477e18, // bondReserves + 119.1741606776616e18, // bondReserveAdjustment + 5.500250311701939e18, // amountIn + 1e18 - 0.08065076081220067e18, // stretchedTimeElapsed + 1e18, // c + 1e18, // mu + false // isBondIn ); + assertEq(result2, 5.031654806080805188e18); } }