From 3c393c2c49504c1d91549aa1c6697ef589f90487 Mon Sep 17 00:00:00 2001 From: Adam Hamden Date: Fri, 3 Jan 2025 12:59:53 -0800 Subject: [PATCH 1/5] Hourglass PT/CT Linear Discount Rate Oracle --- .../hourglass/HourglassLinearOracle.sol | 123 ++++++++++++++++++ src/adapter/hourglass/IHourglassDepositor.sol | 70 ++++++++++ src/adapter/hourglass/IHourglassERC20TBT.sol | 9 ++ 3 files changed, 202 insertions(+) create mode 100644 src/adapter/hourglass/HourglassLinearOracle.sol create mode 100644 src/adapter/hourglass/IHourglassDepositor.sol create mode 100644 src/adapter/hourglass/IHourglassERC20TBT.sol diff --git a/src/adapter/hourglass/HourglassLinearOracle.sol b/src/adapter/hourglass/HourglassLinearOracle.sol new file mode 100644 index 00000000..55aef19d --- /dev/null +++ b/src/adapter/hourglass/HourglassLinearOracle.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {BaseAdapter, Errors, IPriceOracle} from "../BaseAdapter.sol"; +import {ScaleUtils, Scale} from "../../lib/ScaleUtils.sol"; +import {IHourglassDepositor} from "./IHourglassDepositor.sol"; +import {IHourglassERC20TBT} from "./IHourglassERC20TBT.sol"; + +contract HourglassOracle is BaseAdapter { + /// @inheritdoc IPriceOracle + string public constant name = "HourglassOracle"; + + /// @notice The address of the base asset (e.g., PT or CT). + address public immutable base; + /// @notice The address of the quote asset (e.g., underlying asset). + address public immutable quote; + + /// @notice Per second discount rate (scaled by baseAssetDecimals). + uint256 public immutable discountRate; + + /// @notice Address of the Hourglass system. + IHourglassDepositor public immutable hourglassDepositor; + + /// @notice The scale factors used for decimal conversions. + Scale internal immutable scale; + + /// @notice The address of the combined token. + address public immutable combinedToken; + /// @notice The address of the principal token. + address public immutable principalToken; + /// @notice The address of the underlying token. + address public immutable underlyingToken; + + /// @notice The number of decimals for the base token. + uint8 public immutable baseTokenDecimals; + /// @notice The number of decimals for the quote token. + uint8 public immutable quoteTokenDecimals; + + /// @notice Deploy the HourglassLinearDiscountOracle. + /// @param _base The address of the base asset (PT or CT). + /// @param _quote The address of the quote asset (underlying token). + /// @param _discountRate Discount rate (secondly, scaled by baseAssetDecimals). + constructor(address _base, address _quote, uint256 _discountRate) { + if (_discountRate == 0) revert Errors.PriceOracle_InvalidConfiguration(); + + // Initialize key parameters + base = _base; + quote = _quote; + discountRate = _discountRate; + + // Fetch and store Hourglass depositor + hourglassDepositor = IHourglassDepositor(IHourglassERC20TBT(_base).depositor()); + + // Fetch token decimals + uint8 baseDecimals = _getDecimals(_base); + uint8 quoteDecimals = _getDecimals(_quote); + + // Fetch token addresses + address[] memory tokens = hourglassDepositor.getTokens(); + combinedToken = tokens[0]; + principalToken = tokens[1]; + underlyingToken = hourglassDepositor.getUnderlying(); + + // Calculate scale factors for decimal conversions + scale = ScaleUtils.calcScale(baseDecimals, quoteDecimals, quoteDecimals); + + // Store decimals for normalization + baseTokenDecimals = baseDecimals; + quoteTokenDecimals = quoteDecimals; + } + + /// @notice Get a dynamic quote using linear discounting and solvency adjustment. + /// @param inAmount The amount of `base` to convert. + /// @param _base The token being priced (e.g., PT or CT). + /// @param _quote The token used as the unit of account (e.g., underlying). + /// @return The converted amount using the linear discount rate and solvency adjustment. + function _getQuote(uint256 inAmount, address _base, address _quote) internal view override returns (uint256) { + bool inverse = ScaleUtils.getDirectionOrRevert(_base, base, _quote, quote); + + // Get solvency ratio, baseTokenDecimals precision + uint256 solvencyRatio = _getSolvencyRatio(); + + // Calculate present value using linear discounting, baseTokenDecimals precision + uint256 presentValue = _getPresentValue(inAmount, solvencyRatio); + + // Return scaled output amount + return ScaleUtils.calcOutAmount(inAmount, presentValue, scale, inverse); + } + + /// @notice Calculate the present value using linear discounting. + /// @param inAmount The input amount of base tokens (scaled by baseTokenDecimals). + /// @param solvencyRatio Solvency ratio of the Hourglass system (scaled by baseTokenDecimals). + /// @return presentValue The present value of the input amount (scaled by baseTokenDecimals). + function _getPresentValue(uint256 inAmount, uint256 solvencyRatio) internal view returns (uint256 presentValue) { + uint256 baseTokenScale = 10 ** baseTokenDecimals; + uint256 timeToMaturity = _getTimeToMaturity(); + uint256 discountFactor = + (baseTokenScale * baseTokenScale) / (baseTokenScale + ((discountRate * timeToMaturity))); + + presentValue = (inAmount * solvencyRatio * discountFactor) / (baseTokenScale * baseTokenScale); + } + + /// @notice Fetch the time-to-maturity. + /// @return timeToMaturity Time-to-maturity in seconds. + function _getTimeToMaturity() internal view returns (uint256) { + uint256 maturityTime = hourglassDepositor.maturity(); + return maturityTime > block.timestamp ? maturityTime - block.timestamp : 0; + } + + /// @notice Fetch the solvency ratio of the Hourglass system. + /// @return solvencyRatio Solvency ratio of the Hourglass system (scaled by _base token decimals). + function _getSolvencyRatio() internal view returns (uint256 solvencyRatio) { + uint256 baseTokenScale = 10 ** baseTokenDecimals; + uint256 underlyingTokenBalance = IERC20(underlyingToken).balanceOf(address(hourglassDepositor)); + uint256 ptSupply = IERC20(principalToken).totalSupply(); + uint256 ctSupply = IERC20(combinedToken).totalSupply(); + + uint256 totalClaims = ptSupply + ctSupply; + + return (totalClaims > 0) ? (underlyingTokenBalance * baseTokenScale) / totalClaims : baseTokenScale; + } +} diff --git a/src/adapter/hourglass/IHourglassDepositor.sol b/src/adapter/hourglass/IHourglassDepositor.sol new file mode 100644 index 00000000..973a2679 --- /dev/null +++ b/src/adapter/hourglass/IHourglassDepositor.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IHourglassDepositor { + function creationBlock() external view returns (uint256); + function deposit(uint256 amount, bool receiveSplit) external; + function depositFor(address user, uint256 amount, bool receiveSplit) external; + function depositTo(address principalRecipient, address pointRecipient, uint256 amount, bool receiveSplit) + external; + function enter(uint256 amountToBeDeposited) external; + function factory() external view returns (address); + function getPointToken() external view returns (address); + function getPrincipalToken() external view returns (address); + function getTokens() external view returns (address[] memory); + function getUnderlying() external view returns (address); + function maturity() external view returns (uint256); + function recombine(uint256 amount) external; + function recoverToken(address token, address rewardsDistributor) external returns (uint256 amount); + function redeem(uint256 amount) external; + function redeemPrincipal(uint256 amount) external; + function setMaxDeposits(uint256 _depositCap) external; + function split(uint256 amount) external; + + event Deposit(address user, uint256 amount); + event DepositedTo(address principalRecipient, address pointRecipient, uint256 amount); + event Initialized(uint64 version); + event NewMaturityCreated(address combined, address principal, address yield); + event Recombine(address user, uint256 amount); + event Redeem(address user, uint256 amount); + event Split(address user, uint256 amount); + + error AddressEmptyCode(address target); + error AddressInsufficientBalance(address account); + error AlreadyEntered(); + error AmountMismatch(); + error CallerNotEntrant(); + error DepositCapExceeded(); + error FailedInnerCall(); + error InsufficientAssetSupplied(); + error InsufficientDeposit(); + error InsufficientFunds(); + error InvalidDecimals(); + error InvalidInitialization(); + error InvalidMaturity(); + error InvalidUnderlying(); + error Matured(); + error NoCode(); + error NotEntered(); + error NotInitializing(); + error PrematureRedeem(); + error RecipientMismatch(); + error SafeERC20FailedOperation(address token); + error UnauthorizedCaller(); +} + +interface IVedaDepositor { + function mintLockedUnderlying(address depositAsset, uint256 amountOutMinBps) external returns (uint256 amountOut); +} + +interface IEthFiLUSDDepositor { + function mintLockedUnderlying(uint256 minMintReceivedSlippageBps, address lusdDepositAsset, address sourceOfFunds) + external + returns (uint256 amountDepositAssetMinted); +} + +interface IEthFiLiquidDepositor { + function mintLockedUnderlying(uint256 minMintReceivedSlippageBps, address lusdDepositAsset, address sourceOfFunds) + external + returns (uint256 amountDepositAssetMinted); +} diff --git a/src/adapter/hourglass/IHourglassERC20TBT.sol b/src/adapter/hourglass/IHourglassERC20TBT.sol new file mode 100644 index 00000000..63e17086 --- /dev/null +++ b/src/adapter/hourglass/IHourglassERC20TBT.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IHourglassERC20TBT { + function depositor() external view returns (address); + function initialize(string calldata _name, string calldata _symbol, uint8 __decimals) external; + function mint(address _to, uint256 _amount) external; + function burn(address _from, uint256 _amount) external; +} From 81b1e6f5f0eea9ccd9212d89a5b6d3542b653f18 Mon Sep 17 00:00:00 2001 From: Adam Hamden Date: Fri, 3 Jan 2025 20:27:53 -0800 Subject: [PATCH 2/5] wip hourglass oracle helper + mocks --- ...ssLinearOracle.sol => HourglassOracle.sol} | 0 test/adapter/hourglass/HourglassAddresses.sol | 6 ++ .../hourglass/HourglassOracleHelper.sol | 94 +++++++++++++++++++ test/utils/EthereumAddresses.sol | 1 + 4 files changed, 101 insertions(+) rename src/adapter/hourglass/{HourglassLinearOracle.sol => HourglassOracle.sol} (100%) create mode 100644 test/adapter/hourglass/HourglassAddresses.sol create mode 100644 test/adapter/hourglass/HourglassOracleHelper.sol diff --git a/src/adapter/hourglass/HourglassLinearOracle.sol b/src/adapter/hourglass/HourglassOracle.sol similarity index 100% rename from src/adapter/hourglass/HourglassLinearOracle.sol rename to src/adapter/hourglass/HourglassOracle.sol diff --git a/test/adapter/hourglass/HourglassAddresses.sol b/test/adapter/hourglass/HourglassAddresses.sol new file mode 100644 index 00000000..59155e47 --- /dev/null +++ b/test/adapter/hourglass/HourglassAddresses.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +address constant HOURGLASS_LBTCV_01MAR2025_DEPOSITOR = 0xf06617fBECF1BdEa2D62079bdab9595f86801604; +address constant HOURGLASS_LBTCV_01MAR2025_CT = 0xe6dA3BD04cEEE35D6A52fF329e57cC2220a669b1; +address constant HOURGLASS_LBTCV_01MAR2025_PT = 0x97955073caA92028a86Cd3F660FE484d6B89B938; \ No newline at end of file diff --git a/test/adapter/hourglass/HourglassOracleHelper.sol b/test/adapter/hourglass/HourglassOracleHelper.sol new file mode 100644 index 00000000..051e6c73 --- /dev/null +++ b/test/adapter/hourglass/HourglassOracleHelper.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {AdapterHelper} from "test/adapter/AdapterHelper.sol"; +import {boundAddr, distinct} from "test/utils/TestUtils.sol"; +import {HourglassOracle} from "src/adapter/hourglass/HourglassOracle.sol"; +import {IHourglassDepositor} from "src/adapter/hourglass/IHourglassDepositor.sol"; + +contract HourglassOracleHelper is AdapterHelper { + struct FuzzableState { + // Config + address base; + address quote; + uint256 discountRate; + // Market Assets + address depositor; + address pt; + address ct; + address pyt; + address underlyingToken; + // Market State + uint256 expiry; + uint256 underlyingTokenBalance; + uint256 ptSupply; + uint256 ctSupply; + // Environment + uint256 inAmount; + } + + function setUpState(FuzzableState memory s) internal { + s.base = boundAddr(s.base); + s.quote = boundAddr(s.quote); + s.discountRate = bound(s.discountRate, 0, type(uint128).max); + s.depositor = boundAddr(s.depositor); + s.pt = boundAddr(s.pt); + s.ct = boundAddr(s.ct); + s.pyt = boundAddr(s.pyt); + s.underlyingToken = boundAddr(s.underlyingToken); + + s.underlyingTokenBalance = bound(s.underlyingTokenBalance, 0, type(uint128).max); + s.ptSupply = bound(s.ptSupply, 0, type(uint128).max); + s.ctSupply = bound(s.ctSupply, 0, type(uint128).max); + + + vm.assume(distinct(s.base, s.quote, s.depositor, s.pt, s.ct, s.pyt, s.underlyingToken)); + vm.assume(s.base != s.quote); + + s.expiry = bound(s.expiry, 0, block.timestamp); + + // Mock the Hourglass Depositor + vm.mockCall( + address(s.depositor), + abi.encodeWithSelector(IHourglassDepositor.getTokens.selector), + abi.encode(s.ct, s.pt, s.pyt) // Use s.ct, s.pt, and s.pyt for the tokens + ); + + vm.mockCall( + address(s.depositor), + abi.encodeWithSelector(IHourglassDepositor.getUnderlying.selector), + abi.encode(s.underlyingToken) // Use s.underlyingToken for the underlyingToken + ); + + vm.mockCall( + address(s.depositor), + abi.encodeWithSelector(IHourglassDepositor.maturity.selector), + abi.encode(s.expiry) // Use s.expiry for the maturity time + ); + + // Mock the Principal Token + vm.mockCall( + address(s.pt), + abi.encodeWithSelector(IERC20.totalSupply.selector), + abi.encode(s.ptSupply) + ); + + // Mock the Combined Token + vm.mockCall( + address(s.ct), + abi.encodeWithSelector(IERC20.totalSupply.selector), + abi.encode(s.ctSupply) + ); + + // Mock the Underlying Token + vm.mockCall( + address(s.underlyingToken), + abi.encodeWithSelector(IERC20.balanceOf.selector, address(s.depositor)), + abi.encode(s.underlyingTokenBalance) + ); + + oracle = address(new HourglassOracle(s.base, s.quote, s.discountRate)); + s.inAmount = bound(s.inAmount, 0, type(uint128).max); + } +} diff --git a/test/utils/EthereumAddresses.sol b/test/utils/EthereumAddresses.sol index 164e54ed..2d0dfe9a 100644 --- a/test/utils/EthereumAddresses.sol +++ b/test/utils/EthereumAddresses.sol @@ -44,6 +44,7 @@ address constant IMX = 0xF57e7e7C23978C3cAEC3C3548E3D615c346e79fF; address constant KNC = 0xdd974D5C2e2928deA5F71b9825b8b646686BD200; address constant KNCV2 = 0xdeFA4e8a7bcBA345F687a2f1456F5Edd9CE97202; address constant LBTC = 0x8236a87084f8B84306f72007F36F2618A5634494; +address constant LBTCV = 0x5401b8620E5FB570064CA9114fd1e135fd77D57c; address constant LDO = 0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32; address constant LINK = 0x514910771AF9Ca656af840dff83E8264EcF986CA; address constant LUSD = 0x3Fe6a295459FAe07DF8A0ceCC36F37160FE86AA9; From bc098a784c0b06216637fbbe7c298dfa7a1963a6 Mon Sep 17 00:00:00 2001 From: Adam Hamden Date: Sat, 4 Jan 2025 19:17:12 -0800 Subject: [PATCH 3/5] fix oracle implementation: use unit present value no total present value --- src/adapter/hourglass/HourglassOracle.sol | 33 +++++-- .../hourglass/HourglassOracle.unit.t.sol | 51 ++++++++++ .../hourglass/HourglassOracleHelper.sol | 96 +++++++++++++++---- 3 files changed, 154 insertions(+), 26 deletions(-) create mode 100644 test/adapter/hourglass/HourglassOracle.unit.t.sol diff --git a/src/adapter/hourglass/HourglassOracle.sol b/src/adapter/hourglass/HourglassOracle.sol index 55aef19d..b594dc8c 100644 --- a/src/adapter/hourglass/HourglassOracle.sol +++ b/src/adapter/hourglass/HourglassOracle.sol @@ -6,6 +6,7 @@ import {BaseAdapter, Errors, IPriceOracle} from "../BaseAdapter.sol"; import {ScaleUtils, Scale} from "../../lib/ScaleUtils.sol"; import {IHourglassDepositor} from "./IHourglassDepositor.sol"; import {IHourglassERC20TBT} from "./IHourglassERC20TBT.sol"; +import "forge-std/console.sol"; contract HourglassOracle is BaseAdapter { /// @inheritdoc IPriceOracle @@ -16,7 +17,7 @@ contract HourglassOracle is BaseAdapter { /// @notice The address of the quote asset (e.g., underlying asset). address public immutable quote; - /// @notice Per second discount rate (scaled by baseAssetDecimals). + /// @notice Per second discount rate (scaled by 1e18). uint256 public immutable discountRate; /// @notice Address of the Hourglass system. @@ -82,23 +83,39 @@ contract HourglassOracle is BaseAdapter { uint256 solvencyRatio = _getSolvencyRatio(); // Calculate present value using linear discounting, baseTokenDecimals precision - uint256 presentValue = _getPresentValue(inAmount, solvencyRatio); - + uint256 presentValue = _getUnitPresentValue(solvencyRatio); + // Return scaled output amount return ScaleUtils.calcOutAmount(inAmount, presentValue, scale, inverse); } /// @notice Calculate the present value using linear discounting. - /// @param inAmount The input amount of base tokens (scaled by baseTokenDecimals). /// @param solvencyRatio Solvency ratio of the Hourglass system (scaled by baseTokenDecimals). /// @return presentValue The present value of the input amount (scaled by baseTokenDecimals). - function _getPresentValue(uint256 inAmount, uint256 solvencyRatio) internal view returns (uint256 presentValue) { + function _getUnitPresentValue(uint256 solvencyRatio) + internal + view + returns (uint256) + { + // Both inAmount and solvencyRatio have baseTokenDecimals precision uint256 baseTokenScale = 10 ** baseTokenDecimals; + uint256 timeToMaturity = _getTimeToMaturity(); - uint256 discountFactor = - (baseTokenScale * baseTokenScale) / (baseTokenScale + ((discountRate * timeToMaturity))); - presentValue = (inAmount * solvencyRatio * discountFactor) / (baseTokenScale * baseTokenScale); + // The expression (1e18 + discountRate * timeToMaturity) is ~1e18 scale + // We want the denominator to be scaled to baseTokenDecimals so that when + // we divide the (inAmount * solvencyRatio) [which is 2 * baseTokenDecimals in scale], + // we end up back with baseTokenDecimals in scale. + + uint256 scaledDenominator = ( + (1e18 + (discountRate * timeToMaturity)) // ~1e18 scale + * baseTokenScale // multiply by 1e(baseTokenDecimals) + ) / 1e18; // now scaledDenominator has baseTokenDecimals precision + + // (inAmount * solvencyRatio) is scale = 2 * baseTokenDecimals + // dividing by scaledDenominator (scale = baseTokenDecimals) + // => result has scale = baseTokenDecimals + return (baseTokenScale * solvencyRatio) / scaledDenominator; } /// @notice Fetch the time-to-maturity. diff --git a/test/adapter/hourglass/HourglassOracle.unit.t.sol b/test/adapter/hourglass/HourglassOracle.unit.t.sol new file mode 100644 index 00000000..c840a6eb --- /dev/null +++ b/test/adapter/hourglass/HourglassOracle.unit.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {HourglassOracleHelper} from "test/adapter/hourglass/HourglassOracleHelper.sol"; +import {boundAddr} from "test/utils/TestUtils.sol"; +import {HourglassOracle} from "src/adapter/hourglass/HourglassOracle.sol"; + +contract HourglassOracleTest is HourglassOracleHelper { + function test_Constructor_Integrity_Hourglass(FuzzableState memory s) public { + setUpState(s); + assertEq(HourglassOracle(oracle).name(), "HourglassOracle"); + assertEq(HourglassOracle(oracle).base(), s.base); + assertEq(HourglassOracle(oracle).quote(), s.quote); + assertEq(HourglassOracle(oracle).discountRate(), s.discountRate); + } + + function test_Quote_RevertsWhen_InvalidTokens(FuzzableState memory s, address otherA, address otherB) public { + setUpState(s); + otherA = boundAddr(otherA); + otherB = boundAddr(otherB); + vm.assume(otherA != s.base && otherA != s.quote); + vm.assume(otherB != s.base && otherB != s.quote); + expectNotSupported(s.inAmount, s.base, s.base); + expectNotSupported(s.inAmount, s.quote, s.quote); + expectNotSupported(s.inAmount, s.base, otherA); + expectNotSupported(s.inAmount, otherA, s.base); + expectNotSupported(s.inAmount, s.quote, otherA); + expectNotSupported(s.inAmount, otherA, s.quote); + expectNotSupported(s.inAmount, otherA, otherA); + expectNotSupported(s.inAmount, otherA, otherB); + } + + function test_Quote_Integrity(FuzzableState memory s) public { + setUpState(s); + HourglassOracle(oracle).getQuote(s.inAmount, s.base, s.quote); + HourglassOracle(oracle).getQuote(s.inAmount, s.quote, s.base); + } + + function test_Quotes_Integrity(FuzzableState memory s) public { + setUpState(s); + uint256 outAmount = HourglassOracle(oracle).getQuote(s.inAmount, s.base, s.quote); + (uint256 bidOutAmount, uint256 askOutAmount) = HourglassOracle(oracle).getQuotes(s.inAmount, s.base, s.quote); + assertEq(bidOutAmount, outAmount); + assertEq(askOutAmount, outAmount); + uint256 outAmountInv = HourglassOracle(oracle).getQuote(s.inAmount, s.quote, s.base); + (uint256 bidOutAmountInv, uint256 askOutAmountInv) = HourglassOracle(oracle).getQuotes(s.inAmount, s.quote, s.base); + assertEq(bidOutAmountInv, outAmountInv); + assertEq(askOutAmountInv, outAmountInv); + } +} diff --git a/test/adapter/hourglass/HourglassOracleHelper.sol b/test/adapter/hourglass/HourglassOracleHelper.sol index 051e6c73..d680932f 100644 --- a/test/adapter/hourglass/HourglassOracleHelper.sol +++ b/test/adapter/hourglass/HourglassOracleHelper.sol @@ -6,6 +6,8 @@ import {AdapterHelper} from "test/adapter/AdapterHelper.sol"; import {boundAddr, distinct} from "test/utils/TestUtils.sol"; import {HourglassOracle} from "src/adapter/hourglass/HourglassOracle.sol"; import {IHourglassDepositor} from "src/adapter/hourglass/IHourglassDepositor.sol"; + import "forge-std/console.sol"; + contract HourglassOracleHelper is AdapterHelper { struct FuzzableState { @@ -29,66 +31,124 @@ contract HourglassOracleHelper is AdapterHelper { } function setUpState(FuzzableState memory s) internal { + // Set reasonable bounds for addresses s.base = boundAddr(s.base); s.quote = boundAddr(s.quote); - s.discountRate = bound(s.discountRate, 0, type(uint128).max); s.depositor = boundAddr(s.depositor); s.pt = boundAddr(s.pt); s.ct = boundAddr(s.ct); s.pyt = boundAddr(s.pyt); s.underlyingToken = boundAddr(s.underlyingToken); - s.underlyingTokenBalance = bound(s.underlyingTokenBalance, 0, type(uint128).max); - s.ptSupply = bound(s.ptSupply, 0, type(uint128).max); - s.ctSupply = bound(s.ctSupply, 0, type(uint128).max); + // Set reasonable bounds for numeric values + // Minimum could be near zero, or something like 1e8 (≈ ~0.000003% annual) + // Maximum ~3.17e10 for ~100% annual + s.discountRate = bound(s.discountRate, 1, 3.2e10); + s.underlyingTokenBalance = bound(s.underlyingTokenBalance, 0, 1e24); + + // Ensure ptSupply and ctSupply add up to underlyingTokenBalance + uint256 maxSupply = s.underlyingTokenBalance; + s.ptSupply = bound(s.ptSupply, 0, maxSupply); + s.ctSupply = maxSupply - s.ptSupply; + + s.expiry = bound(s.expiry, block.timestamp, block.timestamp + 365 days); + s.inAmount = bound(s.inAmount, 0, 1e18); + + console.log("s.base: %s", s.base); + console.log("s.quote: %s", s.quote); + console.log("s.depositor: %s", s.depositor); + console.log("s.pt: %s", s.pt); + console.log("s.ct: %s", s.ct); + console.log("s.pyt: %s", s.pyt); + console.log("s.underlyingToken: %s", s.underlyingToken); + console.log("s.discountRate: %s", s.discountRate); + console.log("s.underlyingTokenBalance: %s", s.underlyingTokenBalance); + console.log("s.ptSupply: %s", s.ptSupply); + console.log("s.ctSupply: %s", s.ctSupply); + console.log("s.expiry: %s", s.expiry); + console.log("s.inAmount: %s", s.inAmount); + // Assume distinct addresses vm.assume(distinct(s.base, s.quote, s.depositor, s.pt, s.ct, s.pyt, s.underlyingToken)); vm.assume(s.base != s.quote); - s.expiry = bound(s.expiry, 0, block.timestamp); + // Prepare the dynamic array + address[] memory tokens = new address[](3); + tokens[0] = s.ct; + tokens[1] = s.pt; + tokens[2] = s.pyt; - // Mock the Hourglass Depositor + // Then encode *that array* in the mock vm.mockCall( - address(s.depositor), + s.depositor, abi.encodeWithSelector(IHourglassDepositor.getTokens.selector), - abi.encode(s.ct, s.pt, s.pyt) // Use s.ct, s.pt, and s.pyt for the tokens + abi.encode(tokens) // This is the correct way to encode a dynamic array ); vm.mockCall( - address(s.depositor), + s.depositor, abi.encodeWithSelector(IHourglassDepositor.getUnderlying.selector), - abi.encode(s.underlyingToken) // Use s.underlyingToken for the underlyingToken + abi.encode(s.underlyingToken) ); vm.mockCall( - address(s.depositor), + s.depositor, abi.encodeWithSelector(IHourglassDepositor.maturity.selector), - abi.encode(s.expiry) // Use s.expiry for the maturity time + abi.encode(s.expiry) ); // Mock the Principal Token vm.mockCall( - address(s.pt), + s.pt, abi.encodeWithSelector(IERC20.totalSupply.selector), - abi.encode(s.ptSupply) + abi.encode(s.ptSupply) ); // Mock the Combined Token vm.mockCall( - address(s.ct), + s.ct, abi.encodeWithSelector(IERC20.totalSupply.selector), - abi.encode(s.ctSupply) + abi.encode(s.ctSupply) ); // Mock the Underlying Token vm.mockCall( - address(s.underlyingToken), - abi.encodeWithSelector(IERC20.balanceOf.selector, address(s.depositor)), + s.underlyingToken, + abi.encodeWithSelector(IERC20.balanceOf.selector, s.depositor), abi.encode(s.underlyingTokenBalance) ); + // ========== NEW: Mock the TBT (the "base") calls ========== + + // 1) The constructor calls HourglassERC20TBT(_base).depositor() + vm.mockCall( + s.base, + abi.encodeWithSelector(bytes4(keccak256("depositor()"))), // or HourglassERC20TBT.depositor.selector + abi.encode(s.depositor) + ); + + // 2) The constructor calls HourglassERC20TBT(_base).decimals() + vm.mockCall( + s.base, + abi.encodeWithSelector(bytes4(keccak256("decimals()"))), // or HourglassERC20TBT.decimals.selector + abi.encode(uint8(18)) // or whatever decimals you want to simulate + ); + + // 3) The constructor also calls _getDecimals(s.quote) internally + // If your adapter or base code does "IERC20Metadata(_quote).decimals()", + // you might need to mock that if 's.quote' doesn't implement decimals(). + vm.mockCall( + s.quote, + abi.encodeWithSelector(bytes4(keccak256("decimals()"))), + abi.encode(uint8(18)) // or a different decimal count if needed + ); + + // Now actually deploy the oracle: oracle = address(new HourglassOracle(s.base, s.quote, s.discountRate)); + + HourglassOracle hourglassOracle = HourglassOracle(oracle); + console.log("oracle dr: %s", hourglassOracle.discountRate()); // Re-bound s.inAmount to some smaller range if needed s.inAmount = bound(s.inAmount, 0, type(uint128).max); } } From f5084c1791c224db2a48d63fc8dd3f86ba8f9b66 Mon Sep 17 00:00:00 2001 From: Adam Hamden Date: Sat, 4 Jan 2025 19:58:45 -0800 Subject: [PATCH 4/5] add hourglass fork and prop test --- test/adapter/hourglass/HourglassAddresses.sol | 10 +- .../hourglass/HourglassOracle.fork.t.sol | 148 ++++++++++++++++++ .../hourglass/HourglassOracle.prop.t.sol | 44 ++++++ 3 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 test/adapter/hourglass/HourglassOracle.fork.t.sol create mode 100644 test/adapter/hourglass/HourglassOracle.prop.t.sol diff --git a/test/adapter/hourglass/HourglassAddresses.sol b/test/adapter/hourglass/HourglassAddresses.sol index 59155e47..17cb70c8 100644 --- a/test/adapter/hourglass/HourglassAddresses.sol +++ b/test/adapter/hourglass/HourglassAddresses.sol @@ -1,6 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; +import {LBTCV} from "../../utils/EthereumAddresses.sol"; + address constant HOURGLASS_LBTCV_01MAR2025_DEPOSITOR = 0xf06617fBECF1BdEa2D62079bdab9595f86801604; address constant HOURGLASS_LBTCV_01MAR2025_CT = 0xe6dA3BD04cEEE35D6A52fF329e57cC2220a669b1; -address constant HOURGLASS_LBTCV_01MAR2025_PT = 0x97955073caA92028a86Cd3F660FE484d6B89B938; \ No newline at end of file +address constant HOURGLASS_LBTCV_01MAR2025_PT = 0x97955073caA92028a86Cd3F660FE484d6B89B938; +address constant HOURGLASS_LBTCV_01MAR2025_UNDERLYING = LBTCV; + +address constant HOURGLASS_LBTCV_01DEC2024_DEPOSITOR = 0xA285bca8f01c8F18953443e645ef2786D31ada99; +address constant HOURGLASS_LBTCV_01DEC2024_CT = 0x0CB35DC9ADDce18669E2Fd5db4B405Ea655e98Bd; +address constant HOURGLASS_LBTCV_01DEC2024_PT = 0xDB0Ee7308cF1F5A3f376D015a1545B4cB9A878D9; +address constant HOURGLASS_LBTCV_01DEC2024_UNDERLYING = LBTCV; \ No newline at end of file diff --git a/test/adapter/hourglass/HourglassOracle.fork.t.sol b/test/adapter/hourglass/HourglassOracle.fork.t.sol new file mode 100644 index 00000000..0548af7a --- /dev/null +++ b/test/adapter/hourglass/HourglassOracle.fork.t.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +// ============ Imports ============ + +// Foundry's base test that sets up a mainnet or testnet fork +import {ForkTest} from "test/utils/ForkTest.sol"; + +// Import your HourglassOracle +import {HourglassOracle} from "src/adapter/hourglass/HourglassOracle.sol"; +import {Errors} from "src/lib/Errors.sol"; + +// Typically you'd import ERC20 or an interface to check balances if needed +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +import { + HOURGLASS_LBTCV_01MAR2025_DEPOSITOR, + HOURGLASS_LBTCV_01MAR2025_PT, + HOURGLASS_LBTCV_01MAR2025_CT, + HOURGLASS_LBTCV_01MAR2025_UNDERLYING, + HOURGLASS_LBTCV_01DEC2024_DEPOSITOR, + HOURGLASS_LBTCV_01DEC2024_PT, + HOURGLASS_LBTCV_01DEC2024_UNDERLYING +} from "test/adapter/hourglass/HourglassAddresses.sol"; + +/** + * @dev Example discountRate as "per-second" rate in 1e18 form. For instance: + * - 100% annual ~ 3.17e10 if you do (1.0 / 31536000) * 1e18 + * - 50% annual ~ 1.585e10 + * - Adjust to whatever you want for testing + */ +uint256 constant DISCOUNT_RATE_PER_SECOND = 1585489599; // ~ 5% annual + +contract HourglassOracleForkTest is ForkTest { + // For relative assert precision (e.g. 1% = 0.01e18) + uint256 constant REL_PRECISION = 0.01e18; + + /** + * @dev Choose a block where the Hourglass depositor, PT, CT, etc. are deployed + * and in a known state. Adjust as needed. + */ + function setUp() public { + _setUpFork(21_400_000); // Dec-14-2024 09:56:47 AM +UTC + } + + /** + * @dev Basic constructor test: deploy HourglassOracle with the PT as 'base' + * and the "underlying" (or CT, whichever is correct in your design) as 'quote'. + */ + function test_Constructor_Integrity_Hourglass() public { + HourglassOracle oracle = new HourglassOracle( + HOURGLASS_LBTCV_01MAR2025_PT, // base + HOURGLASS_LBTCV_01MAR2025_UNDERLYING, // quote + DISCOUNT_RATE_PER_SECOND // discount rate + ); + + // The contract returns "HourglassOracle" + assertEq(oracle.name(), "HourglassOracle"); + + // The base/quote we passed in + assertEq(oracle.base(), HOURGLASS_LBTCV_01MAR2025_PT); + assertEq(oracle.quote(), HOURGLASS_LBTCV_01MAR2025_UNDERLYING); + + // The discountRate we provided + assertEq(oracle.discountRate(), DISCOUNT_RATE_PER_SECOND); + + // You could also check that the "depositor" is set as expected: + // e.g. (from inside your HourglassOracle) "hourglassDepositor" + // But you only can do that if it's public or there's a getter. + // e.g., if hourglassDepositor is public: + assertEq(address(oracle.hourglassDepositor()), HOURGLASS_LBTCV_01MAR2025_DEPOSITOR); + } + + /** + * @dev Example "active market" test - calls getQuote() both ways (PT -> underlying, and underlying -> PT). + * This is analogous to your Pendle tests where you check the rate with no slippage, + * but you need to know what 1 PT is expected to be in "underlying" at this block. + */ + function test_GetQuote_ActiveMarket_LBTCV_01MAR2025_PT() public { + // Deploy the oracle + HourglassOracle oracle = new HourglassOracle( + HOURGLASS_LBTCV_01MAR2025_PT, // base + HOURGLASS_LBTCV_01MAR2025_UNDERLYING, // quote + DISCOUNT_RATE_PER_SECOND + ); + + // PT -> underlying + uint256 outAmount = oracle.getQuote(1e8, HOURGLASS_LBTCV_01MAR2025_PT, HOURGLASS_LBTCV_01MAR2025_UNDERLYING); + assertApproxEqRel(outAmount, 0.99707e8, REL_PRECISION); + + // Underlying -> PT + uint256 outAmountInv = oracle.getQuote(outAmount, HOURGLASS_LBTCV_01MAR2025_UNDERLYING, HOURGLASS_LBTCV_01MAR2025_PT); + assertApproxEqRel(outAmountInv, 1e8, REL_PRECISION); + } + + /** + * @dev Example "active market" test - calls getQuote() both ways (CT -> underlying, and underlying -> CT). + * This is analogous to your Pendle tests where you check the rate with no slippage, + * but you need to know what 1 CT is expected to be in "underlying" at this block. + */ + function test_GetQuote_ActiveMarket_LBTCV_01MAR2025_CT() public { + // Deploy the oracle + HourglassOracle oracle = new HourglassOracle( + HOURGLASS_LBTCV_01MAR2025_CT, // base + HOURGLASS_LBTCV_01MAR2025_UNDERLYING, // quote + DISCOUNT_RATE_PER_SECOND + ); + + // PT -> underlying + uint256 outAmount = oracle.getQuote(1e8, HOURGLASS_LBTCV_01MAR2025_CT, HOURGLASS_LBTCV_01MAR2025_UNDERLYING); + assertApproxEqRel(outAmount, 0.99707e8, REL_PRECISION); + + // Underlying -> PT + uint256 outAmountInv = oracle.getQuote(outAmount, HOURGLASS_LBTCV_01MAR2025_UNDERLYING, HOURGLASS_LBTCV_01MAR2025_CT); + assertApproxEqRel(outAmountInv, 1e8, REL_PRECISION); + } + + /** + * @dev Example "expired market" test. If your hourglass PT has matured by the fork block, + * then 1 PT might fully be worth exactly 1 underlying, or some final settled ratio. + */ + function test_GetQuote_ExpiredMarket() public { + // If the market for LBTCV_01MAR2025 is expired at the chosen block, you can test that 1 PT = 1 underlying + // or whatever the final settlement is. + HourglassOracle oracle = new HourglassOracle( + HOURGLASS_LBTCV_01DEC2024_PT, + HOURGLASS_LBTCV_01DEC2024_UNDERLYING, + DISCOUNT_RATE_PER_SECOND + ); + + uint256 outAmount = oracle.getQuote(1e8, HOURGLASS_LBTCV_01DEC2024_PT, HOURGLASS_LBTCV_01DEC2024_UNDERLYING); + assertEq(outAmount, 1e8); + } + + /** + * @dev If you expect invalid configuration (like discountRate=0 or base=quote, etc.), + * you can test that your HourglassOracle reverts. + */ + function test_Constructor_InvalidConfiguration() public { + // For example, discountRate = 0 => revert PriceOracle_InvalidConfiguration + vm.expectRevert(Errors.PriceOracle_InvalidConfiguration.selector); + new HourglassOracle( + HOURGLASS_LBTCV_01MAR2025_PT, + HOURGLASS_LBTCV_01MAR2025_UNDERLYING, + 0 // zero discount => revert + ); + } +} \ No newline at end of file diff --git a/test/adapter/hourglass/HourglassOracle.prop.t.sol b/test/adapter/hourglass/HourglassOracle.prop.t.sol new file mode 100644 index 00000000..6f786cdb --- /dev/null +++ b/test/adapter/hourglass/HourglassOracle.prop.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {AdapterPropTest} from "test/adapter/AdapterPropTest.sol"; +import {HourglassOracleHelper} from "test/adapter/hourglass/HourglassOracleHelper.sol"; + +contract HourglassOraclePropTest is HourglassOracleHelper, AdapterPropTest { + function testProp_Bidirectional(FuzzableState memory s, Prop_Bidirectional memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function testProp_NoOtherPaths(FuzzableState memory s, Prop_NoOtherPaths memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function testProp_IdempotentQuoteAndQuotes(FuzzableState memory s, Prop_IdempotentQuoteAndQuotes memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function testProp_SupportsZero(FuzzableState memory s, Prop_SupportsZero memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function testProp_ContinuousDomain(FuzzableState memory s, Prop_ContinuousDomain memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function testProp_OutAmountIncreasing(FuzzableState memory s, Prop_OutAmountIncreasing memory p) public { + setUpPropTest(s); + checkProp(p); + } + + function setUpPropTest(FuzzableState memory s) internal { + setUpState(s); + adapter = address(oracle); + base = s.base; + quote = s.quote; + } +} From a21bcd0d2f7b87ec031ed03f24adfcde7545e6db Mon Sep 17 00:00:00 2001 From: totomanov Date: Mon, 6 Jan 2025 11:16:03 +0200 Subject: [PATCH 5/5] feat: small tweaks --- src/adapter/hourglass/HourglassOracle.sol | 82 ++++++++----------- src/adapter/hourglass/IHourglassDepositor.sol | 48 +---------- src/adapter/hourglass/IHourglassERC20TBT.sol | 5 +- test/adapter/hourglass/HourglassAddresses.sol | 2 +- .../hourglass/HourglassOracle.fork.t.sol | 36 ++++---- .../hourglass/HourglassOracle.unit.t.sol | 3 +- .../hourglass/HourglassOracleHelper.sol | 33 +++----- 7 files changed, 69 insertions(+), 140 deletions(-) diff --git a/src/adapter/hourglass/HourglassOracle.sol b/src/adapter/hourglass/HourglassOracle.sol index b594dc8c..64b9747e 100644 --- a/src/adapter/hourglass/HourglassOracle.sol +++ b/src/adapter/hourglass/HourglassOracle.sol @@ -6,12 +6,16 @@ import {BaseAdapter, Errors, IPriceOracle} from "../BaseAdapter.sol"; import {ScaleUtils, Scale} from "../../lib/ScaleUtils.sol"; import {IHourglassDepositor} from "./IHourglassDepositor.sol"; import {IHourglassERC20TBT} from "./IHourglassERC20TBT.sol"; -import "forge-std/console.sol"; contract HourglassOracle is BaseAdapter { /// @inheritdoc IPriceOracle string public constant name = "HourglassOracle"; + /// @notice The number of decimals for the base token. + uint256 internal immutable baseTokenScale; + /// @notice The scale factors used for decimal conversions. + Scale internal immutable scale; + /// @notice The address of the base asset (e.g., PT or CT). address public immutable base; /// @notice The address of the quote asset (e.g., underlying asset). @@ -20,12 +24,9 @@ contract HourglassOracle is BaseAdapter { /// @notice Per second discount rate (scaled by 1e18). uint256 public immutable discountRate; - /// @notice Address of the Hourglass system. + /// @notice Address of the Hourglass depositor contract (pool-specific). IHourglassDepositor public immutable hourglassDepositor; - /// @notice The scale factors used for decimal conversions. - Scale internal immutable scale; - /// @notice The address of the combined token. address public immutable combinedToken; /// @notice The address of the principal token. @@ -33,15 +34,10 @@ contract HourglassOracle is BaseAdapter { /// @notice The address of the underlying token. address public immutable underlyingToken; - /// @notice The number of decimals for the base token. - uint8 public immutable baseTokenDecimals; - /// @notice The number of decimals for the quote token. - uint8 public immutable quoteTokenDecimals; - /// @notice Deploy the HourglassLinearDiscountOracle. /// @param _base The address of the base asset (PT or CT). /// @param _quote The address of the quote asset (underlying token). - /// @param _discountRate Discount rate (secondly, scaled by baseAssetDecimals). + /// @param _discountRate Discount rate (secondly, scaled by 1e18). constructor(address _base, address _quote, uint256 _discountRate) { if (_discountRate == 0) revert Errors.PriceOracle_InvalidConfiguration(); @@ -49,26 +45,22 @@ contract HourglassOracle is BaseAdapter { base = _base; quote = _quote; discountRate = _discountRate; - - // Fetch and store Hourglass depositor hourglassDepositor = IHourglassDepositor(IHourglassERC20TBT(_base).depositor()); - // Fetch token decimals - uint8 baseDecimals = _getDecimals(_base); - uint8 quoteDecimals = _getDecimals(_quote); - // Fetch token addresses address[] memory tokens = hourglassDepositor.getTokens(); combinedToken = tokens[0]; principalToken = tokens[1]; underlyingToken = hourglassDepositor.getUnderlying(); + // Only allow PT or CT as base token + if (_base != combinedToken && _base != principalToken) revert Errors.PriceOracle_InvalidConfiguration(); + // Calculate scale factors for decimal conversions + uint8 baseDecimals = _getDecimals(_base); + uint8 quoteDecimals = _getDecimals(_quote); scale = ScaleUtils.calcScale(baseDecimals, quoteDecimals, quoteDecimals); - - // Store decimals for normalization - baseTokenDecimals = baseDecimals; - quoteTokenDecimals = quoteDecimals; + baseTokenScale = 10 ** baseDecimals; } /// @notice Get a dynamic quote using linear discounting and solvency adjustment. @@ -84,7 +76,7 @@ contract HourglassOracle is BaseAdapter { // Calculate present value using linear discounting, baseTokenDecimals precision uint256 presentValue = _getUnitPresentValue(solvencyRatio); - + // Return scaled output amount return ScaleUtils.calcOutAmount(inAmount, presentValue, scale, inverse); } @@ -92,15 +84,13 @@ contract HourglassOracle is BaseAdapter { /// @notice Calculate the present value using linear discounting. /// @param solvencyRatio Solvency ratio of the Hourglass system (scaled by baseTokenDecimals). /// @return presentValue The present value of the input amount (scaled by baseTokenDecimals). - function _getUnitPresentValue(uint256 solvencyRatio) - internal - view - returns (uint256) - { - // Both inAmount and solvencyRatio have baseTokenDecimals precision - uint256 baseTokenScale = 10 ** baseTokenDecimals; + function _getUnitPresentValue(uint256 solvencyRatio) internal view returns (uint256) { + uint256 maturityTime = hourglassDepositor.maturity(); + + // Already matured, so PV = solvencyRatio. + if (maturityTime <= block.timestamp) return solvencyRatio; - uint256 timeToMaturity = _getTimeToMaturity(); + uint256 timeToMaturity = maturityTime - block.timestamp; // The expression (1e18 + discountRate * timeToMaturity) is ~1e18 scale // We want the denominator to be scaled to baseTokenDecimals so that when @@ -108,9 +98,10 @@ contract HourglassOracle is BaseAdapter { // we end up back with baseTokenDecimals in scale. uint256 scaledDenominator = ( - (1e18 + (discountRate * timeToMaturity)) // ~1e18 scale - * baseTokenScale // multiply by 1e(baseTokenDecimals) - ) / 1e18; // now scaledDenominator has baseTokenDecimals precision + (1e18 + (discountRate * timeToMaturity)) // ~1e18 scale + * baseTokenScale + ) // multiply by 1e(baseTokenDecimals) + / 1e18; // now scaledDenominator has baseTokenDecimals precision // (inAmount * solvencyRatio) is scale = 2 * baseTokenDecimals // dividing by scaledDenominator (scale = baseTokenDecimals) @@ -118,23 +109,22 @@ contract HourglassOracle is BaseAdapter { return (baseTokenScale * solvencyRatio) / scaledDenominator; } - /// @notice Fetch the time-to-maturity. - /// @return timeToMaturity Time-to-maturity in seconds. - function _getTimeToMaturity() internal view returns (uint256) { - uint256 maturityTime = hourglassDepositor.maturity(); - return maturityTime > block.timestamp ? maturityTime - block.timestamp : 0; - } - /// @notice Fetch the solvency ratio of the Hourglass system. - /// @return solvencyRatio Solvency ratio of the Hourglass system (scaled by _base token decimals). - function _getSolvencyRatio() internal view returns (uint256 solvencyRatio) { - uint256 baseTokenScale = 10 ** baseTokenDecimals; - uint256 underlyingTokenBalance = IERC20(underlyingToken).balanceOf(address(hourglassDepositor)); + /// @dev The ratio is capped to 1. The returned value is scaled by baseTokenDecimals. + /// @return solvencyRatio Solvency ratio of the Hourglass system (scaled by baseTokenDecimals). + function _getSolvencyRatio() internal view returns (uint256) { uint256 ptSupply = IERC20(principalToken).totalSupply(); uint256 ctSupply = IERC20(combinedToken).totalSupply(); - uint256 totalClaims = ptSupply + ctSupply; + if (totalClaims == 0) return baseTokenScale; + + uint256 underlyingTokenBalance = IERC20(underlyingToken).balanceOf(address(hourglassDepositor)); - return (totalClaims > 0) ? (underlyingTokenBalance * baseTokenScale) / totalClaims : baseTokenScale; + // Return the solvency as a ratio capped to 1. + if (underlyingTokenBalance < totalClaims) { + return underlyingTokenBalance * baseTokenScale / totalClaims; + } else { + return baseTokenScale; + } } } diff --git a/src/adapter/hourglass/IHourglassDepositor.sol b/src/adapter/hourglass/IHourglassDepositor.sol index 973a2679..182b8e5a 100644 --- a/src/adapter/hourglass/IHourglassDepositor.sol +++ b/src/adapter/hourglass/IHourglassDepositor.sol @@ -1,56 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.0; interface IHourglassDepositor { - function creationBlock() external view returns (uint256); - function deposit(uint256 amount, bool receiveSplit) external; - function depositFor(address user, uint256 amount, bool receiveSplit) external; - function depositTo(address principalRecipient, address pointRecipient, uint256 amount, bool receiveSplit) - external; - function enter(uint256 amountToBeDeposited) external; - function factory() external view returns (address); - function getPointToken() external view returns (address); - function getPrincipalToken() external view returns (address); function getTokens() external view returns (address[] memory); function getUnderlying() external view returns (address); function maturity() external view returns (uint256); - function recombine(uint256 amount) external; - function recoverToken(address token, address rewardsDistributor) external returns (uint256 amount); - function redeem(uint256 amount) external; - function redeemPrincipal(uint256 amount) external; - function setMaxDeposits(uint256 _depositCap) external; - function split(uint256 amount) external; - - event Deposit(address user, uint256 amount); - event DepositedTo(address principalRecipient, address pointRecipient, uint256 amount); - event Initialized(uint64 version); - event NewMaturityCreated(address combined, address principal, address yield); - event Recombine(address user, uint256 amount); - event Redeem(address user, uint256 amount); - event Split(address user, uint256 amount); - - error AddressEmptyCode(address target); - error AddressInsufficientBalance(address account); - error AlreadyEntered(); - error AmountMismatch(); - error CallerNotEntrant(); - error DepositCapExceeded(); - error FailedInnerCall(); - error InsufficientAssetSupplied(); - error InsufficientDeposit(); - error InsufficientFunds(); - error InvalidDecimals(); - error InvalidInitialization(); - error InvalidMaturity(); - error InvalidUnderlying(); - error Matured(); - error NoCode(); - error NotEntered(); - error NotInitializing(); - error PrematureRedeem(); - error RecipientMismatch(); - error SafeERC20FailedOperation(address token); - error UnauthorizedCaller(); } interface IVedaDepositor { diff --git a/src/adapter/hourglass/IHourglassERC20TBT.sol b/src/adapter/hourglass/IHourglassERC20TBT.sol index 63e17086..9e936511 100644 --- a/src/adapter/hourglass/IHourglassERC20TBT.sol +++ b/src/adapter/hourglass/IHourglassERC20TBT.sol @@ -1,9 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.0; interface IHourglassERC20TBT { function depositor() external view returns (address); - function initialize(string calldata _name, string calldata _symbol, uint8 __decimals) external; - function mint(address _to, uint256 _amount) external; - function burn(address _from, uint256 _amount) external; } diff --git a/test/adapter/hourglass/HourglassAddresses.sol b/test/adapter/hourglass/HourglassAddresses.sol index 17cb70c8..366a7f41 100644 --- a/test/adapter/hourglass/HourglassAddresses.sol +++ b/test/adapter/hourglass/HourglassAddresses.sol @@ -11,4 +11,4 @@ address constant HOURGLASS_LBTCV_01MAR2025_UNDERLYING = LBTCV; address constant HOURGLASS_LBTCV_01DEC2024_DEPOSITOR = 0xA285bca8f01c8F18953443e645ef2786D31ada99; address constant HOURGLASS_LBTCV_01DEC2024_CT = 0x0CB35DC9ADDce18669E2Fd5db4B405Ea655e98Bd; address constant HOURGLASS_LBTCV_01DEC2024_PT = 0xDB0Ee7308cF1F5A3f376D015a1545B4cB9A878D9; -address constant HOURGLASS_LBTCV_01DEC2024_UNDERLYING = LBTCV; \ No newline at end of file +address constant HOURGLASS_LBTCV_01DEC2024_UNDERLYING = LBTCV; diff --git a/test/adapter/hourglass/HourglassOracle.fork.t.sol b/test/adapter/hourglass/HourglassOracle.fork.t.sol index 0548af7a..6895a6a4 100644 --- a/test/adapter/hourglass/HourglassOracle.fork.t.sol +++ b/test/adapter/hourglass/HourglassOracle.fork.t.sol @@ -14,12 +14,12 @@ import {Errors} from "src/lib/Errors.sol"; import {IERC20} from "forge-std/interfaces/IERC20.sol"; import { - HOURGLASS_LBTCV_01MAR2025_DEPOSITOR, - HOURGLASS_LBTCV_01MAR2025_PT, - HOURGLASS_LBTCV_01MAR2025_CT, - HOURGLASS_LBTCV_01MAR2025_UNDERLYING, - HOURGLASS_LBTCV_01DEC2024_DEPOSITOR, - HOURGLASS_LBTCV_01DEC2024_PT, + HOURGLASS_LBTCV_01MAR2025_DEPOSITOR, + HOURGLASS_LBTCV_01MAR2025_PT, + HOURGLASS_LBTCV_01MAR2025_CT, + HOURGLASS_LBTCV_01MAR2025_UNDERLYING, + HOURGLASS_LBTCV_01DEC2024_DEPOSITOR, + HOURGLASS_LBTCV_01DEC2024_PT, HOURGLASS_LBTCV_01DEC2024_UNDERLYING } from "test/adapter/hourglass/HourglassAddresses.sol"; @@ -49,16 +49,16 @@ contract HourglassOracleForkTest is ForkTest { */ function test_Constructor_Integrity_Hourglass() public { HourglassOracle oracle = new HourglassOracle( - HOURGLASS_LBTCV_01MAR2025_PT, // base + HOURGLASS_LBTCV_01MAR2025_PT, // base HOURGLASS_LBTCV_01MAR2025_UNDERLYING, // quote - DISCOUNT_RATE_PER_SECOND // discount rate + DISCOUNT_RATE_PER_SECOND // discount rate ); // The contract returns "HourglassOracle" assertEq(oracle.name(), "HourglassOracle"); // The base/quote we passed in - assertEq(oracle.base(), HOURGLASS_LBTCV_01MAR2025_PT); + assertEq(oracle.base(), HOURGLASS_LBTCV_01MAR2025_PT); assertEq(oracle.quote(), HOURGLASS_LBTCV_01MAR2025_UNDERLYING); // The discountRate we provided @@ -79,7 +79,7 @@ contract HourglassOracleForkTest is ForkTest { function test_GetQuote_ActiveMarket_LBTCV_01MAR2025_PT() public { // Deploy the oracle HourglassOracle oracle = new HourglassOracle( - HOURGLASS_LBTCV_01MAR2025_PT, // base + HOURGLASS_LBTCV_01MAR2025_PT, // base HOURGLASS_LBTCV_01MAR2025_UNDERLYING, // quote DISCOUNT_RATE_PER_SECOND ); @@ -89,11 +89,12 @@ contract HourglassOracleForkTest is ForkTest { assertApproxEqRel(outAmount, 0.99707e8, REL_PRECISION); // Underlying -> PT - uint256 outAmountInv = oracle.getQuote(outAmount, HOURGLASS_LBTCV_01MAR2025_UNDERLYING, HOURGLASS_LBTCV_01MAR2025_PT); + uint256 outAmountInv = + oracle.getQuote(outAmount, HOURGLASS_LBTCV_01MAR2025_UNDERLYING, HOURGLASS_LBTCV_01MAR2025_PT); assertApproxEqRel(outAmountInv, 1e8, REL_PRECISION); } - /** + /** * @dev Example "active market" test - calls getQuote() both ways (CT -> underlying, and underlying -> CT). * This is analogous to your Pendle tests where you check the rate with no slippage, * but you need to know what 1 CT is expected to be in "underlying" at this block. @@ -101,7 +102,7 @@ contract HourglassOracleForkTest is ForkTest { function test_GetQuote_ActiveMarket_LBTCV_01MAR2025_CT() public { // Deploy the oracle HourglassOracle oracle = new HourglassOracle( - HOURGLASS_LBTCV_01MAR2025_CT, // base + HOURGLASS_LBTCV_01MAR2025_CT, // base HOURGLASS_LBTCV_01MAR2025_UNDERLYING, // quote DISCOUNT_RATE_PER_SECOND ); @@ -111,7 +112,8 @@ contract HourglassOracleForkTest is ForkTest { assertApproxEqRel(outAmount, 0.99707e8, REL_PRECISION); // Underlying -> PT - uint256 outAmountInv = oracle.getQuote(outAmount, HOURGLASS_LBTCV_01MAR2025_UNDERLYING, HOURGLASS_LBTCV_01MAR2025_CT); + uint256 outAmountInv = + oracle.getQuote(outAmount, HOURGLASS_LBTCV_01MAR2025_UNDERLYING, HOURGLASS_LBTCV_01MAR2025_CT); assertApproxEqRel(outAmountInv, 1e8, REL_PRECISION); } @@ -123,9 +125,7 @@ contract HourglassOracleForkTest is ForkTest { // If the market for LBTCV_01MAR2025 is expired at the chosen block, you can test that 1 PT = 1 underlying // or whatever the final settlement is. HourglassOracle oracle = new HourglassOracle( - HOURGLASS_LBTCV_01DEC2024_PT, - HOURGLASS_LBTCV_01DEC2024_UNDERLYING, - DISCOUNT_RATE_PER_SECOND + HOURGLASS_LBTCV_01DEC2024_PT, HOURGLASS_LBTCV_01DEC2024_UNDERLYING, DISCOUNT_RATE_PER_SECOND ); uint256 outAmount = oracle.getQuote(1e8, HOURGLASS_LBTCV_01DEC2024_PT, HOURGLASS_LBTCV_01DEC2024_UNDERLYING); @@ -145,4 +145,4 @@ contract HourglassOracleForkTest is ForkTest { 0 // zero discount => revert ); } -} \ No newline at end of file +} diff --git a/test/adapter/hourglass/HourglassOracle.unit.t.sol b/test/adapter/hourglass/HourglassOracle.unit.t.sol index c840a6eb..e294c4dc 100644 --- a/test/adapter/hourglass/HourglassOracle.unit.t.sol +++ b/test/adapter/hourglass/HourglassOracle.unit.t.sol @@ -44,7 +44,8 @@ contract HourglassOracleTest is HourglassOracleHelper { assertEq(bidOutAmount, outAmount); assertEq(askOutAmount, outAmount); uint256 outAmountInv = HourglassOracle(oracle).getQuote(s.inAmount, s.quote, s.base); - (uint256 bidOutAmountInv, uint256 askOutAmountInv) = HourglassOracle(oracle).getQuotes(s.inAmount, s.quote, s.base); + (uint256 bidOutAmountInv, uint256 askOutAmountInv) = + HourglassOracle(oracle).getQuotes(s.inAmount, s.quote, s.base); assertEq(bidOutAmountInv, outAmountInv); assertEq(askOutAmountInv, outAmountInv); } diff --git a/test/adapter/hourglass/HourglassOracleHelper.sol b/test/adapter/hourglass/HourglassOracleHelper.sol index d680932f..8e201cfb 100644 --- a/test/adapter/hourglass/HourglassOracleHelper.sol +++ b/test/adapter/hourglass/HourglassOracleHelper.sol @@ -6,8 +6,7 @@ import {AdapterHelper} from "test/adapter/AdapterHelper.sol"; import {boundAddr, distinct} from "test/utils/TestUtils.sol"; import {HourglassOracle} from "src/adapter/hourglass/HourglassOracle.sol"; import {IHourglassDepositor} from "src/adapter/hourglass/IHourglassDepositor.sol"; - import "forge-std/console.sol"; - +import "forge-std/console.sol"; contract HourglassOracleHelper is AdapterHelper { struct FuzzableState { @@ -28,6 +27,7 @@ contract HourglassOracleHelper is AdapterHelper { uint256 ctSupply; // Environment uint256 inAmount; + bool baseIsPt; } function setUpState(FuzzableState memory s) internal { @@ -48,8 +48,8 @@ contract HourglassOracleHelper is AdapterHelper { // Ensure ptSupply and ctSupply add up to underlyingTokenBalance uint256 maxSupply = s.underlyingTokenBalance; - s.ptSupply = bound(s.ptSupply, 0, maxSupply); - s.ctSupply = maxSupply - s.ptSupply; + s.ptSupply = bound(s.ptSupply, 0, s.underlyingTokenBalance); + s.ctSupply = s.underlyingTokenBalance - s.ptSupply; s.expiry = bound(s.expiry, block.timestamp, block.timestamp + 365 days); s.inAmount = bound(s.inAmount, 0, 1e18); @@ -68,10 +68,9 @@ contract HourglassOracleHelper is AdapterHelper { console.log("s.expiry: %s", s.expiry); console.log("s.inAmount: %s", s.inAmount); - // Assume distinct addresses - vm.assume(distinct(s.base, s.quote, s.depositor, s.pt, s.ct, s.pyt, s.underlyingToken)); - vm.assume(s.base != s.quote); + vm.assume(distinct(s.quote, s.depositor, s.pt, s.ct, s.pyt, s.underlyingToken)); + s.base = s.baseIsPt ? s.pt : s.ct; // Prepare the dynamic array address[] memory tokens = new address[](3); @@ -92,25 +91,13 @@ contract HourglassOracleHelper is AdapterHelper { abi.encode(s.underlyingToken) ); - vm.mockCall( - s.depositor, - abi.encodeWithSelector(IHourglassDepositor.maturity.selector), - abi.encode(s.expiry) - ); + vm.mockCall(s.depositor, abi.encodeWithSelector(IHourglassDepositor.maturity.selector), abi.encode(s.expiry)); // Mock the Principal Token - vm.mockCall( - s.pt, - abi.encodeWithSelector(IERC20.totalSupply.selector), - abi.encode(s.ptSupply) - ); + vm.mockCall(s.pt, abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(s.ptSupply)); // Mock the Combined Token - vm.mockCall( - s.ct, - abi.encodeWithSelector(IERC20.totalSupply.selector), - abi.encode(s.ctSupply) - ); + vm.mockCall(s.ct, abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(s.ctSupply)); // Mock the Underlying Token vm.mockCall( @@ -148,7 +135,7 @@ contract HourglassOracleHelper is AdapterHelper { oracle = address(new HourglassOracle(s.base, s.quote, s.discountRate)); HourglassOracle hourglassOracle = HourglassOracle(oracle); - console.log("oracle dr: %s", hourglassOracle.discountRate()); // Re-bound s.inAmount to some smaller range if needed + console.log("oracle dr: %s", hourglassOracle.discountRate()); // Re-bound s.inAmount to some smaller range if needed s.inAmount = bound(s.inAmount, 0, type(uint128).max); } }