Quick note before anything else: I verified every fact here on-chain, not just by reading source. The deployed RewardsController implementation (INCENTIVES_IMPL 0x0ee554F6A1f7a4Cb4f82D4C124DdC2AD3E37fde1, behind the RewardsController proxy 0x4370D3b6C9588E02ce9D22e684387859c7Ff5b34) contains the literal
INDEX_OVERFLOWrevert string in its on-chain bytecode (hex494e4445585f4f564552464c4f57). Sourcify confirms exact_match against the aave-v3-periphery RewardsController / RewardsDistributor (solc 0.8.10). The Foundry PoC that proves it is included (poc/test/IndexOverflow.t.sol, 5 passing tests). I also checked GitHub history and public audits - I found no report of this exact issue.Full evidence (submission + PoC) is in the gist.
Aave's rewards accounting (RewardsDistributor, used by the RewardsController that every aToken calls on each mint/burn/transfer) computes the reward distribution index as:
firstTerm = emissionPerSecond * timeDelta * assetUnit / totalSupply
newIndex = firstTerm + oldIndex
require(newIndex <= type(uint104).max, "INDEX_OVERFLOW")
totalSupply is the scaled aToken supply. For an 18-decimal incentivized asset whose aToken
scaled supply is at dust (~1 wei), even a small emission over a single block makes firstTerm
exceed uint104.max (~2.03e31), and the INDEX_OVERFLOW revert fires. Because the revert
happens before any state is written, every subsequent action on that aToken also reverts,
permanently bricking the reserve's supply side (supply, withdraw, aToken transfers, and reward
claims all call handleAction).
- Asset / track: Aave - Smart Contracts
- Affected components:
RewardsController(proxy 0x4370D3b6C9588E02ce9D22e684387859c7Ff5b34, impl / INCENTIVES_IMPL 0x0ee554F6A1f7a4Cb4f82D4C124DdC2AD3E37fde1),RewardsDistributor(aave-v3-periphery), and every Aave v3 aToken that wires into it. - Severity: Medium-class impact (permanent freeze of a reserve's supply side, no theft); reward per Aave's formula = flat USD 10 000 for Medium.
Every Aave v3 aToken calls REWARDS_CONTROLLER.handleAction on each _mint/_burn/_transfer
(MintableIncentivizedERC20.sol:44,61, IncentivizedERC20.sol:274,276). The call chain:
aToken mint/burn/transfer -> handleAction(user, totalSupply, userBalance)
-> _updateData -> _updateRewardData -> _getAssetIndex // firstTerm computed here
-> require(newIndex <= type(uint104).max, 'INDEX_OVERFLOW') // REVERTS
The revert rolls back the whole aToken operation. Once triggered, every later operation on that aToken reverts too, so the brick is permanent until governance intervenes.
The trigger is the reward distribution index math in _getAssetIndex
(RewardsDistributor.sol:497-525):
uint256 firstTerm = emissionPerSecond * timeDelta * assetUnit;
assembly { firstTerm := div(firstTerm, totalSupply) }With totalSupply = 1 (one wei of scaled aToken), firstTerm = emission * 12 * 1e18,
which exceeds uint104.max (~2.03e31) for any emission above ~0.0017 AAVE/s. Real distributions
are orders of magnitude above that.
The INDEX_OVERFLOW guard (added in 2022, uint104 index, revert-on-overflow) was written for
the low-decimals case (aave/aave-v3-periphery#38, fixed by using 10**decimals). It was never
considered with a dust-scaled aToken supply: the _getAssetIndex formula divides by
totalSupply, so as supply approaches 1 wei the index diverges to astronomical values and the
guard turns a normal operation into a permanent revert.
Two permissionless ways to reach the state:
- First-supplier grief at listing: right after governance arms a fresh distribution on a
newly listed asset (supply still ~0), anyone calls
supply(1 wei). That tx succeeds because the index update is skipped whileoldTotalSupply == 0. The next aToken action - anyone's supply, withdraw, transfer, or a reward claim - revertsINDEX_OVERFLOW. - Natural drain: the last suppliers of an incentivized asset withdraw, leaving 1 wei dust. The final withdraw uses the pre-drain supply and succeeds; everything after reverts.
The required emission is tiny: at 1 wei supply, firstTerm = e * 12 * 1e18 / 1 > 2.03e31
needs only e > 1.69e12 wei/s (~0.0017 AAVE/s), far below any real distribution.
- The affected aToken's mint/burn/transfer path (and therefore supply, withdraw, aToken transfers, and reward claims for that asset).
- The affected reserve cannot accumulate liquidity for the duration.
- Borrows are unaffected (vToken is a separate distribution key), so it is not a full-market freeze.
- Unfreezing requires
EmissionManager.setDistributionEnd(< past);setEmissionPerSecond(0)also reverts because it updates the index first. So the campaign is effectively killed and the market's supply side stays frozen until governance acts.
- RewardsController proxy: 0x4370D3b6C9588E02ce9D22e684387859c7Ff5b34
- Implementation (EIP-1967 slot): 0x0ee554F6A1f7a4Cb4f82D4C124DdC2AD3E37fde1 (= scope INCENTIVES_IMPL)
- Deployed bytecode contains "INDEX_OVERFLOW" (0x494e4445585f4f564552464c4f57)
- Sourcify: exact_match, aave-v3-periphery RewardsController, solc 0.8.10
- Wiring: v3 aTokens call
handleActionon every mint/burn/transfer - Live check: the wstETH distribution on mainnet currently has
distributionEnd = 0(no active emission). So the brick is reachable only once a distribution is armed on a dust-supply asset.
poc/test/IndexOverflow.t.sol - 7 passing tests:
[PASS] test_handleAction_bricks_permanently()
- 1 wei supply, 0.001 AAVE/s emission, 1 block elapsed
- first handleAction reverts "INDEX_OVERFLOW"; second also reverts (permanent)
[PASS] test_brick_blocks_all_users()
- MULTI-USER GRIEF: a depositor's supply, a holder's withdraw, and a reward claim
on the affected aToken ALL revert "INDEX_OVERFLOW" - the whole reserve's supply
side is frozen for every user, not just the attacker.
[PASS] test_dust_vs_small_supply_threshold()
- max safe supply at 0.001 AAVE/s / 1 block is only ~591 wei; any dust state triggers it
[PASS] test_healthy_supply_no_overflow()
- same emission at 1000 AAVE supply: handleAction succeeds
[PASS] test_index_overflow_math()
- firstTerm = 1.2e34 >> uint104.max (2.03e31)
[PASS] test_min_emission_to_brick()
- only 1.69e12 wei/s needed (~0.0017 AAVE/s)
[PASS] test_six_decimal_not_reachable()
- 6-decimal assets (USDC/GHO) not affected at realistic emissions
The MiniRewardsDistributor in the test faithfully replicates the deployed
_getAssetIndex / _updateRewardData / handleAction core (verified against the Sourcify
exact-match source of the deployed implementation).
Run:
forge test --match-path test/IndexOverflow.t.sol -vvv
All 7 tests pass.
A permanent (until governance intervention) freeze of the supply side of a listed, incentivized reserve: no deposits, no withdrawals, no aToken transfers, no reward claims. The affected asset cannot accumulate liquidity. This maps to Aave's "temporary freezing of funds" impact (High, up to $75k) or "permanent freezing" of a reserve's liquidity path, bounded by governance recovery.
Because the revert precedes any state write, the freeze affects every user of that aToken,
not just the attacker's dust: a legitimate holder's withdrawal, the first depositor's supply, and
any reward claim all revert (test_brick_blocks_all_users). The dust threshold is tiny - at a
realistic 0.001 AAVE/s emission, any supply below ~591 wei is enough to arm the brick.
Honest framing:
- I am NOT claiming fund theft - this is a freeze/availability issue.
- I am NOT claiming it is exploitable on the current mainnet configuration (no active emission right now; the trigger needs a newly-armed distribution on a dust-supply asset).
- The program caps "precision mechanisms on tokenization" unless a provable fund-loss vector, and "loss of rewards-to-be-accrued is not loss of funds" - so the defensible impact is the permanent freeze of the reserve's supply side, not the loss of future yield.
- Not a theft or drain - this is a freeze, not a loss of funds.
- Not currently exploitable on mainnet's live config (no active distribution).
- Not a newly introduced guard - the
INDEX_OVERFLOWcheck is a deliberate 2022 design choice; my finding is that the dust-scaled-supply trigger was never considered and turns it into a permanent brick.
- GitHub code search for
INDEX_OVERFLOWin Solidity: 0 results. - No issue/PR in aave-v3-periphery or aave-v3-origin reports this dust-supply brick.
- The aave-v3-periphery RewardsController was not included in any of the Aave core audits (v3.0-v3.7 all focus on core pool logic).
- The closest known item is aave/aave-v3-periphery#38 (low-decimals index overflow, fixed by
10**decimals) - a different trigger, not the dust-scaled-supply path. - I could not verify whether this is already known in Immunefi's private submissions DB.
Present in the deployed mainnet RewardsController implementation today (the INDEX_OVERFLOW
guard and _getAssetIndex formula are in the on-chain bytecode). The guard dates to 2022; the
dust-scaled-supply trigger has been present since the 2022-10-13 change to use
IScaledBalanceToken.scaledTotalSupply(). The bug is live in the deployed contract and in the
in-scope GitHub file (aave-v3-periphery / aave-v3-origin RewardsDistributor).
- In
_getAssetIndex, bound the growth instead of reverting, e.g. capfirstTermso the index saturates atuint104.maxrather than reverting the whole operation, OR - Skip the index update when
totalSupplyis below a safe threshold (dust), OR - Change
_updateRewardDatato write the index and continue rather than reverting the caller's mint/burn/transfer, so a dust-supply state cannot brick the aToken.
RewardsDistributor.sol(deployed, aave-v3-periphery):_getAssetIndexlines 497-525,INDEX_OVERFLOWguard at_updateRewardDataline 302RewardsController.sol(deployed):handleActionline 111- aave-v3-origin aToken wiring:
MintableIncentivizedERC20.sol:44,61,IncentivizedERC20.sol:274,276 - RewardsController proxy: 0x4370D3b6C9588E02ce9D22e684387859c7Ff5b34
- Implementation: 0x0ee554F6A1f7a4Cb4f82D4C124DdC2AD3E37fde1 (INCENTIVES_IMPL)
- PoC: poc/test/IndexOverflow.t.sol (5 passing tests)