Skip to content

fee on swap v2: immediate WETH transfer, simplified totalAssets#222

Draft
clement-ux wants to merge 3 commits into
nicka/withdraw-on-swapfrom
clement/fee-on-swap-v2
Draft

fee on swap v2: immediate WETH transfer, simplified totalAssets#222
clement-ux wants to merge 3 commits into
nicka/withdraw-on-swapfrom
clement/fee-on-swap-v2

Conversation

@clement-ux
Copy link
Copy Markdown
Contributor

Summary

Replaces the storage-accrued performance-fee model with an immediate transfer of the fee to the fee collector in the liquidity asset (WETH) at swap time.

The previous model (nicka/withdraw-on-swap baseline) accrued fees into a feesAccrued storage slot on every buy-side swap, required a separate collectFees() call, and had totalAssets() subtract pending fees to avoid LP-share inflation.

The new model:

  • Computes the fee value from the precomputed swapFeeMultiplier
  • Transfers the fee directly to feeCollector (out of the ARM's WETH reserves)
  • Has nothing to net out in totalAssets()

PR #215 explored the same idea but transferred the fee in the base asset (stETH/eETH/OETH), which is expensive because of rebasing-token transfer overhead (+33k gas per Lido swap). Transferring in WETH is much cheaper since WETH is a plain ERC20 and the ARM's WETH balance is already warm from the trader payout.

Gas comparison (--isolate, Lido fork swap stETH → WETH, ~100 ETH)

Scenario Gas Δ vs Current Δ vs StoreFee
Current deployed (mainnet impl) 122,973
StoreFee baseline (nicka/withdraw-on-swap) 130,789 +7,816 (+6.4%)
TransFee WETH (this PR) 138,162 +15,189 (+12.4%) +7,373 (+5.6%)
  • Isolated cost of immediate WETH transfer vs storage accrual: ~+7.4k gas per buy-side swap.
  • PR Clement/fee on swap #215 (TransFee in stETH) measured ~+33k vs the same baseline → WETH transfer is ~4.5× cheaper.

Contract size comparison

Runtime bytecode size (bytes). Baseline = nicka/withdraw-on-swap (StoreFee accrual + collectFees + feesAccrued storage + _availableAssets helper).

Contract Baseline This PR Δ Δ% Runtime margin (24,576 limit)
LidoARM 24,116 23,406 −710 −2.94% 460 → 1,170
EthenaARM 22,664 21,978 −686 −3.03% 1,912 → 2,598
EtherFiARM 22,405 21,695 −710 −3.17% 2,171 → 2,881
OriginARM 20,957 20,271 −686 −3.27% 3,619 → 4,305

~700 bytes shaved off every ARM (~3%). LidoARM's EIP-170 margin nearly triples (460 → 1,170 B), which is useful headroom for future evolutions.

Sources of the saving

  • collectFees() + its require checks
  • _availableAssets() helper (now inlined into totalAssets)
  • _requireLiquidityAvailable() helper
  • Fee subtraction branch + MIN_TOTAL_SUPPLY floor in totalAssets()
  • swapFeeMultiplier-based storage accrual code path (feesAccrued slot is no longer written)

Known trade-off

The fee comes out of the ARM's WETH reserves at swap time, so a buy-side swap can now revert in a tight liquidity edge case where the ARM has exactly enough WETH for amountOut + outstandingWithdrawals and the active market is dry/paused. The fee is typically <1% of amountOut, so this is a narrow window in practice but worth flagging.

Migration

script/deploy/mainnet/030_UpgradeLidoARMSwapFeeScript.s.sol, 029_UpgradeEtherFiARMSwapFeeScript.s.sol, and script/deploy/sonic/006_UpgradeOriginARMSwapFeeScript.s.sol each call collectFees() on the proxy (still using the old impl, via low-level call) to drain any pending fees, then upgrade to the new impl.

Test plan

  • forge build clean
  • forge test --match-contract Fork_Concrete_LidoARM_SwapGas --isolate -vv passes (6/6)
  • Full fork suite (some tests assertive on the old feesAccrued/collectFees semantics may need follow-up adjustments)
  • Invariant suite (the OriginARM sum_feesCollected ghost is no longer tracked through swap-time transfers; needs revisiting if invariants are run)

Replaces storage-accrued fees with an immediate transfer of the fee to the
fee collector in the liquidity asset (WETH) at swap time.

AbstractARM.sol:
- Drop feesAccrued storage and feesAccrued/totalAssets coupling
- Drop collectFees() and _requireLiquidityAvailable() helpers
- Drop _availableAssets() helper (inlined into totalAssets)
- Drop MIN_TOTAL_SUPPLY floor in totalAssets
- Inline fee transfer into the two swap paths

Deploy scripts: switch the pre-upgrade collectFees() call on the proxy to a
low-level call so the new impl (which no longer exposes collectFees) compiles.

Tests: remove now-obsolete CollectFees test files, handlers, modifiers, and
feesAccrued/collectFees assertions across LidoARM, EthenaARM, EtherFiARM, and
OriginARM suites.
@naddison36
Copy link
Copy Markdown
Collaborator

The gas increase is relatively small with WETH, which is not proxied.

It'd be interesting to understand the impact when USDC is the liquid asset, as that is proxied. We can test with a multi asset Paxos ARM

// If the ARM becomes insolvent enough that the available assets in the ARM, lending market and
// external withdrawal queue are less than the outstanding withdrawals, return 0 to avoid an
// underflow. Otherwise return the assets net of the liquidity reserved for the withdrawal queue.
return assets < outstandingWithdrawals ? 0 : assets - outstandingWithdrawals;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm still not 100% sure of this. Maybe we should have something like:

Suggested change
return assets < outstandingWithdrawals ? 0 : assets - outstandingWithdrawals;
return assets < outstandingWithdrawals ? MIN_TOTAL_SUPPLY : assets - outstandingWithdrawals;

@clement-ux
Copy link
Copy Markdown
Contributor Author

It'd be interesting to understand the impact when USDC is the liquid asset, as that is proxied. We can test with a multi asset Paxos ARM

@naddison36 Here are Claude estimation:
Capture d’écran 2026-05-13 à 13 25 01

@clement-ux clement-ux marked this pull request as draft May 14, 2026 15:18
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.

2 participants