0xc27c3ec61c61309c9af35af062a834e0d6914f9352113617400577c0f2b0e9de0x02e7b8511831b1b02d9018215a0f8f500ea5c6b3EthereumAn unprivileged attacker exploited Aave v3's public ParaSwapRepayAdapter at 0x02e7B8511831B1b02d9018215a0f8f500Ea5c6B3 in Ethereum block 20624704 via transaction 0xc27c3ec61c61309c9af35af062a834e0d6914f9352113617400577c0f2b0e9de. The attacker wrapped the sequence in a Balancer flash loan and a freshly deployed helper, then used repeated public swapAndRepay calls to spend ERC20 balances that were already stranded on the adapter. The root cause was not a privileged rescue path or governance action. The vulnerability was the adapter's accounting in BaseParaSwapBuyAdapter._buyOnParaSwap, which measured token usage from the adapter's aggregate balance instead of isolating the active caller's contributed collateral. That let the active caller consume residual adapter inventory and route resulting value back to the attacker's own positions. The observed end state transferred 425966524925658359 wstETH and 21426349775 USDC to the funded attacker EOA.
Aave's ParaSwapRepayAdapter is intended to let any user swap withdrawn collateral into a debt asset and repay debt in a single call. The verified source shows that swapAndRepay is publicly callable and only requires the caller to provide aTokens and ParaSwap calldata; it is not gated by ownership or governance. The same codebase also documents an important operational assumption in BaseParaSwapAdapter.rescueTokens: funds are not expected to remain on the adapter except transiently, and only the owner can rescue them later. That assumption matters because any nonzero adapter balance is economically sensitive if normal execution paths can accidentally account for it as caller-owned inventory.
The incident transaction used only public contracts and public state. The sender 0x6ea83f23795f55434c38ba67fcc428aec0c296dc was funded one block earlier by transaction 0x279e776b64b081aaf4d3e91b5c6a6f9074612649a1424bb0d702460821c070c5, then sent the exploit as its first transaction. The exploit transaction created init contract 0x5e2fff7bbc7c634992170ff18240b8f10c4d48c6, which in turn deployed helper 0x78B0168a18eF61D7460FAbb4795e5f1A9226583E. The trace and receipt show the helper taking a Balancer flash loan, opening temporary Aave positions, and driving the adapter path end to end.
The vulnerable component is Aave's verified BaseParaSwapBuyAdapter._buyOnParaSwap implementation used by ParaSwapRepayAdapter.swapAndRepay. Inside _buyOnParaSwap, the adapter snapshots balanceBeforeAssetFrom = assetToSwapFrom.balanceOf(address(this)), performs the ParaSwap call, then computes amountSold = balanceBeforeAssetFrom - balanceAfterAssetFrom. That calculation is global to the adapter, not scoped to the current user. In swapAndRepay, the returned amountSold is then used to compute collateralBalanceLeft = collateralAmount - amountSold, and any leftover collateral is deposited back on behalf of msg.sender.
The violated invariant is straightforward: during each swapAndRepay execution, the adapter must account only for collateral withdrawn from the active caller, while any preexisting adapter balances must remain outside that caller's repay accounting. Instead, if the adapter already holds residual inventory, a ParaSwap route can spend that residual inventory and _buyOnParaSwap will still book it as if it came from the current caller. Because swapAndRepay is permissionless, any user who can make the function reachable can monetize those stranded balances. The owner-only rescueTokens function does not prevent this, because the drain occurs through the normal public execution path before the owner intervenes.
The verified source establishes the bug mechanics directly:
// BaseParaSwapBuyAdapter._buyOnParaSwap
uint256 balanceBeforeAssetFrom = assetToSwapFrom.balanceOf(address(this));
require(balanceBeforeAssetFrom >= maxAmountToSwap, "INSUFFICIENT_BALANCE_BEFORE_SWAP");
...
(bool success, ) = address(augustus).call(buyCalldata);
...
uint256 balanceAfterAssetFrom = assetToSwapFrom.balanceOf(address(this));
amountSold = balanceBeforeAssetFrom - balanceAfterAssetFrom;
// ParaSwapRepayAdapter.swapAndRepay
_pullATokenAndWithdraw(address(collateralAsset), msg.sender, collateralAmount, permitSignature);
uint256 amountSold = _buyOnParaSwap(..., collateralAmount, debtRepayAmount);
uint256 collateralBalanceLeft = collateralAmount - amountSold;
if (collateralBalanceLeft > 0) {
POOL.deposit(address(collateralAsset), collateralBalanceLeft, msg.sender, 0);
}
POOL.repay(address(debtAsset), debtRepayAmount, debtRateMode, msg.sender);
This means the adapter treats the delta of its total balance as the caller's spend, regardless of whether some of that inventory was already stranded on the contract before the call started. The seed pre-state at block 20624703 confirms exactly such residual balances were present on the adapter: 425966524925658359 wstETH and 21426349774 USDC. The attack path then forced those balances through ParaSwap while preserving the current caller's crediting path.
The trace gives a concrete example for the stranded USDC leg. During the call swapAndRepay(WBTC, USDT, 1, 2784859272, ...), ParaSwap pulled 21426349774 USDC from the adapter and returned 21426349774 USDC-equivalent value to the helper path:
TokenTransferProxy::transferFrom(FiatTokenProxy, 0x02e7...c6B3, AugustusSwapper, 21426349774)
emit Transfer(from: 0x02e7...c6B3, to: AugustusSwapper, value: 21426349774)
...
emit Transfer(from: AugustusSwapper, to: 0x78B0168a18eF61D7460FAbb4795e5f1A9226583E, value: 21426349774)
Later in the same transaction, the helper transferred the realized profit to the attacker EOA:
WstETH::transfer(0x6ea83f23795F55434C38bA67FCc428aec0C296DC, 425966524925658359)
FiatTokenProxy::transfer(0x6ea83f23795F55434C38bA67FCc428aec0C296DC, 21426349775)
These trace facts match the reported invariant break. The adapter's public path spent preexisting adapter-owned balances, and the attacker cluster captured the value. No privileged role, private key, or hidden actor capability is required to explain the observed outcome.
The sender 0x6ea83f23795f55434c38ba67fcc428aec0c296dc received 482208500000000000 wei in block 20624692, then submitted the exploit as its first transaction. Transaction 0xc27c3ec61c61309c9af35af062a834e0d6914f9352113617400577c0f2b0e9de created init contract 0x5e2fff7bbc7c634992170ff18240b8f10c4d48c6, which deployed helper 0x78B0168a18eF61D7460FAbb4795e5f1A9226583E. The helper took a multi-asset Balancer flash loan, posted temporary collateral into Aave, borrowed debt assets, and repeatedly invoked the public adapter path.
The trace shows multiple swapAndRepay invocations using attacker-controlled ParaSwap payloads. Those payloads routed value through Augustus while the adapter acted as the formal swap beneficiary. Because the adapter-wide balance delta was mis-accounted, the residual adapter balances were consumed as if they were part of the current caller's collateral flow. After each drain leg, the helper retained value and continued the sequence. At the end of the flash-loan callback, the helper repaid Balancer and transferred the remaining profit to the EOA. The receipt shows status=1, log_count=365, and the trace shows the final direct profit transfers to the sender.
The direct measurable loss in the reported incident is the value extracted from stranded balances on the adapter. The root cause artifact records two concrete depleted balances:
[
{ "token_symbol": "wstETH", "amount": "425966524925658359", "decimal": 18 },
{ "token_symbol": "USDC", "amount": "21426349774", "decimal": 6 }
]
Using the pre-state Aave oracle values cited in the root cause, the attacker realized a net value gain of about 22626.675426 USD after gas. More importantly, the incident demonstrates that any residual funds left on this permissionless adapter were economically exposed to arbitrary public callers. The impact is therefore not limited to one route or one token; the broken security property is that adapter-held leftovers were not isolated from future callers.
0xc27c3ec61c61309c9af35af062a834e0d6914f9352113617400577c0f2b0e9de at Ethereum block 20624704.0x279e776b64b081aaf4d3e91b5c6a6f9074612649a1424bb0d702460821c070c5 to attacker EOA 0x6ea83f23795f55434c38ba67fcc428aec0c296dc.ParaSwapRepayAdapter, BaseParaSwapBuyAdapter, and BaseParaSwapAdapter in the collected source for 0x02e7B8511831B1b02d9018215a0f8f500Ea5c6B3.swapAndRepay calls, ParaSwap transfers, and final profit transfers from helper 0x78B0168a18eF61D7460FAbb4795e5f1A9226583E to the attacker EOA.