Skip to content

Commit 0c5329b

Browse files
committed
feat: add expected external storage slots verification to verifyAndUpdate
- Add ExternalStorageSlot struct to track external storage accesses - Update verifyAndUpdate signature to include expectedExternalSlots parameter - Add verification in StateChangeHandlerLib._runStateUpdates that CALL operations only access the expected external storage slots (as proven in ZK proof) - Include expectedExternalSlots in message hash for signature verification - Add ExternalStorageSlotMismatch error for mismatched slots - Update tests with new parameter and CALL args encoding
1 parent 11d689f commit 0c5329b

File tree

6 files changed

+92
-24
lines changed

6 files changed

+92
-24
lines changed

foundry.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@
22
src = "src"
33
out = "out"
44
libs = ["lib"]
5+
via_ir = true
6+
optimizer = true
7+
optimizer_runs = 200
58

69
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

src/GasKillerSDK.sol

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from "@eigenlayer-middleware/interfaces/IBLSSignatureChecker.sol";
88
import {IERC165} from "forge-std/interfaces/IERC165.sol";
99

10-
import {IGasKillerSDK} from "./interface/IGasKillerSDK.sol";
10+
import {IGasKillerSDK, ExternalStorageSlot} from "./interface/IGasKillerSDK.sol";
1111
import {StateTracker} from "./StateTracker.sol";
1212
import {StateChangeHandlerLib, StateUpdateType} from "./StateChangeHandlerLib.sol";
1313

@@ -51,12 +51,13 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
5151
/**
5252
* @notice Function to verify if a signature is valid and contains correct storage updates
5353
* @dev The message hash must be computed as:
54-
* sha256(abi.encode(transitionIndex, address(this), anchorHash, callerAddress, contractCalldata, storageUpdates))
54+
* sha256(abi.encode(transitionIndex, address(this), anchorHash, callerAddress, contractCalldata, storageUpdates, expectedExternalSlots))
5555
* This format enables slashing by including all inputs needed to reproduce execution.
5656
* @param msgHash The hash of the message to verify
5757
* @param quorumNumbers The quorum numbers to check signatures for
5858
* @param referenceBlockNumber The block number to use as reference for operator set
5959
* @param storageUpdates The storage updates to verify
60+
* @param expectedExternalSlots Array of external storage slots that were read during execution (as proven in ZK proof)
6061
* @param transitionIndex The transition index
6162
* @param anchorHash The block hash anchoring the execution to a specific Ethereum state
6263
* @param callerAddress The address that initiated the original call (msg.sender)
@@ -68,6 +69,7 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
6869
bytes calldata quorumNumbers,
6970
uint32 referenceBlockNumber,
7071
bytes calldata storageUpdates,
72+
ExternalStorageSlot[] calldata expectedExternalSlots,
7173
uint256 transitionIndex,
7274
bytes32 anchorHash,
7375
address callerAddress,
@@ -82,21 +84,23 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
8284

8385
// Verify transition index and message hash
8486
require(transitionIndex + 1 == stateTransitionCount(), InvalidTransitionIndex());
85-
87+
8688
// Compute expected hash with all slashing-required fields:
8789
// - transitionIndex: replay protection
8890
// - address(this): target contract
8991
// - anchorHash: block hash for state anchoring (enables slashing verification)
9092
// - callerAddress: msg.sender (affects execution via access control, balances)
9193
// - contractCalldata: full calldata with arguments (enables execution reproduction)
9294
// - storageUpdates: the claimed storage changes
95+
// - expectedExternalSlots: external storage slots that were read during execution
9396
bytes32 expectedHash = sha256(abi.encode(
9497
transitionIndex,
9598
address(this),
9699
anchorHash,
97100
callerAddress,
98101
contractCalldata,
99-
storageUpdates
102+
storageUpdates,
103+
expectedExternalSlots
100104
));
101105
require(expectedHash == msgHash, InvalidSignature());
102106

@@ -113,8 +117,8 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
113117
);
114118
}
115119

116-
// Apply the state changes
117-
_stateChangeHandler(storageUpdates);
120+
// Apply the state changes, verifying external storage accesses match expected
121+
_stateChangeHandler(storageUpdates, expectedExternalSlots);
118122
}
119123

120124
/**
@@ -135,22 +139,25 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
135139
* @param callerAddress The caller address (msg.sender)
136140
* @param contractCalldata The full contract calldata
137141
* @param storageUpdates The storage updates
142+
* @param expectedExternalSlots The expected external storage slots that were read
138143
* @return bytes32 The expected message hash
139144
*/
140145
function getMessageHash(
141146
uint256 transitionIndex,
142147
bytes32 anchorHash,
143148
address callerAddress,
144149
bytes calldata contractCalldata,
145-
bytes calldata storageUpdates
150+
bytes calldata storageUpdates,
151+
ExternalStorageSlot[] calldata expectedExternalSlots
146152
) external view returns (bytes32) {
147153
return sha256(abi.encode(
148154
transitionIndex,
149155
address(this),
150156
anchorHash,
151157
callerAddress,
152158
contractCalldata,
153-
storageUpdates
159+
storageUpdates,
160+
expectedExternalSlots
154161
));
155162
}
156163

@@ -179,12 +186,13 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
179186
}
180187

181188
/**
182-
* @notice Function to apply storage updates
189+
* @notice Function to apply storage updates with external slot verification
183190
* @param storageUpdates The storage updates to apply
191+
* @param expectedExternalSlots The expected external storage slots that were read during execution
184192
*/
185-
function _stateChangeHandler(bytes calldata storageUpdates) internal {
193+
function _stateChangeHandler(bytes calldata storageUpdates, ExternalStorageSlot[] calldata expectedExternalSlots) internal {
186194
(StateUpdateType[] memory types, bytes[] memory args) = abi.decode(storageUpdates, (StateUpdateType[], bytes[]));
187-
StateChangeHandlerLib._runStateUpdates(types, args);
195+
StateChangeHandlerLib._runStateUpdates(types, args, expectedExternalSlots);
188196
}
189197

190198
/**

src/StateChangeHandlerLib.sol

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-License-Identifier: AGPL-3.0-only
22
pragma solidity ^0.8.0;
33

4+
import {ExternalStorageSlot} from "./interface/IGasKillerSDK.sol";
5+
46
enum StateUpdateType {
57
STORE,
68
CALL,
@@ -12,16 +14,25 @@ enum StateUpdateType {
1214
}
1315

1416
library StateChangeHandlerLib {
15-
/// @notice Decodes and executes a series of state updates
17+
/// @notice Decodes and executes a series of state updates with external storage slot verification
1618
/// @dev This function processes an array of state updates, executing them in sequence. Each update can be one of:
1719
/// - STORE: Direct storage writes using assembly
18-
/// - CALL: External contract calls with value transfer
20+
/// - CALL: External contract calls with value transfer (verified against expectedExternalSlots)
1921
/// - LOG0-LOG4: Event emission with 0-4 indexed topics
2022
/// @param types Array of StateUpdateType enums indicating the type of each state update operation
2123
/// @param args Array of ABI-encoded arguments corresponding to each operation type
24+
/// @param expectedExternalSlots Array of external storage slots that were read during execution (as proven in ZK proof)
2225
/// @dev types and args arrays must be equal length, with args[i] containing the encoded parameters for types[i]
23-
function _runStateUpdates(StateUpdateType[] memory types, bytes[] memory args) internal {
26+
function _runStateUpdates(
27+
StateUpdateType[] memory types,
28+
bytes[] memory args,
29+
ExternalStorageSlot[] calldata expectedExternalSlots
30+
) internal {
2431
require(types.length == args.length, InvalidArguments());
32+
33+
// Track which external slots have been verified
34+
uint256 externalSlotIndex = 0;
35+
2536
for (uint256 i = 0; i < types.length; i++) {
2637
StateUpdateType stateUpdateType = types[i];
2738
bytes memory arg = args[i];
@@ -32,7 +43,29 @@ library StateChangeHandlerLib {
3243
sstore(slot, value)
3344
}
3445
} else if (stateUpdateType == StateUpdateType.CALL) {
35-
(address target, uint256 value, bytes memory callargs) = abi.decode(arg, (address, uint256, bytes));
46+
(
47+
address target,
48+
uint256 value,
49+
bytes memory callargs,
50+
bytes32[] memory externalSlotsAccessed
51+
) = abi.decode(arg, (address, uint256, bytes, bytes32[]));
52+
53+
// Verify each external storage slot accessed matches the expected list
54+
for (uint256 j = 0; j < externalSlotsAccessed.length; j++) {
55+
require(
56+
externalSlotIndex < expectedExternalSlots.length,
57+
ExternalStorageSlotMismatch(target, externalSlotsAccessed[j])
58+
);
59+
60+
ExternalStorageSlot calldata expectedSlot = expectedExternalSlots[externalSlotIndex];
61+
require(
62+
expectedSlot.contractAddress == target && expectedSlot.slot == externalSlotsAccessed[j],
63+
ExternalStorageSlotMismatch(target, externalSlotsAccessed[j])
64+
);
65+
66+
externalSlotIndex++;
67+
}
68+
3669
bool success;
3770
// TOOD: might need better gas handling
3871
uint256 callgas = gasleft();
@@ -85,4 +118,5 @@ library StateChangeHandlerLib {
85118

86119
error InvalidArguments();
87120
error RevertingContext(uint256 index, address target, bytes revertData, bytes callargs);
121+
error ExternalStorageSlotMismatch(address contractAddress, bytes32 slot);
88122
}

src/interface/IGasKillerSDK.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ pragma solidity ^0.8.0;
44
import {IERC165} from "forge-std/interfaces/IERC165.sol";
55
import {IBLSSignatureCheckerTypes} from "@eigenlayer-middleware/interfaces/IBLSSignatureChecker.sol";
66

7+
/// @notice Represents an external storage slot access (address + slot)
8+
struct ExternalStorageSlot {
9+
address contractAddress;
10+
bytes32 slot;
11+
}
12+
713
/**
814
* @title IGasKillerSDK
915
* @notice Interface for GasKillerSDK contracts
@@ -18,13 +24,15 @@ interface IGasKillerSDK is IERC165 {
1824
error InsufficientQuorumThreshold();
1925
error StaleBlockNumber();
2026
error FutureBlockNumber();
27+
error ExternalStorageSlotMismatch(address contractAddress, bytes32 slot);
2128

2229
/**
2330
* @notice Function to verify if a signature is valid and contains correct storage updates
2431
* @param msgHash The hash of the message to verify
2532
* @param quorumNumbers The quorum numbers to check signatures for
2633
* @param referenceBlockNumber The block number to use as reference for operator set
2734
* @param storageUpdates The storage updates to verify
35+
* @param expectedExternalSlots Array of external storage slots that were read during execution (as proven in ZK proof)
2836
* @param transitionIndex The transition index
2937
* @param anchorHash The block hash anchoring the execution to a specific Ethereum state
3038
* @param callerAddress The address that initiated the original call (msg.sender)
@@ -36,6 +44,7 @@ interface IGasKillerSDK is IERC165 {
3644
bytes calldata quorumNumbers,
3745
uint32 referenceBlockNumber,
3846
bytes calldata storageUpdates,
47+
ExternalStorageSlot[] calldata expectedExternalSlots,
3948
uint256 transitionIndex,
4049
bytes32 anchorHash,
4150
address callerAddress,

test/GasKillerSDK.t.sol

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import "../src/GasKillerSDK.sol";
88
import "./exposed/GasKillerSDKExposed.sol";
99
import {StateUpdateType} from "../src/StateChangeHandlerLib.sol";
1010
import {StateChangeHandlerLib} from "../src/StateChangeHandlerLib.sol";
11+
import {ExternalStorageSlot} from "../src/interface/IGasKillerSDK.sol";
1112

1213
contract GasKillerSDKTest is Test {
1314
GasKillerSDKExposed public sdk;
@@ -25,7 +26,8 @@ contract GasKillerSDKTest is Test {
2526
bytes32 value = bytes32(uint256(100));
2627
args[0] = abi.encode(slot, value);
2728

28-
sdk.stateChangeHandlerExternal(abi.encode(types, args));
29+
ExternalStorageSlot[] memory expectedExternalSlots = new ExternalStorageSlot[](0);
30+
sdk.stateChangeHandlerExternal(abi.encode(types, args), expectedExternalSlots);
2931

3032
assertEq(vm.load(address(sdk), slot), value);
3133
}
@@ -37,10 +39,13 @@ contract GasKillerSDKTest is Test {
3739
StateUpdateType[] memory types = new StateUpdateType[](1);
3840
types[0] = StateUpdateType.CALL;
3941

42+
// CALL args now include externalSlotsAccessed array (empty for this test)
43+
bytes32[] memory externalSlotsAccessed = new bytes32[](0);
4044
bytes[] memory args = new bytes[](1);
41-
args[0] = abi.encode(address(target), uint256(0), abi.encodeWithSignature("setValue(uint256)", 42));
45+
args[0] = abi.encode(address(target), uint256(0), abi.encodeWithSignature("setValue(uint256)", 42), externalSlotsAccessed);
4246

43-
sdk.stateChangeHandlerExternal(abi.encode(types, args));
47+
ExternalStorageSlot[] memory expectedExternalSlots = new ExternalStorageSlot[](0);
48+
sdk.stateChangeHandlerExternal(abi.encode(types, args), expectedExternalSlots);
4449

4550
assertEq(target.value(), 42);
4651
}
@@ -51,8 +56,10 @@ contract GasKillerSDKTest is Test {
5156
StateUpdateType[] memory types = new StateUpdateType[](1);
5257
types[0] = StateUpdateType.CALL;
5358

59+
// CALL args now include externalSlotsAccessed array (empty for this test)
60+
bytes32[] memory externalSlotsAccessed = new bytes32[](0);
5461
bytes[] memory args = new bytes[](1);
55-
args[0] = abi.encode(address(target), uint256(0), abi.encodeWithSignature("revertCall()"));
62+
args[0] = abi.encode(address(target), uint256(0), abi.encodeWithSignature("revertCall()"), externalSlotsAccessed);
5663

5764
vm.expectRevert(
5865
abi.encodeWithSelector(
@@ -63,7 +70,8 @@ contract GasKillerSDKTest is Test {
6370
abi.encodeWithSignature("revertCall()")
6471
)
6572
);
66-
sdk.stateChangeHandlerExternal(abi.encode(types, args));
73+
ExternalStorageSlot[] memory expectedExternalSlots = new ExternalStorageSlot[](0);
74+
sdk.stateChangeHandlerExternal(abi.encode(types, args), expectedExternalSlots);
6775
}
6876

6977
function test_stateChangeHandlerExternal_Log1() public {
@@ -76,7 +84,8 @@ contract GasKillerSDKTest is Test {
7684

7785
vm.recordLogs();
7886

79-
sdk.stateChangeHandlerExternal(abi.encode(types, args));
87+
ExternalStorageSlot[] memory expectedExternalSlots = new ExternalStorageSlot[](0);
88+
sdk.stateChangeHandlerExternal(abi.encode(types, args), expectedExternalSlots);
8089

8190
Vm.Log[] memory logs = vm.getRecordedLogs();
8291
assertEq(logs.length, 1);
@@ -89,8 +98,9 @@ contract GasKillerSDKTest is Test {
8998
StateUpdateType[] memory types = new StateUpdateType[](2);
9099
bytes[] memory args = new bytes[](1);
91100

101+
ExternalStorageSlot[] memory expectedExternalSlots = new ExternalStorageSlot[](0);
92102
vm.expectRevert(StateChangeHandlerLib.InvalidArguments.selector);
93-
sdk.stateChangeHandlerExternal(abi.encode(types, args));
103+
sdk.stateChangeHandlerExternal(abi.encode(types, args), expectedExternalSlots);
94104
}
95105

96106
function test_ERC165_supportsInterface() public {

test/exposed/GasKillerSDKExposed.sol

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
pragma solidity ^0.8.0;
33

44
import {GasKillerSDK} from "../../src/GasKillerSDK.sol";
5+
import {ExternalStorageSlot} from "../../src/interface/IGasKillerSDK.sol";
56

67
contract GasKillerSDKExposed is GasKillerSDK {
78
constructor(address _avsAddress, address _blsSignatureChecker) {
89
_setAvsAddress(_avsAddress);
910
_setBlsSignatureChecker(_blsSignatureChecker);
1011
}
1112

12-
function stateChangeHandlerExternal(bytes calldata storageUpdates) external {
13-
super._stateChangeHandler(storageUpdates);
13+
function stateChangeHandlerExternal(
14+
bytes calldata storageUpdates,
15+
ExternalStorageSlot[] calldata expectedExternalSlots
16+
) external {
17+
super._stateChangeHandler(storageUpdates, expectedExternalSlots);
1418
}
1519
}

0 commit comments

Comments
 (0)