diff --git a/src/adapter/hourglass/HourglassOracle.sol b/src/adapter/hourglass/HourglassOracle.sol new file mode 100644 index 00000000..64b9747e --- /dev/null +++ b/src/adapter/hourglass/HourglassOracle.sol @@ -0,0 +1,130 @@ +// 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 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). + address public immutable quote; + + /// @notice Per second discount rate (scaled by 1e18). + uint256 public immutable discountRate; + + /// @notice Address of the Hourglass depositor contract (pool-specific). + IHourglassDepositor public immutable hourglassDepositor; + + /// @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 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 1e18). + constructor(address _base, address _quote, uint256 _discountRate) { + if (_discountRate == 0) revert Errors.PriceOracle_InvalidConfiguration(); + + // Initialize key parameters + base = _base; + quote = _quote; + discountRate = _discountRate; + hourglassDepositor = IHourglassDepositor(IHourglassERC20TBT(_base).depositor()); + + // 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); + baseTokenScale = 10 ** baseDecimals; + } + + /// @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 = _getUnitPresentValue(solvencyRatio); + + // Return scaled output amount + return ScaleUtils.calcOutAmount(inAmount, presentValue, scale, inverse); + } + + /// @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) { + uint256 maturityTime = hourglassDepositor.maturity(); + + // Already matured, so PV = solvencyRatio. + if (maturityTime <= block.timestamp) return solvencyRatio; + + 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 + // 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 solvency ratio of the Hourglass system. + /// @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 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 new file mode 100644 index 00000000..182b8e5a --- /dev/null +++ b/src/adapter/hourglass/IHourglassDepositor.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IHourglassDepositor { + function getTokens() external view returns (address[] memory); + function getUnderlying() external view returns (address); + function maturity() external view returns (uint256); +} + +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..9e936511 --- /dev/null +++ b/src/adapter/hourglass/IHourglassERC20TBT.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IHourglassERC20TBT { + function depositor() external view returns (address); +} diff --git a/test/adapter/hourglass/HourglassAddresses.sol b/test/adapter/hourglass/HourglassAddresses.sol new file mode 100644 index 00000000..366a7f41 --- /dev/null +++ b/test/adapter/hourglass/HourglassAddresses.sol @@ -0,0 +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; +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; diff --git a/test/adapter/hourglass/HourglassOracle.fork.t.sol b/test/adapter/hourglass/HourglassOracle.fork.t.sol new file mode 100644 index 00000000..6895a6a4 --- /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 + ); + } +} 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; + } +} diff --git a/test/adapter/hourglass/HourglassOracle.unit.t.sol b/test/adapter/hourglass/HourglassOracle.unit.t.sol new file mode 100644 index 00000000..e294c4dc --- /dev/null +++ b/test/adapter/hourglass/HourglassOracle.unit.t.sol @@ -0,0 +1,52 @@ +// 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 new file mode 100644 index 00000000..8e201cfb --- /dev/null +++ b/test/adapter/hourglass/HourglassOracleHelper.sol @@ -0,0 +1,141 @@ +// 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"; +import "forge-std/console.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; + bool baseIsPt; + } + + function setUpState(FuzzableState memory s) internal { + // Set reasonable bounds for addresses + s.base = boundAddr(s.base); + s.quote = boundAddr(s.quote); + 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); + + // 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, 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); + + 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.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); + tokens[0] = s.ct; + tokens[1] = s.pt; + tokens[2] = s.pyt; + + // Then encode *that array* in the mock + vm.mockCall( + s.depositor, + abi.encodeWithSelector(IHourglassDepositor.getTokens.selector), + abi.encode(tokens) // This is the correct way to encode a dynamic array + ); + + vm.mockCall( + s.depositor, + abi.encodeWithSelector(IHourglassDepositor.getUnderlying.selector), + abi.encode(s.underlyingToken) + ); + + 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)); + + // Mock the Combined Token + vm.mockCall(s.ct, abi.encodeWithSelector(IERC20.totalSupply.selector), abi.encode(s.ctSupply)); + + // Mock the Underlying Token + vm.mockCall( + 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); + } +} 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;