YieldSkimmingTokenizedStrategy
Inherits: TokenizedStrategy
Title: YieldSkimmingTokenizedStrategy
Author: Golem Foundation
Specialized TokenizedStrategy for yield-bearing assets with appreciating exchange rates.
Mechanism:
- Shares represent value-units of the underlying asset at the strategy exchange rate (1 share = 1 unit of underlying-asset value as returned by getCurrentExchangeRate) rather than raw asset amounts. Note: the underlying-asset value is NOT pegged to native ETH, even when the asset references stETH/wstETH/rETH — value follows the live exchange rate.
- On report(), compares total vault value (assets * rate) vs total outstanding shares • Profit: mints dragon shares equal to excess value above total share debt • Loss: burns dragon shares (if enabled) up to available balance to cover shortfall
- Insolvency determination: vault cannot cover user debt (excludes dragon shares as loss buffer)
- Dual conversion modes: • Solvent: rate-based conversions using current exchange rate (RAY precision) • Insolvent: proportional distribution using base TokenizedStrategy logic
- Dragon solvency protection: prevents dragon operations that would compromise user debt coverage
- Dragon restrictions: cannot deposit/mint, cannot transfer to self, operations blocked during insolvency
Note: security-contact: [email protected]
Yield-skimming shares represent value units of the underlying asset at the strategy exchange rate. They are not native ETH-denominated debt unless the underlying asset itself is ETH-denominated.
For YSS, the Reported.loss value is a gross shortfall level, not a per-report delta. Integrators should not sum consecutive loss values as if each one were a new realized loss.
Quick Start - What Matters Most
This extends TokenizedStrategy for appreciating assets. On report(), exchange-rate appreciation is captured and routed to the donation address.
Key functions to understand:
report()- Captures value appreciation or reports gross shortfallpreviewDeposit/previewMint- Mirror the same gates as the real deposit and mint paths_getCurrentExchangeRate()- Determines how asset appreciation is trackedsetEnableBurning- Toggles dragon-share burn protection; disabling can revert while a pending burn should still be reportedreportAndDisableBurning- Reports current accounting and disables burn protection atomically
1.2.0-develop.15 Behavior Notes
Disabling dragon-share burning
When burning is enabled, setEnableBurning(false) is blocked while the strategy has an unreported shortfall that should burn dragon shares first. In that case the call reverts with report before disabling burning.
Management should call reportAndDisableBurning() when it wants to synchronize the current rate/accounting and disable burn protection in one transaction.
function setEnableBurning(bool _enableBurning) external override onlyManagement;
function reportAndDisableBurning() external onlyManagement returns (uint256 profit, uint256 loss);
Constants
YIELD_SKIMMING_STORAGE_SLOT
bytes32 private constant YIELD_SKIMMING_STORAGE_SLOT =
keccak256(abi.encode(uint256(keccak256("octant.yieldSkimming.exchangeRate")) - 1)) & ~bytes32(uint256(0xff))
Functions
deposit
Deposit assets into the strategy with value debt tracking
Requirements:
- Vault must be solvent (reverts otherwise)
- Receiver cannot be dragon router (dragon shares minted via report())
- Tracks asset value debt
function deposit(uint256 assets, address receiver) public virtual override nonReentrant returns (uint256 shares);
Parameters
| Name | Type | Description |
|---|---|---|
assets | uint256 | Amount of assets to deposit in asset base units |
receiver | address | Address to receive the shares (cannot be dragon router) |
Returns
| Name | Type | Description |
|---|---|---|
shares | uint256 | Amount of shares minted (1 share = 1 unit of underlying-asset value at the current rate) |
mint
Mint exact shares from the strategy with value debt tracking
Implements insolvency protection and tracks underlying-asset-value debt
function mint(uint256 shares, address receiver) public virtual override nonReentrant returns (uint256 assets);
Parameters
| Name | Type | Description |
|---|---|---|
shares | uint256 | Amount of shares to mint |
receiver | address | Address to receive the shares |
Returns
| Name | Type | Description |
|---|---|---|
assets | uint256 | Amount of assets deposited in asset base units (1 share = 1 unit of underlying-asset value, except in case of uncovered loss) |
redeem
Redeem shares from the strategy with value debt tracking
Shares represent underlying-asset value (1 share = 1 unit of underlying-asset value at the current rate, except in case of uncovered loss)
function redeem(uint256 shares, address receiver, address owner, uint256 maxLoss)
public
override
nonReentrant
returns (uint256 assets);
Parameters
| Name | Type | Description |
|---|---|---|
shares | uint256 | Amount of shares to redeem |
receiver | address | Address to receive the assets |
owner | address | Address whose shares are being redeemed |
maxLoss | uint256 | Maximum acceptable loss in basis points |
Returns
| Name | Type | Description |
|---|---|---|
assets | uint256 | Amount of assets returned in asset base units |
withdraw
Withdraw assets from the strategy with value debt tracking
Calculates shares needed for the asset amount requested
function withdraw(uint256 assets, address receiver, address owner, uint256 maxLoss)
public
override
nonReentrant
returns (uint256 shares);
Parameters
| Name | Type | Description |
|---|---|---|
assets | uint256 | Amount of assets to withdraw in asset base units |
receiver | address | Address to receive the assets |
owner | address | Address whose shares are being redeemed |
maxLoss | uint256 | Maximum acceptable loss in basis points |
Returns
| Name | Type | Description |
|---|---|---|
shares | uint256 | Amount of shares burned in share base units |
maxDeposit
Get the maximum amount of assets that can be deposited by a user
Returns 0 for dragon router as they cannot deposit
function maxDeposit(address receiver) public view virtual override returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
receiver | address | Address that would receive the shares |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Maximum deposit amount in asset base units |
maxMint
Get the maximum amount of shares that can be minted by a user
Returns 0 for dragon router as they cannot mint
function maxMint(address receiver) public view virtual override returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
receiver | address | Address that would receive the shares |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Maximum mint amount in shares |
maxWithdraw
Get the maximum amount of assets that can be withdrawn by a user
Dragon router has restrictions based on solvency protection to ensure user debt coverage. For non-dragon users during insolvency, simulates the lazy dragon burn that will occur in withdraw() to return the correct post-burn amount (ERC4626 compliance).
function maxWithdraw(address owner) public view override returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | Address whose shares would be burned |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Maximum withdraw amount in asset base units |
maxRedeem
Get the maximum amount of shares that can be redeemed by a user
Dragon router has restrictions based on solvency protection to ensure user debt coverage. For non-dragon users, super.maxRedeem returns _balanceOf(owner) because availableWithdrawLimit is uncapped — no burn simulation needed here.
function maxRedeem(address owner) public view override returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | Address whose shares would be burned |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Maximum redeem amount in shares |
gettotalDebtOwedToUserInAssetValue
Get the total underlying-asset-value debt owed to users
function gettotalDebtOwedToUserInAssetValue() external view returns (uint256);
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Total user debt in underlying-asset value units |
getDragonRouterDebtInAssetValue
Get the total underlying-asset-value debt owed to dragon router
function getDragonRouterDebtInAssetValue() external view returns (uint256);
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Total dragon router debt in underlying-asset value units |
getTotalValueDebtInAssetValue
Get the total underlying-asset-value debt owed to both users and dragon router combined
function getTotalValueDebtInAssetValue() external view returns (uint256);
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Total debt in underlying-asset value units combining users and dragon router |
previewDeposit
Preview the shares that would be minted for a deposit of assets.
Returns 0 when the vault is insolvent or the exchange rate is 0. The real
deposit path reverts via _requireVaultSolvency, so an inherited preview
would lie to ERC-4626 integrators that expect previewDeposit to match the
call they are about to make. In the solvent branch the inherited
_convertToShares override already uses the same rate-based math as
deposit, so delegating to super.previewDeposit is accurate.
function previewDeposit(uint256 assets) public view virtual override returns (uint256 shares);
Parameters
| Name | Type | Description |
|---|---|---|
assets | uint256 | Amount of assets hypothetically deposited |
Returns
| Name | Type | Description |
|---|---|---|
shares | uint256 | Shares a depositor would receive (0 when a real deposit would revert) |
previewMint
Preview the assets required to mint exactly shares.
Mirror of previewDeposit: returns 0 when the real mint path would revert
(vault insolvent or exchange rate missing). Solvent branch uses the same
Ceil-rounded conversion as mint.
function previewMint(uint256 shares) public view virtual override returns (uint256 assets);
Parameters
| Name | Type | Description |
|---|---|---|
shares | uint256 | Amount of shares hypothetically minted |
Returns
| Name | Type | Description |
|---|---|---|
assets | uint256 | Assets a minter would deposit (0 when a real mint would revert) |
previewWithdraw
Preview the shares burned to withdraw assets.
The real withdraw path applies the lazy dragon burn before pricing the
exit, so the post-burn totalSupply is the correct denominator in the
insolvent branch. Simulate that burn via _simulateDragonBurnAmount and
mirror the parent pro-rata math (Ceil rounding) against the reduced supply.
When no burn is pending, the inherited _convertToShares override already
matches withdraw, so super.previewWithdraw is accurate.
function previewWithdraw(uint256 assets) public view virtual override returns (uint256 shares);
Parameters
| Name | Type | Description |
|---|---|---|
assets | uint256 | Amount of assets hypothetically withdrawn |
Returns
| Name | Type | Description |
|---|---|---|
shares | uint256 | Shares that would be burned (reflects pending dragon burn) |
previewRedeem
Preview the assets returned for redeeming shares.
Mirror of previewWithdraw. Dragon-share burning never changes totalAssets
or totalDebtOwedToUserInAssetValue, so _isVaultInsolvent stays true after
the simulated burn and the parent pro-rata branch is the one that will fire
in redeem. Floor rounding matches redeem's _convertToAssets call.
The preview is owner-agnostic by ERC-4626 shape; it models the non-dragon
redemption path because that is where the lazy burn applies. Dragon-owner
redemptions use maxRedeem for sizing, which already has its own override.
function previewRedeem(uint256 shares) public view virtual override returns (uint256 assets);
Parameters
| Name | Type | Description |
|---|---|---|
shares | uint256 | Amount of shares hypothetically redeemed |
Returns
| Name | Type | Description |
|---|---|---|
assets | uint256 | Assets a redeemer would receive (reflects pending dragon burn) |
transfer
Transfer shares with dragon solvency protection and debt rebalancing
Special behaviors for dragon router:
- Dragon cannot transfer to itself (reverts)
- Dragon transfers trigger solvency checks to prevent user debt undercoverage
- Transfers blocked if they would make vault unable to cover user debt
- Dragon-involved transfers rebalance debt tracking (sender loses debt, receiver gains it) For non-dragon transfers, behaves like standard ERC20 transfer
function transfer(address to, uint256 amount) external override returns (bool success);
Parameters
| Name | Type | Description |
|---|---|---|
to | address | Address receiving shares |
amount | uint256 | Amount of shares to transfer |
Returns
| Name | Type | Description |
|---|---|---|
success | bool | Whether the transfer succeeded |
transferFrom
Transfer shares from one address to another with dragon solvency protection and debt rebalancing
Special behaviors for dragon router:
- Dragon cannot transfer to itself (reverts)
- Dragon transfers trigger solvency checks to prevent user debt undercoverage
- Transfers blocked if they would make vault unable to cover user debt
- Dragon-involved transfers rebalance debt tracking (sender loses debt, receiver gains it) For non-dragon transfers, behaves like standard ERC20 transferFrom
function transferFrom(address from, address to, uint256 amount) external override returns (bool success);
Parameters
| Name | Type | Description |
|---|---|---|
from | address | Address transferring shares |
to | address | Address receiving shares |
amount | uint256 | Amount of shares to transfer |
Returns
| Name | Type | Description |
|---|---|---|
success | bool | Whether the transfer succeeded |
report
Reports yield skimming strategy performance and handles profit distribution and loss coverage
Overrides report to handle yield appreciation and loss recovery through dragon share minting/burning. Health check effectiveness depends on report() frequency. Exchange rate checks become less effective over time if reports are infrequent, as profit limits may be exceeded. Management should ensure regular reporting or adjust profit/loss ratios based on expected frequency. Key behaviors:
- Value Comparison: Compares current total value (assets * exchange rate) vs total outstanding shares
- Profit Capture: When current value exceeds total shares, mints dragon shares equal to excess value
- Loss Protection: When current value is less than total shares, burns dragon shares (if enabled) to cover shortfall
- Insolvency Handling: If dragon buffer insufficient for losses, remaining shortfall is handled through proportional asset distribution during withdrawals
Event semantics: the
lossvalue emitted viaReportedis a gross shortfall — the full gap between total value debt and current vault value at the time of the call — not an incremental delta since the last report. Ifreport()is called multiple times during the same impairment (e.g. dragon shares cannot fully cover the loss), the same gross shortfall is re-emitted on each call. Integrators must not naively sumlossacross consecutiveReportedevents to compute cumulative damage; doing so double-counts persistent impairments. Treat the event as a level signal, not a delta signal.
function report()
public
override(TokenizedStrategy)
nonReentrant
onlyKeepers
returns (uint256 profit, uint256 loss);
Returns
| Name | Type | Description |
|---|---|---|
profit | uint256 | Profit in assets from underlying value appreciation since the last report |
loss | uint256 | Loss in assets — gross shortfall (level), not a delta. See event semantics above. |
getLastRateRay
Get the last reported exchange rate (RAY precision)
function getLastRateRay() external view returns (uint256);
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Last reported exchange rate in RAY precision |
isVaultInsolvent
Check if the vault is currently insolvent
function isVaultInsolvent() external view returns (bool);
Returns
| Name | Type | Description |
|---|---|---|
<none> | bool | isInsolvent True if vault cannot cover user debt (excludes dragon shares as they absorb losses) |
setEnableBurning
Sets whether dragon-share burning is enabled for loss protection.
Disabling burning is blocked while there is an unreported shortfall against combined user + dragon value debt. The shortfall must be reported while burning is still enabled so dragon shares absorb their pending first loss.
function setEnableBurning(bool _enableBurning) external override onlyManagement;
Parameters
| Name | Type | Description |
|---|---|---|
_enableBurning | bool | Whether to enable the burning mechanism |
reportAndDisableBurning
Reports current accounting, then disables dragon burn loss protection.
This explicit helper gives operators an atomic path for disabling burning without leaving a between-transaction window for rate changes.
function reportAndDisableBurning() external onlyManagement returns (uint256 profit, uint256 loss);
Returns
| Name | Type | Description |
|---|---|---|
profit | uint256 | Profit reported by the accounting sync |
loss | uint256 | Loss reported by the accounting sync |
_convertToShares
Converts assets to shares using value debt approach with solvency awareness
function _convertToShares(StrategyData storage S, uint256 assets, Math.Rounding rounding)
internal
view
virtual
override
returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
S | StrategyData | Strategy storage |
assets | uint256 | Amount of assets to convert |
rounding | Math.Rounding | Rounding mode for division |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Amount of shares equivalent in value (1 share = 1 unit of underlying-asset value, except in case of uncovered loss) |
_convertToAssets
Converts shares to assets using value debt approach with solvency awareness
function _convertToAssets(StrategyData storage S, uint256 shares, Math.Rounding rounding)
internal
view
virtual
override
returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
S | StrategyData | Strategy storage |
shares | uint256 | Amount of shares to convert |
rounding | Math.Rounding | Rounding mode for division |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Amount of assets user would receive in asset base units |
_isVaultInsolvent
Checks if the vault is currently insolvent
function _isVaultInsolvent() internal view returns (bool isInsolvent);
Returns
| Name | Type | Description |
|---|---|---|
isInsolvent | bool | True if vault cannot cover user value debt |
_hasPendingDragonBurn
Returns true when dragon shares still have a pending first-loss burn.
This intentionally checks combined debt, not _isVaultInsolvent(), because
users can still be fully covered while the dragon tranche is impaired.
function _hasPendingDragonBurn(StrategyData storage S, YieldSkimmingStorage storage YS)
internal
view
returns (bool);
_maxDragonRedeemableShares
Calculates the maximum amount of shares the dragon can redeem without making the vault unable to cover user debt
function _maxDragonRedeemableShares() internal view returns (uint256 maxDragonRedeemable);
Returns
| Name | Type | Description |
|---|---|---|
maxDragonRedeemable | uint256 | Maximum shares the dragon can redeem |
_requireDragonSolvency
Blocks dragon router from withdrawing during vault insolvency
function _requireDragonSolvency(address account) internal view;
Parameters
| Name | Type | Description |
|---|---|---|
account | address | The address to check (only blocks if it's the dragon router) |
_requireDragonSolvencyAfterOperation
Checks vault solvency when dragon sends shares out (transfer, redeem, withdraw)
Only checks when dragon is SENDING shares. Transfers TO dragon always improve user debt coverage so no check is needed for those.
function _requireDragonSolvencyAfterOperation(address from, uint256 amount) internal view;
Parameters
| Name | Type | Description |
|---|---|---|
from | address | Address shares are coming from |
amount | uint256 | Amount of shares being moved |
_rebalanceDebtOnDragonTransfer
Rebalances debt tracking when dragon transfers shares in or out
function _rebalanceDebtOnDragonTransfer(address from, address to, uint256 transferAmount) internal;
_requireVaultSolvency
Blocks all operations when vault is insolvent
function _requireVaultSolvency() internal view;
_currentRateRay
Get the current exchange rate scaled to RAY precision
function _currentRateRay() internal view virtual returns (uint256);
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Current exchange rate in RAY format (1e27 = 1.0) |
_simulateDragonBurnAmount
Simulates how many dragon shares the lazy burn would remove from totalSupply. Used by maxWithdraw/maxRedeem view functions to reflect post-burn pricing.
function _simulateDragonBurnAmount() internal view returns (uint256 burnAmount);
Returns
| Name | Type | Description |
|---|---|---|
burnAmount | uint256 | Number of dragon shares that would be burned (0 if no burn would occur) |
_applyDragonLossProtectionIfNeeded
Lazily burns dragon shares when the vault is insolvent, so that user exits are not diluted by stale junior capital in the totalSupply denominator. Only mutates state when burning is enabled AND the vault is currently insolvent.
function _applyDragonLossProtectionIfNeeded(StrategyData storage S, YieldSkimmingStorage storage YS) internal;
Parameters
| Name | Type | Description |
|---|---|---|
S | StrategyData | Strategy storage pointer |
YS | YieldSkimmingStorage | Yield skimming storage pointer |
_handleDragonLossProtection
Internal function to handle loss protection by burning dragon shares
function _handleDragonLossProtection(
StrategyData storage S,
YieldSkimmingStorage storage YS,
uint256 lossValue,
uint256 currentRate
) internal returns (uint256 loss);
Parameters
| Name | Type | Description |
|---|---|---|
S | StrategyData | Strategy storage pointer |
YS | YieldSkimmingStorage | Yield skimming storage pointer |
lossValue | uint256 | Loss amount in underlying-asset value terms |
currentRate | uint256 | Current exchange rate in RAY format |
Returns
| Name | Type | Description |
|---|---|---|
loss | uint256 | Loss amount in asset terms for reporting |
finalizeDragonRouterChange
Finalizes the dragon router change with proper debt accounting migration
Migrates debt tracking when dragon router changes to maintain correct accounting. The solvency check runs AFTER debt migration so that:
- Migrations that would restore solvency (new dragon holds user shares whose conversion to dragon debt drops user debt below vault value) are allowed.
- Migrations that would create insolvency (old dragon balance becoming user debt pushes user debt above vault value) are blocked. A pre-migration check inspecting the old state misclassifies both directions.
function finalizeDragonRouterChange() external override;
_strategyYieldSkimmingStorage
function _strategyYieldSkimmingStorage() internal pure returns (YieldSkimmingStorage storage S);
Events
Harvest
Event emitted when harvest is performed
event Harvest(address indexed caller, uint256 currentRate);
DonationMinted
Events for donation tracking
event DonationMinted(address indexed dragonRouter, uint256 amount, uint256 exchangeRate);
Parameters
| Name | Type | Description |
|---|---|---|
dragonRouter | address | Address receiving or burning donation shares |
amount | uint256 | Amount of value-shares minted or burned (1 share = 1 value unit) |
exchangeRate | uint256 | Current exchange rate (scaled to wad) at the time of the event |
DonationBurned
Emitted when dragon shares are burned to cover value losses
event DonationBurned(address indexed dragonRouter, uint256 amount, uint256 exchangeRate);
Structs
YieldSkimmingStorage
Storage for yield skimming strategy
struct YieldSkimmingStorage {
uint256 totalDebtOwedToUserInAssetValue; // Track underlying-asset-value debt owed to users only
uint256 lastReportedRate; // Track the last reported rate
uint256 dragonRouterDebtInAssetValue; // Track the underlying-asset-value debt owed to dragon router
}