Skip to main content

Understanding Hook Architecture

The trampoline contract provides protection through:
  • Isolated execution context — hooks run from the trampoline’s address, not the settlement contract
  • Gas limit enforcement — maximum gas per hook is specified
  • Revert tolerance — individual hook failures don’t cascade

Hook Structure

struct Hook {
    address target;      // Contract address to call
    bytes callData;      // Encoded function call data
    uint256 gasLimit;    // Maximum gas allowed for this hook
}

Creating a Hook

Basic Hook

import {HooksTrampoline} from "./HooksTrampoline.sol";

HooksTrampoline.Hook[] memory hooks = new HooksTrampoline.Hook[](1);
hooks[0] = HooksTrampoline.Hook({
    target: address(myContract),
    callData: abi.encodeCall(MyContract.myFunction, (arg1, arg2)),
    gasLimit: 50000
});

Multiple Hooks

HooksTrampoline.Hook[] memory hooks = new HooksTrampoline.Hook[](3);

hooks[0] = HooksTrampoline.Hook({
    target: address(counter),
    callData: abi.encodeCall(Counter.increment, ()),
    gasLimit: 50000
});

hooks[1] = HooksTrampoline.Hook({
    target: address(approver),
    callData: abi.encodeCall(Approver.approveToken, (tokenAddress, spender)),
    gasLimit: 75000
});

hooks[2] = HooksTrampoline.Hook({
    target: address(notifier),
    callData: abi.encodeCall(Notifier.notify, ("Trade completed")),
    gasLimit: 30000
});

Executing Hooks

Execute Function Signature

function execute(Hook[] calldata hooks) external onlySettlement

Settlement-Only Modifier

modifier onlySettlement() {
    if (msg.sender != settlement) {
        revert NotASettlement();
    }
    _;
}
Direct calls to execute() from any address other than the settlement contract will revert with NotASettlement() error.

Settlement Integration Example

contract MySettlement {
    HooksTrampoline public immutable trampoline;

    constructor(address trampolineAddress) {
        trampoline = HooksTrampoline(trampolineAddress);
    }

    function settle(
        /* settlement parameters */,
        HooksTrampoline.Hook[] calldata preHooks,
        HooksTrampoline.Hook[] calldata postHooks
    ) external {
        if (preHooks.length > 0) {
            trampoline.execute(preHooks);
        }

        performSwap();

        if (postHooks.length > 0) {
            trampoline.execute(postHooks);
        }
    }
}

Hook Implementation

contract MyHook {
    address public immutable HOOKS_TRAMPOLINE;

    constructor(address trampoline) {
        HOOKS_TRAMPOLINE = trampoline;
    }

    function executeAction() external {
        require(
            msg.sender == HOOKS_TRAMPOLINE,
            "not a settlement"
        );

        // Hook logic here
    }
}
This pattern enables semi-permissioned hook implementations, ensuring execution only within legitimate settlements.

Gas Limit Best Practices

How Gas Limits Work

(bool success,) = hook.target.call{gas: hook.gasLimit}(hook.callData);

Gas Forwarding Mechanics

uint256 forwardedGas = gasleft() * 63 / 64;
if (forwardedGas < hook.gasLimit) {
    revertByWastingGas();
}
If there’s insufficient gas to forward the requested gasLimit, the transaction reverts by consuming all remaining gas. This prevents partial execution issues.

Setting Gas Limits

1

Measure function gas usage

function test_MeasureGas() public {
    uint256 gasBefore = gasleft();
    myHook.executeAction();
    uint256 gasUsed = gasBefore - gasleft();

    console.log("Gas used:", gasUsed);
}
2

Add overhead buffer

uint256 measuredGas = 45000;
uint256 buffer = 5000;
uint256 gasLimit = measuredGas + buffer; // 50000
Overhead considerations:
  • EVM call costs (~700 gas for warm, ~2600 for cold)
  • Storage access costs
  • Solidity runtime setup
3

Consider worst-case scenarios

Account for variable costs:
  • Cold vs. warm storage access
  • Varying array lengths
  • Conditional logic branches

Gas Limit Examples from Tests

Simple Counter (50K gas):
HooksTrampoline.Hook({
    target: address(counter),
    callData: abi.encodeCall(Counter.increment, ()),
    gasLimit: 50000
})
Gas Recording (133K gas):
HooksTrampoline.Hook({
    target: address(gasRecorder),
    callData: abi.encodeCall(GasRecorder.record, ()),
    gasLimit: 133700
})
Ordered Operations (25K gas):
HooksTrampoline.Hook({
    target: address(order),
    callData: abi.encodeCall(CallInOrder.called, (i)),
    gasLimit: 25000
})

Handling Reverts

Revert Behavior

  1. Revert caught by trampoline
  2. Trampoline continues with remaining hooks
  3. Settlement proceeds normally

Code Implementation

(bool success,) = hook.target.call{gas: hook.gasLimit}(hook.callData);

// In order to prevent custom hooks from DoS-ing settlements, we
// explicitly allow them to revert.
success;

Partial Hook Failure Example

Counter counter = new Counter();
Reverter reverter = new Reverter();

HooksTrampoline.Hook[] memory hooks = new HooksTrampoline.Hook[](3);

hooks[0] = HooksTrampoline.Hook({
    target: address(counter),
    callData: abi.encodeCall(Counter.increment, ()),
    gasLimit: 50000
});

hooks[1] = HooksTrampoline.Hook({
    target: address(reverter),
    callData: abi.encodeCall(Reverter.doRevert, ("boom")),
    gasLimit: 50000
});

hooks[2] = HooksTrampoline.Hook({
    target: address(counter),
    callData: abi.encodeCall(Counter.increment, ()),
    gasLimit: 50000
});

vm.prank(settlement);
trampoline.execute(hooks);

assert(counter.value() == 2);
Individual hook failures don’t compromise other traders’ orders by preventing entire settlement completion.

Execution Order

Sequential Processing

Hook calldata hook;
for (uint256 i; i < hooks.length; ++i) {
    hook = hooks[i];
    (bool success,) = hook.target.call{gas: hook.gasLimit}(hook.callData);
    success;
}
If hooks depend on each other’s state changes, ensure they’re ordered correctly. A reverting hook will not execute its state changes, potentially affecting subsequent hooks.

Security Considerations

Protected Settlement Context

  • Hooks execute from trampoline’s address
  • Cannot access settlement contract’s token balances
  • Cannot call privileged settlement functions
  • Cannot interfere with other orders

Gas Consumption Protection Example

HooksTrampoline.Hook memory hook = HooksTrampoline.Hook({
    target: address(gasGuzzler),
    callData: abi.encodeCall(GasGuzzler.consumeAll, ()),
    gasLimit: 133700
});
Without gas limits, an INVALID opcode would consume 63/64ths of all transaction gas.

Best Practices

  • Measure actual gas usage in tests
  • Add 10-20% buffer for variations
  • Never set arbitrarily high limits
  • Don’t assume all hooks will succeed
  • Design hooks to be idempotent when possible
  • Consider using post-hooks for critical operations
  • Ensure target contracts are trusted
  • Verify hook implementations before deployment
  • Consider using allowlists for critical operations
  • Test hooks with exact gas limits
  • Test with insufficient available gas
  • Verify settlement continues after hook failures

Complete Example

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

import {HooksTrampoline} from "./HooksTrampoline.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract TokenApprovalHook {
    address public immutable HOOKS_TRAMPOLINE;

    constructor(address trampoline) {
        HOOKS_TRAMPOLINE = trampoline;
    }

    /// @notice Approve a token for a spender
    /// @dev Only callable through HooksTrampoline during a settlement
    function approveToken(
        address token,
        address spender,
        uint256 amount
    ) external {
        require(
            msg.sender == HOOKS_TRAMPOLINE,
            "TokenApprovalHook: not a settlement"
        );

        IERC20(token).approve(spender, amount);
    }
}

contract Settlement {
    HooksTrampoline public immutable trampoline;
    TokenApprovalHook public immutable approvalHook;

    constructor(address trampolineAddress, address approvalHookAddress) {
        trampoline = HooksTrampoline(trampolineAddress);
        approvalHook = TokenApprovalHook(approvalHookAddress);
    }

    function executeTradeWithApproval(
        address token,
        address spender,
        uint256 amount
    ) external {
        HooksTrampoline.Hook[] memory preHooks = new HooksTrampoline.Hook[](1);
        preHooks[0] = HooksTrampoline.Hook({
            target: address(approvalHook),
            callData: abi.encodeCall(
                TokenApprovalHook.approveToken,
                (token, spender, amount)
            ),
            gasLimit: 75000
        });

        trampoline.execute(preHooks);

        // Perform trade
    }
}

Next Steps

Last modified on March 4, 2026