Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/contracts/AbstractARM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -786,11 +786,12 @@ abstract contract AbstractARM is OwnableOperable, ERC20Upgradeable, ReentrancyGu
uint256 grossAssets = _availableAssets();
uint256 feesAccruedMem = feesAccrued;

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

uint256 netAssets = atAssetFloor ? MIN_LIQUIDITY : grossAssets - feesAccruedMem;
shares = assets * totalSupply() / netAssets;
Expand Down
11 changes: 7 additions & 4 deletions test/invariants/EthenaARM/TargetFunctions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,13 @@ abstract contract TargetFunctions is Setup, StdUtils {
}

function targetARMDeposit(uint256 amount, uint256 randomAddressIndex) external ensureExchangeRateIncrease {
// Mirror AbstractARM._deposit's Insolvent() guard: when the ARM sits at the asset floor
// (totalAssets() clamped to MIN_LIQUIDITY == 1e12), deposits revert if any senior liability
// (accrued fees OR reserved LP withdrawals) is outstanding. Skip those inputs instead of reverting.
if (assume(arm.totalAssets() > 1e12 || (arm.feesAccrued() == 0 && arm.reservedWithdrawLiquidity() == 0))) {
// Mirror AbstractARM._deposit's Insolvent() guard: at the asset floor, deposits are allowed
// only before any live LP shares exist and when there are no senior liabilities.
bool initialDeposit = arm.totalSupply() == DEFAULT_MIN_TOTAL_SUPPLY;
if (assume(
arm.totalAssets() > 1e12
|| (initialDeposit && arm.feesAccrued() == 0 && arm.reservedWithdrawLiquidity() == 0)
)) {
return;
}
// Select a random user from makers
Expand Down
10 changes: 6 additions & 4 deletions test/invariants/LidoARM/TargetFunction.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,13 @@ abstract contract TargetFunction is Invariant_LidoARM_Setup_Test {
(address user, uint256 balance) = selectUserWithLiqudity(from);
vm.assume(user != address(0)); // Ensure we found a user with liquidity

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

// Bound amount
Expand Down
22 changes: 10 additions & 12 deletions test/unit/MultiAssetARM/concrete/Deposit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,20 @@ abstract contract Deposit_Test is Unit_MultiAssetARM_Shared_Test {
arm.deposit(LIQUIDITY_UNIT());
}

function test_Deposit_WhenAccruedFeesExactlyBacked() public {
uint256 fees = _generateFees();
function test_Deposit_RevertWhen_AssetLossReachesFloorWithLiveLps() public {
firstDeposit(alice, DEFAULT_AMOUNT());
assertGt(arm.totalSupply(), MIN_TOTAL_SUPPLY, "live LP shares exist");
assertEq(arm.feesAccrued(), 0, "no accrued fees");
assertEq(arm.reservedWithdrawLiquidity(), 0, "no reserved withdrawals");

_setArmBalances(fees + MIN_LIQUIDITY(), 0);

uint256 amount = LIQUIDITY_UNIT();
uint256 expectedShares = amount * arm.totalSupply() / MIN_LIQUIDITY();
_mint(liquidity, bobby, amount);
// Simulate a real loss that leaves only the native-liquidity floor backing live LP shares.
_setArmBalances(MIN_LIQUIDITY(), 0);
assertEq(arm.totalAssets(), MIN_LIQUIDITY(), "at asset floor");

_mint(liquidity, bobby, LIQUIDITY_UNIT());
vm.expectRevert(AbstractARM.Insolvent.selector);
vm.prank(bobby);
uint256 shares = arm.deposit(amount);

assertEq(shares, expectedShares, "shares returned");
assertEq(arm.balanceOf(bobby), expectedShares, "bobby shares");
assertEq(arm.totalAssets(), MIN_LIQUIDITY() + amount, "net assets increase by deposit");
arm.deposit(LIQUIDITY_UNIT());
}

function _generateFees() internal returns (uint256 fees) {
Expand Down
Loading