Skip to content

Instantly share code, notes, and snippets.

@zmanian
Created February 27, 2026 07:12
Show Gist options
  • Select an option

  • Save zmanian/ff8275f140459990d703265b730c70f2 to your computer and use it in GitHub Desktop.

Select an option

Save zmanian/ff8275f140459990d703265b730c70f2 to your computer and use it in GitHub Desktop.

RYUSD Arbitrum: Why Withdrawals Are Forced Into the Queue

Summary

All withdrawals from RYUSD on Arbitrum (0x392B1E6905bb8449d26af701Cdea6Ff47bF6e5A8) are being routed to the withdrawal queue because totalAssetsWithdrawable reports only $3.97 USDC out of $29,303 total assets (0.014% liquid). The ~$27,300 in Aave V3 aToken positions are not counted as withdrawable due to two compounding configuration issues in the Aave V3 aToken Adaptor.

On-Chain State (2026-02-26)

Metric Value
Total Assets $29,303 USDC
Total Assets Withdrawable $3.97 USDC
Aave aUSDC (pos 10, holding) $14,174
Aave aUSDC.e (pos 11) $2,512
Aave aDAI (pos 12) $8,166
Aave aUSDT (pos 13) $2,451
Raw USDC balance (pos 1) $3.97
Raw DAI balance (pos 3) $627
Raw USDT balance (pos 4) $1,365
Aave Debt (variableDebtArbUSDCn) $1.16
Aave Health Factor 22,346x
Aave EMode 1 (stablecoin)

Root Cause

The Aave V3 aToken Adaptor V1.2 (0x4AEC526182F64d4c75DaDf43c8A86a69F91ef5AE) has two independent checks in its withdrawableFrom() function that each independently force a return of 0:

Issue 1: EMode is non-zero while debt exists

From AaveV3ATokenAdaptor.sol line 172-175:

// If Cellar has no Aave debt, then return the cellars balance of the aToken.
if (totalDebtBase == 0) return ERC20(address(token)).balanceOf(msg.sender);

// Cellar has Aave debt, so if cellar is entered into a non zero emode, return 0.
if (pool.getUserEMode(msg.sender) != 0) return 0;

The cellar has $1.16 of USDC variable debt AND is in EMode category 1. This causes an immediate return of 0 for ALL four Aave positions (~$27,300 blocked).

Issue 2: configurationData is zero on all Aave positions

All four Aave aToken positions (10, 11, 12, 13) have configurationData = 0x00. The adaptor interprets this as "the strategist does not want users to withdraw from this position":

From AaveV3ATokenAdaptor.sol lines 178-181:

uint256 minHealthFactor = abi.decode(configData, (uint256));
// Check if minimum health factor is set.
// If not the strategist does not want users to withdraw from this position.
if (minHealthFactor == 0) return 0;

Even if Issue 1 were resolved, this would still block all Aave withdrawals.

Why ERC20 positions (DAI, USDT) are also blocked

The ERC20 Adaptor (0x2fD86717d9Bba566Fe637Ac6D3C69369CE998555) also uses configurationData to gate withdrawals. From the trace:

  • Position 1 (USDC): configurationData = 0x01withdrawable (returns $3.97)
  • Position 2 (USDC.e): configurationData = 0x00 → returns 0
  • Position 3 (DAI): configurationData = 0x00 → returns 0 (despite $627 balance)
  • Position 4 (USDT): configurationData = 0x00 → returns 0 (despite $1,365 balance)

Only USDC (position 1) has its configurationData set to allow withdrawals.

How the Frontend Routes to the Queue

  1. User submits redeem() on the cellar contract
  2. The cellar's _withdrawInOrder() iterates credit positions, calling _withdrawableFrom() on each
  3. Every position except USDC returns 0, so the cellar can only service ~$3.97 of redemptions
  4. For any meaningful withdrawal amount, redeem() reverts with Cellar__IncompleteWithdraw
  5. The frontend catches this, checks totalAssetsWithdrawable < redeemAmount, and routes to the withdrawal queue modal

Resolution Options (Strategist/Governance Actions)

Option A: Repay debt and exit EMode

  1. Repay the $1.16 USDC variable debt via strategist callOnAdaptor
  2. Call changeEMode(0) to exit stablecoin EMode
  3. Set configurationData to a non-zero minimumHealthFactor (e.g., abi.encode(1.05e18)) on at least one Aave position

This removes the EMode block. With no debt, withdrawableFrom returns the full aToken balance regardless of configurationData.

Option B: Set configurationData while keeping EMode

  1. Repay the $1.16 debt
  2. Keep EMode but ensure totalDebtBase == 0 so the adaptor takes the early-return path at line 172 (returns full balance when no debt)

Option C: Reconfigure positions

  1. Set configurationData to non-zero minimumHealthFactor on exactly ONE Aave position (the adaptor docs specify only one should have it set to avoid blocking)
  2. Optionally also set configurationData = 0x01 on DAI and USDT ERC20 positions to make those withdrawable too

Contract References

Contract Address Chain
RYUSD Cellar 0x392B1E6905bb8449d26af701Cdea6Ff47bF6e5A8 Arbitrum
Registry 0xB7f57c1a22E9cBac58016cd01749433f390091BB Arbitrum
Withdraw Queue 0x516AD60801b62fCABCCDA7be178e4478D4018071 Arbitrum
ERC20 Adaptor 0x2fD86717d9Bba566Fe637Ac6D3C69369CE998555 Arbitrum
Aave V3 aToken Adaptor 0x4AEC526182F64d4c75DaDf43c8A86a69F91ef5AE Arbitrum
Aave V3 Debt Adaptor 0xDC238246E8Cd154170D27Ea452Cb6Bb0216d2443 Arbitrum
UniV3 Adaptor 0x92c3363D3c7a313C3f1A929294aAd981Bf8a283B Arbitrum
Aave V3 Pool 0x794a61358D6845594F94dc1DB02A252b5b4814aD Arbitrum

Methodology

Analysis performed by tracing totalAssetsWithdrawable() via cast call --trace against the live Arbitrum state, then cross-referencing the verified Aave V3 aToken Adaptor source from Arbiscan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment