Skip to content

Commit 090d28d

Browse files
committed
fix: block deposits at asset floor with live LPs
1 parent ceb065d commit 090d28d

4 files changed

Lines changed: 34 additions & 12 deletions

File tree

src/contracts/AbstractARM.sol

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -783,11 +783,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
783783
uint256 grossAssets = _availableAssets();
784784
uint256 feesAccruedMem = feesAccrued;
785785

786-
// Treat accrued fees as a senior liability. If gross assets no longer cover
787-
// accrued fees plus the minimum native-liquidity floor, new deposits would be
788-
// minted against the floor and backfill the shortfall, so block deposits.
786+
// At the native-liquidity floor, a new deposit would either backfill senior liabilities
787+
// or dilute live LPs after a real asset loss. Allow only the initial deposit, when the dead
788+
// shares are the entire supply and no fees or withdrawals are outstanding.
789789
bool atAssetFloor = feesAccruedMem + MIN_LIQUIDITY >= grossAssets;
790-
if (atAssetFloor && (feesAccruedMem != 0 || reservedWithdrawLiquidity != 0)) revert Insolvent();
790+
bool hasLiveLps = totalSupply() > MIN_TOTAL_SUPPLY;
791+
if (atAssetFloor && (hasLiveLps || feesAccruedMem != 0 || reservedWithdrawLiquidity != 0)) revert Insolvent();
791792

792793
uint256 netAssets = atAssetFloor ? MIN_LIQUIDITY : grossAssets - feesAccruedMem;
793794
shares = assets * totalSupply() / netAssets;

test/invariants/EthenaARM/TargetFunctions.sol

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,13 @@ abstract contract TargetFunctions is Setup, StdUtils {
8282
}
8383

8484
function targetARMDeposit(uint88 amount, uint256 randomAddressIndex) external ensureExchangeRateIncrease {
85-
// Mirror AbstractARM._deposit's Insolvent() guard: when the ARM sits at the asset floor
86-
// (totalAssets() clamped to MIN_LIQUIDITY == 1e12), deposits revert if any senior liability
87-
// (accrued fees OR reserved LP withdrawals) is outstanding. Skip those inputs instead of reverting.
88-
if (assume(arm.totalAssets() > 1e12 || (arm.feesAccrued() == 0 && arm.reservedWithdrawLiquidity() == 0))) {
85+
// Mirror AbstractARM._deposit's Insolvent() guard: at the asset floor, deposits are allowed
86+
// only before any live LP shares exist and when there are no senior liabilities.
87+
bool initialDeposit = arm.totalSupply() == DEFAULT_MIN_TOTAL_SUPPLY;
88+
if (assume(
89+
arm.totalAssets() > 1e12
90+
|| (initialDeposit && arm.feesAccrued() == 0 && arm.reservedWithdrawLiquidity() == 0)
91+
)) {
8992
return;
9093
}
9194
// Select a random user from makers

test/invariants/LidoARM/TargetFunction.t.sol

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,13 @@ abstract contract TargetFunction is Invariant_LidoARM_Setup_Test {
199199
(address user, uint256 balance) = selectUserWithLiqudity(from);
200200
vm.assume(user != address(0)); // Ensure we found a user with liquidity
201201

202-
// Mirror AbstractARM._deposit's Insolvent() guard: at the asset floor (totalAssets() clamped to
203-
// MIN_LIQUIDITY == 1e12) deposits revert when any senior liability (accrued fees or reserved LP
204-
// redeems) is outstanding. Skip those inputs so strict-mode fuzzing does not fail on the revert.
202+
// Mirror AbstractARM._deposit's Insolvent() guard: at the asset floor, deposits are allowed
203+
// only before any live LP shares exist and when there are no senior liabilities.
205204
vm.assume(
206-
lidoARM.totalAssets() > 1e12 || (lidoARM.feesAccrued() == 0 && lidoARM.reservedWithdrawLiquidity() == 0)
205+
lidoARM.totalAssets() > 1e12
206+
|| (lidoARM.totalSupply() == MIN_TOTAL_SUPPLY
207+
&& lidoARM.feesAccrued() == 0
208+
&& lidoARM.reservedWithdrawLiquidity() == 0)
207209
);
208210

209211
// Bound amount

test/unit/MultiAssetARM/concrete/Deposit.t.sol

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,22 @@ abstract contract Deposit_Test is Unit_MultiAssetARM_Shared_Test {
9797
arm.deposit(LIQUIDITY_UNIT());
9898
}
9999

100+
function test_Deposit_RevertWhen_AssetLossReachesFloorWithLiveLps() public {
101+
firstDeposit(alice, DEFAULT_AMOUNT());
102+
assertGt(arm.totalSupply(), MIN_TOTAL_SUPPLY, "live LP shares exist");
103+
assertEq(arm.feesAccrued(), 0, "no accrued fees");
104+
assertEq(arm.reservedWithdrawLiquidity(), 0, "no reserved withdrawals");
105+
106+
// Simulate a real loss that leaves only the native-liquidity floor backing live LP shares.
107+
_setArmBalances(MIN_LIQUIDITY(), 0);
108+
assertEq(arm.totalAssets(), MIN_LIQUIDITY(), "at asset floor");
109+
110+
_mint(liquidity, bobby, LIQUIDITY_UNIT());
111+
vm.expectRevert(AbstractARM.Insolvent.selector);
112+
vm.prank(bobby);
113+
arm.deposit(LIQUIDITY_UNIT());
114+
}
115+
100116
function _generateFees() internal returns (uint256 fees) {
101117
firstDeposit(alice, DEFAULT_AMOUNT());
102118

0 commit comments

Comments
 (0)