Skip to content

New ARM features before Ethena audit#208

Open
naddison36 wants to merge 55 commits into
mainfrom
nicka/withdraw-on-swap
Open

New ARM features before Ethena audit#208
naddison36 wants to merge 55 commits into
mainfrom
nicka/withdraw-on-swap

Conversation

@naddison36
Copy link
Copy Markdown
Collaborator

@naddison36 naddison36 commented Apr 10, 2026

Summary

Adds the next ARM upgrade set ahead of the Ethena audit. This PR now includes active-market liquidity sourcing during swaps, discounted base-asset swap fees, per-price liquidity limits, multi-base-asset support, adapter-based redemption flows, updated loss/fee accounting, pause controls, reentrancy protection, and safer upgrade/migration handling.

Merged Changes

Key Changes

Withdraw From Active Market During Swaps

Swaps that need more liquidity than is currently held in the ARM can now pull the shortfall from the active ERC-4626 lending market.

This lets the ARM keep more liquidity deployed while still supporting swaps, provided the active market has enough withdrawable liquidity.

Discount-Based Swap Fees

Replaces the old performance fee on asset growth with fees accrued when the ARM buys base assets at a discount.

Fees are calculated using a swap fee multiplier and measured against the configured cross price, aligning fee accrual with the ARM's valuation price for each base asset.

Per-Price Liquidity Limits

Buy and sell liquidity limits are tracked per configured price and emitted in traderate updates.

Operators can cap how much liquidity can be consumed at the current buy/sell prices before prices are refreshed. Swap-side liquidity checks now cover both buy and sell paths.

Multi-Base-Asset ARM

Moves reusable multi-base logic into AbstractARM, allowing a single ARM to support multiple base assets against one liquidity asset.

Examples:

  • Lido ARM: stETH, wstETH
  • EtherFi ARM: eETH, weETH
  • Ethena ARM: sUSDe
  • Origin ARM: OETH, wOETH

Each base asset has its own config:

  • buy price
  • sell price
  • buy liquidity remaining
  • sell liquidity remaining
  • cross price
  • pending redeem assets
  • pegged/non-pegged conversion mode
  • redemption adapter

Asset Adapter Redemption Flow

Protocol-specific redemption logic has moved out of the ARM and into per-asset adapters.

Adapters now own details such as:

  • Lido withdrawal NFT queue
  • wstETH unwrap and Lido withdrawal request
  • EtherFi withdrawal NFTs
  • Ethena cooldown/unstaker flow
  • Origin vault withdrawal requests
  • wrapped Origin asset redemption

The ARM tracks generic pending redeem accounting in liquidity-asset terms. Lido and EtherFi-style claim adapters also sweep adapter-held ETH/WETH when claiming, so donated balances do not remain stranded.

LP Loss Accounting

Updates LP redeem accounting so losses are shared pro-rata between redeemers and remaining LPs instead of allowing one side to absorb the full impact.

If assets per share falls after a redeem request is created, the claim uses the lower claim-time value.

Legacy Queue Migration And Claim Support

Adds a one-time, atomic legacy withdraw queue migration guarded by reinitializer(2).

Upgrade scripts call the migration selector directly, and protocol-specific legacy checks run through internal hooks instead of external helper functions.

Legacy LP redeem requests remain claimable after the upgrade. Additional safety checks prevent upgrades while unsupported legacy protocol withdrawal queues are still pending.

Pause / Unpause

Adds a shared ARM pause circuit breaker.

  • Operator or owner can pause.
  • Only owner/governor can unpause.
  • Paused state blocks user-facing flows: swaps, deposits, new LP redeem requests, and LP redeem claims.
  • Operational and recovery flows remain available, including allocation, price/buffer updates, fee collection, and base-asset redeem/claim flows.

Reentrancy Protection

Adds ReentrancyGuardUpgradeable protection around liquidity-moving entrypoints, including swap entrypoints, deposits, LP redeem request/claim, and fee collection.

This prevents nested callbacks from reusing stale pre-payout balances during liquidity-moving flows.

Ethena Adapter Storage And Queue Cleanup

Restores deprecated Ethena ARM storage slots for upgrade layout compatibility.

The Ethena adapter now tracks queued unstaker requests using a total request counter plus FIFO pending index, deriving each unstaker index from request position modulo the unstaker count instead of storing a pending index array.

Custom Errors and Contract Size Cleanup

Replaces admin/operator-only revert strings with custom errors across ownership, operator, pricing, fee, market, migration, and cap-manager paths.

Custom errors include selector documentation where required.

User-facing revert strings remain largely unchanged for swap, deposit, LP redeem, and adapter flows where preserving existing behavior is useful.

Additional refactors reduce ARM runtime size and keep the ARM implementations below the EIP-170 contract size limit.

Deployment, Task, and Test Updates

Includes updated deploy/upgrade scripts, ABIs, Hardhat tasks, diagrams, fork tests, smoke tests, invariant tests, and upgrade guard tests for the new architecture.

Testing

Coverage includes:

  • Unit tests for swap liquidity limits, fees, reserves, LP deposits/redeems, pause behavior, reentrancy protection, active-market liquidity sourcing, and adapter behavior
  • Fork tests for Lido, EtherFi, Ethena, Origin, and Harvester flows
  • Smoke tests for upgraded ARM deployments
  • Invariant tests for ARM accounting and redemption queues
  • Upgrade guard tests for Lido, EtherFi, and Ethena legacy queue migrations
  • Contract size checks confirming the ARM implementations remain under the EIP-170 runtime limit

Gas comparison

Measured on mainnet fork block 24,846,066.

ARM Scenario Gas
Lido ARM Current deployed, enough liquidity 110,041
Lido ARM Upgraded, enough liquidity 169,976
Lido ARM Upgraded, needs Morpho liquidity 600,216
Ethena ARM Current deployed, enough liquidity 93,333
Ethena ARM Upgraded, enough liquidity 133,155
Ethena ARM Upgraded, needs Aave liquidity 202,789

Relative to current deployed:

ARM Scenario Delta
Lido ARM Upgraded, enough liquidity +59,935 gas / +54.47%
Lido ARM Upgraded, needs Morpho liquidity +490,175 gas / +445.45%
Ethena ARM Upgraded, enough liquidity +39,822 gas / +42.67%
Ethena ARM Upgraded, needs Aave liquidity +109,456 gas / +117.27%

@naddison36 naddison36 marked this pull request as ready for review April 29, 2026 02:38
@naddison36 naddison36 changed the title WIP Withdraw from lending market on swap if not enough liquidity Withdraw from lending market on swap if not enough liquidity Apr 29, 2026
@naddison36 naddison36 marked this pull request as draft April 29, 2026 04:08
@naddison36 naddison36 changed the title Withdraw from lending market on swap if not enough liquidity New ARM features before Ethena audit Apr 29, 2026
@naddison36 naddison36 marked this pull request as ready for review April 29, 2026 08:17
Comment thread src/contracts/AbstractARM.sol Outdated
Comment thread src/contracts/AbstractARM.sol Outdated
Comment thread src/contracts/AbstractARM.sol Outdated
naddison36 and others added 6 commits May 5, 2026 21:05
* Using swap multiplier to calculate accrued fee

* Removed Maths lib to get contract size down
Removed lastAvailableAssets() function

* Improved gas usage of _accrueSwapFee by removing if swapFeeMultiplier is zero

* Fixed gap after storage variables were compacted

* Removed outToken from _accrueSwapFee

* Simplified _updateSwapFeeMultiplier

* More gas savings
Allow price liquidity cap to be unlimited

* Moved state changes before token transfers

* Update swap gas compare test
* WIP multi base assets

* - Move reusable multi-base asset config and swap accounting into AbstractARM
- Replace protocol-specific ARM redemption logic with generic asset adapters
- removed legacy max-cap sentinel handling
- Upgrade all ARMs to multi base assets
- Added wstETH to the Lido ARM
- Added weETH to EtherFi ARM

* Updated AI context

* Added wOETH to OETH ARM

* Updated ARM contract dependencies

* Updated Hardhat tasks

* Proxied the adapters

* Lido ARM upgrade checks

* Added Ethena upgrade guard

* Updated migration error

* More cleanup of legacy immutables

* fmt

* Update EtherFi ABI

* Update claimEthenaWithdrawals Hardhat task

* Fixed fork tests

* Fixed smoke tests

* Fix invariant tests

* Updated contract diagrams

* Renamed the ARM adapter-facing methods to requestBaseAssetRedeem and claimBaseAssetRedeem

* Used modifiers on adapters

* Pro-rata losses to redeemers and remaining LPs (#223)

* Pro-rata losses to redeemers and remaining LPs

* Calculate swap fee against cross price rather than par value (#224)
naddison36 added 10 commits May 18, 2026 12:08
- Gate migrateLegacyWithdrawQueue with reinitializer(2)
- Run protocol-specific legacy queue checks through internal hooks
- Use migration selector in ARM upgrade scripts
- Remove obsolete external Ethena and Lido legacy check helpers
Refactored swap helpers to reduce contract size
unchecked {
config.buyLiquidityRemaining = uint128(remaining - amountOut);
}
_ensureLiquidityAvailableForSwap(amountOut);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a small asymmetry here:

If side:

  1. Check against buyLiquidity
  2. Ensure there is enough tokens

Else side:

  1. Ensure there is enough tokens
  2. Check against buyLiquidity

* Custom errors for Operator and Admin functions

* Fixed smoke tests

* More smoke test fixes

* moved error location
* Added pause and unpause

* Fixed compile and tests

* Using ContractPaused Error
Comment thread src/contracts/AbstractARM.sol Outdated

// Calculate the amount of shares to mint after the performance fees have been accrued
// which reduces the available assets, and before new assets are deposited.
require(totalAssets() > MIN_TOTAL_SUPPLY || reservedWithdrawLiquidity == 0, "ARM: insolvent");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we are really protecting from depositing when ARM is insolvent.
The attacker can still give 0.01 ETH to ARM (donation), which unblock this require, then deposit and get way more shares that expected.
I agree that this is still better than returning 0 though. But now that we have the pausing mechanism, I'm wondering if this is still relevant. WDYT?

Comment thread src/contracts/AbstractARM.sol Outdated
Comment on lines 813 to 815
uint256 assetsAtClaim = request.shares > 0 ? convertToAssets(request.shares) : request.assets;
// Use the minimum of the asset value of the redeemed shares at request or claim.
assets = request.assets < assetsAtClaim ? request.assets : assetsAtClaim;
Copy link
Copy Markdown
Contributor

@clement-ux clement-ux May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, but I think this line isn't necessary.
If request.shares == 0 then convertToAssets(request.shares) will return 0 too. And to have request.shares == 0 this implies that user call requestRedeem(0). Calling requestRedeem(0) force request.asset == 0 too.

So we can maybe have:

- uint256 assetsAtClaim = request.shares > 0 ? convertToAssets(request.shares) : request.assets;
- assets = request.assets < assetsAtClaim ? request.assets : assetsAtClaim;
+ assets = min(request.assets, convertToAssets(request.shares));

I agree with Shahul that we should not allow requestRedeem(0) though.

* Added Natspec to Adapter contracts
Moved modifiers to before functions

* Fix formatting
* Support claiming legacy redeem requests

* deposit, requestRedeem and claimRedeem now using custom errors

* Change why legacy requests are detected

* Format

* Fix invariant test
…#235)

* Added _checkNoLegacyWithdrawQueue to OriginARM

* Use custom error in _checkNoLegacyWithdrawQueue

* Added comment with 4-byte selector to each custom error

* Format

* Added missing comment of 4-byte selector to errors

* Ethena and Origin ARMs to use LegacyWithdrawalsPending instead of their own custom error
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants