Skip to content

Commit deee063

Browse files
authored
Block deposits at the asset floor when live LPs exist (#311)
* fix: block deposits at asset floor with live LPs * test(invariants): make Lido handlers strict-safe * fix: update targetARMDeposit function to accept uint256 amount type
1 parent 9f37d1d commit deee063

4 files changed

Lines changed: 29 additions & 25 deletions

File tree

src/contracts/AbstractARM.sol

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -786,11 +786,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
786786
uint256 grossAssets = _availableAssets();
787787
uint256 feesAccruedMem = feesAccrued;
788788

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

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

test/invariants/EthenaARM/TargetFunctions.sol

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

8686
function targetARMDeposit(uint256 amount, uint256 randomAddressIndex) external ensureExchangeRateIncrease {
87-
// Mirror AbstractARM._deposit's Insolvent() guard: when the ARM sits at the asset floor
88-
// (totalAssets() clamped to MIN_LIQUIDITY == 1e12), deposits revert if any senior liability
89-
// (accrued fees OR reserved LP withdrawals) is outstanding. Skip those inputs instead of reverting.
90-
if (assume(arm.totalAssets() > 1e12 || (arm.feesAccrued() == 0 && arm.reservedWithdrawLiquidity() == 0))) {
87+
// Mirror AbstractARM._deposit's Insolvent() guard: at the asset floor, deposits are allowed
88+
// only before any live LP shares exist and when there are no senior liabilities.
89+
bool initialDeposit = arm.totalSupply() == DEFAULT_MIN_TOTAL_SUPPLY;
90+
if (assume(
91+
arm.totalAssets() > 1e12
92+
|| (initialDeposit && arm.feesAccrued() == 0 && arm.reservedWithdrawLiquidity() == 0)
93+
)) {
9194
return;
9295
}
9396
// 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
@@ -208,11 +208,13 @@ abstract contract TargetFunction is Invariant_LidoARM_Setup_Test {
208208
(address user, uint256 balance) = selectUserWithLiqudity(from);
209209
vm.assume(user != address(0)); // Ensure we found a user with liquidity
210210

211-
// Mirror AbstractARM._deposit's Insolvent() guard: at the asset floor (totalAssets() clamped to
212-
// MIN_LIQUIDITY == 1e12) deposits revert when any senior liability (accrued fees or reserved LP
213-
// redeems) is outstanding. Skip those inputs so strict-mode fuzzing does not fail on the revert.
211+
// Mirror AbstractARM._deposit's Insolvent() guard: at the asset floor, deposits are allowed
212+
// only before any live LP shares exist and when there are no senior liabilities.
214213
vm.assume(
215-
lidoARM.totalAssets() > 1e12 || (lidoARM.feesAccrued() == 0 && lidoARM.reservedWithdrawLiquidity() == 0)
214+
lidoARM.totalAssets() > 1e12
215+
|| (lidoARM.totalSupply() == MIN_TOTAL_SUPPLY
216+
&& lidoARM.feesAccrued() == 0
217+
&& lidoARM.reservedWithdrawLiquidity() == 0)
216218
);
217219

218220
// Bound amount

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

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

100-
function test_Deposit_WhenAccruedFeesExactlyBacked() public {
101-
uint256 fees = _generateFees();
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");
102104
assertEq(arm.reservedWithdrawLiquidity(), 0, "no reserved withdrawals");
103105

104-
_setArmBalances(fees + MIN_LIQUIDITY(), 0);
105-
106-
uint256 amount = LIQUIDITY_UNIT();
107-
uint256 expectedShares = amount * arm.totalSupply() / MIN_LIQUIDITY();
108-
_mint(liquidity, bobby, amount);
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");
109109

110+
_mint(liquidity, bobby, LIQUIDITY_UNIT());
111+
vm.expectRevert(AbstractARM.Insolvent.selector);
110112
vm.prank(bobby);
111-
uint256 shares = arm.deposit(amount);
112-
113-
assertEq(shares, expectedShares, "shares returned");
114-
assertEq(arm.balanceOf(bobby), expectedShares, "bobby shares");
115-
assertEq(arm.totalAssets(), MIN_LIQUIDITY() + amount, "net assets increase by deposit");
113+
arm.deposit(LIQUIDITY_UNIT());
116114
}
117115

118116
function _generateFees() internal returns (uint256 fees) {

0 commit comments

Comments
 (0)