Rewards Claiming Flow¶
End-to-end execution flow for claiming rewards from Aave V3 RewardsController.
Quick Reference¶
Aspect |
Details |
|---|---|
Entry Points |
|
Key Transformations |
|
State Changes |
|
Events Emitted |
|
Flow Diagram¶
flowchart TD
%% Styling definitions
classDef validation fill:#ffcccc,stroke:#ff0000,stroke-width:2px
classDef transformation fill:#ccffcc,stroke:#00aa00,stroke-width:2px
classDef storage fill:#ccccff,stroke:#0000ff,stroke-width:2px
classDef event fill:#ffffcc,stroke:#aaaa00,stroke-width:2px
classDef error fill:#ff0000,stroke:#000000,color:#fff
classDef critical stroke:#ff0000,stroke-width:3px
%% Entry points
Entry1["claimRewards / claimRewardsToSelf"] --> ValidateTo1["VALIDATION<br/>to != address"]
Entry2["claimRewardsOnBehalf"] --> ValidateUser["VALIDATION<br/>claimer authorized<br/>user != address"]
Entry3["claimAllRewards / claimAllRewardsToSelf"] --> ValidateTo2["VALIDATION<br/>to != address"]
Entry4["claimAllRewardsOnBehalf"] --> ValidateUser2["VALIDATION<br/>claimer authorized<br/>user != address"]
class ValidateTo1 validation
class ValidateUser validation
class ValidateTo2 validation
class ValidateUser2 validation
ValidateTo1 --> GetBalances["_getUserAssetBalances<br/>Get scaled balances<br/>for all assets"]
ValidateUser --> GetBalances
ValidateTo2 --> GetBalances
ValidateUser2 --> GetBalances
subgraph DataUpdate ["1. Data Update Phase"]
direction TB
UpdateData["_updateDataMultiple<br/>Iterate all assets"] --> UpdateAsset["For each asset:<br/>_updateData"]
UpdateAsset --> UpdateReward["TRANSFORMATION<br/>_updateRewardData<br/>Calculate new index<br/>based on time delta"]
class UpdateReward transformation
UpdateReward --> UpdateUser["TRANSFORMATION<br/>_updateUserData<br/>Calculate accrued rewards"]
class UpdateUser transformation
UpdateUser --> StoreAccrued["STORAGE UPDATE<br/>usersData[user].accrued +=<br/>rewardsAccrued"]
class StoreAccrued storage
StoreAccrued --> EmitAccrued["EMIT<br/>Accrued"]
class EmitAccrued event
end
GetBalances --> DataUpdate
subgraph ClaimCalc ["2. Claim Calculation Phase"]
direction TB
SingleReward{"Single or<br/>All Rewards?"}
SingleReward -->|Single| SumAccrued["Sum accrued rewards<br/>across all assets<br/>for specific reward"]
SingleReward -->|All| IterateRewards["Iterate all rewards<br/>across all assets"]
SumAccrued --> AmountCheck{"amount == 0?"}
class AmountCheck validation
AmountCheck -->|Yes| ReturnZero["Return 0"]
AmountCheck -->|No| CheckLimit{"totalRewards ><br/>requested amount?"}
CheckLimit -->|total <= amount| ClearAll["STORAGE UPDATE<br/>accrued = 0<br/>for all assets"]
class ClearAll storage
CheckLimit -->|total > amount| PartialClaim["Calculate difference<br/>STORAGE UPDATE<br/>accrued = difference"]
class PartialClaim storage
IterateRewards --> AccumulateAll["Accumulate all<br/>reward amounts<br/>per reward token"]
AccumulateAll --> ClearAllMulti["STORAGE UPDATE<br/>accrued = 0<br/>for claimed amounts"]
class ClearAllMulti storage
end
DataUpdate --> SingleReward
subgraph TransferPhase ["3. Reward Transfer Phase"]
direction TB
CheckZero{"totalRewards<br/>== 0?"}
class CheckZero validation
CheckZero -->|Yes| ReturnZero2["Return 0"]
CheckZero -->|No| GetStrategy["Get TransferStrategy<br/>for reward token"]
GetStrategy --> PerformTransfer["_transferRewards<br/>delegatecall to<br/>TransferStrategy"]
PerformTransfer --> TransferSuccess{"Transfer<br/>successful?"}
class TransferSuccess validation
TransferSuccess -->|No| RevertError["REVERT<br/>TRANSFER_ERROR"]
class RevertError error
TransferSuccess -->|Yes| EmitClaimed["EMIT<br/>RewardsClaimed"]
class EmitClaimed event
end
ClearAll --> CheckZero
PartialClaim --> CheckZero
ClearAllMulti --> CheckZero
%% Single reward transfer vs All rewards transfer
SingleReward -.->|All| MultiTransfer["Loop through<br/>all reward tokens<br/>_transferRewards each"]
MultiTransfer --> EmitClaimed
EmitClaimed --> ReturnAmount["Return<br/>claimed amount(s)"]
Step-by-Step Execution¶
1. Entry Points¶
File: contracts/rewards/RewardsController.sol
function claimRewards(
address[] calldata assets,
uint256 amount,
address to,
address reward
) external override returns (uint256) {
require(to != address(0), 'INVALID_TO_ADDRESS');
return _claimRewards(assets, amount, msg.sender, msg.sender, to, reward);
}
function claimRewardsOnBehalf(
address[] calldata assets,
uint256 amount,
address user,
address to,
address reward
) external override onlyAuthorizedClaimers(msg.sender, user) returns (uint256) {
require(user != address(0), 'INVALID_USER_ADDRESS');
require(to != address(0), 'INVALID_TO_ADDRESS');
return _claimRewards(assets, amount, msg.sender, user, to, reward);
}
function claimRewardsToSelf(
address[] calldata assets,
uint256 amount,
address reward
) external override returns (uint256) {
return _claimRewards(assets, amount, msg.sender, msg.sender, msg.sender, reward);
}
function claimAllRewards(address[] calldata assets, address to)
external
override
returns (address[] memory rewardsList, uint256[] memory claimedAmounts)
{
require(to != address(0), 'INVALID_TO_ADDRESS');
return _claimAllRewards(assets, msg.sender, msg.sender, to);
}
function claimAllRewardsOnBehalf(
address[] calldata assets,
address user,
address to
)
external
override
onlyAuthorizedClaimers(msg.sender, user)
returns (address[] memory rewardsList, uint256[] memory claimedAmounts)
{
require(user != address(0), 'INVALID_USER_ADDRESS');
require(to != address(0), 'INVALID_TO_ADDRESS');
return _claimAllRewards(assets, msg.sender, user, to);
}
function claimAllRewardsToSelf(address[] calldata assets)
external
override
returns (address[] memory rewardsList, uint256[] memory claimedAmounts)
{
return _claimAllRewards(assets, msg.sender, msg.sender, msg.sender);
}
2. Get User Asset Balances¶
File: contracts/rewards/RewardsController.sol
function _getUserAssetBalances(address[] calldata assets, address user)
internal
view
override
returns (RewardsDataTypes.UserAssetBalance[] memory userAssetBalances)
{
userAssetBalances = new RewardsDataTypes.UserAssetBalance[](assets.length);
for (uint256 i = 0; i < assets.length; i++) {
userAssetBalances[i].asset = assets[i];
(userAssetBalances[i].userBalance, userAssetBalances[i].totalSupply) = IScaledBalanceToken(
assets[i]
).getScaledUserBalanceAndSupply(user);
}
return userAssetBalances;
}
3. Update Data Multiple¶
File: contracts/rewards/RewardsDistributor.sol
function _updateDataMultiple(
address user,
RewardsDataTypes.UserAssetBalance[] memory userAssetBalances
) internal {
for (uint256 i = 0; i < userAssetBalances.length; i++) {
_updateData(
userAssetBalances[i].asset,
user,
userAssetBalances[i].userBalance,
userAssetBalances[i].totalSupply
);
}
}
4. Update Data (Per Asset)¶
File: contracts/rewards/RewardsDistributor.sol
function _updateData(
address asset,
address user,
uint256 userBalance,
uint256 totalSupply
) internal {
uint256 assetUnit;
uint256 numAvailableRewards = _assets[asset].availableRewardsCount;
unchecked {
assetUnit = 10**_assets[asset].decimals;
}
if (numAvailableRewards == 0) {
return;
}
unchecked {
for (uint128 r = 0; r < numAvailableRewards; r++) {
address reward = _assets[asset].availableRewards[r];
RewardsDataTypes.RewardData storage rewardData = _assets[asset].rewards[reward];
(uint256 newAssetIndex, bool rewardDataUpdated) = _updateRewardData(
rewardData,
totalSupply,
assetUnit
);
(uint256 rewardsAccrued, bool userDataUpdated) = _updateUserData(
rewardData,
user,
userBalance,
newAssetIndex,
assetUnit
);
if (rewardDataUpdated || userDataUpdated) {
emit Accrued(asset, reward, user, newAssetIndex, newAssetIndex, rewardsAccrued);
}
}
}
}
5. Update Reward Data (Index Calculation)¶
File: contracts/rewards/RewardsDistributor.sol
function _updateRewardData(
RewardsDataTypes.RewardData storage rewardData,
uint256 totalSupply,
uint256 assetUnit
) internal returns (uint256, bool) {
(uint256 oldIndex, uint256 newIndex) = _getAssetIndex(rewardData, totalSupply, assetUnit);
bool indexUpdated;
if (newIndex != oldIndex) {
require(newIndex <= type(uint104).max, 'INDEX_OVERFLOW');
indexUpdated = true;
//optimization: storing one after another saves one SSTORE
rewardData.index = uint104(newIndex);
rewardData.lastUpdateTimestamp = block.timestamp.toUint32();
} else {
rewardData.lastUpdateTimestamp = block.timestamp.toUint32();
}
return (newIndex, indexUpdated);
}
6. Get Asset Index¶
File: contracts/rewards/RewardsDistributor.sol
function _getAssetIndex(
RewardsDataTypes.RewardData storage rewardData,
uint256 totalSupply,
uint256 assetUnit
) internal view returns (uint256, uint256) {
uint256 oldIndex = rewardData.index;
uint256 distributionEnd = rewardData.distributionEnd;
uint256 emissionPerSecond = rewardData.emissionPerSecond;
uint256 lastUpdateTimestamp = rewardData.lastUpdateTimestamp;
if (
emissionPerSecond == 0 ||
totalSupply == 0 ||
lastUpdateTimestamp == block.timestamp ||
lastUpdateTimestamp >= distributionEnd
) {
return (oldIndex, oldIndex);
}
uint256 currentTimestamp = block.timestamp > distributionEnd
? distributionEnd
: block.timestamp;
uint256 timeDelta = currentTimestamp - lastUpdateTimestamp;
uint256 firstTerm = emissionPerSecond * timeDelta * assetUnit;
assembly {
firstTerm := div(firstTerm, totalSupply)
}
return (oldIndex, (firstTerm + oldIndex));
}
7. Update User Data¶
File: contracts/rewards/RewardsDistributor.sol
function _updateUserData(
RewardsDataTypes.RewardData storage rewardData,
address user,
uint256 userBalance,
uint256 newAssetIndex,
uint256 assetUnit
) internal returns (uint256, bool) {
uint256 userIndex = rewardData.usersData[user].index;
uint256 rewardsAccrued;
bool dataUpdated;
if ((dataUpdated = userIndex != newAssetIndex)) {
// already checked for overflow in _updateRewardData
rewardData.usersData[user].index = uint104(newAssetIndex);
if (userBalance != 0) {
rewardsAccrued = _getRewards(userBalance, newAssetIndex, userIndex, assetUnit);
rewardData.usersData[user].accrued += rewardsAccrued.toUint128();
}
}
return (rewardsAccrued, dataUpdated);
}
8. Get Rewards Calculation¶
File: contracts/rewards/RewardsDistributor.sol
function _getRewards(
uint256 userBalance,
uint256 reserveIndex,
uint256 userIndex,
uint256 assetUnit
) internal pure returns (uint256) {
uint256 result = userBalance * (reserveIndex - userIndex);
assembly {
result := div(result, assetUnit)
}
return result;
}
9. Claim Rewards (Single Reward)¶
File: contracts/rewards/RewardsController.sol
function _claimRewards(
address[] calldata assets,
uint256 amount,
address claimer,
address user,
address to,
address reward
) internal returns (uint256) {
if (amount == 0) {
return 0;
}
uint256 totalRewards;
_updateDataMultiple(user, _getUserAssetBalances(assets, user));
for (uint256 i = 0; i < assets.length; i++) {
address asset = assets[i];
totalRewards += _assets[asset].rewards[reward].usersData[user].accrued;
if (totalRewards <= amount) {
_assets[asset].rewards[reward].usersData[user].accrued = 0;
} else {
uint256 difference = totalRewards - amount;
totalRewards -= difference;
_assets[asset].rewards[reward].usersData[user].accrued = difference.toUint128();
break;
}
}
if (totalRewards == 0) {
return 0;
}
_transferRewards(to, reward, totalRewards);
emit RewardsClaimed(user, reward, to, claimer, totalRewards);
return totalRewards;
}
10. Claim All Rewards¶
File: contracts/rewards/RewardsController.sol
function _claimAllRewards(
address[] calldata assets,
address claimer,
address user,
address to
) internal returns (address[] memory rewardsList, uint256[] memory claimedAmounts) {
uint256 rewardsListLength = _rewardsList.length;
rewardsList = new address[](rewardsListLength);
claimedAmounts = new uint256[](rewardsListLength);
_updateDataMultiple(user, _getUserAssetBalances(assets, user));
for (uint256 i = 0; i < assets.length; i++) {
address asset = assets[i];
for (uint256 j = 0; j < rewardsListLength; j++) {
if (rewardsList[j] == address(0)) {
rewardsList[j] = _rewardsList[j];
}
uint256 rewardAmount = _assets[asset].rewards[rewardsList[j]].usersData[user].accrued;
if (rewardAmount != 0) {
claimedAmounts[j] += rewardAmount;
_assets[asset].rewards[rewardsList[j]].usersData[user].accrued = 0;
}
}
}
for (uint256 i = 0; i < rewardsListLength; i++) {
_transferRewards(to, rewardsList[i], claimedAmounts[i]);
emit RewardsClaimed(user, rewardsList[i], to, claimer, claimedAmounts[i]);
}
return (rewardsList, claimedAmounts);
}
11. Transfer Rewards¶
File: contracts/rewards/RewardsController.sol
function _transferRewards(
address to,
address reward,
uint256 amount
) internal {
ITransferStrategyBase transferStrategy = _transferStrategy[reward];
bool success = transferStrategy.performTransfer(to, reward, amount);
require(success == true, 'TRANSFER_ERROR');
}
Amount Transformations¶
Reward Index Calculation¶
oldIndex = rewardData.index
emissionPerSecond = rewardData.emissionPerSecond
lastUpdateTimestamp = rewardData.lastUpdateTimestamp
distributionEnd = rewardData.distributionEnd
totalSupply = IScaledBalanceToken(asset).scaledTotalSupply()
assetUnit = 10^decimals
↓
if (emissionPerSecond == 0 || totalSupply == 0 ||
lastUpdateTimestamp == block.timestamp ||
lastUpdateTimestamp >= distributionEnd):
newIndex = oldIndex
↓
currentTimestamp = min(block.timestamp, distributionEnd)
timeDelta = currentTimestamp - lastUpdateTimestamp
firstTerm = (emissionPerSecond * timeDelta * assetUnit) / totalSupply
newIndex = firstTerm + oldIndex
↓
rewardData.index = newIndex (uint104)
rewardData.lastUpdateTimestamp = block.timestamp (uint32)
User Rewards Accrual¶
userIndex = rewardData.usersData[user].index (stored user index)
newAssetIndex = calculated above
userBalance = user's scaled balance
↓
if (userIndex != newAssetIndex):
rewardsAccrued = (userBalance * (newAssetIndex - userIndex)) / assetUnit
rewardData.usersData[user].index = newAssetIndex (uint104)
rewardData.usersData[user].accrued += rewardsAccrued (uint128)
Claim Amount Calculation¶
Single Reward Claim:
totalRewards = sum of accrued across all assets for specific reward
if (totalRewards <= amount):
claimed = totalRewards
accrued = 0 for all assets
else:
claimed = amount
difference = totalRewards - amount
accrued = difference (partial claim)
All Rewards Claim:
For each reward in _rewardsList:
claimedAmounts[j] = sum of accrued across all assets
accrued = 0 for claimed amounts
Key Points:
Reward index is stored as
uint104(max ~2.02e31)Accrued rewards stored as
uint128(max ~3.4e38)Timestamps stored as
uint32(valid until year 2106)Emission per second stored as
uint88(max ~3.1e26)Index calculation uses assembly for division to save gas
Event Details¶
RewardsClaimed Event¶
event RewardsClaimed(
address indexed user, // User whose rewards were claimed
address indexed reward, // Reward token address
address indexed to, // Recipient of the rewards
address claimer, // Address that initiated the claim
uint256 amount // Amount of rewards claimed
);
Accrued Event¶
Emitted during the data update phase when rewards are accrued for a user.
event Accrued(
address indexed asset, // Asset being incentivized
address indexed reward, // Reward token address
address indexed user, // User address
uint256 assetIndex, // New asset index
uint256 userIndex, // New user index
uint256 rewardsAccrued // Amount of rewards accrued
);
ClaimerSet Event¶
Emitted when an authorized claimer is set for a user.
event ClaimerSet(
address indexed user, // User address
address indexed claimer // Authorized claimer address
);
TransferStrategyInstalled Event¶
Emitted when a transfer strategy is configured for a reward token.
event TransferStrategyInstalled(
address indexed reward, // Reward token address
address indexed transferStrategy // Transfer strategy contract
);
RewardOracleUpdated Event¶
Emitted when a price oracle is set for a reward token.
event RewardOracleUpdated(
address indexed reward, // Reward token address
address indexed rewardOracle // Oracle contract address
);
Error Conditions¶
Error |
Condition |
File |
|---|---|---|
|
|
RewardsController.sol |
|
|
RewardsController.sol |
|
|
RewardsController.sol |
|
Transfer strategy returns |
RewardsController.sol |
|
|
RewardsDistributor.sol |
|
|
RewardsDistributor.sol |
|
Array length mismatch in |
RewardsDistributor.sol |
|
Asset not configured |
RewardsDistributor.sol |
|
Transfer strategy is zero address |
RewardsController.sol |
|
Transfer strategy is not a contract |
RewardsController.sol |
|
Oracle returns zero or negative price |
RewardsController.sol |
Source File Locations¶
contracts/rewards/RewardsController.sol
contracts/rewards/RewardsDistributor.sol
contracts/rewards/interfaces/IRewardsController.sol
contracts/rewards/interfaces/IRewardsDistributor.sol
contracts/rewards/interfaces/ITransferStrategyBase.sol
contracts/rewards/libraries/RewardsDataTypes.sol
contracts/misc/interfaces/IEACAggregatorProxy.sol
Data Structures¶
RewardData¶
struct RewardData {
uint104 index; // Liquidity index of the reward distribution
uint88 emissionPerSecond; // Amount of reward tokens distributed per second
uint32 lastUpdateTimestamp; // Timestamp of the last reward index update
uint32 distributionEnd; // The end of the distribution of rewards (in seconds)
mapping(address => UserData) usersData;
}
UserData¶
struct UserData {
uint104 index; // Liquidity index of the reward distribution for the user
uint128 accrued; // Amount of accrued rewards for the user since last update
}
UserAssetBalance¶
struct UserAssetBalance {
address asset; // Asset address
uint256 userBalance; // User's scaled balance
uint256 totalSupply; // Total scaled supply
}