Gas Killer Docs
Solidity Reference

Integrate the SDK

Make your contract a valid Gas Killer target.

The HTTP API returns a ready-to-sign verifyAndUpdate transaction. That call only exists if your contract inherits the Gas Killer Solidity SDK, so the contract side comes first: the router refuses to accept a task for a target that does not advertise the SDK interface.

A valid target needs three things. Miss any one and the failure shows up at a different stage:

RequirementEnforced bySymptom when missing
Inherits GasKillerSDKRouter, at submit timeTask rejected — the target does not report the interface
avsAddress and schnorrRegistry wired to the live deploymentRouter, while building the payloadTask fails — the router will not return a payload that cannot settle
Mutating functions marked trackStateNothing — it is your replay protectionSilent: stale payloads stay applicable

The router proves the settlement call before handing it back, so a mis-wired target fails as a task with the underlying error rather than as a reverted transaction you paid for. Get the addresses right up front anyway, see Configuration.

Requirements

  • Solidity ^0.8.27 and Foundry.
  • An EVM at Cancun or later. TransitionGuard uses EIP-1153 transient storage and has no pre-Cancun fallback path, so deploying to a chain without it breaks settlement itself rather than just the guard. The public testnet settles on Sepolia, which qualifies.

Install

forge install gas-killer/solidity-sdk

Add the remapping to remappings.txt (or foundry.toml):

gas-killer-sdk/=lib/solidity-sdk/src/

The SDK pins evm_version = "cancun" in its own foundry.toml. Set the same in yours.

A minimal target

Everything an integration needs, and nothing else. Counter keeps one value and one tracked function that recomputes it; in a real contract that function is whatever is too expensive to run on-chain.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {GasKillerSDK} from "gas-killer-sdk/GasKillerSDK.sol";

contract Counter is GasKillerSDK {
    /// Declared first, so `total` occupies storage slot 0 and `runs` slot 1.
    /// The signed diff writes raw slots, see Tracked functions.
    uint256 public total;
    uint256 public runs;

    event Recomputed(uint256 indexed runs, uint256 total);

    constructor(address avs, address schnorrRegistry) {
        _setAvsAddress(avs);
        _setSchnorrRegistry(schnorrRegistry);
    }

    /// The expensive path. On-chain this is the reference implementation the
    /// operators reproduce off-chain; you never have to call it on-chain.
    function recompute(uint32 rounds) external trackState {
        uint256 acc = total;
        for (uint32 i = 0; i < rounds; i++) {
            acc = uint256(keccak256(abi.encode(acc, i)));
        }
        total = acc;
        runs += 1;
        emit Recomputed(runs, total);
    }
}

That is the whole contract-side integration. verifyAndUpdate, verifyAndUpdateBatch, stateTransitionCount(), supportsInterface, and the transition guard all come from the base.

Deploy and verify

Pass the live addresses from Configuration:

forge create src/Counter.sol:Counter \
  --rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" \
  --constructor-args "$AVS_ADDRESS" "$SCHNORR_REGISTRY_ADDRESS"

Then check all four of these before submitting a task. Each maps to a failure you would otherwise debug from a revert:

The router will accept it. Must return true, or task submission is rejected:

cast call "$TARGET" "supportsInterface(bytes4)(bool)" 0x82b35a01 --rpc-url "$RPC_URL"

The wiring took. Both must equal the values you passed:

cast call "$TARGET" "avsAddress()(address)" --rpc-url "$RPC_URL"
cast call "$TARGET" "schnorrRegistry()(address)" --rpc-url "$RPC_URL"

The registry is the live operator set. This is the check that catches a stale address, see Verify your own wiring.

The counter starts at zero. Confirms StateTracker is reachable:

cast call "$TARGET" "stateTransitionCount()(uint256)" --rpc-url "$RPC_URL"

Then submit a task

With the contract deployed, the API side is unchanged from the Quickstart: POST /tasks with your target address and the ABI-encoded calldata for the tracked function, poll until ready, submit the payload.

call_data: Array.from(
  Buffer.from(
    encodeFunctionData({ abi, functionName: "recompute", args: [1000] }).slice(2),
    "hex",
  ).values(),
)

The operators execute recompute(1000) off-chain, sign the resulting diff, and the payload you get back is a verifyAndUpdate call that writes total, runs, and the Recomputed log directly, without running the loop on-chain.

Coming from the BLS SDK

Earlier SDK releases verified BLS signatures (constructor argument blsSigChecker, interface ID 0x93de4531). The operators now sign with aggregate Schnorr only, so a target built on that release no longer settles. To move one across, update the SDK, swap _setBlsSignatureChecker for _setSchnorrRegistry, and redeploy against the addresses in Configuration. The task digest, trackState and the state update encoding are unchanged, so the contract's own logic stays as it is.

Where to go next

  • Configuration — the addresses to wire, how to verify them, and how to stay reconfigurable.
  • Tracked functions — what trackState guarantees, storage layout rules, and what keeps a function extractable.
  • Reference — full API surface, state update types, and a revert-selector lookup table.

On this page