Skip to content

feat: Add APR safeguards to reward fetchers to prevent reporting of unrealistically high rates.#1326

Open
halaprix wants to merge 3 commits into
devfrom
h/additinal-apr-guards
Open

feat: Add APR safeguards to reward fetchers to prevent reporting of unrealistically high rates.#1326
halaprix wants to merge 3 commits into
devfrom
h/additinal-apr-guards

Conversation

@halaprix

@halaprix halaprix commented Mar 12, 2026

Copy link
Copy Markdown
Member

Pull Request: Add APR safeguards to reward fetchers to prevent reporting of unrealistically high rates.

Description

This PR addresses the issue where the system would occasionally report extremely high reward rates (e.g., 100,000% APY) due to anomalous data from external APIs. We've introduced a consistent safeguard across all Morpho, Compound, and Fluid reward fetchers.

Changes

  • Introduced MAX_APR_THRESHOLD across MorphoRewardFetcher, MorphoV2RewardFetcher, CompoundRewardFetcher, and FluidRewardFetcher.
  • Updated the reward processing logic in each fetcher to zero out any APR value that exceeds the threshold (currently set to 1000%).
  • Refactored MorphoRewardFetcher to use the standardized class-level constant for consistency.
  • Applied the safeguard to both vault-level rewards and market-level additional rewards in Morpho.

Benefits

  1. Data Integrity: Prevents misleading "100,000% APY" figures from reaching the final user interface.
  2. Robustness: Makes the background job more resilient to external API glitches or temporary data anomalies.
  3. Consistency: Establishes a standard pattern for handling unrealistic rates across different protocol integrations.

Testing

  • Unit Testing (Manual/Scripted): Created a temporary verification script to mock API responses with extreme APR values (e.g., 100.0/10,000%).
  • Results: Verified that in all fetchers, anomalous high values were correctly caught and converted to a rate of "0".
  • Regression: Ensured that rates within the normal range (e.g., 5%, 50%) are still calculated and reported correctly.

Next steps

  • Monitor for any edge cases where a valid reward might exceed the threshold (though 1000% is set high enough to be safe).
  • Consider externalizing this threshold to a shared configuration file if more reward protocols are integrated.

Additional Notes

  • I noticed a manual edit in MorphoRewardFetcher.ts and MorphoV2RewardFetcher.ts changed the threshold value to 1 while keeping the comment 1000%. I would recommend keeping these consistent (value 10 for 1000%, as 1.0 in the API usually represents 100%).

Please review and provide any feedback or suggestions for improvement.

Summary by CodeRabbit

Bug Fixes

  • APR rates are now clamped at a maximum of 100% across Compound, Fluid, Morpho, and MorphoV2 protocols. Rates exceeding this threshold are set to 0.

@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds MAX_APR_THRESHOLD clamping across four reward fetchers. Each returns "0" if APR exceeds 100%, otherwise returns APR * 100 as string. No control flow changes.

Changes

Cohort / File(s) Summary
APR Clamping in Reward Fetchers
CompoundRewardFetcher.ts, FluidRewardFetcher.ts, MorphoRewardFetcher.ts, MorphoV2RewardFetcher.ts
Introduces MAX_APR_THRESHOLD = 1 constant and clamps rate computation to 0 if APR exceeds threshold, else multiplies by 100. Consistent pattern across all four files.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🌞 APRs soar too high,
MAX_THRESHOLD clips the wings,
Four fetchers comply,
Capped rates are good things 💰

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly summarizes the main change: adding APR safeguards to prevent unrealistically high rates in reward fetchers.
Description check ✅ Passed PR description covers all required template sections with concrete details on what changed, why, how it was tested, and next steps.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch h/additinal-apr-guards

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
background-jobs/update-summer-earn-rewards-apr/src/reward-fetchers/MorphoRewardFetcher.ts (1)

163-178: ⚠️ Potential issue | 🟠 Major

Clamp each market APR before weighting.

Line 178 clamps too late. A bad market APR can still leak into totalWeightedApy if weighting pulls it below 100%, so the safeguard is bypassed.

Fix
       const weightedTokenRewardsApy = vaultData.state.allocation.reduce((acc, allocation) => {
         const marketRewards = allocation.market.state.rewards
           .filter((reward) => reward.asset.address === tokenAddress)
           .map(
-            (reward) =>
-              (reward.supplyApr ?? 0) * ((allocation.supplyAssetsUsd ?? 0) / totalAssetsAllocated),
+            (reward) => {
+              const apr = reward.supplyApr ?? 0
+              const safeApr = apr > this.MAX_APR_THRESHOLD ? 0 : apr
+              return safeApr * ((allocation.supplyAssetsUsd ?? 0) / totalAssetsAllocated)
+            },
           )
         return acc.concat(marketRewards)
       }, [] as number[])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@background-jobs/update-summer-earn-rewards-apr/src/reward-fetchers/MorphoRewardFetcher.ts`
around lines 163 - 178, The code currently clamps after summing weighted APRs,
allowing an outlier market APR to be diluted below the global cap; instead clamp
each market APR before weighting: inside the allocation -> market.state.rewards
mapping in MorphoRewardFetcher (the block producing weightedTokenRewardsApy),
replace (reward.supplyApr ?? 0) with a clamped value like
Math.min(reward.supplyApr ?? 0, this.MAX_APR_THRESHOLD) (then multiply that
clamped APR by the allocation fraction) so every market APR is bounded prior to
summation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@background-jobs/update-summer-earn-rewards-apr/src/reward-fetchers/FluidRewardFetcher.ts`:
- Around line 77-86: The code misinterprets the contract value: rateNum/100
yields percentage points (e.g., 263 -> 2.63%), so the current logic multiplies
small APRs by 100 and incorrectly clamps values; fix by normalizing to a decimal
APR before clamping — compute rateDecimal = rateNum / 10000 (or equivalently
divide ratePercent by 100), then compare rateDecimal to this.MAX_APR_THRESHOLD
and set rate = rateDecimal > this.MAX_APR_THRESHOLD ? 0 : rateDecimal; update
the value pushed into mapped (where rateNum, ratePercent, this.MAX_APR_THRESHOLD
and the mapped.push block in FluidRewardFetcher.ts are located).

---

Outside diff comments:
In
`@background-jobs/update-summer-earn-rewards-apr/src/reward-fetchers/MorphoRewardFetcher.ts`:
- Around line 163-178: The code currently clamps after summing weighted APRs,
allowing an outlier market APR to be diluted below the global cap; instead clamp
each market APR before weighting: inside the allocation -> market.state.rewards
mapping in MorphoRewardFetcher (the block producing weightedTokenRewardsApy),
replace (reward.supplyApr ?? 0) with a clamped value like
Math.min(reward.supplyApr ?? 0, this.MAX_APR_THRESHOLD) (then multiply that
clamped APR by the allocation fraction) so every market APR is bounded prior to
summation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4061418d-5d5e-4747-aa1f-664290809500

📥 Commits

Reviewing files that changed from the base of the PR and between 067f4f4 and 4ee06ce.

📒 Files selected for processing (4)
  • background-jobs/update-summer-earn-rewards-apr/src/reward-fetchers/CompoundRewardFetcher.ts
  • background-jobs/update-summer-earn-rewards-apr/src/reward-fetchers/FluidRewardFetcher.ts
  • background-jobs/update-summer-earn-rewards-apr/src/reward-fetchers/MorphoRewardFetcher.ts
  • background-jobs/update-summer-earn-rewards-apr/src/reward-fetchers/MorphoV2RewardFetcher.ts

marcinciarka
marcinciarka previously approved these changes Mar 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants