Skip to main content

YieldForwarder

Git Source

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 -- enableBurning loss absorption: A YieldForwarder is not a reliable dragonRouter for loss absorption. reportAndForward tries to drain the forwarder's share balance after every report, but the redemption is only best-effort: it is capped at strategy.maxRedeem(forwarder) (residual shares remain when external vault headroom is tight) and it is skipped entirely when convertToAssets(shares) rounds to zero on a loss-impaired strategy (dust share balances remain). Under enableBurning = 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's keeper, any strategy function gated by onlyKeepers (for example SparkStrategy.sweepAirdrop) cannot be invoked by this contract -- this forwarder only exposes reportAndForward (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 through sweepAirdrop, management should coordinate an immediate keeper call to YieldForwarder.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 both keeper and dragonRouter, migrating a strategy to or away from a forwarder is coupled to the TokenizedStrategy dragon-router cooldown. On TokenizedStrategy, setKeeper(address) takes effect immediately, but setDragonRouter(address) only enqueues the change -- it emits PendingDragonRouterChange and starts a 14-day timer, and the new router only becomes active once anyone calls finalizeDragonRouterChange() 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:
  1. Call setKeeper(newKeeper) on the strategy (onlyManagement). This takes effect immediately and neutralises the compromised forwarder: the forwarder's reportAndForward (and reportSwapAndForward) now revert inside strategy.report() because the strategy no longer recognises the old forwarder as a keeper. No 14-day wait is involved in this step.
  2. Concurrently call setDragonRouter(newRouter) on the strategy to queue the donation-address change and start the 14-day cooldown.
  3. After 14 days, call finalizeDragonRouterChange() on the strategy to activate the new dragon router.
  4. Optional: shutdownStrategy() can be used at any point during the response to block new deposits and mints. It does NOT stop report(), 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]

Keeper and donation address coupling

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.

Loss absorption

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) - Calls report(), redeems available profit shares, and forwards assets
  • forwardToken(token) - Moves accidentally held ERC-20 balances to the receiver
  • keeper - 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

NameTypeDescription
_receiveraddressAddress that will receive all forwarded assets
_keeperaddressAddress 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

NameTypeDescription
strategyaddressAddress of the strategy contract (must implement IReportable, IRedeemable, IERC20)
maxLossuint256Maximum acceptable loss in basis points for the redemption

Returns

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

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

NameTypeDescription
strategyaddressAddress of the strategy whose shares were redeemed
receiveraddressAddress that received the underlying assets
sharesuint256Amount of shares redeemed
assetsuint256Amount 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

NameTypeDescription
tokenaddressAddress of the token whose balance was flushed
receiveraddressAddress that received the token balance
amountuint256Amount 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();