Static Aggregation Hook Deployment¶
This document sketches how a StaticAggregationHook is composed and deployed in Hyperlane, and how it can be used as a Mailbox requiredHook so its checks cannot be bypassed by a caller-supplied custom hook.
Summary¶
A StaticAggregationHook does not store its child hooks in mutable contract storage.
Instead:
- A
StaticAggregationHookFactorydeploys aStaticAggregationHookimplementation contract. - The factory deploys a
MetaProxyinstance for a specific ordered list of hook addresses. - That ordered hook list is embedded as immutable proxy metadata.
StaticAggregationHook.hooks()reads the metadata back out and decodes it asaddress[].
This means:
- the hook composition is fixed at deployment time
- there is no
setHooks(...)function - changing the composition requires deploying a new aggregation hook address
Primary references:
- StaticAggregationHookFactory.sol
- StaticAggregationHook.sol
- StaticAddressSetFactory.sol
- MetaProxy.sol
- Mailbox.sol
How Composition Is Defined¶
The concrete composition is passed to the factory as an ordered array of hook addresses:
address aggregationHook = staticAggregationHookFactory.deploy(
new address[](3)
);
More concretely:
address[] memory hooks = new address[](3);
hooks[0] = address(merkleTreeHook);
hooks[1] = address(pausableHook);
hooks[2] = address(rateLimitedHook);
address aggregationHook = staticAggregationHookFactory.deploy(hooks);
Under the hood:
StaticAggregationHookFactoryinheritsStaticAddressSetFactoryStaticAddressSetFactory.deploy(address[] calldata _values)forwards to the threshold-based base class- the base class computes:
_metadata = abi.encode(_values, _threshold)_bytecode = MetaProxy.bytecode(implementation, _metadata)_salt = keccak256(_metadata)- the factory then deploys the metaproxy instance with
CREATE2
Relevant code:
StaticAggregationHookFactory._deployImplementation()returnsnew StaticAggregationHook()StaticAddressSetFactory.deploy(address[] calldata _values)uses_values.lengthas the thresholdStaticThresholdAddressSetFactory._saltAndBytecode(...)packs the metadata and metaproxy bytecodeMetaProxy.bytecode(...)appends the metadata to the proxy bytecode
How The Aggregation Hook Reads Its Members¶
The StaticAggregationHook contract itself has no constructor args and no setter for child hooks.
Its hooks(...) function does this:
return abi.decode(MetaProxy.metadata(), (address[]));
That is the key behavior:
- the proxy metadata is read from calldata by
MetaProxy.metadata() - the aggregation hook decodes that metadata as the ordered address array of child hooks
- the hook executes child hooks in that same order during
postDispatch
Operationally, order matters. For example:
MerkleTreeHookPausableHookRateLimitedHook
would run in that exact order.
Why Use It As requiredHook¶
The Mailbox executes hooks in two stages:
requiredHook- selected hook, which is either:
- the caller-supplied custom hook, or
- the Mailbox
defaultHookif no custom hook is supplied
The relevant behavior in Mailbox.dispatch(...) is:
requiredHook.postDispatch{value: requiredValue}(metadata, message);
hook.postDispatch{value: msg.value - requiredValue}(metadata, message);
This makes requiredHook the correct enforcement point for controls that must never be bypassed.
If a user overrides the Mailbox defaultHook, they still cannot skip the requiredHook.
Recommended Pattern For Non-Bypassable Warp Transfer Controls¶
If the goal is to make these controls mandatory for warp transfers:
- Merkle insertion
- pause switch
- transfer volume rate limit
then the intended shape is:
Mailbox.requiredHook
= StaticAggregationHook([
MerkleTreeHook,
PausableHook,
RateLimitedHook
])
With that setup:
MerkleTreeHookalways runsPausableHookalways runsRateLimitedHookalways runs- a caller may still provide a custom default hook, but cannot bypass the required path
Sketch Deployment Sequence¶
This is the high-level deployment sequence for an EVM Mailbox using a required aggregation hook.
1. Deploy or identify the Mailbox¶
You need the target Mailbox address first.
The Mailbox owner must be able to set:
requiredHook- optionally
defaultHook
2. Deploy the component hooks¶
Deploy the concrete subhooks you want the aggregation hook to call.
Example:
- Deploy
MerkleTreeHook(mailbox) - Deploy
PausableHook() - Deploy
RateLimitedHook(mailbox, maxCapacity, sender)
Notes:
RateLimitedHookis token-message specific and is intended for warp-route token messages.PausableHookisOwnable, so decide who should control pause and unpause.RateLimitedHookinherits theRateLimitedcontrol surface, so decide who should own refill-rate changes.
3. Deploy the aggregation hook factory¶
Deploy StaticAggregationHookFactory if it does not already exist in your environment.
Its constructor deploys the shared StaticAggregationHook implementation once and stores that implementation address internally.
4. Deploy the concrete aggregation hook instance¶
Call the factory with the ordered list of subhook addresses:
address[] memory hooks = new address[](3);
hooks[0] = address(merkleTreeHook);
hooks[1] = address(pausableHook);
hooks[2] = address(rateLimitedHook);
address requiredAggregationHook = staticAggregationHookFactory.deploy(hooks);
This returns the address of the concrete MetaProxy instance representing exactly that hook list.
Important properties:
- the address is deterministic for the same ordered list
- reordering the hooks gives a different deployed address
- changing one subhook address gives a different deployed address
5. Set the Mailbox requiredHook¶
As the Mailbox owner, set:
mailbox.setRequiredHook(requiredAggregationHook);
After that, every Mailbox dispatch through that Mailbox executes the aggregation hook path first.
6. Optionally set a defaultHook¶
You may still set a Mailbox defaultHook for normal dispatch fee/payment behavior.
That defaultHook remains overrideable by callers that use the custom-hook dispatch path, but the requiredHook remains mandatory.
Example Solidity Sketch¶
MerkleTreeHook merkleTreeHook = new MerkleTreeHook(address(mailbox));
PausableHook pausableHook = new PausableHook();
RateLimitedHook rateLimitedHook =
new RateLimitedHook(address(mailbox), maxCapacity, address(tokenRouter));
StaticAggregationHookFactory factory = new StaticAggregationHookFactory();
address[] memory hooks = new address[](3);
hooks[0] = address(merkleTreeHook);
hooks[1] = address(pausableHook);
hooks[2] = address(rateLimitedHook);
address requiredAggregationHook = factory.deploy(hooks);
mailbox.setRequiredHook(requiredAggregationHook);
Verification Checklist¶
After deployment, verify:
mailbox.requiredHook()returns the aggregation hook address.StaticAggregationHook(requiredAggregationHook).hooks("")returns the expected ordered list of child hooks.mailbox.quoteDispatch(...)includes the required hook path.- A paused
PausableHookcauses dispatch to revert. - A transfer above the configured rate limit causes dispatch to revert.
- A caller-supplied custom hook still cannot bypass the required aggregation hook.
Caveats¶
requiredHookapplies to every dispatch through that Mailbox path, not just one route, unless routing or app-level separation is introduced elsewhere.RateLimitedHookexpects token-message semantics and an authorized sender address. It is not a generic message limiter for arbitrary Hyperlane applications.StaticAggregationHookuses the same metadata blob for all subhooks in the aggregation path. The hooks in this document do not depend on custom metadata, but other hook combinations may.- If any subhook reverts, the whole dispatch reverts.
Practical Conclusion¶
To compose MerkleTreeHook, PausableHook, and RateLimitedHook, you do not configure the aggregation hook after deployment.
You compose it by:
- deploying the subhooks
- passing their addresses in order to
StaticAggregationHookFactory.deploy(address[]) - setting the resulting aggregation hook address as the Mailbox
requiredHook