Skip to main content

YieldSkimmingTokenizedStrategy

Git Source

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]

Value units

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.

Reported loss semantics

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 shortfall
  • previewDeposit / previewMint - Mirror the same gates as the real deposit and mint paths
  • _getCurrentExchangeRate() - Determines how asset appreciation is tracked
  • setEnableBurning - Toggles dragon-share burn protection; disabling can revert while a pending burn should still be reported
  • reportAndDisableBurning - 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

NameTypeDescription
assetsuint256Amount of assets to deposit in asset base units
receiveraddressAddress to receive the shares (cannot be dragon router)

Returns

NameTypeDescription
sharesuint256Amount 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

NameTypeDescription
sharesuint256Amount of shares to mint
receiveraddressAddress to receive the shares

Returns

NameTypeDescription
assetsuint256Amount 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

NameTypeDescription
sharesuint256Amount of shares to redeem
receiveraddressAddress to receive the assets
owneraddressAddress whose shares are being redeemed
maxLossuint256Maximum acceptable loss in basis points

Returns

NameTypeDescription
assetsuint256Amount 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

NameTypeDescription
assetsuint256Amount of assets to withdraw in asset base units
receiveraddressAddress to receive the assets
owneraddressAddress whose shares are being redeemed
maxLossuint256Maximum acceptable loss in basis points

Returns

NameTypeDescription
sharesuint256Amount 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

NameTypeDescription
receiveraddressAddress that would receive the shares

Returns

NameTypeDescription
<none>uint256Maximum 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

NameTypeDescription
receiveraddressAddress that would receive the shares

Returns

NameTypeDescription
<none>uint256Maximum 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

NameTypeDescription
owneraddressAddress whose shares would be burned

Returns

NameTypeDescription
<none>uint256Maximum 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

NameTypeDescription
owneraddressAddress whose shares would be burned

Returns

NameTypeDescription
<none>uint256Maximum redeem amount in shares

gettotalDebtOwedToUserInAssetValue

Get the total underlying-asset-value debt owed to users

function gettotalDebtOwedToUserInAssetValue() external view returns (uint256);

Returns

NameTypeDescription
<none>uint256Total 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

NameTypeDescription
<none>uint256Total 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

NameTypeDescription
<none>uint256Total 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

NameTypeDescription
assetsuint256Amount of assets hypothetically deposited

Returns

NameTypeDescription
sharesuint256Shares 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

NameTypeDescription
sharesuint256Amount of shares hypothetically minted

Returns

NameTypeDescription
assetsuint256Assets 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

NameTypeDescription
assetsuint256Amount of assets hypothetically withdrawn

Returns

NameTypeDescription
sharesuint256Shares 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

NameTypeDescription
sharesuint256Amount of shares hypothetically redeemed

Returns

NameTypeDescription
assetsuint256Assets 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

NameTypeDescription
toaddressAddress receiving shares
amountuint256Amount of shares to transfer

Returns

NameTypeDescription
successboolWhether 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

NameTypeDescription
fromaddressAddress transferring shares
toaddressAddress receiving shares
amountuint256Amount of shares to transfer

Returns

NameTypeDescription
successboolWhether 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:

  1. Value Comparison: Compares current total value (assets * exchange rate) vs total outstanding shares
  2. Profit Capture: When current value exceeds total shares, mints dragon shares equal to excess value
  3. Loss Protection: When current value is less than total shares, burns dragon shares (if enabled) to cover shortfall
  4. Insolvency Handling: If dragon buffer insufficient for losses, remaining shortfall is handled through proportional asset distribution during withdrawals Event semantics: the loss value emitted via Reported is 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. If report() 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 sum loss across consecutive Reported events 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

NameTypeDescription
profituint256Profit in assets from underlying value appreciation since the last report
lossuint256Loss 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

NameTypeDescription
<none>uint256Last reported exchange rate in RAY precision

isVaultInsolvent

Check if the vault is currently insolvent

function isVaultInsolvent() external view returns (bool);

Returns

NameTypeDescription
<none>boolisInsolvent 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

NameTypeDescription
_enableBurningboolWhether 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

NameTypeDescription
profituint256Profit reported by the accounting sync
lossuint256Loss 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

NameTypeDescription
SStrategyDataStrategy storage
assetsuint256Amount of assets to convert
roundingMath.RoundingRounding mode for division

Returns

NameTypeDescription
<none>uint256Amount 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

NameTypeDescription
SStrategyDataStrategy storage
sharesuint256Amount of shares to convert
roundingMath.RoundingRounding mode for division

Returns

NameTypeDescription
<none>uint256Amount 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

NameTypeDescription
isInsolventboolTrue 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

NameTypeDescription
maxDragonRedeemableuint256Maximum shares the dragon can redeem

_requireDragonSolvency

Blocks dragon router from withdrawing during vault insolvency

function _requireDragonSolvency(address account) internal view;

Parameters

NameTypeDescription
accountaddressThe 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

NameTypeDescription
fromaddressAddress shares are coming from
amountuint256Amount 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

NameTypeDescription
<none>uint256Current 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

NameTypeDescription
burnAmountuint256Number 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

NameTypeDescription
SStrategyDataStrategy storage pointer
YSYieldSkimmingStorageYield 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

NameTypeDescription
SStrategyDataStrategy storage pointer
YSYieldSkimmingStorageYield skimming storage pointer
lossValueuint256Loss amount in underlying-asset value terms
currentRateuint256Current exchange rate in RAY format

Returns

NameTypeDescription
lossuint256Loss 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

NameTypeDescription
dragonRouteraddressAddress receiving or burning donation shares
amountuint256Amount of value-shares minted or burned (1 share = 1 value unit)
exchangeRateuint256Current 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
}