Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
11 changes: 11 additions & 0 deletions src/accounting/oracles/AbstractYieldSourceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ abstract contract AbstractYieldSourceOracle is IYieldSourceOracle {
virtual
returns (uint256);

/// @inheritdoc IYieldSourceOracle
function getWithdrawalShareOutput(
address yieldSourceAddress,
address assetIn,
uint256 assetsIn
)
external
view
virtual
returns (uint256);

/// @inheritdoc IYieldSourceOracle
function getAssetOutput(
address yieldSourceAddress,
Expand Down
14 changes: 14 additions & 0 deletions src/accounting/oracles/ERC4626YieldSourceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ contract ERC4626YieldSourceOracle is AbstractYieldSourceOracle {
return IERC4626(yieldSourceAddress).previewDeposit(assetsIn);
}

/// @inheritdoc AbstractYieldSourceOracle
function getWithdrawalShareOutput(
address yieldSourceAddress,
address,
uint256 assetsIn
)
external
view
override
returns (uint256)
{
return IERC4626(yieldSourceAddress).previewWithdraw(assetsIn);
}

/// @inheritdoc AbstractYieldSourceOracle
function getAssetOutput(
address yieldSourceAddress,
Expand Down
16 changes: 16 additions & 0 deletions src/accounting/oracles/ERC5115YieldSourceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ contract ERC5115YieldSourceOracle is AbstractYieldSourceOracle {
return IStandardizedYield(yieldSourceAddress).previewDeposit(assetIn, assetsIn);
}

/// @inheritdoc AbstractYieldSourceOracle
function getWithdrawalShareOutput(
address yieldSourceAddress,
address assetIn,
uint256 assetsIn
)
external
view
override
returns (uint256)
{
uint256 assetsPerShare = IStandardizedYield(yieldSourceAddress).previewRedeem(assetIn, 1e18);
if (assetsPerShare == 0) return 0;
return Math.mulDiv(assetsIn, 1e18, assetsPerShare, Math.Rounding.Ceil);
}

/// @inheritdoc AbstractYieldSourceOracle
function getAssetOutput(
address yieldSourceAddress,
Expand Down
18 changes: 18 additions & 0 deletions src/accounting/oracles/ERC7540YieldSourceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity 0.8.30;
import { IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
import { IERC7540 } from "../../vendor/vaults/7540/IERC7540.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

// Superform
import { AbstractYieldSourceOracle } from "./AbstractYieldSourceOracle.sol";
Expand Down Expand Up @@ -38,6 +39,22 @@ contract ERC7540YieldSourceOracle is AbstractYieldSourceOracle {
return IERC7540(yieldSourceAddress).convertToShares(assetsIn);
}

/// @inheritdoc AbstractYieldSourceOracle
function getWithdrawalShareOutput(
address yieldSourceAddress,
address,
uint256 assetsIn
)
external
view
override
returns (uint256)
{
uint256 assetsPerShare = IERC7540(yieldSourceAddress).convertToAssets(1e18);
if (assetsPerShare == 0) return 0;
return Math.mulDiv(assetsIn, 1e18, assetsPerShare, Math.Rounding.Ceil);
}

/// @inheritdoc AbstractYieldSourceOracle
function getAssetOutput(
address yieldSourceAddress,
Expand Down Expand Up @@ -91,4 +108,5 @@ contract ERC7540YieldSourceOracle is AbstractYieldSourceOracle {
function getTVL(address yieldSourceAddress) public view override returns (uint256) {
return IERC7540(yieldSourceAddress).totalAssets();
}

}
30 changes: 30 additions & 0 deletions src/accounting/oracles/PendlePTYieldSourceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,37 @@ contract PendlePTYieldSourceOracle is AbstractYieldSourceOracle {
// Scale down if assetDecimals < 18
// Avoids underflow in 10**(PRICE_DECIMALS - assetDecimals) which happens in the division below
assetsOut = Math.mulDiv(assetsOut18, 1, 10 ** (PRICE_DECIMALS - assetDecimals));
}
}

/// @inheritdoc AbstractYieldSourceOracle
function getWithdrawalShareOutput(
address market,
address,
uint256 assetsIn
)
external
view
override
returns (uint256)
{
uint256 pricePerShare = getPricePerShare(market); // PT / Asset in 1e18
if (pricePerShare == 0) return 0;

IStandardizedYield sY = IStandardizedYield(_sy(market));
(,, uint8 assetDecimals) = _getAssetInfo(sY);
uint8 ptDecimals = IERC20Metadata(_pt(market)).decimals();

// First scale assetsIn into 1e18 terms
uint256 assetsIn18;
if (assetDecimals <= PRICE_DECIMALS) {
assetsIn18 = assetsIn * (10 ** (PRICE_DECIMALS - assetDecimals));
} else {
assetsIn18 = Math.mulDiv(assetsIn, 1, 10 ** (assetDecimals - PRICE_DECIMALS));
}

// Compute how many PT shares are needed: shares = assetsIn18 * 10^ptDecimals / pricePerShare
return Math.mulDiv(assetsIn18, 10 ** uint256(ptDecimals), pricePerShare, Math.Rounding.Ceil);
}

/// @inheritdoc IYieldSourceOracle
Expand Down
19 changes: 17 additions & 2 deletions src/accounting/oracles/SpectraPTYieldSourceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.30;

// external
import { IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

import { IPrincipalToken } from "../../vendor/spectra/IPrincipalToken.sol";
// Superform
Expand All @@ -25,10 +26,25 @@ contract SpectraPTYieldSourceOracle is AbstractYieldSourceOracle {

/// @inheritdoc AbstractYieldSourceOracle
function getShareOutput(address ptAddress, address, uint256 assetsIn) external view override returns (uint256) {
// Use convertToPrincipal to get shares (PTs) for assets
return IPrincipalToken(ptAddress).convertToPrincipal(assetsIn);
}

/// @inheritdoc AbstractYieldSourceOracle
function getWithdrawalShareOutput(
address ptAddress,
address,
uint256 assetsIn
)
external
view
override
returns (uint256)
{
uint256 underlyingPerShare = IPrincipalToken(ptAddress).convertToUnderlying(10 ** _decimals(ptAddress));
if (underlyingPerShare == 0) return 0;
return Math.mulDiv(assetsIn, 10 ** _decimals(ptAddress), underlyingPerShare, Math.Rounding.Ceil);
}

/// @inheritdoc AbstractYieldSourceOracle
function getAssetOutput(
address ptAddress,
Expand All @@ -40,7 +56,6 @@ contract SpectraPTYieldSourceOracle is AbstractYieldSourceOracle {
override
returns (uint256)
{
// Use convertToUnderlying to get assets for shares (PTs)
return IPrincipalToken(ptAddress).convertToUnderlying(sharesIn);
}

Expand Down
15 changes: 15 additions & 0 deletions src/accounting/oracles/StakingYieldSourceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity 0.8.30;

// external
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

// Superform
import { AbstractYieldSourceOracle } from "./AbstractYieldSourceOracle.sol";
Expand Down Expand Up @@ -32,6 +33,20 @@ contract StakingYieldSourceOracle is AbstractYieldSourceOracle {
return assetsIn;
}

/// @inheritdoc AbstractYieldSourceOracle
function getWithdrawalShareOutput(
address,
address,
uint256 assetsIn
)
external
view
override
returns (uint256)
{
return assetsIn;
}

/// @inheritdoc AbstractYieldSourceOracle
function getAssetOutput(address, address, uint256 sharesIn) public pure override returns (uint256) {
return sharesIn;
Expand Down
15 changes: 15 additions & 0 deletions src/interfaces/accounting/IYieldSourceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ interface IYieldSourceOracle {
view
returns (uint256);

/// @notice Calculates the amount of shares that would be obtained when withdrawing a given amount of assets
/// @dev Used by oracles to simulate withdrawals and to derive the current exchange rate
/// @param yieldSourceAddress The address of the yield-bearing token (e.g., aUSDC, cDAI)
/// @param assetIn The address of the underlying asset to be withdrawn (e.g., USDC, DAI)
/// @param assetsIn The amount of underlying assets to withdraw, denominated in the asset’s native units
/// @return shares The amount of yield-bearing shares that would be received after withdrawal
function getWithdrawalShareOutput(
address yieldSourceAddress,
address assetIn,
uint256 assetsIn
)
external
view
returns (uint256);

/// @notice Calculates the number of underlying assets that would be received for a given amount of shares
/// @dev Used for withdrawal simulations and to calculate current yield
/// @param yieldSourceAddress The yield-bearing token address (e.g., aUSDC, cDAI)
Expand Down
4 changes: 4 additions & 0 deletions test/mocks/MockYieldSourceOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ contract MockYieldSourceOracle is IYieldSourceOracle {
return assetsIn;
}

function getWithdrawalShareOutput(address, address, uint256 assetsIn) external pure override returns (uint256) {
return assetsIn;
}

function getAssetOutput(address, address, uint256 sharesIn) public pure returns (uint256) {
return sharesIn;
}
Expand Down
4 changes: 4 additions & 0 deletions test/unit/accounting/AbstractYieldSourceOracleTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ contract MockYieldSourceOracle is AbstractYieldSourceOracle {
return assetsIn;
}

function getWithdrawalShareOutput(address, address, uint256 assetsIn) external pure override returns (uint256) {
return assetsIn;
}

function getAssetOutput(address, address, uint256 sharesIn) public pure override returns (uint256) {
return sharesIn;
}
Expand Down
39 changes: 39 additions & 0 deletions test/unit/accounting/YieldSourceOracles.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Mock5115Vault } from "../../mocks/Mock5115Vault.sol";

import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { IStakingVault } from "../../../src/vendor/staking/IStakingVault.sol";

import { ERC4626YieldSourceOracle } from "../../../src/accounting/oracles/ERC4626YieldSourceOracle.sol";
Expand Down Expand Up @@ -131,6 +132,44 @@ contract YieldSourceOraclesTest is Helpers {
assertEq(actualShares, assetsIn); // For staking vaults, shares = assets
}

/*//////////////////////////////////////////////////////////////
WITHDRAWAL SHARE OUTPUT TESTS
//////////////////////////////////////////////////////////////*/
function test_ERC4626_getWithdrawalShareOutput() public view {
uint256 assetsIn = 1e18;
uint256 expectedShares = erc4626.previewWithdraw(assetsIn);
uint256 actualShares =
erc4626YieldSourceOracle.getWithdrawalShareOutput(address(erc4626), address(0), assetsIn);
assertEq(actualShares, expectedShares);
}

function test_ERC7540_getWithdrawalShareOutput() public view {
uint256 assetsIn = 1e18;
// For ERC7540: calculate shares needed to withdraw assetsIn
uint256 assetsPerShare = erc7540.convertToAssets(1e18);
uint256 expectedShares = Math.mulDiv(assetsIn, 1e18, assetsPerShare, Math.Rounding.Ceil);
uint256 actualShares =
erc7540YieldSourceOracle.getWithdrawalShareOutput(address(erc7540), address(0), assetsIn);
assertEq(actualShares, expectedShares);
}

function test_ERC5115_getWithdrawalShareOutput() public view {
uint256 assetsIn = 1e18;
// For ERC5115: calculate shares needed to withdraw assetsIn
uint256 assetsPerShare = erc5115.previewRedeem(address(asset), 1e18);
uint256 expectedShares = Math.mulDiv(assetsIn, 1e18, assetsPerShare, Math.Rounding.Ceil);
uint256 actualShares =
erc5115YieldSourceOracle.getWithdrawalShareOutput(address(erc5115), address(0), assetsIn);
assertEq(actualShares, expectedShares);
}

function test_Staking_getWithdrawalShareOutput() public view {
uint256 assetsIn = 1e18;
uint256 actualShares =
stakingYieldSourceOracle.getWithdrawalShareOutput(address(stakingVault), address(0), assetsIn);
assertEq(actualShares, assetsIn); // For staking vaults, shares needed = assets (1:1 ratio)
}

/*//////////////////////////////////////////////////////////////
ASSET OUTPUT TESTS
//////////////////////////////////////////////////////////////*/
Expand Down