Reproduced Exploit
Reserve `quoteCustomRedemption` reverts once an old basket asset is unregistered (redemption DoS)
Chain
Other
Category
logic
Date
Jan 1970
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 27331-h-01-custom-redemption-might-revert-if-old-assets-were-unreg. Standalone Foundry PoC and full write-up: 27331-h-01-custom-redemption-might-revert-if-old-assets-were-unreg_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/array-out-of-bounds · vuln/defi/denial-of-service
Reproduction: the test deploys the REAL
AssetRegistryP1and the REAL (vulnerable)BasketHandlerP1at the audited commit and drives them through the real governance path. It shows the same legitimate custom-redemption request going from working to reverting index-out-of-bounds purely because governance unregistered one asset of a historical basket.
Root cause#
Reserve's BasketHandlerP1.quoteCustomRedemption prices a redemption against one or more historical baskets. It allocates its working array from the current registry size:
IERC20[] memory erc20sAll = new IERC20[](https://github.com/sanbir/evm-hack-registry/blob/main/27331-h-01-custom-redemption-might-revert-if-old-assets-were-unreg_exp/assetRegistry.size()); // L391 - root cause
...
for (uint256 j = 0; j < b.erc20s.length; ++j) { // iterates the HISTORICAL basket
...
erc20sAll[len] = erc20; // L418 - out-of-bounds write
++len;
}
The historical basket (basketHistory[nonce]) can contain more distinct ERC20s than assetRegistry.size() returns, because an asset that was in an old basket can later be unregistered. When len reaches assetRegistry.size(), the next erc20sAll[len] = erc20 writes past the end of the array and the transaction reverts with Panic(0x32) (array index out-of-bounds).
Redemption is a core protocol guarantee that must remain available at all times (even while frozen). Bricking it can cause a depeg, or let a malicious governance trap collateral by unregistering an asset that lives in a still-redeemable historical basket. The fix (PR #857) reworks redeemCustom so the array is no longer under-sized.
Exploit walkthrough (real numbers)#
The test deploys the real components behind ERC1967 proxies (the audited ComponentP1 constructor locks the implementation initializer, so proxies are mandatory) and wires them through a minimal Main (Main is pure infrastructure - component registry + OWNER gating - and is not part of the bug). The BasketHandlerP1<->BasketLibP1 external-library link is honoured; safeMulDivFloor delegates its fixed-point math to the real FixLib.mulDiv on the backing manager.
- Register three collateral (USDC, USDT, DAI) in the real
AssetRegistryP1->size() == 3. - Governance sets the prime basket
{0.9 USDC, 0.05 USDT, 0.05 DAI}and callsrefreshBasket(); the real_switchBasket()/BasketLibP1.nextBasketrecords basket nonce 1 = {USDC, USDT, DAI}. quoteCustomRedemption([nonce 1], [1e18], 1e18)succeeds and returns the 3 backing assets.- Governance switches the prime basket to
{0.9 DAI, 0.1 USDC}(nonce 2) and unregisters USDT. The realAssetRegistry._erc20s(anEnumerableSet) shrinks:size() == 2. - The identical
quoteCustomRedemption([nonce 1], [1e18], 1e18)now revertsPanic(0x32):erc20sAllis sized 2, but the 3-asset historical basket writes index 2.
The assertion proves the concrete harm: the same redemption request that returned 3 assets in step 3 reverts index-out-of-bounds in step 5 - redemption of that basket is permanently unusable.
Reproduction#
_shared/run-poc/run_poc.sh 27331-h-01-custom-redemption-might-revert-if-old-assets-were-unreg_exp -vvvvv
Expected result: 1 passed. See test/27331-h-01-custom-redemption-might-revert-if-old-assets-were-unreg_exp.sol. The real audited sources are vendored under src/reserve/target/ (byte-identical to the audited commit); only the opaque collateral tokens, the collateral price plugin, and the Main/BackingManager infrastructure - none of which are part of the index-out-of-bounds bug - are minimal stand-ins in src/reserve/target/poc/PoCEnv.sol.
Sources#
- AuditVault finding #27331
- Reserve
BasketHandler.sol@c4ec2473 - Reserve mitigation PR #857
- Code4rena 2023-06 Reserve report
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 27331-h-01-custom-redemption-might-revert-if-old-assets-were-unreg_exp (evm-hack-registry mirror).
- AuditVault finding: 27331-h-01-custom-redemption-might-revert-if-old-assets-were-unreg.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "Reserve
quoteCustomRedemptionreverts once an old basket asset is unregistered (redemption DoS)". - Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.