#55002 [SC-Low] rewards claims increase pool collateral but do not notify assetmanager stale cr accounting after fix for 45893
Submitted on Sep 20th 2025 at 17:44:42 UTC by @Disqualified-User for Mitigation Audit | Flare | FAssets
Report ID: #55002
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/flare-foundation/fassets/commit/92e1e2bdc6e8f75f61cfd9f10ddb05df4a7c8c6b
Impacts: Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
The fix for Immunefi report #45893 changed CollateralPool.claimDelegationRewards and CollateralPool.claimAirdropDistribution to compute the actual amount received by measuring wNat.balanceOf(address(this)) before/after the external call, and to increment totalCollateral by the measured delta. That closes the original “fake return value” manipulation.
However, the fix forgot to propagate the successful collateral increase to AssetManager. Other flows (e.g., enter() and upgradeWNatContract()) explicitly call assetManager.updateCollateral(agentVault, wNat) to keep global accounting and CR-related logic in sync. The two claim functions don’t. As a result, the system’s global view of an agent’s pool collateral can remain stale in AssetManager after rewards/airdrop claims, even though CollateralPool.totalCollateral increased.
This doesn’t steal funds or break exits, but it fails to deliver the expected returns throughout the system because the component that drives broader protocol behavior (liquidation thresholds, redemptions that rely on AssetManager’s view, health monitoring, etc.) is not updated to reflect the new pool collateral immediately after a claim.
Severity
Low – Contract fails to deliver promised returns, but doesn’t lose value.
The claim correctly credits the pool and benefits direct exits denominated in pool NAT, but the protocol’s canonical accounting (via AssetManager) is left stale until some other path (e.g., a later enter() or governance action) eventually triggers updateCollateral. This is a correctness/consistency issue without direct loss of value.
Vulnerability Details
What changed in the fix: In commit 92e1e2bd (https://github.com/flare-foundation/fassets/commit/92e1e2bdc6e8f75f61cfd9f10ddb05df4a7c8c6b) the project replaced “trust the return value” with “measure actual balance change”:
claimDelegationRewardsnow:reads
balanceBefore = wNat.balanceOf(address(this)),calls
_rewardManager.claim(...),reads
balanceAfter = wNat.balanceOf(address(this)),sets
claimed = balanceAfter - balanceBefore,increases
totalCollateral += claimed,emits
ClaimedReward.
claimAirdropDistributiondoes the same with_distribution.claim(...).
The diff on GitHub shows these exact insertions and no additional calls at the end of either function. There is no call to assetManager.updateCollateral(agentVault, wNat) after the claims.
By contrast, enter() still updates the AssetManager immediately after adding NAT (via _depositWNat()), by calling assetManager.updateCollateral(agentVault, wNat) (visible in the file content the team provided with the competition; this call predates and is unrelated to the fix).
Why this matters: CollateralPool is the ledger of pool NAT used for mint/exit math and fee handling. AssetManager is the protocol brain that calculates agent/pool health, CR thresholds, and coordinates redemptions and liquidations. When a claim adds new wNat into the pool, both the local pool accounting and the global protocol accounting should reflect that increase.
Because claimDelegationRewards/claimAirdropDistribution never call assetManager.updateCollateral, the global, cross-contract view remains stale. This can produce inconsistent behavior such as:
Underreported pool backing in protocol health/CR views immediately after a claim. Internal logic in
AssetManagerthat relies on the updated pool collateral may continue acting as if the pool hadn’t been replenished.Unnecessarily restrictive actions driven by stale CR checks elsewhere (e.g., redemptions that consult
AssetManagerlimits, warnings/thresholds, or liquidations initiated based on the global view).Misleading system telemetry (events/metrics sourced from
AssetManager), undermining the “rewards flow to pool” promise at the protocol level until some later action triggers a collateral refresh.
To be clear: exits from the pool that read totalCollateral still see the new funds. The inconsistency is cross-module: AssetManager doesn’t learn about the collateral increase, so the broader system may not deliver the operational and UX benefits expected from claims until a different code path updates AssetManager.
Impact Details
The system fails to deliver the full protocol-level benefit of rewards/airdrop claims right away. Pool providers expect “claims increase pool health,” but AssetManager’s stale view can cause conservative CR gating, inaccurate health reporting, or temporarily suboptimal redemption limits. There’s no direct user fund loss; instead, the fix introduces an accounting propagation gap that leads to delayed realization of claim benefits outside the pool contract.
Recommendation
Mirror the pattern already used in enter() and upgradeWNatContract():
After computing
claimedand increasingtotalCollateral, immediately call:
Keep the call after the
wNat.balanceOfdelta calculation (so the update reflects the new state).The functions are already
nonReentrant, andenter()shows this call is safe in practice in this contract.
Minimal patch (file: contracts/assetManager/implementation/CollateralPool.sol):
This restores invariants: whenever pool collateral increases on-chain, both local pool accounting and the protocol’s global accounting are updated.
References
Smart Contract - Fix of Report - 45893: https://github.com/flare-foundation/fassets/commit/92e1e2bdc6e8f75f61cfd9f10ddb05df4a7c8c6b
Mitigation Audit scope & run instructions: https://immunefi.com/audit-competition/flare-fassets--mitigation-audit/scope/#top
Proof of Concept
Below is a self-contained, runnable PoC that demonstrates the missing propagation. It uses a minimal AssetManagerProbe that implements only what CollateralPool touches in these paths and counts updateCollateral calls. A FakeDistribution sends real wNat to the pool in claim, and we assert that:
the pool’s
wNatbalance increases,totalCollateralincreases,AssetManagerProbe.updateCollateralwas NOT called (count stays0), proving the propagation gap.
The PoC plugs into the repository’s test rig (per Flare Mitigation Audit instructions) and can be run with the standard build/test scripts.
New test contracts
contracts/test/AssetManagerProbe.sol:
contracts/test/FakeDistribution.sol:
Test: demonstrates the missing propagation
test/unit/fasset/implementation/CollateralPool.UpdatePropagation.spec.ts:
How to run
Clone & install:
Add the three files above at the indicated paths.
Compile & test:
The test should pass and print no updateCollateral calls while showing the pool’s wNat balance and totalCollateral increased by the drop.
Observed result
The pool’s balance and
totalCollateralincrease by the rewarded amount (correct).The
AssetManagerProbe.updateCollateralcounter remains0, demonstrating the missing propagation, i.e., the fix leftAssetManager’s global view stale after claims.
Last updated
Was this helpful?