@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
218 lines • 12.2 kB
JavaScript
import { constants } from 'ethers';
import { CrossCollateralRouter__factory, Mailbox__factory, PredicateCrossCollateralRouterWrapper__factory, PredicateRouterWrapper__factory, StaticAggregationHook__factory, TokenRouter__factory, } from '@hyperlane-xyz/core';
import { rootLogger } from '@hyperlane-xyz/utils';
import { OnchainHookType } from '../hook/types.js';
import { TokenType } from '../token/config.js';
export class PredicateWrapperDeployer {
multiProvider;
staticAggregationHookFactory;
logger;
constructor(multiProvider, staticAggregationHookFactory, logger) {
this.multiProvider = multiProvider;
this.staticAggregationHookFactory = staticAggregationHookFactory;
this.logger =
logger ?? rootLogger.child({ module: 'PredicateWrapperDeployer' });
}
async deployPredicateWrapper(chain, warpRouteAddress, config, tokenType) {
const signer = this.multiProvider.getSigner(chain);
const isCrossCollateral = tokenType === TokenType.crossCollateral;
const wrapperName = isCrossCollateral
? 'PredicateCrossCollateralRouterWrapper'
: 'PredicateRouterWrapper';
this.logger.info({
chain,
warpRoute: warpRouteAddress,
registry: config.predicateRegistry,
tokenType,
wrapperType: wrapperName,
}, `Deploying ${wrapperName}`);
const overrides = this.multiProvider.getTransactionOverrides(chain);
// Deploy the appropriate wrapper based on token type
// Token address is fetched from warpRoute.token() in constructor
const wrapper = isCrossCollateral
? await new PredicateCrossCollateralRouterWrapper__factory(signer).deploy(warpRouteAddress, config.predicateRegistry, config.policyId, overrides)
: await new PredicateRouterWrapper__factory(signer).deploy(warpRouteAddress, config.predicateRegistry, config.policyId, overrides);
await wrapper.deployed();
// Transfer wrapper ownership to the warp route owner so that admin functions
// (setPolicyID, setRegistry, withdrawETH) are controlled by the same key/multisig
// that owns the warp route, not the ephemeral deployer key.
// Use the explicit owner from config rather than reading from on-chain, because
// during initial deployment the on-chain owner is still the deployer signer
// (transferOwnership hasn't run yet).
const routeOwner = config.owner;
await this.multiProvider.handleTx(chain, wrapper.transferOwnership(routeOwner, overrides));
this.logger.info({
chain,
address: wrapper.address,
owner: routeOwner,
wrapperType: wrapperName,
}, `${wrapperName} deployed and ownership transferred`);
return wrapper.address;
}
async createAggregationHook(chain, predicateWrapperAddress, existingHookAddress) {
const signer = this.multiProvider.getSigner(chain);
this.logger.info({
chain,
predicateWrapper: predicateWrapperAddress,
existingHook: existingHookAddress,
}, 'Creating aggregation hook');
const hooks = [predicateWrapperAddress, existingHookAddress];
const threshold = hooks.length;
const factory = this.staticAggregationHookFactory.connect(signer);
const existingAddress = await factory['getAddress(address[],uint8)'](hooks, threshold);
const code = await this.multiProvider
.getProvider(chain)
.getCode(existingAddress);
let aggregationHookAddress;
if (code === '0x') {
const overrides = this.multiProvider.getTransactionOverrides(chain);
const tx = await factory['deploy(address[],uint8)'](hooks, threshold, overrides);
await this.multiProvider.handleTx(chain, tx);
aggregationHookAddress = existingAddress;
}
else {
this.logger.debug({ chain, address: existingAddress }, 'Recovered existing aggregation hook');
aggregationHookAddress = existingAddress;
}
this.logger.info({ chain, address: aggregationHookAddress }, 'Aggregation hook ready');
return aggregationHookAddress;
}
/**
* Deploys the predicate wrapper and aggregation hook on-chain as a side effect, then
* returns the populated setHook transaction for the caller to include in its transaction
* array.
*
* IMPORTANT — irreversible side effects: deployPredicateWrapper submits a real on-chain
* transaction before this method returns. If the caller discards the returned setHookTx
* (dry-run, cancellation, error), the PredicateRouterWrapper is orphaned — deployed but
* unreferenced by any warp route. The aggregation hook is safe because
* StaticAggregationHookFactory uses CREATE2 (idempotent). Eliminating the wrapper orphan
* risk requires a CREATE2 factory for PredicateRouterWrapper (future contract work).
*
* This differs from EvmHookModule/EvmIsmModule: those modules own the full configuration
* lifecycle (deploy + configure in one atomic step). Here, deployment is eager but the
* final wiring (setHook) is deferred to EvmWarpModule.update(), which may choose not to
* submit it.
*
* @param existingHookOverride - When provided, skips the on-chain hook() read and uses
* this address instead. Pass the pending new hook address when a hook update is being
* applied in the same update() call to avoid wrapping a stale on-chain hook.
*/
async deployAndConfigure(chain, warpRouteAddress, config, tokenType, existingHookOverride) {
const signer = this.multiProvider.getSigner(chain);
// Connect to the appropriate router type
const isCrossCollateral = tokenType === TokenType.crossCollateral;
const warpRoute = isCrossCollateral
? CrossCollateralRouter__factory.connect(warpRouteAddress, signer)
: TokenRouter__factory.connect(warpRouteAddress, signer);
// Use the override when provided (e.g. when a hook update is pending in the same
// update() call and the on-chain value would be stale).
const rawExistingHook = existingHookOverride ?? (await warpRoute.hook());
// If the existing hook is already an aggregation containing a predicate wrapper,
// unwrap it to the base (non-predicate) hook before re-aggregating. Without this,
// updating a predicate config would stack wrappers:
// newAggregation([newWrapper, oldAggregation([oldWrapper, IGP])])
// instead of the correct:
// newAggregation([newWrapper, IGP])
const existingHook = await this.stripPredicateAndReaggregateHook(chain, rawExistingHook);
// WARNING: deployPredicateWrapper submits a real on-chain transaction here.
// If the caller discards the returned setHookTx, this wrapper will be orphaned.
this.logger.warn({ chain, warpRoute: warpRouteAddress }, 'Deploying PredicateRouterWrapper — this on-chain deployment is irreversible. ' +
'Submit the returned setHookTx to complete wiring; discarding it will leave the wrapper orphaned.');
const wrapperAddress = await this.deployPredicateWrapper(chain, warpRouteAddress, config, tokenType);
let hookToAggregateWith;
if (existingHook !== constants.AddressZero) {
hookToAggregateWith = existingHook;
}
else {
const mailboxAddress = await warpRoute.mailbox();
const mailbox = Mailbox__factory.connect(mailboxAddress, signer);
hookToAggregateWith = await mailbox.defaultHook();
this.logger.info({ chain, defaultHook: hookToAggregateWith }, 'Using mailbox default hook for aggregation (warp route had no existing hook)');
}
const aggregationHookAddress = await this.createAggregationHook(chain, wrapperAddress, hookToAggregateWith);
const setHookTx = await warpRoute.populateTransaction.setHook(aggregationHookAddress);
return {
wrapperAddress,
aggregationHookAddress,
setHookTx,
};
}
/**
* If hookAddress is a StaticAggregationHook that contains a predicate wrapper,
* strips it and (when multiple non-predicate sub-hooks remain) re-aggregates
* them via CREATE2 before returning, so the caller can safely wrap the result
* in a new aggregation without stacking wrappers.
*
* Falls back to hookAddress unchanged when:
* - The address is zero / not an aggregation hook
* - No predicate wrapper is found among sub-hooks
* - Multiple non-predicate sub-hooks remain (cannot safely re-aggregate here)
*/
async stripPredicateAndReaggregateHook(chain, hookAddress) {
if (!hookAddress || hookAddress === constants.AddressZero) {
return hookAddress;
}
const provider = this.multiProvider.getProvider(chain);
let subHooks;
try {
subHooks = await StaticAggregationHook__factory.connect(hookAddress, provider).hooks('0x');
}
catch {
// Any failure means hookAddress is not a StaticAggregationHook.
// SmartProvider wraps CALL_EXCEPTION as "Invalid response from provider" with code: undefined.
return hookAddress;
}
if (!subHooks || subHooks.length === 0)
return hookAddress;
const nonPredicateHooks = [];
for (const sub of subHooks) {
try {
const hookType = await PredicateRouterWrapper__factory.connect(sub, provider).hookType();
if (hookType === OnchainHookType.PREDICATE_ROUTER_WRAPPER) {
this.logger.debug({ chain, predicateWrapper: sub }, 'Stripping existing predicate wrapper from aggregation to avoid stacking');
}
else {
nonPredicateHooks.push(sub);
}
}
catch {
// hookType() failed — not a recognisable hook (or SmartProvider wrapped the
// revert as "Invalid response from provider" with code: undefined); keep it.
nonPredicateHooks.push(sub);
}
}
if (nonPredicateHooks.length === subHooks.length) {
// No predicate wrapper found — use hook as-is
return hookAddress;
}
if (nonPredicateHooks.length === 1) {
// Happy path: exactly one base hook remains
return nonPredicateHooks[0];
}
// Multiple non-predicate sub-hooks remain after removing the predicate wrapper.
// Re-aggregate them via CREATE2 (idempotent) so the caller produces:
// outerAgg([newWrapper, innerAgg([hookA, hookB])])
// instead of the stacking anti-pattern:
// newAgg([newWrapper, oldAgg([oldWrapper, hookA, hookB])])
this.logger.debug({ chain, nonPredicateHooks }, 'Multiple non-predicate sub-hooks found — re-aggregating without predicate wrapper');
const signer = this.multiProvider.getSigner(chain);
const overrides = this.multiProvider.getTransactionOverrides(chain);
const factory = this.staticAggregationHookFactory.connect(signer);
const threshold = nonPredicateHooks.length;
const innerAggAddress = await factory['getAddress(address[],uint8)'](nonPredicateHooks, threshold);
const code = await this.multiProvider
.getProvider(chain)
.getCode(innerAggAddress);
if (code === '0x') {
const tx = await factory['deploy(address[],uint8)'](nonPredicateHooks, threshold, overrides);
await this.multiProvider.handleTx(chain, tx);
this.logger.info({ chain, innerAgg: innerAggAddress, hooks: nonPredicateHooks }, 'Inner aggregation hook deployed for predicate-stripped sub-hooks');
}
else {
this.logger.debug({ chain, innerAgg: innerAggAddress }, 'Recovered existing inner aggregation hook');
}
return innerAggAddress;
}
}
//# sourceMappingURL=PredicateDeployer.js.map