@@ -2264,94 +2264,6 @@ library SafeCastUpgradeable {
22642264 }
22652265}
22662266
2267- // src/StateChangeHandlerLib.sol
2268-
2269- enum StateUpdateType {
2270- STORE,
2271- CALL,
2272- LOG0,
2273- LOG1,
2274- LOG2,
2275- LOG3,
2276- LOG4
2277- }
2278-
2279- library StateChangeHandlerLib {
2280- /// @notice Decodes and executes a series of state updates
2281- /// @dev This function processes an array of state updates, executing them in sequence. Each update can be one of:
2282- /// - STORE: Direct storage writes using assembly
2283- /// - CALL: External contract calls with value transfer
2284- /// - LOG0-LOG4: Event emission with 0-4 indexed topics
2285- /// @param types Array of StateUpdateType enums indicating the type of each state update operation
2286- /// @param args Array of ABI-encoded arguments corresponding to each operation type
2287- /// @dev types and args arrays must be equal length, with args[i] containing the encoded parameters for types[i]
2288- function _runStateUpdates (StateUpdateType[] memory types , bytes [] memory args ) internal {
2289- require (types.length == args.length , InvalidArguments ());
2290- for (uint256 i = 0 ; i < types.length ; i++ ) {
2291- StateUpdateType stateUpdateType = types[i];
2292- bytes memory arg = args[i];
2293-
2294- if (stateUpdateType == StateUpdateType.STORE) {
2295- (bytes32 slot , bytes32 value ) = abi.decode (arg, (bytes32 , bytes32 ));
2296- assembly {
2297- sstore (slot, value)
2298- }
2299- } else if (stateUpdateType == StateUpdateType.CALL) {
2300- (address target , uint256 value , bytes memory callargs ) = abi.decode (arg, (address , uint256 , bytes ));
2301- bool success;
2302- // TOOD: might need better gas handling
2303- uint256 callgas = gasleft ();
2304- assembly {
2305- success := call (callgas, target, value, add (callargs, 0x20 ), mload (callargs), 0 , 0 )
2306- }
2307- // TODO: this section needs heavy testing
2308- if (! success) {
2309- uint256 _returndatasize;
2310- assembly {
2311- _returndatasize := returndatasize ()
2312- }
2313- bytes memory revertData = new bytes (_returndatasize);
2314- assembly {
2315- returndatacopy (add (revertData, 0x20 ), 0 , _returndatasize)
2316- }
2317- revert RevertingContext (i, target, revertData, callargs);
2318- }
2319- } else if (stateUpdateType == StateUpdateType.LOG0) {
2320- // NOTE: For consistency I decode an abi encoding of bytes from bytes, but technically it's redundant
2321- (bytes memory data ) = abi.decode (arg, (bytes ));
2322- assembly {
2323- log0 (add (data, 0x20 ), mload (data))
2324- }
2325- } else if (stateUpdateType == StateUpdateType.LOG1) {
2326- (bytes memory data , bytes32 topic1 ) = abi.decode (arg, (bytes , bytes32 ));
2327- assembly {
2328- log1 (add (data, 0x20 ), mload (data), topic1)
2329- }
2330- } else if (stateUpdateType == StateUpdateType.LOG2) {
2331- (bytes memory data , bytes32 topic1 , bytes32 topic2 ) = abi.decode (arg, (bytes , bytes32 , bytes32 ));
2332- assembly {
2333- log2 (add (data, 0x20 ), mload (data), topic1, topic2)
2334- }
2335- } else if (stateUpdateType == StateUpdateType.LOG3) {
2336- (bytes memory data , bytes32 topic1 , bytes32 topic2 , bytes32 topic3 ) =
2337- abi.decode (arg, (bytes , bytes32 , bytes32 , bytes32 ));
2338- assembly {
2339- log3 (add (data, 0x20 ), mload (data), topic1, topic2, topic3)
2340- }
2341- } else if (stateUpdateType == StateUpdateType.LOG4) {
2342- (bytes memory data , bytes32 topic1 , bytes32 topic2 , bytes32 topic3 , bytes32 topic4 ) =
2343- abi.decode (arg, (bytes , bytes32 , bytes32 , bytes32 , bytes32 ));
2344- assembly {
2345- log4 (add (data, 0x20 ), mload (data), topic1, topic2, topic3, topic4)
2346- }
2347- }
2348- }
2349- }
2350-
2351- error InvalidArguments ();
2352- error RevertingContext (uint256 index , address target , bytes revertData , bytes callargs );
2353- }
2354-
23552267// src/StateTracker.sol
23562268
23572269/**
@@ -5447,6 +5359,12 @@ interface IBLSSignatureChecker is IBLSSignatureCheckerErrors, IBLSSignatureCheck
54475359
54485360// src/interface/IGasKillerSDK.sol
54495361
5362+ /// @notice Represents an external storage slot access (address + slot)
5363+ struct ExternalStorageSlot {
5364+ address contractAddress;
5365+ bytes32 slot;
5366+ }
5367+
54505368/**
54515369 * @title IGasKillerSDK
54525370 * @notice Interface for GasKillerSDK contracts
@@ -5461,13 +5379,15 @@ interface IGasKillerSDK is IERC165 {
54615379 error InsufficientQuorumThreshold ();
54625380 error StaleBlockNumber ();
54635381 error FutureBlockNumber ();
5382+ error ExternalStorageSlotMismatch (address contractAddress , bytes32 slot );
54645383
54655384 /**
54665385 * @notice Function to verify if a signature is valid and contains correct storage updates
54675386 * @param msgHash The hash of the message to verify
54685387 * @param quorumNumbers The quorum numbers to check signatures for
54695388 * @param referenceBlockNumber The block number to use as reference for operator set
54705389 * @param storageUpdates The storage updates to verify
5390+ * @param expectedExternalSlots Array of external storage slots that were read during execution (as proven in ZK proof)
54715391 * @param transitionIndex The transition index
54725392 * @param anchorHash The block hash anchoring the execution to a specific Ethereum state
54735393 * @param callerAddress The address that initiated the original call (msg.sender)
@@ -5479,6 +5399,7 @@ interface IGasKillerSDK is IERC165 {
54795399 bytes calldata quorumNumbers ,
54805400 uint32 referenceBlockNumber ,
54815401 bytes calldata storageUpdates ,
5402+ ExternalStorageSlot[] calldata expectedExternalSlots ,
54825403 uint256 transitionIndex ,
54835404 bytes32 anchorHash ,
54845405 address callerAddress ,
@@ -5487,6 +5408,126 @@ interface IGasKillerSDK is IERC165 {
54875408 ) external ;
54885409}
54895410
5411+ // src/StateChangeHandlerLib.sol
5412+
5413+ enum StateUpdateType {
5414+ STORE,
5415+ CALL,
5416+ LOG0,
5417+ LOG1,
5418+ LOG2,
5419+ LOG3,
5420+ LOG4
5421+ }
5422+
5423+ library StateChangeHandlerLib {
5424+ /// @notice Decodes and executes a series of state updates with external storage slot verification
5425+ /// @dev This function processes an array of state updates, executing them in sequence. Each update can be one of:
5426+ /// - STORE: Direct storage writes using assembly
5427+ /// - CALL: External contract calls with value transfer (verified against expectedExternalSlots)
5428+ /// - LOG0-LOG4: Event emission with 0-4 indexed topics
5429+ /// @param types Array of StateUpdateType enums indicating the type of each state update operation
5430+ /// @param args Array of ABI-encoded arguments corresponding to each operation type
5431+ /// @param expectedExternalSlots Array of external storage slots that were read during execution (as proven in ZK proof)
5432+ /// @dev types and args arrays must be equal length, with args[i] containing the encoded parameters for types[i]
5433+ function _runStateUpdates (
5434+ StateUpdateType[] memory types ,
5435+ bytes [] memory args ,
5436+ ExternalStorageSlot[] calldata expectedExternalSlots
5437+ ) internal {
5438+ require (types.length == args.length , InvalidArguments ());
5439+
5440+ // Track which external slots have been verified
5441+ uint256 externalSlotIndex = 0 ;
5442+
5443+ for (uint256 i = 0 ; i < types.length ; i++ ) {
5444+ StateUpdateType stateUpdateType = types[i];
5445+ bytes memory arg = args[i];
5446+
5447+ if (stateUpdateType == StateUpdateType.STORE) {
5448+ (bytes32 slot , bytes32 value ) = abi.decode (arg, (bytes32 , bytes32 ));
5449+ assembly {
5450+ sstore (slot, value)
5451+ }
5452+ } else if (stateUpdateType == StateUpdateType.CALL) {
5453+ (
5454+ address target ,
5455+ uint256 value ,
5456+ bytes memory callargs ,
5457+ bytes32 [] memory externalSlotsAccessed
5458+ ) = abi.decode (arg, (address , uint256 , bytes , bytes32 []));
5459+
5460+ // Verify each external storage slot accessed matches the expected list
5461+ for (uint256 j = 0 ; j < externalSlotsAccessed.length ; j++ ) {
5462+ require (
5463+ externalSlotIndex < expectedExternalSlots.length ,
5464+ ExternalStorageSlotMismatch (target, externalSlotsAccessed[j])
5465+ );
5466+
5467+ ExternalStorageSlot calldata expectedSlot = expectedExternalSlots[externalSlotIndex];
5468+ require (
5469+ expectedSlot.contractAddress == target && expectedSlot.slot == externalSlotsAccessed[j],
5470+ ExternalStorageSlotMismatch (target, externalSlotsAccessed[j])
5471+ );
5472+
5473+ externalSlotIndex++ ;
5474+ }
5475+
5476+ bool success;
5477+ // TOOD: might need better gas handling
5478+ uint256 callgas = gasleft ();
5479+ assembly {
5480+ success := call (callgas, target, value, add (callargs, 0x20 ), mload (callargs), 0 , 0 )
5481+ }
5482+ // TODO: this section needs heavy testing
5483+ if (! success) {
5484+ uint256 _returndatasize;
5485+ assembly {
5486+ _returndatasize := returndatasize ()
5487+ }
5488+ bytes memory revertData = new bytes (_returndatasize);
5489+ assembly {
5490+ returndatacopy (add (revertData, 0x20 ), 0 , _returndatasize)
5491+ }
5492+ revert RevertingContext (i, target, revertData, callargs);
5493+ }
5494+ } else if (stateUpdateType == StateUpdateType.LOG0) {
5495+ // NOTE: For consistency I decode an abi encoding of bytes from bytes, but technically it's redundant
5496+ (bytes memory data ) = abi.decode (arg, (bytes ));
5497+ assembly {
5498+ log0 (add (data, 0x20 ), mload (data))
5499+ }
5500+ } else if (stateUpdateType == StateUpdateType.LOG1) {
5501+ (bytes memory data , bytes32 topic1 ) = abi.decode (arg, (bytes , bytes32 ));
5502+ assembly {
5503+ log1 (add (data, 0x20 ), mload (data), topic1)
5504+ }
5505+ } else if (stateUpdateType == StateUpdateType.LOG2) {
5506+ (bytes memory data , bytes32 topic1 , bytes32 topic2 ) = abi.decode (arg, (bytes , bytes32 , bytes32 ));
5507+ assembly {
5508+ log2 (add (data, 0x20 ), mload (data), topic1, topic2)
5509+ }
5510+ } else if (stateUpdateType == StateUpdateType.LOG3) {
5511+ (bytes memory data , bytes32 topic1 , bytes32 topic2 , bytes32 topic3 ) =
5512+ abi.decode (arg, (bytes , bytes32 , bytes32 , bytes32 ));
5513+ assembly {
5514+ log3 (add (data, 0x20 ), mload (data), topic1, topic2, topic3)
5515+ }
5516+ } else if (stateUpdateType == StateUpdateType.LOG4) {
5517+ (bytes memory data , bytes32 topic1 , bytes32 topic2 , bytes32 topic3 , bytes32 topic4 ) =
5518+ abi.decode (arg, (bytes , bytes32 , bytes32 , bytes32 , bytes32 ));
5519+ assembly {
5520+ log4 (add (data, 0x20 ), mload (data), topic1, topic2, topic3, topic4)
5521+ }
5522+ }
5523+ }
5524+ }
5525+
5526+ error InvalidArguments ();
5527+ error RevertingContext (uint256 index , address target , bytes revertData , bytes callargs );
5528+ error ExternalStorageSlotMismatch (address contractAddress , bytes32 slot );
5529+ }
5530+
54905531// src/GasKillerSDK.sol
54915532
54925533/**
@@ -5529,12 +5570,13 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
55295570 /**
55305571 * @notice Function to verify if a signature is valid and contains correct storage updates
55315572 * @dev The message hash must be computed as:
5532- * sha256(abi.encode(transitionIndex, address(this), anchorHash, callerAddress, contractCalldata, storageUpdates))
5573+ * sha256(abi.encode(transitionIndex, address(this), anchorHash, callerAddress, contractCalldata, storageUpdates, expectedExternalSlots ))
55335574 * This format enables slashing by including all inputs needed to reproduce execution.
55345575 * @param msgHash The hash of the message to verify
55355576 * @param quorumNumbers The quorum numbers to check signatures for
55365577 * @param referenceBlockNumber The block number to use as reference for operator set
55375578 * @param storageUpdates The storage updates to verify
5579+ * @param expectedExternalSlots Array of external storage slots that were read during execution (as proven in ZK proof)
55385580 * @param transitionIndex The transition index
55395581 * @param anchorHash The block hash anchoring the execution to a specific Ethereum state
55405582 * @param callerAddress The address that initiated the original call (msg.sender)
@@ -5546,6 +5588,7 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
55465588 bytes calldata quorumNumbers ,
55475589 uint32 referenceBlockNumber ,
55485590 bytes calldata storageUpdates ,
5591+ ExternalStorageSlot[] calldata expectedExternalSlots ,
55495592 uint256 transitionIndex ,
55505593 bytes32 anchorHash ,
55515594 address callerAddress ,
@@ -5560,21 +5603,23 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
55605603
55615604 // Verify transition index and message hash
55625605 require (transitionIndex + 1 == stateTransitionCount (), InvalidTransitionIndex ());
5563-
5606+
55645607 // Compute expected hash with all slashing-required fields:
55655608 // - transitionIndex: replay protection
55665609 // - address(this): target contract
55675610 // - anchorHash: block hash for state anchoring (enables slashing verification)
55685611 // - callerAddress: msg.sender (affects execution via access control, balances)
55695612 // - contractCalldata: full calldata with arguments (enables execution reproduction)
55705613 // - storageUpdates: the claimed storage changes
5614+ // - expectedExternalSlots: external storage slots that were read during execution
55715615 bytes32 expectedHash = sha256 (abi.encode (
55725616 transitionIndex,
55735617 address (this ),
55745618 anchorHash,
55755619 callerAddress,
55765620 contractCalldata,
5577- storageUpdates
5621+ storageUpdates,
5622+ expectedExternalSlots
55785623 ));
55795624 require (expectedHash == msgHash, InvalidSignature ());
55805625
@@ -5591,8 +5636,8 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
55915636 );
55925637 }
55935638
5594- // Apply the state changes
5595- _stateChangeHandler (storageUpdates);
5639+ // Apply the state changes, verifying external storage accesses match expected
5640+ _stateChangeHandler (storageUpdates, expectedExternalSlots );
55965641 }
55975642
55985643 /**
@@ -5613,22 +5658,25 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
56135658 * @param callerAddress The caller address (msg.sender)
56145659 * @param contractCalldata The full contract calldata
56155660 * @param storageUpdates The storage updates
5661+ * @param expectedExternalSlots The expected external storage slots that were read
56165662 * @return bytes32 The expected message hash
56175663 */
56185664 function getMessageHash (
56195665 uint256 transitionIndex ,
56205666 bytes32 anchorHash ,
56215667 address callerAddress ,
56225668 bytes calldata contractCalldata ,
5623- bytes calldata storageUpdates
5669+ bytes calldata storageUpdates ,
5670+ ExternalStorageSlot[] calldata expectedExternalSlots
56245671 ) external view returns (bytes32 ) {
56255672 return sha256 (abi.encode (
56265673 transitionIndex,
56275674 address (this ),
56285675 anchorHash,
56295676 callerAddress,
56305677 contractCalldata,
5631- storageUpdates
5678+ storageUpdates,
5679+ expectedExternalSlots
56325680 ));
56335681 }
56345682
@@ -5657,12 +5705,13 @@ abstract contract GasKillerSDK is StateTracker, IGasKillerSDK {
56575705 }
56585706
56595707 /**
5660- * @notice Function to apply storage updates
5708+ * @notice Function to apply storage updates with external slot verification
56615709 * @param storageUpdates The storage updates to apply
5710+ * @param expectedExternalSlots The expected external storage slots that were read during execution
56625711 */
5663- function _stateChangeHandler (bytes calldata storageUpdates ) internal {
5712+ function _stateChangeHandler (bytes calldata storageUpdates , ExternalStorageSlot[] calldata expectedExternalSlots ) internal {
56645713 (StateUpdateType[] memory types , bytes [] memory args ) = abi.decode (storageUpdates, (StateUpdateType[], bytes []));
5665- StateChangeHandlerLib._runStateUpdates (types, args);
5714+ StateChangeHandlerLib._runStateUpdates (types, args, expectedExternalSlots );
56665715 }
56675716
56685717 /**
0 commit comments