How a Single Math.min() Broke Cross-Chain Security
Dissecting a validator signature verification bug in Hyperlane's WeightedMultisigIsm — independently discovered and confirmed with Foundry PoC
The Short Version
While auditing Hyperlane's Interchain Security Module (ISM) implementations, I found that AbstractStaticWeightedMultisigIsm.verify() restricts its validator search space using Math.min(validators.length, signatureCount). This means validators at higher indices in the sorted array can never have their signatures verified — even when they carry sufficient weight to meet the threshold.
The bug was independently reported by another researcher in February 2026 (Immunefi report #66748) and fixed in PR #8483. This writeup covers the technical root cause, why it matters for cross-chain security, and what protocol teams should check in their own ISM implementations.
Background: How Hyperlane Verifies Cross-Chain Messages
Hyperlane is a modular interoperability protocol that allows applications to send messages between blockchains. At its core, every inbound message must pass through an Interchain Security Module (ISM) — a contract that decides whether a cross-chain message is authentic.
The most common ISM type is the MultisigIsm, which requires m-of-n validator signatures on a checkpoint. Validators sign Merkle roots of the message tree, and the ISM verifies enough valid signatures exist before allowing the message to be processed.
Source Chain → Mailbox.dispatch() → MerkleTreeHook → Validators sign checkpoint
↓
Destination Chain ← Mailbox.process() ← ISM.verify(metadata, message) ← Relayer
The Standard MultisigIsm: Correct Implementation
The standard AbstractMultisigIsm.verify() uses a two-pointer technique to match signatures against a sorted validator array:
// AbstractMultisigIsm.sol — CORRECT
function verify(bytes calldata _metadata, bytes calldata _message) public view returns (bool) {
bytes32 _digest = digest(_metadata, _message);
(address[] memory _validators, uint8 _threshold) = validatorsAndThreshold(_message);
uint256 _validatorCount = _validators.length; // ← searches ALL validators
uint256 _validatorIndex = 0;
for (uint256 i = 0; i < _threshold; ++i) {
address _signer = ECDSA.recover(_digest, signatureAt(_metadata, i));
while (_validatorIndex < _validatorCount && _signer != _validators[_validatorIndex]) {
++_validatorIndex;
}
require(_validatorIndex < _validatorCount, "!threshold");
++_validatorIndex;
}
return true;
}
The key insight: _validatorCount = _validators.length means the inner while loop can scan through all validators to find a match. This is correct — any validator at any position can have their signature verified.
The Weighted Variant: Where It Breaks
Hyperlane later introduced AbstractStaticWeightedMultisigIsm for scenarios where validators have different trust weights. Instead of requiring a fixed count of signatures, it requires signatures whose combined weight meets a threshold.
Here's the problematic code:
// AbstractWeightedMultisigIsm.sol — BUGGY
function verify(bytes calldata _metadata, bytes calldata _message) public view virtual returns (bool) {
bytes32 _digest = digest(_metadata, _message);
(ValidatorInfo[] memory _validators, uint96 _thresholdWeight) = validatorsAndThresholdWeight(_message);
// BUG: Math.min limits the search space
uint256 _validatorCount = Math.min(_validators.length, signatureCount(_metadata));
uint256 _validatorIndex = 0;
uint96 _totalWeight = 0;
for (uint256 signatureIndex = 0;
_totalWeight < _thresholdWeight && signatureIndex < _validatorCount;
++signatureIndex)
{
address _signer = ECDSA.recover(_digest, signatureAt(_metadata, signatureIndex));
while (_validatorIndex < _validatorCount && _signer != _validators[_validatorIndex].signingAddress) {
++_validatorIndex;
}
require(_validatorIndex < _validatorCount, "Invalid signer");
_totalWeight += _validators[_validatorIndex].weight;
++_validatorIndex;
}
require(_totalWeight >= _thresholdWeight, "Insufficient validator weight");
return true;
}
The bug is in the _validatorCount assignment: Math.min(_validators.length, signatureCount(_metadata))
Both ISM variants use a two-pointer technique that requires signatures to be submitted in ascending validator-address order. The inner while loop advances through the validator array to find each signer. The standard ISM searches the full array. The weighted ISM artificially caps the search.
With 5 validators and 2 signatures, _validatorCount = min(5, 2) = 2. The inner while loop can only check indices 0 and 1. Validators at indices 2, 3, and 4 are completely invisible — their signatures always trigger require(_validatorIndex < _validatorCount) failure, regardless of their weight.
Concrete Example
Consider this validator set:
| Index | Address | Weight | |-------|---------|--------| | 0 | 0x0645... | 3e9 (30%) | | 1 | 0x4bcD... | 3e9 (30%) | | 2 | 0x87b6... | 3e9 (30%) | | 3 | 0xB5E7... | 3e9 (30%) | | 4 | 0xb1ba... | 3e9 (30%) |
Threshold weight: 6e9 (60%) — any 2 validators should suffice.
Validators 0+1 sign → PASS (searched indices 0-1, both found) Validators 3+4 sign → REVERT (searched indices 0-1 only, neither 3 nor 4 found)
The first two validators in the sorted array always work. Everyone else is structurally excluded.
Impact: Permanent Fund Freezing
In Hyperlane's architecture, warp routes use ISMs to verify cross-chain token transfers. When a user bridges tokens via HypERC20Collateral, the tokens are locked on the source chain and minted on the destination chain after ISM verification.
If a warp route uses WeightedMultisigIsm and the signing validators happen to be at higher sorted indices, the valid message cannot be delivered. The tokens remain locked on the source chain with no mechanism to recover them — permanent fund freezing.
The factories for this ISM were deployed on Ethereum mainnet:
StaticMerkleRootWeightedMultisigIsmFactory:0xA2502bF73e5313c1bf48E47C887cdcbf2640FA41StaticMessageIdWeightedMultisigIsmFactory:0x4272124Fba59CbA076D85375895f94B6a3485c3E
Both had non-zero nonces, confirming live ISM instances were created.
The Fix
The fix decouples the search bound from the signature count. The outer for-loop still iterates over signatures, but the inner while-loop must be free to scan the entire validator array:
// FIXED — search bound uses full validator array
uint256 _validatorCount = _validators.length;
The outer loop's signatureIndex < signatureCount already limits how many signatures are checked. Using _validators.length for the inner search restores the two-pointer invariant without any gas regression — the total work is still O(validators + signatures).
This was implemented in PR #8483.
Foundry PoC
I built a Foundry test that demonstrates the bug with 5 validators and a 60% threshold:
forge test --match-contract HyperlaneWeightedMultisigPoc -vvv
[PASS] test_BuggyRejectsValidSignaturesFromLaterValidators
Buggy verify REVERTS on valid sigs from validators at index 3,4
Fixed verify ACCEPTS the same valid signatures
[PASS] test_FirstValidatorsAlwaysPass
First validators always pass (structural advantage)
Suite result: ok. 2 passed; 0 failed; 0 skipped
The full PoC is available at github.com/sgInnora.
Lessons for Protocol Teams
1. Porting optimizations between variants is dangerous. The Math.min was likely added as a gas optimization — "don't search more validators than there are signatures." Reasonable logic, but it breaks the two-pointer invariant that the standard ISM relies on.
2. Weighted/threshold variants need separate test coverage. The standard MultisigIsm was thoroughly tested. The weighted variant inherited the same architecture but introduced a subtle change that invalidated a core assumption.
3. Cross-chain bugs are higher severity. Unlike DeFi lending bugs where users lose yield, bridge verification failures can permanently freeze tokens with no recovery path.
4. Check your ISM factory nonces. If your protocol uses Hyperlane's weighted ISM factories, verify that deployed instances have been upgraded or redeployed with the fix.
Timeline
- 2026-02-19: First reported via Immunefi (report #66748)
- 2026-03-xx: Fixed in PR #8483
- 2026-04-09: Independently discovered and reported by Innora (report #72802, closed as duplicate)
- 2026-04-10: This writeup published
About the Author
Feng Ning is the founder of Innora.ai, a security research firm specializing in smart contract auditing, cryptographic protocol analysis, and AI-powered vulnerability detection. His research has been published at IACR, and he has independently discovered vulnerabilities in protocols securing billions in TVL.
For smart contract audit inquiries or responsible disclosure coordination: [email protected]
Follow @Innora_sg for more Web3 security research.

Related Chronicles
ERC-4337 Paymaster Attacks: The Gas Fee Extraction Gap Nobody Is Fixing
ERC-4337 paymasters have a gas accounting gap. Here is the PoC and the fix.
CUDA BIP39 Kernel Bug: When Negative Shifts Silently Corrupt Your Entropy
A CUDA BIP39 kernel bug: missing checksum-bit guard causes wrap-around negative shifts to silently corrupt entropy. Bug, PoC, and one-line fix.
CVE-2026-37555: Pre-Auth DoS in Vanetza V2X via Uncaught ECC Exception
A pre-auth DoS in Vanetza V2X: one crafted 802.11p packet crashes the ITS-G5 stack via an uncaught off-curve ECC exception. CVSS 6.5, no fix available.
Subscribe for AI Security Insights
Join 5,000+ engineers and security researchers. Get our latest deep dives into Sovereign AI, Red Teaming, and System Architecture.
No spam. Unsubscribe at any time.
Comments are currently disabled.