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:
| Position | Contract | Reward Tokens | Claim Function |
|---|---|---|---|
| SY holder | SYBaseWithRewards | SY-native (e.g. AAVE) | claimRewards(user) |
| YT holder | PendleYieldToken | SY-native (via SY indexes) | redeemDueInterestAndRewards(user, false, true) |
| LP holder | PendleMarket (PendleGauge) | SY-native + PENDLE | redeemRewards(user) |
| Any | PendleMultiTokenMerkleDistributor | Any (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 tokenuserReward[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 Value | Type | Description |
|---|---|---|
tokens | address[] | 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);
| Parameter | Type | Description |
|---|---|---|
user | address | The user address |
| Return Value | Type | Description |
|---|---|---|
amounts | uint256[] | 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 Value | Type | Description |
|---|---|---|
indexes | uint256[] | 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 Value | Type | Description |
|---|---|---|
indexes | uint256[] | 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);
| Parameter | Type | Description |
|---|---|---|
user | address | The user to claim rewards for |
| Return Value | Type | Description |
|---|---|---|
rewardAmounts | uint256[] | 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 Value | Type | Description |
|---|---|---|
tokens | address[] | 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);
| Parameter | Type | Description |
|---|---|---|
user | address | The user to claim for |
redeemInterest | bool | Whether to claim accrued interest |
redeemRewards | bool | Whether to claim accrued rewards |
| Return Value | Type | Description |
|---|---|---|
interestOut | uint256 | Interest amount transferred (0 if redeemInterest = false) |
rewardsOut | uint256[] | 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:
- SY-native rewards — pulled via
IStandardizedYield(SY).claimRewards(market) - 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 Value | Type | Description |
|---|---|---|
tokens | address[] | 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);
| Parameter | Type | Description |
|---|---|---|
user | address | The user to claim rewards for |
| Return Value | Type | Description |
|---|---|---|
rewardAmounts | uint256[] | 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.
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();