datahaven/contracts/test/storage/StorageLayout.t.sol
Ahmad Kaouk da2847bbbf
test: Add storage layout checks for upgradeable contracts (#420)
## Summary

Implements storage layout testing for the upgradeable
`DataHavenServiceManager` contract to prevent state
   corruption during proxy upgrades.

  ## Changes

  ### New Files
- **`contracts/storage-snapshots/DataHavenServiceManager.storage.json`**
- Baseline storage layout
  snapshot
- **`contracts/storage-snapshots/README.md`** - Documentation for
updating snapshots and known
  limitations
- **`contracts/scripts/check-storage-layout.sh`** - CI script that
compares current layout against
  snapshot
- **`contracts/test/storage/StorageLayout.t.sol`** - Upgrade simulation
tests verifying state
  preservation
- **`.github/workflows/task-storage-layout.yml`** - CI workflow for
storage layout checks

  ### Modified Files
- **`.github/workflows/CI.yml`** - Added `storage-layout` job to run in
parallel with other checks

  ## How It Works

  **Two-pronged approach:**

1. **Snapshot Diff** - Compares current storage layout against committed
snapshot using `forge inspect`.
Catches unintended variable reordering, type changes, or gap
modifications.

2. **Upgrade Simulation** - Foundry tests that populate state, perform a
proxy upgrade, and verify all
  values survive:
     - `test_upgradePreservesState` - Verifies core state variables
- `test_upgradePreservesValidatorMappings` - Verifies
`validatorEthAddressToSolochainAddress` mapping
- `test_upgradePreservesMultipleValidators` - Verifies
`validatorsAllowlist` with multiple entries
- `test_functionalityAfterUpgrade` - Verifies contract remains
functional post-upgrade

  ## Normalization

  The snapshot comparison normalizes JSON to avoid false positives:
  - Removes `astId` (changes with compiler runs)
  - Removes `contract` (contains full file path)
- Removes `.types` section (contains unstable AST IDs embedded in type
keys)
  - Sorts by slot number

  ## Usage

  ```bash
  # Check storage layout against snapshot
  ./scripts/check-storage-layout.sh

  # Run upgrade simulation tests
  forge test --match-contract StorageLayoutTest -vvv

  # Update snapshot (when intentionally changing storage)
  forge inspect DataHavenServiceManager storage --json >
  storage-snapshots/DataHavenServiceManager.storage.json
```
  ## Test Plan

  - ./scripts/check-storage-layout.sh passes
  - forge test --match-contract StorageLayoutTest -vvv passes (4 tests)
  - CI workflow runs successfully
2026-02-05 11:08:35 +00:00

175 lines
6.6 KiB
Solidity

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.27;
import {Test} from "forge-std/Test.sol";
import {
ITransparentUpgradeableProxy
} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {AVSDeployer} from "../utils/AVSDeployer.sol";
import {DataHavenServiceManager} from "../../src/DataHavenServiceManager.sol";
/// @title Storage Layout Tests for DataHavenServiceManager
/// @notice Verifies that proxy upgrades preserve state correctly
contract StorageLayoutTest is AVSDeployer {
function setUp() public {
_deployMockEigenLayerAndAVS();
}
/// @notice Proves state is preserved across proxy upgrade
function test_upgradePreservesState() public {
// 1. Populate state
address testValidator = address(0x1234);
address newRewardsInitiator = address(0x9999);
vm.startPrank(avsOwner);
serviceManager.addValidatorToAllowlist(testValidator);
serviceManager.setRewardsInitiator(newRewardsInitiator);
vm.stopPrank();
// 2. Record state before upgrade
bool allowlistBefore = serviceManager.validatorsAllowlist(testValidator);
address rewardsInitiatorBefore = serviceManager.rewardsInitiator();
address ownerBefore = serviceManager.owner();
address gatewayBefore = serviceManager.snowbridgeGateway();
// 3. Deploy new implementation
DataHavenServiceManager newImpl =
new DataHavenServiceManager(rewardsCoordinator, allocationManager);
// 4. Upgrade proxy
vm.prank(proxyAdminOwner);
proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(serviceManager)), address(newImpl));
// 5. Verify state preserved
assertEq(
serviceManager.validatorsAllowlist(testValidator),
allowlistBefore,
"validatorsAllowlist should be preserved"
);
assertEq(
serviceManager.rewardsInitiator(),
rewardsInitiatorBefore,
"rewardsInitiator should be preserved"
);
assertEq(serviceManager.owner(), ownerBefore, "owner should be preserved");
assertEq(
serviceManager.snowbridgeGateway(),
gatewayBefore,
"snowbridgeGateway should be preserved"
);
}
/// @notice Verifies validatorEthAddressToSolochainAddress mapping is preserved
function test_upgradePreservesValidatorMappings() public {
address testValidator = address(0xABCD);
address testSolochainAddress = address(0xDEF0);
// Add validator to allowlist first
vm.prank(avsOwner);
serviceManager.addValidatorToAllowlist(testValidator);
// Register operator via allocationManager to set the solochain address mapping
uint32[] memory operatorSetIds = new uint32[](1);
operatorSetIds[0] = 0; // VALIDATORS_SET_ID
vm.prank(address(allocationManager));
serviceManager.registerOperator(
testValidator,
address(serviceManager),
operatorSetIds,
abi.encodePacked(testSolochainAddress)
);
// Record state before upgrade
bool inAllowlistBefore = serviceManager.validatorsAllowlist(testValidator);
address solochainAddressBefore =
serviceManager.validatorEthAddressToSolochainAddress(testValidator);
// Verify the mapping was set correctly before upgrade
assertEq(solochainAddressBefore, testSolochainAddress, "Solochain address should be set");
// Deploy new implementation and upgrade
DataHavenServiceManager newImpl =
new DataHavenServiceManager(rewardsCoordinator, allocationManager);
vm.prank(proxyAdminOwner);
proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(serviceManager)), address(newImpl));
// Verify both mappings preserved after upgrade
assertEq(
serviceManager.validatorsAllowlist(testValidator),
inAllowlistBefore,
"validatorsAllowlist mapping should be preserved after upgrade"
);
assertEq(
serviceManager.validatorEthAddressToSolochainAddress(testValidator),
solochainAddressBefore,
"validatorEthAddressToSolochainAddress mapping should be preserved after upgrade"
);
assertEq(
serviceManager.validatorEthAddressToSolochainAddress(testValidator),
testSolochainAddress,
"validatorEthAddressToSolochainAddress should have correct value after upgrade"
);
}
/// @notice Verifies multiple validators in allowlist are preserved
function test_upgradePreservesMultipleValidators() public {
address[] memory validators = new address[](3);
validators[0] = address(0x1111);
validators[1] = address(0x2222);
validators[2] = address(0x3333);
// Add multiple validators
vm.startPrank(avsOwner);
for (uint256 i = 0; i < validators.length; i++) {
serviceManager.addValidatorToAllowlist(validators[i]);
}
vm.stopPrank();
// Deploy new implementation and upgrade
DataHavenServiceManager newImpl =
new DataHavenServiceManager(rewardsCoordinator, allocationManager);
vm.prank(proxyAdminOwner);
proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(serviceManager)), address(newImpl));
// Verify all validators still in allowlist
for (uint256 i = 0; i < validators.length; i++) {
assertTrue(
serviceManager.validatorsAllowlist(validators[i]),
"All validators should remain in allowlist after upgrade"
);
}
}
/// @notice Verifies that upgrade doesn't affect functionality
function test_functionalityAfterUpgrade() public {
// Deploy new implementation and upgrade
DataHavenServiceManager newImpl =
new DataHavenServiceManager(rewardsCoordinator, allocationManager);
vm.prank(proxyAdminOwner);
proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(serviceManager)), address(newImpl));
// Verify functionality still works
address newValidator = address(0xBEEF);
vm.prank(avsOwner);
serviceManager.addValidatorToAllowlist(newValidator);
assertTrue(
serviceManager.validatorsAllowlist(newValidator),
"Should be able to add validators after upgrade"
);
vm.prank(avsOwner);
serviceManager.removeValidatorFromAllowlist(newValidator);
assertFalse(
serviceManager.validatorsAllowlist(newValidator),
"Should be able to remove validators after upgrade"
);
}
}