Skip to content

fix: small tweaks #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 36 additions & 46 deletions src/adapter/hourglass/HourglassOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -20,55 +24,43 @@ 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.
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).
/// @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;

// 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.
Expand All @@ -84,57 +76,55 @@ 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);
}

/// @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
// 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
(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.
/// @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;
}
}
}
48 changes: 1 addition & 47 deletions src/adapter/hourglass/IHourglassDepositor.sol
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
5 changes: 1 addition & 4 deletions src/adapter/hourglass/IHourglassERC20TBT.sol
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion test/adapter/hourglass/HourglassAddresses.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
address constant HOURGLASS_LBTCV_01DEC2024_UNDERLYING = LBTCV;
36 changes: 18 additions & 18 deletions test/adapter/hourglass/HourglassOracle.fork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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
Expand All @@ -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
);
Expand All @@ -89,19 +89,20 @@ 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.
*/
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
);
Expand All @@ -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);
}

Expand All @@ -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);
Expand All @@ -145,4 +145,4 @@ contract HourglassOracleForkTest is ForkTest {
0 // zero discount => revert
);
}
}
}
3 changes: 2 additions & 1 deletion test/adapter/hourglass/HourglassOracle.unit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading