YieldForwarder
Inherits: ReentrancyGuard
Title: YieldForwarder
Author: Golem Foundation
Immutable, single-purpose contract that calls report() on a strategy, redeems any resulting profit shares, and forwards the underlying assets to a hardcoded receiver
Designed for trust-minimized yield flows where this contract serves as both the strategy's keeper (authorized to call report()) and the donation address (receives profit shares). Only an authorized keeper EOA can trigger it. CALL CHAIN: Keeper EOA → reportAndForward() → strategy.report() → profit shares minted to this contract → redeem shares → assets forwarded to receiver DESIGN:
- Fully immutable: no admin, no upgrades, no sweep
- Keeper-gated: only the designated keeper can trigger
- Single-purpose: assets can only flow to the hardcoded receiver
- Strategy is passed as a call-time parameter to avoid circular dependencies
COMPATIBILITY NOTE --
enableBurningloss absorption: A YieldForwarder is not a reliabledragonRouterfor loss absorption.reportAndForwardtries to drain the forwarder's share balance after every report, but the redemption is only best-effort: it is capped atstrategy.maxRedeem(forwarder)(residual shares remain when external vault headroom is tight) and it is skipped entirely whenconvertToAssets(shares)rounds to zero on a loss-impaired strategy (dust share balances remain). UnderenableBurning = true, only whatever happens to be sitting at the forwarder at loss time can be burned, and that amount is not guaranteed. Operators who want to actually rely on burn-based loss protection must use a non-forwarder dragon (EOA, multisig, or a splitter that retains balance between reports). OPERATIONAL NOTE -- airdrop / keeper-only strategy APIs: When this contract is wired in as a strategy'skeeper, any strategy function gated byonlyKeepers(for example SparkStrategy.sweepAirdrop) cannot be invoked by this contract -- this forwarder only exposesreportAndForward(and, for the swapping variant,reportSwapAndForward). Management must retain an operational channel (multisig, automation key, etc.) to call those keeper-gated strategy functions directly. For airdrops routed throughsweepAirdrop, management should coordinate an immediate keeper call toYieldForwarder.forwardToken(airdropToken)to move the swept balance onward to the hardcoded receiver. MIGRATION NOTE -- 14-day dragon-router cooldown: Because this contract is intended to act as bothkeeperanddragonRouter, migrating a strategy to or away from a forwarder is coupled to the TokenizedStrategy dragon-router cooldown. On TokenizedStrategy,setKeeper(address)takes effect immediately, butsetDragonRouter(address)only enqueues the change -- it emitsPendingDragonRouterChangeand starts a 14-day timer, and the new router only becomes active once anyone callsfinalizeDragonRouterChange()after the cooldown has elapsed. In practice every forwarder swap is a >=14-day operation. The delay is an intentional security invariant of the yield-skim design and is not bypassable by design. For compromised-keeper incident response, the correct posture is:
- Call
setKeeper(newKeeper)on the strategy (onlyManagement). This takes effect immediately and neutralises the compromised forwarder: the forwarder'sreportAndForward(andreportSwapAndForward) now revert insidestrategy.report()because the strategy no longer recognises the old forwarder as a keeper. No 14-day wait is involved in this step. - Concurrently call
setDragonRouter(newRouter)on the strategy to queue the donation-address change and start the 14-day cooldown. - After 14 days, call
finalizeDragonRouterChange()on the strategy to activate the new dragon router. - Optional:
shutdownStrategy()can be used at any point during the response to block new deposits and mints. It does NOT stopreport(),tend(), or donation-share minting on profit -- those continue to work post-shutdown -- so shutdown is a deposit-inflow brake, not a way to neutralise a compromised keeper. Use step 1 for that.
Note: security-contact: [email protected]
A YieldForwarder is intended to be both the strategy keeper and the donation address. setKeeper is immediate, but changing the donation address still goes through the 14-day TokenizedStrategy dragon-router cooldown.
Forwarders drain received shares after reports. Do not rely on a forwarder as the retained balance for enableBurning loss protection.
Quick Start - What Matters Most
Use this when donation shares should be redeemed and forwarded as the same underlying asset.
Key functions to understand:
reportAndForward(strategy, maxLoss)- Callsreport(), redeems available profit shares, and forwards assetsforwardToken(token)- Moves accidentally held ERC-20 balances to the receiverkeeper- The only address authorized to trigger the main flow
State Variables
receiver
The address that receives all redeemed assets
Set once at construction, cannot be changed
address public immutable receiver
keeper
The address authorized to trigger report and forward
Set once at construction, cannot be changed
address public immutable keeper
Functions
constructor
Creates a new YieldForwarder with a fixed receiver and keeper
constructor(address _receiver, address _keeper) ;
Parameters
| Name | Type | Description |
|---|---|---|
_receiver | address | Address that will receive all forwarded assets |
_keeper | address | Address authorized to call reportAndForward |
reportAndForward
Calls report() on the strategy, redeems any profit shares, and forwards assets
Only callable by the authorized keeper. This contract must be set as the
strategy's keeper (so it can call report()) and as its donation address
(so profit shares are minted here).
If report() produces no profit shares, the function returns 0 without reverting
(the report itself may still be useful for loss accounting).
The inner redemption is capped at strategy.maxRedeem(this) so that a tight
external vault (idle + vaultMax shrinking below the forwarder's share balance)
cannot roll back report(). Residual shares remain at the forwarder and are
picked up on a later call; report()'s side effects always commit.
Note:
security: Only callable by the immutable keeper; destination is the immutable receiver
function reportAndForward(address strategy, uint256 maxLoss) external nonReentrant returns (uint256 assets);
Parameters
| Name | Type | Description |
|---|---|---|
strategy | address | Address of the strategy contract (must implement IReportable, IRedeemable, IERC20) |
maxLoss | uint256 | Maximum acceptable loss in basis points for the redemption |
Returns
| Name | Type | Description |
|---|---|---|
assets | uint256 | Amount of underlying assets forwarded to receiver (0 if no profit shares) |
forwardToken
Forwards the full balance of an arbitrary ERC-20 token to the immutable receiver
Only callable by the authorized keeper. The caller picks which token to
flush, but cannot pick where it goes -- the destination is the
construction-time receiver, so no new destination trust surface is
introduced relative to the ordinary report path. Intended for airdrops
and other non-pipeline tokens that arrive at this contract (for example
when this forwarder is the dragonRouter of a strategy whose
sweepAirdrop delivers non-asset tokens here).
Strategy share tokens SHOULD NOT be flushed through this path for normal
yield accounting -- use reportAndForward so shares are redeemed to the
underlying asset first. Calling forwardToken(strategy) is still safe
(receiver ends up holding redeemable shares, no loss of funds), and is in
fact the operational workaround when reportAndForward is temporarily
blocked by tight maxRedeem liquidity on the strategy.
Returns silently (without reverting) if the balance is zero so keepers and
bots can call it speculatively without having to pre-check every token.
function forwardToken(address token) external nonReentrant;
Parameters
| Name | Type | Description |
|---|---|---|
token | address | ERC-20 token whose balance should be flushed to the receiver |
_authorizeForwardToken
Authorizes forwardToken callers. Derived forwarders can extend this when they have an additional governance source.
function _authorizeForwardToken() internal view virtual;
Events
YieldForwarded
Emitted when shares are redeemed and assets forwarded to the receiver
event YieldForwarded(address indexed strategy, address indexed receiver, uint256 shares, uint256 assets);
Parameters
| Name | Type | Description |
|---|---|---|
strategy | address | Address of the strategy whose shares were redeemed |
receiver | address | Address that received the underlying assets |
shares | uint256 | Amount of shares redeemed |
assets | uint256 | Amount of underlying assets forwarded |
TokenForwarded
Emitted when an arbitrary token balance is forwarded to the receiver
event TokenForwarded(address indexed token, address indexed receiver, uint256 amount);
Parameters
| Name | Type | Description |
|---|---|---|
token | address | Address of the token whose balance was flushed |
receiver | address | Address that received the token balance |
amount | uint256 | Amount of the token transferred |
Errors
InvalidReceiver
Thrown when the receiver address is zero
error InvalidReceiver();
InvalidKeeper
Thrown when the keeper address is zero
error InvalidKeeper();
OnlyKeeper
Thrown when caller is not the authorized keeper
error OnlyKeeper();