Hello,
I ran into a problem today and would like to propose a small change to address it:
[TEMP CHECK] Base Pool: allow repay(MaxUint256) for third-party onBehalfOf (prevents permanent fund locks)
Author
NestorKurtz
Summary
On Aave V3 Base, Pool.repay(asset, type(uint256).max, rateMode, onBehalfOf) reverts when the caller is a third-party contract and onBehalfOf != msg.sender. This can permanently lock funds in non-upgradeable helper contracts that use MaxUint256 as “repay as much as possible” to safely handle interest drift.This post proposes a minimal backward-compatible Pool patch so that the MaxUint256 sentinel clamps to payer balance / user debt instead of reverting in the third-party onBehalfOf path.
Motivation
Aave has previously shipped Pool patches to restore compatibility for integrators with immutable infrastructure (e.g. v3.2 legacy periphery patch proposalId=182). This is a similar class of issue: an integration pattern that is safe and widely used elsewhere (MaxUint256 as “repay all”) is not executable on Base for third-party repayments, and the failure mode is catastrophic for immutable helpers.
Real incident impact (reproducible)
-
Helper contract (non-upgradeable): 0x7e3a8d6154adec875D40583fC527c18cBf4330c6
-
Locked funds: 1052.246268 USDC
-
Pool (Base): 0xA238Dd80C259a72e81d7e4664a9801593F98d1c5
-
USDC: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
-
Observed revert selector: 0xcd3779c3
-
Reverted txs:
-
0xb46c6a9f36c797d6fe10d3cd87103c3fd825e2974608bf971a6e818e4ea82d13 (block 42269963)
-
0x3cc4dedd31b2ee4e2458f65f6a9ac2ee66d1805f60718aafaea82b9e443b8b55 (block 42270155)
Static-call proof:
-
repay(MaxUint256, onBehalfOf=borrower) from helper => reverts (0xcd3779c3)
-
repay(exactAmount, onBehalfOf=borrower) from helper => succeeds
Specification (proposed behavior change)
Current behavior (Base)
When onBehalfOf != msg.sender, repay(amount=type(uint256).max) reverts.
Proposed behavior
When amount == type(uint256).max in a third-party repay call, treat it as a sentinel value meaning:
amountToRepay = min(
userOutstandingDebt(onBehalfOf, interestRateMode),
IERC20(asset).balanceOf(msg.sender)
)
Then proceed with repayment using amountToRepay.If amountToRepay == 0, return 0 (no revert).This preserves the intent of “repay all you can” and prevents bricking immutable helpers.
Why this is safe / desirable
-
It does not let a third-party repay “too much” (it is explicitly bounded by payer balance and user debt).
-
It matches the mental model integrators already use on many chains/protocols: MaxUint256 as “repay max possible.”
-
It avoids catastrophic irrecoverable-funds outcomes for immutable contracts.
-
It aligns with Aave’s history of making minimal Pool upgrades to improve backwards compatibility (e.g. proposalId=182).
Security considerations
-
Needs review for any edge cases where balanceOf(msg.sender) differs from transferable amount (fee-on-transfer tokens, etc.). For Base USDC this is standard ERC20.
-
Must ensure stable/variable debt modes remain correct; sentinel should apply per debt mode.
-
Should maintain existing revert conditions for invalid rate modes / inactive reserves.
-
Should include full regression tests around repay flows:
-
self repay vs onBehalfOf repay
-
partial repay vs full repay
-
zero debt case (must not revert)
-
repay when payer has 0 balance (must not revert)
Implementation / upgrade mechanism
Aave governance can upgrade Pool logic on Base via PoolAddressesProvider.setPoolImpl(newPoolImpl) (governance-owned). Official docs: Pool Addresses Provider | Aave Protocol Documentation
References / evidence
Public sanitized incident pack:
Full evidence (private, shareable with Aave contributors on request).
Ask
-
Is the current revert behavior expected by design on Base?
-
If yes, would Aave consider this sentinel-clamp behavior as a compatibility improvement?
-
If feasible, what’s the path/timeline for a Pool patch on Base?