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
Measure function gas usage
function test_MeasureGas () public {
uint256 gasBefore = gasleft ();
myHook. executeAction ();
uint256 gasUsed = gasBefore - gasleft ();
console. log ( "Gas used:" , gasUsed);
}
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
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
Revert caught by trampoline
Trampoline continues with remaining hooks
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
Always set reasonable gas limits
Measure actual gas usage in tests
Add 10-20% buffer for variations
Never set arbitrarily high limits
Handle hook failures gracefully
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 with insufficient gas scenarios
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