Write Solidity integration tests for EigenLayer contracts. Use when the user asks to write integration tests, test user flows, test cross-contract interactions, or test upgrade scenarios...
Write comprehensive integration tests for EigenLayer Solidity contracts following the project's established conventions.
Integration tests orchestrate the deployment of all EigenLayer core contracts to test high-level user flows across multiple contracts. There are three test modes:
All integration test functions MUST:
testFuzz_action1_action2_... describing the flowuint24 _random (or _r) as the only parameter - this seeds randomnessrand(_random) modifier to initialize the random seedfunction testFuzz_deposit_delegate_queue_complete(uint24 _random) public rand(_random) {
// Test implementation
}
The rand() modifier initializes the test's random seed, which is used by helper functions like _newRandomStaker() to generate deterministic random values for reproducible tests.
| Type | Location |
|---|---|
| Normal integration tests | src/test/integration/tests/ |
| Upgrade tests | src/test/integration/tests/upgrade/ |
| Check functions | src/test/integration/IntegrationChecks.t.sol |
| Multichain checks | src/test/integration/MultichainIntegrationChecks.t.sol |
Never call contracts directly. Use the User or AVS actor contracts:
// ✅ CORRECT - Actions through User/AVS
staker.depositIntoEigenlayer(strategies, tokenBalances);
operator.delegateTo(operator);
avs.createOperatorSet(strategies);
// ❌ WRONG - Direct contract calls
strategyManager.depositIntoStrategy(...);
delegationManager.delegateTo(...);
After each numbered action, verify state changes using check_* functions from IntegrationChecks.t.sol:
// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
Use comments to number each action step for clarity:
// 1. Deposit Into Strategies
staker.depositIntoEigenlayer(strategies, tokenBalances);
check_Deposit_State(staker, strategies, shares);
// 2. Delegate to an operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, shares);
// 3. Queue Withdrawals
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
check_QueuedWithdrawal_State(staker, operator, strategies, shares, withdrawableShares, withdrawals, withdrawalRoots);
User (Staker/Operator)Located in src/test/integration/users/User.t.sol. A User can act as both a staker AND an operator.
Key methods:
depositIntoEigenlayer(strategies, tokenBalances) - Deposit tokensdelegateTo(operator) - Delegate to an operatorregisterAsOperator() - Register as an operatorqueueWithdrawals(strategies, shares) - Queue withdrawalscompleteWithdrawalAsTokens(withdrawal) / completeWithdrawalAsShares(withdrawal) - Complete withdrawalsregisterForOperatorSet(operatorSet) - Register for an operator setmodifyAllocations(params) - Allocate magnitude to operator setsstartValidators() / verifyWithdrawalCredentials(validators) - Native ETH stakingstartCheckpoint() / completeCheckpoint() - Checkpoint EigenPodAVSLocated in src/test/integration/users/AVS.t.sol. Represents an AVS that creates operator sets and slashes operators.
Key methods:
createOperatorSet(strategies) - Create an operator setcreateRedistributingOperatorSets(strategies, recipients) - Create redistributing operator setsslashOperator(params) - Slash an operatorupdateSlasher(operatorSetId, slasher) - Update the slasher for an operator set// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/IntegrationChecks.t.sol";
/// @notice Base contract for shared setup across test variants
contract Integration_FlowName_Base is IntegrationCheckUtils {
using ArrayLib for *;
// Declare state used across tests
AVS avs;
OperatorSet operatorSet;
User operator;
User staker;
IStrategy[] strategies;
uint[] initTokenBalances;
uint[] initDepositShares;
/// @dev Setup state used across all test functions
function _init() internal virtual override {
_configAssetTypes(HOLDS_LST | HOLDS_ETH); // Configure asset types
// Create actors
(staker, strategies, initTokenBalances) = _newRandomStaker();
(operator,,) = _newRandomOperator();
(avs,) = _newRandomAVS();
// 1. Deposit into strategies
staker.depositIntoEigenlayer(strategies, initTokenBalances);
initDepositShares = _calculateExpectedShares(strategies, initTokenBalances);
check_Deposit_State(staker, strategies, initDepositShares);
// 2. Delegate staker to operator
staker.delegateTo(operator);
check_Delegation_State(staker, operator, strategies, initDepositShares);
// 3. Create operator set and register
operatorSet = avs.createOperatorSet(strategies);
operator.registerForOperatorSet(operatorSet);
check_Registration_State_NoAllocation(operator, operatorSet, allStrats);
}
}
/// @notice Test contract for specific flow variant
contract Integration_FlowName_Variant is Integration_FlowName_Base {
/// @dev All test functions must:
/// 1. Be named testFuzz_action1_action2_...
/// 2. Take uint24 _r as parameter (seeds randomness)
/// 3. Use rand(_r) modifier
function testFuzz_action1_action2(uint24 _r) public rand(_r) {
// 4. Next action
// ... action ...
// ... check ...
// 5. Another action
// ... action ...
// ... check ...
}
}
Upgrade tests verify that upgrades correctly handle pre-upgrade state.
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "src/test/integration/UpgradeTest.t.sol";
contract Integration_Upgrade_FeatureName is UpgradeTest {
User staker;
IStrategy[] strategies;
uint[] tokenBalances;
function _init() internal override {
// Pre-upgrade setup - NO check_ functions here!
(staker, strategies, tokenBalances) = _newRandomStaker();
staker.depositIntoEigenlayer(strategies, tokenBalances);
// ... more pre-upgrade actions WITHOUT checks
}
function testFuzz_upgrade_scenario(uint24 _r) public rand(_r) {
/// Pre-upgrade actions (no checks - old contracts)
Withdrawal[] memory withdrawals = staker.queueWithdrawals(strategies, shares);
/// Upgrade to new contracts
_upgradeEigenLayerContracts();
/// Post-upgrade actions WITH checks
_rollBlocksForCompleteWithdrawals(withdrawals);
for (uint i = 0; i < withdrawals.length; i++) {
staker.completeWithdrawalAsShares(withdrawals[i]);
check_Withdrawal_AsShares_State(staker, operator, withdrawals[i], strategies, shares);
}
}
}
Important for Upgrade Tests:
check_* functions (old contracts have different invariants)_upgradeEigenLayerContracts() to upgrade to new contractscheck_* functionsenv FOUNDRY_PROFILE=forktest forge t --mc Integration_UpgradeAll state verification should be in IntegrationChecks.t.sol. There are two types:
check_* FunctionsHigh-level state checks that verify multiple invariants after an action:
check_Deposit_State(staker, strategies, shares);
check_Delegation_State(staker, operator, strategies, shares);
check_QueuedWithdrawal_State(staker, operator, strategies, shares, withdrawableShares, withdrawals, withdrawalRoots);
check_Withdrawal_AsTokens_State(staker, operator, withdrawal, strategies, shares, tokens, expectedTokens);
assert_Snap_* FunctionsTime-machine assertions that compare state before/after an action:
assert_Snap_Added_Staker_DepositShares(staker, strategy, amount, "error message");
assert_Snap_Removed_Staker_WithdrawableShares(staker, strategy, amount, "error message");
assert_Snap_Unchanged_Staker_DepositShares(staker, "error message");
If a check doesn't exist, add it to IntegrationChecks.t.sol:
function check_NewAction_State(
User staker,
IStrategy[] memory strategies,
uint[] memory expectedValues
) internal {
// Use assert_Snap_* for before/after comparisons
assert_Snap_Added_Staker_DepositShares(
staker, strategies[0], expectedValues[0], "should have added shares"
);
// Or use regular assertions for absolute checks
assertEq(
someContract.getValue(address(staker)),
expectedValue,
"value should match expected"
);
}
rand(_r) ModifierEvery test function takes a uint24 _r parameter and uses the rand(_r) modifier:
function testFuzz_deposit_delegate(uint24 _r) public rand(_r) {
// _r seeds all random generation in this test
// This makes tests reproducible - same _r = same test execution
}
The rand() modifier initializes the random seed used by all _newRandom* helper functions. This ensures:
Use _configRand or _configAssetTypes to control what types of users/assets are created:
function testFuzz_example(uint24 _r) public rand(_r) {
// Full configuration
_configRand({
_randomSeed: _r,
_assetTypes: HOLDS_LST | HOLDS_ETH,
_userTypes: DEFAULT | ALT_METHODS
});
// Or just configure asset types (simpler)
_configAssetTypes(HOLDS_LST);
// Create users - will use the configured randomization
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
}
Asset Types:
HOLDS_LST - User holds liquid staking tokensHOLDS_ETH - User holds native ETH (beacon chain)HOLDS_ALL - User holds bothUser Types:
DEFAULT - Standard User contractALT_METHODS - User that uses alternative method signaturesCommon helpers available in IntegrationBase:
// Create actors
(User staker, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomStaker();
(User operator, IStrategy[] memory strategies, uint[] memory tokenBalances) = _newRandomOperator();
User operator = _newRandomOperator_NoAssets();
(AVS avs, OperatorSet[] memory operatorSets) = _newRandomAVS();
// Calculate expected values
uint[] memory shares = _calculateExpectedShares(strategies, tokenBalances);
uint[] memory tokens = _calculateExpectedTokens(strategies, shares);
uint[] memory withdrawableShares = _getStakerWithdrawableShares(staker, strategies);
// Generate params
AllocateParams memory params = _genAllocation_AllAvailable(operator, operatorSet);
SlashingParams memory slashParams = _genSlashing_Rand(operator, operatorSet);
// Time advancement
_rollBlocksForCompleteWithdrawals(withdrawals);
_rollBlocksForCompleteAllocation(operator, operatorSet, strategies);
_rollForward_AllocationConfigurationDelay();
// Get state
bytes32[] memory withdrawalRoots = _getWithdrawalHashes(withdrawals);
IERC20[] memory tokens = _getUnderlyingTokens(strategies);
# Run all integration tests locally (fresh contract deployment)
forge t --mc Integration
# Run mainnet fork tests (upgrades mainnet contracts to latest, then runs tests)
# Requires RPC_MAINNET environment variable
env FOUNDRY_PROFILE=forktest forge t --mc Integration
# Run upgrade tests only (tests upgrade compatibility)
env FOUNDRY_PROFILE=forktest forge t --mc Integration_Upgrade
# Run specific test
forge t --match-test testFuzz_deposit_delegate
# Run with verbosity
forge t --mc Integration -vvv
| Mode | Command | What Happens |
|---|---|---|
| Local | forge t --mc Integration |
Deploys fresh contracts, runs tests |
| Fork | env FOUNDRY_PROFILE=forktest forge t --mc Integration |
Forks mainnet, upgrades ALL contracts to latest repo implementations, runs tests |
| Upgrade | env FOUNDRY_PROFILE=forktest forge t --mc Integration_Upgrade |
Forks mainnet, runs pre-upgrade actions on OLD contracts, then upgrades and tests compatibility |
Fork tests ensure that the latest contract code works correctly when upgrading from the current mainnet state. The test framework automatically upgrades all proxy contracts to the latest implementations before running tests.
| Pattern | Purpose |
|---|---|
Integration_FlowName_Base |
Base contract with shared _init() setup |
Integration_FlowName_Variant |
Test contract for specific flow variant |
Integration_Upgrade_FeatureName |
Upgrade test for a feature |
All test functions follow the pattern: testFuzz_action1_action2_...(uint24 _random) public rand(_random)
| Example | Description |
|---|---|
testFuzz_deposit_delegate_queue_completeAsTokens |
Deposit → Delegate → Queue → Complete as tokens |
testFuzz_deposit_delegate_undelegate |
Deposit → Delegate → Undelegate |
testFuzz_verifyWC_checkpoint_slash |
Verify withdrawal credentials → Checkpoint → Slash |
testFuzz_upgrade_migrate_slash |
Upgrade contracts → Migrate → Slash |
| Pattern | Purpose |
|---|---|
check_ActionName_State |
High-level check function in IntegrationChecks |
assert_Snap_Added_* |
Assert value increased from snapshot |
assert_Snap_Removed_* |
Assert value decreased from snapshot |
assert_Snap_Unchanged_* |
Assert value unchanged from snapshot |
check_* functions to use after each actionIntegrationChecks.t.solReference: src/test/integration/tests/DualSlashing.t.sol
This file demonstrates:
_init() for shared setupcheck_* after every actionUser and AVS actors