Skip to content

Commit 71ae51d

Browse files
feat: added withdrawal output with asset in for oracles [SUP-15736] (#822)
Co-authored-by: 0xTimepunk <[email protected]>
1 parent 20bc881 commit 71ae51d

12 files changed

+209
-6
lines changed

SECURITY.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,7 @@ The destination executor supports only one leaf per destination in the Merkle ro
3939
The protocol allows signatures with no expiration. While convenient, these signatures can pose risks in certain operational contexts and should be used carefully.
4040

4141
#### 11. Multiple valid execution paths
42-
Once an intent is signed, there are several valid methods to execute it, even in cases where the associated bridge transaction has not completed successfully. This provides flexibility but requires careful handling by integrators to avoid unintended consequences.
42+
Once an intent is signed, there are several valid methods to execute it, even in cases where the associated bridge transaction has not completed successfully. This provides flexibility but requires careful handling by integrators to avoid unintended consequences.
43+
44+
#### 12. Token decimals
45+
Superform's yield sources and oracle calculations are designed with ERC-20 assets that use up to 18 decimals of precision in mind. Yield source assets with more than 18 decimals are considered non-standard and unsupported.

src/accounting/oracles/AbstractYieldSourceOracle.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ abstract contract AbstractYieldSourceOracle is IYieldSourceOracle {
4848
virtual
4949
returns (uint256);
5050

51+
/// @inheritdoc IYieldSourceOracle
52+
function getWithdrawalShareOutput(
53+
address yieldSourceAddress,
54+
address assetIn,
55+
uint256 assetsIn
56+
)
57+
external
58+
view
59+
virtual
60+
returns (uint256);
61+
5162
/// @inheritdoc IYieldSourceOracle
5263
function getAssetOutput(
5364
address yieldSourceAddress,

src/accounting/oracles/ERC4626YieldSourceOracle.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ contract ERC4626YieldSourceOracle is AbstractYieldSourceOracle {
3636
return IERC4626(yieldSourceAddress).previewDeposit(assetsIn);
3737
}
3838

39+
/// @inheritdoc AbstractYieldSourceOracle
40+
function getWithdrawalShareOutput(
41+
address yieldSourceAddress,
42+
address,
43+
uint256 assetsIn
44+
)
45+
external
46+
view
47+
override
48+
returns (uint256)
49+
{
50+
return IERC4626(yieldSourceAddress).previewWithdraw(assetsIn);
51+
}
52+
3953
/// @inheritdoc AbstractYieldSourceOracle
4054
function getAssetOutput(
4155
address yieldSourceAddress,

src/accounting/oracles/ERC5115YieldSourceOracle.sol

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,28 @@ contract ERC5115YieldSourceOracle is AbstractYieldSourceOracle {
1717
/*//////////////////////////////////////////////////////////////
1818
EXTERNAL FUNCTIONS
1919
//////////////////////////////////////////////////////////////*/
20-
/// @inheritdoc AbstractYieldSourceOracle
21-
function decimals(address /*yieldSourceAddress*/ ) public pure override returns (uint8) {
20+
/// exchangeRate() returns price scaled to 1e18 precision, independent of SY token or asset decimals.
21+
/// This ensures correct normalization in mulDiv operations.
22+
/// See https://eips.ethereum.org/EIPS/eip-5115#methods -> exchangeRate()
23+
/// The name decimals() here is ambiguous because it is a function used in other areas of the code for scaling (but
24+
/// it doesn't refer to the SY decimals)
25+
/// Calculation Examples in the Oracle:
26+
/// - In getTVL: Math.mulDiv(totalShares, yieldSource.exchangeRate(), 1e18). Here, totalShares is in SY decimals
27+
/// (D), exchangeRate is (totalAssets * 1e18) / totalShares (per EIP, with totalAssets in asset decimals A). This
28+
/// simplifies to totalAssets, correctly outputting the asset amount regardless of D or A.
29+
/// - In getWithdrawalShareOutput: previewRedeem(assetIn, 1e18) gets assets for 1e18 SY share units, then
30+
/// mulDiv(assetsIn, 1e18, assetsPerShare, Ceil) computes the required share units. The 1e18 acts as a precision
31+
/// scaler (matching EIP), not an assumption about D. For example, with a 6-decimal SY (like Pendle's SY-syrupUSDC)
32+
/// and initial 1:1 rate, it correctly computes shares without issues.
33+
/// - This pattern holds for other functions like getAssetOutput (direct previewRedeem without scaling assumptions).
34+
function decimals(
35+
address /*yieldSourceAddress*/
36+
)
37+
public
38+
pure
39+
override
40+
returns (uint8)
41+
{
2242
return 18;
2343
}
2444

@@ -36,6 +56,22 @@ contract ERC5115YieldSourceOracle is AbstractYieldSourceOracle {
3656
return IStandardizedYield(yieldSourceAddress).previewDeposit(assetIn, assetsIn);
3757
}
3858

59+
/// @inheritdoc AbstractYieldSourceOracle
60+
function getWithdrawalShareOutput(
61+
address yieldSourceAddress,
62+
address assetIn,
63+
uint256 assetsIn
64+
)
65+
external
66+
view
67+
override
68+
returns (uint256)
69+
{
70+
uint256 assetsPerShare = IStandardizedYield(yieldSourceAddress).previewRedeem(assetIn, 1e18);
71+
if (assetsPerShare == 0) return 0;
72+
return Math.mulDiv(assetsIn, 1e18, assetsPerShare, Math.Rounding.Ceil);
73+
}
74+
3975
/// @inheritdoc AbstractYieldSourceOracle
4076
function getAssetOutput(
4177
address yieldSourceAddress,

src/accounting/oracles/ERC7540YieldSourceOracle.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pragma solidity 0.8.30;
55
import { IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
66
import { IERC7540 } from "../../vendor/vaults/7540/IERC7540.sol";
77
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8+
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
89

910
// Superform
1011
import { AbstractYieldSourceOracle } from "./AbstractYieldSourceOracle.sol";
@@ -38,6 +39,22 @@ contract ERC7540YieldSourceOracle is AbstractYieldSourceOracle {
3839
return IERC7540(yieldSourceAddress).convertToShares(assetsIn);
3940
}
4041

42+
/// @inheritdoc AbstractYieldSourceOracle
43+
function getWithdrawalShareOutput(
44+
address yieldSourceAddress,
45+
address,
46+
uint256 assetsIn
47+
)
48+
external
49+
view
50+
override
51+
returns (uint256)
52+
{
53+
uint256 assetsPerShare = IERC7540(yieldSourceAddress).convertToAssets(1e18);
54+
if (assetsPerShare == 0) return 0;
55+
return Math.mulDiv(assetsIn, 1e18, assetsPerShare, Math.Rounding.Ceil);
56+
}
57+
4158
/// @inheritdoc AbstractYieldSourceOracle
4259
function getAssetOutput(
4360
address yieldSourceAddress,
@@ -91,4 +108,5 @@ contract ERC7540YieldSourceOracle is AbstractYieldSourceOracle {
91108
function getTVL(address yieldSourceAddress) public view override returns (uint256) {
92109
return IERC7540(yieldSourceAddress).totalAssets();
93110
}
111+
94112
}

src/accounting/oracles/PendlePTYieldSourceOracle.sol

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,37 @@ contract PendlePTYieldSourceOracle is AbstractYieldSourceOracle {
115115
// Scale down if assetDecimals < 18
116116
// Avoids underflow in 10**(PRICE_DECIMALS - assetDecimals) which happens in the division below
117117
assetsOut = Math.mulDiv(assetsOut18, 1, 10 ** (PRICE_DECIMALS - assetDecimals));
118+
}
119+
}
120+
121+
/// @inheritdoc AbstractYieldSourceOracle
122+
function getWithdrawalShareOutput(
123+
address market,
124+
address,
125+
uint256 assetsIn
126+
)
127+
external
128+
view
129+
override
130+
returns (uint256)
131+
{
132+
uint256 pricePerShare = getPricePerShare(market); // PT / Asset in 1e18
133+
if (pricePerShare == 0) return 0;
134+
135+
IStandardizedYield sY = IStandardizedYield(_sy(market));
136+
(,, uint8 assetDecimals) = _getAssetInfo(sY);
137+
uint8 ptDecimals = IERC20Metadata(_pt(market)).decimals();
138+
139+
// First scale assetsIn into 1e18 terms
140+
uint256 assetsIn18;
141+
if (assetDecimals <= PRICE_DECIMALS) {
142+
assetsIn18 = assetsIn * (10 ** (PRICE_DECIMALS - assetDecimals));
143+
} else {
144+
assetsIn18 = Math.mulDiv(assetsIn, 1, 10 ** (assetDecimals - PRICE_DECIMALS));
118145
}
146+
147+
// Compute how many PT shares are needed: shares = assetsIn18 * 10^ptDecimals / pricePerShare
148+
return Math.mulDiv(assetsIn18, 10 ** uint256(ptDecimals), pricePerShare, Math.Rounding.Ceil);
119149
}
120150

121151
/// @inheritdoc IYieldSourceOracle

src/accounting/oracles/SpectraPTYieldSourceOracle.sol

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity 0.8.30;
33

44
// external
55
import { IERC20Metadata } from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
6+
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
67

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

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

32+
/// @inheritdoc AbstractYieldSourceOracle
33+
function getWithdrawalShareOutput(
34+
address ptAddress,
35+
address,
36+
uint256 assetsIn
37+
)
38+
external
39+
view
40+
override
41+
returns (uint256)
42+
{
43+
uint8 _dec = _decimals(ptAddress);
44+
uint256 underlyingPerShare = IPrincipalToken(ptAddress).convertToUnderlying(10 ** _dec);
45+
if (underlyingPerShare == 0) return 0;
46+
return Math.mulDiv(assetsIn, 10 ** _dec, underlyingPerShare, Math.Rounding.Ceil);
47+
}
48+
3249
/// @inheritdoc AbstractYieldSourceOracle
3350
function getAssetOutput(
3451
address ptAddress,
@@ -40,7 +57,6 @@ contract SpectraPTYieldSourceOracle is AbstractYieldSourceOracle {
4057
override
4158
returns (uint256)
4259
{
43-
// Use convertToUnderlying to get assets for shares (PTs)
4460
return IPrincipalToken(ptAddress).convertToUnderlying(sharesIn);
4561
}
4662

src/accounting/oracles/StakingYieldSourceOracle.sol

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity 0.8.30;
33

44
// external
55
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
6+
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
67

78
// Superform
89
import { AbstractYieldSourceOracle } from "./AbstractYieldSourceOracle.sol";
@@ -32,6 +33,20 @@ contract StakingYieldSourceOracle is AbstractYieldSourceOracle {
3233
return assetsIn;
3334
}
3435

36+
/// @inheritdoc AbstractYieldSourceOracle
37+
function getWithdrawalShareOutput(
38+
address,
39+
address,
40+
uint256 assetsIn
41+
)
42+
external
43+
pure
44+
override
45+
returns (uint256)
46+
{
47+
return assetsIn;
48+
}
49+
3550
/// @inheritdoc AbstractYieldSourceOracle
3651
function getAssetOutput(address, address, uint256 sharesIn) public pure override returns (uint256) {
3752
return sharesIn;

src/interfaces/accounting/IYieldSourceOracle.sol

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@ interface IYieldSourceOracle {
7373
view
7474
returns (uint256);
7575

76+
/// @notice Calculates the amount of shares that would be burnt when withdrawing a given amount of assets
77+
/// @dev Used by oracles to simulate withdrawals and to derive the current exchange rate
78+
/// @param yieldSourceAddress The address of the yield-bearing token (e.g., aUSDC, cDAI)
79+
/// @param assetIn The address of the underlying asset to be withdrawn (e.g., USDC, DAI)
80+
/// @param assetsIn The amount of underlying assets to withdraw, denominated in the asset’s native units
81+
/// @return shares The amount of yield-bearing shares that would be burnt after withdrawal
82+
function getWithdrawalShareOutput(
83+
address yieldSourceAddress,
84+
address assetIn,
85+
uint256 assetsIn
86+
)
87+
external
88+
view
89+
returns (uint256);
90+
7691
/// @notice Calculates the number of underlying assets that would be received for a given amount of shares
7792
/// @dev Used for withdrawal simulations and to calculate current yield
7893
/// @param yieldSourceAddress The yield-bearing token address (e.g., aUSDC, cDAI)

test/mocks/MockYieldSourceOracle.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ contract MockYieldSourceOracle is IYieldSourceOracle {
4646
return assetsIn;
4747
}
4848

49+
function getWithdrawalShareOutput(address, address, uint256 assetsIn) external pure override returns (uint256) {
50+
return assetsIn;
51+
}
52+
4953
function getAssetOutput(address, address, uint256 sharesIn) public pure returns (uint256) {
5054
return sharesIn;
5155
}

0 commit comments

Comments
 (0)