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
5 changes: 4 additions & 1 deletion test/invariant/ValidatorStateMachine.invariant.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,12 @@ contract ValidatorStateMachineInvariantTest is TestSetup, Deployed {
// The production queue proxy on mainnet still runs the master impl which
// has no receive(); initializeOnUpgradeV2 below sweeps queue-locked ETH
// into the queue and would revert with SendFail. Upgrade the queue first.
// Constructor order is (liquidityPool, eETH, weETH, blacklister, roleRegistry, minDelay).
// Pass blacklisterInstance + roleRegistryInstance in their correct slots so the
// upgraded mainnet queue proxy's role checks match production wiring.
address newPQ = address(new PriorityWithdrawalQueue(
address(liquidityPoolInstance), address(eETHInstance), address(weEthInstance),
address(roleRegistryInstance), treasuryInstance, 1 hours
address(blacklisterInstance), address(roleRegistryInstance), 1 hours
));
vm.prank(UPGRADE_TIMELOCK);
PriorityWithdrawalQueue(payable(PRIORITY_WITHDRAWAL_QUEUE)).upgradeTo(newPQ);
Expand Down
17 changes: 7 additions & 10 deletions test/invariant/WithdrawalSolvency.invariant.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,6 @@ contract WithdrawalSolvencyInvariantTest is TestSetup {
WithdrawalSolvencyHandler internal handler;
address[5] internal handlerActors;

// Captured initial mainnet baselines (delta-aware assertions).
uint256 internal baseLocked;
uint256 internal baseOutOfLp;

function setUp() public {
initializeRealisticFork(MAINNET_FORK);

Expand All @@ -113,9 +109,6 @@ contract WithdrawalSolvencyInvariantTest is TestSetup {
eETHInstance.approve(address(liquidityPoolInstance), type(uint256).max);
}

baseLocked = uint256(withdrawRequestNFTInstance.ethAmountLockedForWithdrawal());
baseOutOfLp = uint256(liquidityPoolInstance.totalValueOutOfLp());

// Finalize the PRE-EXISTING mainnet pending range once, mirroring what
// EtherFiAdmin does in production (lock the summed eETH of the range via
// addEthAmountLockedForWithdrawal, then finalizeRequests). This clears
Expand All @@ -133,7 +126,6 @@ contract WithdrawalSolvencyInvariantTest is TestSetup {
eETHInstance,
withdrawRequestNFTInstance,
address(etherFiAdminInstance),
address(membershipManagerInstance),
handlerActors
);

Expand All @@ -142,12 +134,17 @@ contract WithdrawalSolvencyInvariantTest is TestSetup {
// Restrict fuzzing to the action functions. Without this, the engine
// also targets the handler's public getters/counters, burning call
// budget on no-ops. probeOverLiquidityLock is included so the I3/P1
// liquidity guard is positively driven on every run.
bytes4[] memory sel = new bytes4[](4);
// liquidity guard is positively driven on every run. rebasePositive/
// rebaseNegative add rate churn (bounded so the share rate stays well
// above minAmountForShare) and are driven through the real
// etherFiAdminContract caller that LiquidityPool.rebase requires.
bytes4[] memory sel = new bytes4[](6);
sel[0] = handler.requestWithdraw.selector;
sel[1] = handler.finalizeRequests.selector;
sel[2] = handler.claimWithdraw.selector;
sel[3] = handler.probeOverLiquidityLock.selector;
sel[4] = handler.rebasePositive.selector;
sel[5] = handler.rebaseNegative.selector;
targetSelector(FuzzSelector({addr: address(handler), selectors: sel}));

// Pre-seed a batch of OUR OWN requests and finalize them, so every fuzz
Expand Down
13 changes: 7 additions & 6 deletions test/invariant/handlers/WithdrawalSolvencyHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ contract WithdrawalSolvencyHandler is StdUtils {
EETH public immutable eETH;
WithdrawRequestNFT public immutable wrn;
address public immutable etherFiAdminAddr;
address public immutable membershipManager;

address[N_EOAS] public actors;

Expand Down Expand Up @@ -130,14 +129,12 @@ contract WithdrawalSolvencyHandler is StdUtils {
EETH _eETH,
WithdrawRequestNFT _wrn,
address _etherFiAdmin,
address _membershipManager,
address[N_EOAS] memory _actors
) {
lp = _lp;
eETH = _eETH;
wrn = _wrn;
etherFiAdminAddr = _etherFiAdmin;
membershipManager = _membershipManager;
for (uint256 i = 0; i < N_EOAS; i++) {
actors[i] = _actors[i];
}
Expand Down Expand Up @@ -369,13 +366,17 @@ contract WithdrawalSolvencyHandler is StdUtils {

/// @notice Positive rebase stress — keeps the rate climbing (frozen-rate
/// shield is irrelevant to solvency here, just adds state churn).
/// Bounded to <= 0.2% of TVL so it stays under the contract's
/// MAX_POSITIVE_REBASE_BPS (0.25%) cap and actually applies.
/// Caller is the etherFiAdminContract — the only address
/// `LiquidityPool.rebase` accepts (LiquidityPool.sol:456).
function rebasePositive(uint128 deltaSeed) external {
uint256 outOfLp = uint256(lp.totalValueOutOfLp());
uint256 cap = outOfLp == 0 ? 1 ether : (outOfLp * 50) / 1e4; // <=0.5%
uint256 cap = outOfLp == 0 ? 1 ether : (outOfLp * 20) / 1e4; // <=0.2%
if (cap == 0) cap = 1;
if (cap > uint256(uint128(type(int128).max))) cap = uint256(uint128(type(int128).max));
int128 delta = int128(int256(bound(uint256(deltaSeed), 0, cap)));
vm.prank(membershipManager);
vm.prank(etherFiAdminAddr);
try lp.rebase(delta, 0) { callCounts["rebase_pos"]++; }
catch { callCounts["rebase_pos_revert"]++; }
}
Expand All @@ -398,7 +399,7 @@ contract WithdrawalSolvencyHandler is StdUtils {
if (cap == 0) { callCounts["rebase_neg_skipped"]++; return; }
if (cap > uint256(uint128(type(int128).max))) cap = uint256(uint128(type(int128).max));
int128 delta = -int128(int256(bound(uint256(deltaSeed), 1, cap)));
vm.prank(membershipManager);
vm.prank(etherFiAdminAddr);
try lp.rebase(delta, 0) { callCounts["rebase_neg"]++; }
catch { callCounts["rebase_neg_revert"]++; }
}
Expand Down