fee on swap v2: immediate WETH transfer, simplified totalAssets#222
Draft
clement-ux wants to merge 3 commits into
Draft
fee on swap v2: immediate WETH transfer, simplified totalAssets#222clement-ux wants to merge 3 commits into
clement-ux wants to merge 3 commits into
Conversation
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.
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 |
clement-ux
commented
May 13, 2026
| // 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; |
Contributor
Author
There was a problem hiding this comment.
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; |
Contributor
Author
@naddison36 Here are Claude estimation: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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-swapbaseline) accrued fees into afeesAccruedstorage slot on every buy-side swap, required a separatecollectFees()call, and hadtotalAssets()subtract pending fees to avoid LP-share inflation.The new model:
swapFeeMultiplierfeeCollector(out of the ARM's WETH reserves)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)nicka/withdraw-on-swap)Contract size comparison
Runtime bytecode size (bytes). Baseline =
nicka/withdraw-on-swap(StoreFee accrual +collectFees+feesAccruedstorage +_availableAssetshelper).~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 intototalAssets)_requireLiquidityAvailable()helperMIN_TOTAL_SUPPLYfloor intotalAssets()swapFeeMultiplier-based storage accrual code path (feesAccruedslot 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 + outstandingWithdrawalsand the active market is dry/paused. The fee is typically <1% ofamountOut, so this is a narrow window in practice but worth flagging.Migration
script/deploy/mainnet/030_UpgradeLidoARMSwapFeeScript.s.sol,029_UpgradeEtherFiARMSwapFeeScript.s.sol, andscript/deploy/sonic/006_UpgradeOriginARMSwapFeeScript.s.soleach callcollectFees()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 buildcleanforge test --match-contract Fork_Concrete_LidoARM_SwapGas --isolate -vvpasses (6/6)feesAccrued/collectFeessemantics may need follow-up adjustments)sum_feesCollectedghost is no longer tracked through swap-time transfers; needs revisiting if invariants are run)