Hooks Trampoline enables traders to execute custom Ethereum calls atomically within CoW Protocol settlements. This guide covers creating hooks, understanding execution flow, and implementing best practices.
Understanding Hook Architecture
The trampoline contract protects the protocol by:
Isolated execution context : Hooks run from the trampoline’s address, not the privileged settlement contract
Gas limit enforcement : Each hook specifies a maximum gas limit to prevent excessive consumption
Revert tolerance : Individual hook failures don’t cause the entire settlement to fail
Hook Structure
Each hook is defined using the Hook struct:
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
Here’s how to create a hook that calls a function on a target contract:
import { HooksTrampoline } from "./HooksTrampoline.sol" ;
// Create a single hook
HooksTrampoline.Hook[] memory hooks = new HooksTrampoline.Hook[]( 1 );
hooks[ 0 ] = HooksTrampoline. Hook ({
target : address (myContract),
callData : abi . encodeCall (MyContract.myFunction, (arg1, arg2)),
gasLimit : 50000
});
Executing Hooks
Hooks are executed by calling the execute() function from the settlement contract:
function execute ( Hook [] calldata hooks ) external onlySettlement
Settlement-Only Execution
The trampoline enforces that only the settlement contract can execute hooks:
src/HooksTrampoline.sol:32-37
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.
Example: Settlement Integration
Settlement Contract Integration
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 {
// Execute pre-hooks before the swap
if (preHooks.length > 0 ) {
trampoline. execute (preHooks);
}
// Perform the swap
performSwap ();
// Execute post-hooks after the swap
if (postHooks.length > 0 ) {
trampoline. execute (postHooks);
}
}
}
Hook Implementation
When building contracts that serve as hook targets, you can verify they’re being called during a settlement:
contract MyHook {
address public immutable HOOKS_TRAMPOLINE;
constructor ( address trampoline ) {
HOOKS_TRAMPOLINE = trampoline;
}
function executeAction () external {
// Verify this is being called from a settlement
require (
msg.sender == HOOKS_TRAMPOLINE,
"not a settlement"
);
// Hook logic here
// ...
}
}
This pattern allows hook implementations to be semi-permissioned, ensuring they only execute as part of legitimate settlements.
Gas Limit Best Practices
Setting appropriate gas limits is critical for both security and efficiency.
How Gas Limits Work
The trampoline forwards gas using the EVM’s call opcode with a specified gas limit:
src/HooksTrampoline.sol:70
( bool success,) = hook.target.call{gas : hook.gasLimit}(hook.callData);
Gas Forwarding Mechanics
The call opcode forwards at most 63/64th of available gas. The trampoline checks available gas before each hook:
src/HooksTrampoline.sol:63-68
// A call forwards all but 1/64th of the available gas. The
// math is used as a heuristic to account for this.
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
Use Foundry to measure your hook’s gas consumption: function test_MeasureGas () public {
uint256 gasBefore = gasleft ();
myHook. executeAction ();
uint256 gasUsed = gasBefore - gasleft ();
console. log ( "Gas used:" , gasUsed);
}
Add overhead buffer
Add overhead for:
EVM call costs (~700 gas for warm, ~2600 for cold)
Storage access costs
Solidity runtime setup
uint256 measuredGas = 45000 ;
uint256 buffer = 5000 ;
uint256 gasLimit = measuredGas + buffer; // 50000
Consider worst-case scenarios
Account for variable gas costs:
Cold vs. warm storage access
Varying array lengths
Conditional logic branches
Gas Limit Examples from Tests
Simple Counter (50K gas)
Gas Recording (133K gas)
Ordered Operations (25K gas)
// Simple state changes typically need 50,000 gas
HooksTrampoline. Hook ({
target : address (counter),
callData : abi . encodeCall (Counter.increment, ()),
gasLimit : 50000
})
Handling Reverts
One of the key features of Hooks Trampoline is its ability to handle hook failures gracefully.
Revert Behavior
When a hook reverts:
The revert is caught by the trampoline
The trampoline continues executing remaining hooks
The settlement proceeds normally
src/HooksTrampoline.sol:70-74
( 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;
Example: Partial Hook Failure
This example shows how the trampoline handles a reverting hook:
Counter counter = new Counter ();
Reverter reverter = new Reverter ();
HooksTrampoline.Hook[] memory hooks = new HooksTrampoline.Hook[]( 3 );
// First hook succeeds
hooks[ 0 ] = HooksTrampoline. Hook ({
target : address (counter),
callData : abi . encodeCall (Counter.increment, ()),
gasLimit : 50000
});
// Second hook reverts
hooks[ 1 ] = HooksTrampoline. Hook ({
target : address (reverter),
callData : abi . encodeCall (Reverter.doRevert, ( "boom" )),
gasLimit : 50000
});
// Third hook still executes and succeeds
hooks[ 2 ] = HooksTrampoline. Hook ({
target : address (counter),
callData : abi . encodeCall (Counter.increment, ()),
gasLimit : 50000
});
vm. prank (settlement);
trampoline. execute (hooks);
// Counter was incremented twice despite middle hook failing
assert (counter. value () == 2 );
This behavior prevents individual user hooks from causing entire settlements to fail, protecting other traders’ orders.
Execution Order
Hooks are executed sequentially in the order they appear in the array:
src/HooksTrampoline.sol:60-76
Hook calldata hook;
for ( uint256 i; i < hooks.length; ++ i) {
hook = hooks[i];
// Execute hook
( 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
The trampoline prevents hooks from accessing the settlement contract’s privileged 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
The gas limit mechanism prevents malicious or buggy hooks from consuming excessive gas:
// If a hook hits an INVALID opcode, gas consumption is capped
HooksTrampoline.Hook memory hook = HooksTrampoline. Hook ({
target : address (gasGuzzler),
callData : abi . encodeCall (GasGuzzler.consumeAll, ()),
gasLimit : 133700 // Maximum gas this hook can use
});
Without gas limits, an INVALID opcode would consume 63/64ths of all transaction gas, making settlements extremely expensive.
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
Here’s a complete example showing a custom hook for token approvals:
// 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);
}
}
// Usage in settlement
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 {
// Create pre-hook for token approval
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
});
// Execute pre-hooks
trampoline. execute (preHooks);
// Perform trade
// ...
}
}
Next Steps
API Reference Explore the complete API documentation
Architecture Learn about the security model and architecture