YieldDonatingTokenizedStrategy
Inherits: TokenizedStrategy
Title: YieldDonatingTokenizedStrategy
Author: Golem Foundation
Specialized TokenizedStrategy for productive assets with discrete harvesting; profits are donated by minting shares to the dragon router.
Behavior overview:
- On report(), harvests the underlying position via BaseStrategy.harvestAndReport()
- If newTotalAssets > oldTotalAssets, mints shares equal to the profit (asset value) to the dragon router
- If losses occur and burning is enabled, burns dragon router shares (up to its balance) using rounding-down shares-to-burn
- No tracked-loss bucket exists; any loss not covered by dragon router burning reduces totalAssets and affects PPS for all holders Economic notes:
- Profit donations are realized via share mints at the time of report
- Losses first attempt dragon share burning when enabled; residual losses decrease PPS
- Dragon router change follows TokenizedStrategy cooldown and two-step finalization Terminal-state recovery (operator-managed):
- After a catastrophic loss that reduces
totalAssetsto 0 whiletotalSupplyremains positive (all dragon shares burned and residual loss socialized), the strategy enters a terminal state:_convertToSharesreturns 0 while tracked assets are 0, so deposit() and mint() revert until accounting is restored. - A direct donation followed by report() can heal that state. When report() observes recovered assets from a zero-asset state, surplus recovery shares are minted to the dragon router as donation yield. This restores a usable PPS without assigning the recovered surplus to stale dust holders.
Note: security-contact: [email protected]
Quick Start - What Matters Most
This is the implementation for realized-profit donation flows. A keeper calls report(); the strategy harvests, reports current assets, and mints any profit shares to the donation address.
Key functions to understand:
report()- Realizes profit/loss and mints donation shares when profit is non-zero_harvestAndReport()- Strategy-specific hook that returns total assetsdeposit/mint- Seed permanently locked first shares on an empty yield-donating strategysetEnableBurning- Toggles loss protection; disabling from an enabled state can revert until pending dragon shares are reportedreportAndDisableBurning- Reports current accounting and disables burn protection atomically
1.2.0-develop.15 Behavior Notes
Minimum liquidity on the first deposit
Yield-donating strategies permanently lock the first 1_000 shares at address(0xdead), following the Uniswap V2 minimum-liquidity pattern. On the first successful deposit into an empty strategy, the depositor receives assets - 1000 shares and 1000 shares are minted to 0x000000000000000000000000000000000000dEaD.
Consequences for integrations and tests:
- A first deposit with
assets <= 1000reverts through the zero-share path. previewMint(shares)on an empty strategy returnsshares + 1000.maxDeposit(receiver)returns zero on an empty strategy when upstream deposit capacity is<= 1000.maxMint(receiver)andmaxRedeem(owner)return zero in the zero-asset terminal state.- Tutorial tests must not assert that the first YDS deposit mints shares 1:1.
Relevant constants:
uint256 internal constant MINIMUM_LIQUIDITY = 1_000;
address internal constant MINIMUM_LIQUIDITY_RECEIVER = address(0xdead);
Disabling dragon-share burning
When burning is enabled, setEnableBurning(false) requires the current dragon-router share balance to be zero. If pending dragon shares exist, the call reverts with report before disabling burning. Management should use reportAndDisableBurning() when it wants to report current 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
MINIMUM_LIQUIDITY
Permanently locked first shares, following the Uniswap V2 minimum-liquidity pattern
uint256 internal constant MINIMUM_LIQUIDITY = 1_000
MINIMUM_LIQUIDITY_RECEIVER
Receiver for permanently locked minimum-liquidity shares
address internal constant MINIMUM_LIQUIDITY_RECEIVER = address(0xdead)
Functions
deposit
Mints proportional shares to receiver according to how the strategy calculates the assets to shares conversion
ERC4626-compliant deposit function with reentrancy protection
Note: security: Reentrancy protected
function deposit(uint256 assets, address receiver) public virtual override nonReentrant returns (uint256 shares);
Parameters
| Name | Type | Description |
|---|---|---|
assets | uint256 | Amount of assets to deposit (or type(uint256).max for full balance) |
receiver | address | Address to receive the minted shares |
Returns
| Name | Type | Description |
|---|---|---|
shares | uint256 | Amount of shares minted to receiver |
_convertToShares
Yield-donating first deposits fund permanently locked shares.
function _convertToShares(StrategyData storage S, uint256 assets, Math.Rounding _rounding)
internal
view
virtual
override
returns (uint256);
previewMint
Previews assets required to mint exact shares
Uses Ceil rounding. Marked virtual so strategies that gate the real
mint path (e.g. yield-skimming on insolvency) can mirror the gate in
the preview and stay ERC-4626-compliant.
function previewMint(uint256 shares) public view virtual override returns (uint256 assets);
Parameters
| Name | Type | Description |
|---|---|---|
shares | uint256 | Amount of shares to mint |
Returns
| Name | Type | Description |
|---|---|---|
assets | uint256 | assets_ Required assets for mint |
mint
Mints exact shares by depositing calculated asset amount
ERC4626-compliant mint function with reentrancy protection CHECKS:
- Shares
<= maxMint(also checks if strategy shutdown) - Assets != 0 (prevents rounding to zero)
Note: security: Reentrancy protected
function mint(uint256 shares, address receiver) public virtual override nonReentrant returns (uint256 assets);
Parameters
| Name | Type | Description |
|---|---|---|
shares | uint256 | Exact amount of shares to mint |
receiver | address | Address to receive the minted shares |
Returns
| Name | Type | Description |
|---|---|---|
assets | uint256 | Amount of assets deposited from caller |
maxDeposit
Returns maximum assets that can be deposited
Returns 0 if strategy is shutdown Returns type(uint256).max if not shutdown (no hard cap)
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 | max Maximum deposit amount |
maxMint
Total number of shares that can be minted to receiver
of a mint call.
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 | _maxMint Maximum shares that can be minted |
maxRedeem
Maximum number of shares that can be redeemed by owner.
function maxRedeem(address owner) public view virtual override returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | Address that owns the shares |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | _maxRedeem Maximum shares that can be redeemed |
_deposit
Seeds permanently locked shares on the first successful yield-donating deposit/mint.
function _deposit(StrategyData storage S, address receiver, uint256 assets, uint256 shares)
internal
virtual
override;
_addMinimumLiquidity
function _addMinimumLiquidity(uint256 shares) internal pure returns (uint256);
_maxDepositWithMinimumLiquidity
function _maxDepositWithMinimumLiquidity(StrategyData storage S, address receiver)
internal
view
returns (uint256 maxAssets);
_isZeroAssetTerminalState
function _isZeroAssetTerminalState(StrategyData storage S) internal view returns (bool);
report
Reports strategy performance and distributes profits as donations
Mints profit-derived shares to dragon router when newTotalAssets > oldTotalAssets; on loss, attempts
dragon share burning if enabled. Residual loss reduces PPS (no tracked-loss bucket).
Keeper trust assumption: report() timing is at the keeper's discretion and directly controls
when dragon shares mint (on profit) and burn (on loss). A compromised keeper can time calls
adversarially — for example, call report() during a temporary dip to burn dragon shares at
the depressed PPS and then call again on recovery so the rebound is captured as fresh
dragon-mint profit rather than offsetting the earlier dip; or delay report() through a real
loss to let users exit at a stale, inflated PPS and socialise the loss across remaining
holders. These paths are bounded by the dragon router's share balance and degrade yield
quality rather than drain funds, but they are genuine keeper-side risks.
The keeper is a SEMI-TRUSTED role. Mitigation is operational: keeper key custody under
multisig / MPC, off-chain alerting on report() calls during volatility spikes, and the
no-cooldown setKeeper() rotation path if the key is compromised. shutdownStrategy
can be used as containment to halt new deposits/mints while rotation and assessment
happen, but it does not block tend() or report(). No on-chain cap on reporting cadence
is enforced — that would constrain legitimate operation for a threat the trust model
already accepts.
function report()
public
virtual
override(TokenizedStrategy)
nonReentrant
onlyKeepers
returns (uint256 profit, uint256 loss);
setEnableBurning
Sets whether dragon-share burning is enabled for loss protection.
Yield-donating strategies learn their authoritative current asset value
through report(). When dragon shares exist, disabling burning without
reporting first could retroactively preserve dragon shares through an
unreported loss. Use reportAndDisableBurning() in that case.
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 unreported losses.
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 |
_handleDragonLossProtection
Internal function to handle loss protection for dragon principal
function _handleDragonLossProtection(StrategyData storage S, uint256 loss) internal;
Parameters
| Name | Type | Description |
|---|---|---|
S | StrategyData | Storage struct pointer to access strategy's storage variables |
loss | uint256 | Amount of loss to protect against in asset base units If burning is enabled, this function will try to burn shares from the dragon router equivalent to the loss amount. |
Events
DonationMinted
Emitted when profit or recovery shares are minted
event DonationMinted(address indexed dragonRouter, uint256 amount);
Parameters
| Name | Type | Description |
|---|---|---|
dragonRouter | address | Address receiving minted donation or recovery shares |
amount | uint256 | Amount of shares minted in share base units |
DonationBurned
Emitted when dragon shares are burned to cover losses
event DonationBurned(address indexed dragonRouter, uint256 amount);
Parameters
| Name | Type | Description |
|---|---|---|
dragonRouter | address | Address whose shares are burned |
amount | uint256 | Amount of shares burned in share base units |