Reproduced Exploit

Reserve `quoteCustomRedemption` reverts once an old basket asset is unregistered (redemption DoS)

Jan 1970Otherlogic4 min read

Chain

Other

Category

logic

Date

Jan 1970

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

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.

Loading fork state…

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-registry mirror.


Vulnerability classes: vuln/logic/array-out-of-bounds · vuln/defi/denial-of-service

Reproduction: the test deploys the REAL AssetRegistryP1 and the REAL (vulnerable) BasketHandlerP1 at 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:

SOLIDITY
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.

  1. Register three collateral (USDC, USDT, DAI) in the real AssetRegistryP1 -> size() == 3.
  2. Governance sets the prime basket {0.9 USDC, 0.05 USDT, 0.05 DAI} and calls refreshBasket(); the real _switchBasket()/BasketLibP1.nextBasket records basket nonce 1 = {USDC, USDT, DAI}.
  3. quoteCustomRedemption([nonce 1], [1e18], 1e18) succeeds and returns the 3 backing assets.
  4. Governance switches the prime basket to {0.9 DAI, 0.1 USDC} (nonce 2) and unregisters USDT. The real AssetRegistry._erc20s (an EnumerableSet) shrinks: size() == 2.
  5. The identical quoteCustomRedemption([nonce 1], [1e18], 1e18) now reverts Panic(0x32): erc20sAll is 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.

sequenceDiagram participant Gov as Governance OWNER participant AR as AssetRegistryP1 real participant BH as BasketHandlerP1 real Gov->>AR: register USDC, USDT, DAI, size = 3 Gov->>BH: setPrimeBasket + refreshBasket BH-->>BH: basketHistory nonce 1 = USDC, USDT, DAI Gov->>BH: quoteCustomRedemption nonce 1 -> OK, 3 assets Gov->>BH: setPrimeBasket 2-asset + refreshBasket, nonce 2 Gov->>AR: unregister USDT, size = 2 Gov->>BH: quoteCustomRedemption nonce 1 BH-->>BH: erc20sAll sized 2, writes index 2 BH-->>Gov: revert Panic 0x32 - redemption bricked

Reproduction#

BASH
_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#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

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.