Skip to main content

Rewards

Pendle distributes three categories of rewards across positions:

  • SY-native rewards — external protocol rewards generated by the underlying yield source (e.g., AAVE tokens from an Aave vault, Morpho rewards)
  • PENDLE incentives — PENDLE tokens streamed by the GaugeController to LP positions
  • Off-chain rewards — rewards computed off-chain (e.g. voter incentives, partner point programs) and distributed via MerkleDistributor

The table below summarises which contract, reward tokens, and claim function apply to each position type:

PositionContractReward TokensClaim Function
SY holderSYBaseWithRewardsSY-native (e.g. AAVE)claimRewards(user)
YT holderPendleYieldTokenSY-native (via SY indexes)redeemDueInterestAndRewards(user, false, true)
LP holderPendleMarket (PendleGauge)SY-native + PENDLEredeemRewards(user)
AnyPendleMultiTokenMerkleDistributorAny (off-chain computed)claim(receiver, tokens, totalAccrueds, proofs)

The Reward Index Accounting Model

All three position types share the same underlying accounting mechanism, implemented in RewardManagerAbstract and RewardManager.

Core idea

A global index (per reward token) increases monotonically as rewards accumulate. Each user tracks their own index snapshot. When a user's snapshot lags behind the global index, the difference represents rewards they have not yet been credited.

userAccrued += userShares × (globalIndex − userIndex)
userIndex = globalIndex

Storage structs

struct RewardState {
uint128 index; // current global index (scaled by 1e18)
uint128 lastBalance; // reward token balance at last index update
}

struct UserReward {
uint128 index; // user's snapshot of the global index
uint128 accrued; // unclaimed reward tokens owed to this user
}
  • rewardState[token] — global state, one entry per reward token
  • userReward[token][user] — per-user state

Per-block update guard

RewardManager updates the global index at most once per block (lastRewardBlock != block.number). Within the same block all calls read the cached index, keeping gas costs predictable.

Index update flow

On reward interaction:
1. _redeemExternalReward() ← pull rewards from external source into contract balance
2. accrued = currentBalance − lastBalance
3. index += accrued / totalShares
4. lastBalance = currentBalance
5. For each user: userAccrued += userShares × (index − userIndex)

SY Rewards

Contracts: SYBaseWithRewards, RewardManager

SY calculates reward indexes locally: it tracks the raw token balance of each reward token and computes accruals from balance deltas.

Reward shares

Each SY holder's reward share equals their SY token balance. Total shares = total SY supply.

getRewardTokens

Returns the list of external reward tokens that this SY distributes.

function getRewardTokens() external view returns (address[] memory tokens);
Return ValueTypeDescription
tokensaddress[]List of reward token addresses

accruedRewards

View function. Returns the amount of each reward token currently credited to user without triggering a state change. Does not include rewards pending since the last block update.

function accruedRewards(address user) external view returns (uint256[] memory amounts);
ParameterTypeDescription
useraddressThe user address
Return ValueTypeDescription
amountsuint256[]Accrued reward amounts, one per getRewardTokens() entry

rewardIndexesCurrent

Updates reward indexes for the current block and returns them. State-changing.

function rewardIndexesCurrent() external returns (uint256[] memory indexes);
Return ValueTypeDescription
indexesuint256[]Current reward indexes, one per getRewardTokens() entry

rewardIndexesStored

View function. Returns the cached reward indexes without triggering an update.

function rewardIndexesStored() external view returns (uint256[] memory indexes);
Return ValueTypeDescription
indexesuint256[]Stored reward indexes, one per getRewardTokens() entry

claimRewards

Updates the global index, credits the user's accrued rewards, and transfers them to user.

function claimRewards(address user) external returns (uint256[] memory rewardAmounts);
ParameterTypeDescription
useraddressThe user to claim rewards for
Return ValueTypeDescription
rewardAmountsuint256[]Amounts transferred, one per getRewardTokens() entry

YT Rewards

Contract: PendleYieldToken

YT holders earn SY-native rewards in proportion to the SY value their YT represents. The key difference from SY is that YT does not maintain its own reward indexes — it delegates entirely to SY.rewardIndexesCurrent().

Reward shares

A YT holder's reward share is the amount of SY their YT position currently represents, plus any interest they have accrued but not yet redeemed:

rewardShares = SYUtils.assetToSy(userInterest[user].index, balanceOf(user))
+ userInterest[user].accrued;

This means reward weight grows over time as yield accumulates, even without any new YT minting.

Post-expiry behaviour

When a YT expires, _setPostExpiryData() snapshots the reward indexes at the moment of expiry (postExpiry.firstRewardIndex). After expiry:

  • No new rewards accrue to YT holders — the index is frozen at the expiry snapshot
  • Any rewards that flow into the YT contract after expiry go to the treasury via redeemInterestAndRewardsPostExpiryForTreasury()
  • Users can still claim their pre-expiry accrued rewards via redeemDueInterestAndRewards

Fee

A rewardFeeRate (set by IPYieldContractFactory, max 20%) is deducted from rewards before transfer. The fee amount is sent to the treasury.

getRewardTokens

Delegates to SY.getRewardTokens().

function getRewardTokens() external view returns (address[] memory tokens);
Return ValueTypeDescription
tokensaddress[]List of reward token addresses

redeemDueInterestAndRewards

Claims interest and/or rewards for user. To claim only rewards, pass redeemInterest = false, redeemRewards = true.

function redeemDueInterestAndRewards(
address user,
bool redeemInterest,
bool redeemRewards
) external returns (uint256 interestOut, uint256[] memory rewardsOut);
ParameterTypeDescription
useraddressThe user to claim for
redeemInterestboolWhether to claim accrued interest
redeemRewardsboolWhether to claim accrued rewards
Return ValueTypeDescription
interestOutuint256Interest amount transferred (0 if redeemInterest = false)
rewardsOutuint256[]Reward amounts transferred, one per getRewardTokens() entry

The reward update must happen before the interest transfer (since interest redemption changes reward shares). The contract enforces this ordering internally.

See YieldTokenization for the full API including interest redemption.


Market (LP) Rewards

Contract: PendleGauge (inherited by PendleMarket)

LP holders receive rewards from two sources simultaneously:

  1. SY-native rewards — pulled via IStandardizedYield(SY).claimRewards(market)
  2. PENDLE incentives — pulled via IPGaugeController(gaugeController).redeemMarketReward()

Both sources are collected inside _redeemExternalReward(). After collection, the standard index mechanism distributes rewards proportionally to LP holders based on their LP token balance.

Reward shares

Each LP holder's reward share equals their LP token balance. Total shares = total LP supply.

_redeemExternalReward() internals

function _redeemExternalReward() internal override {
IStandardizedYield(SY).claimRewards(address(this));
IPGaugeController(gaugeController).redeemMarketReward();
}

getRewardTokens

Returns the SY's reward tokens with PENDLE appended (if not already present).

function getRewardTokens() external view returns (address[] memory tokens);
Return ValueTypeDescription
tokensaddress[]List of reward token addresses ([...SY reward tokens, PENDLE])

redeemRewards

Updates the global reward index, credits the user, and transfers all accumulated rewards to user.

function redeemRewards(address user) external returns (uint256[] memory rewardAmounts);
ParameterTypeDescription
useraddressThe user to claim rewards for
Return ValueTypeDescription
rewardAmountsuint256[]Amounts transferred, one per getRewardTokens() entry

Off-Chain Rewards

Contract: PendleMultiTokenMerkleDistributor

Some reward programs (e.g. voter incentives, partner point campaigns) are computed off-chain and distributed via a Merkle tree rather than the on-chain index mechanism. The protocol owner periodically commits a new Merkle root encoding cumulative (token, user, totalAccrued) entitlements. Users claim by supplying a Merkle proof.

See MerkleDistributor for full documentation.


Batch Claiming via Router

The Pendle Router provides a convenience function to claim rewards across multiple SY, YT, and market positions in a single transaction. For a higher-level interface that handles routing complexity for you, see the Hosted SDK.

Important

The code below is for illustration only. In production, always interact through the Pendle Router to ensure correct token approvals, slippage protection, and callback handling.

router.redeemDueInterestAndRewards(
user,
sys[], // SY addresses to claim from
yts[], // YT addresses to claim from
markets[] // Market addresses to claim from
);

See MiscFunctions — redeemDueInterestAndRewards for full parameter documentation.

Example

import { ethers } from "ethers";

const router = new ethers.Contract(ROUTER_ADDRESS, ROUTER_ABI, signer);

// Claim from one SY, one YT, and one market in a single tx
const tx = await router.redeemDueInterestAndRewards(
userAddress,
[syAddress], // SY positions
[ytAddress], // YT positions
[marketAddress], // Market (LP) positions
);
await tx.wait();