Skip to content
Quantix Security Labs

Cross-chain message replay on a second destination

Two attested transfers accepted on a chain they were never sent to. How monitoring caught the replay on Axeline Bridge, why the attestation did not bind the destination, and how typed digests fixed it.
CriticalSignature replayIncident response · 10 min read
Illustration: a message sent from a source chain to destination A, and the same message replayed onto destination B along a red path.

The case at a glance

Axeline Bridge

Optimism · InteroperabilitySecurity profileResearch case · Quantix Protocol Research

During the case
CCC64.5
Current profile
BB71.5
To containment
7 min
To verified fix
21 h 58 min
  1. 03Problem

    On 22 July 2026 the cross-chain replay rule raised a critical alert: the digest of message 48211, a 206,000 axUSD transfer from Optimism delivered on Avalanche the previous evening, had just been accepted by the Arbitrum endpoint.

    Read section
  2. 06Vulnerability

    CriticalSignature replay

    Attestations authorised a message, not a message on a particular chain at a particular contract.

    Read section
  3. 08Remediation

    1. 01Pause the executor on the affected destination
    2. 02Make verifier v2, with destination-bound EIP-712 digests, the default for every application
    3. 03Reject messages whose destination does not match the executing chain

    and 2 more steps

    Read section
  4. 09Outcome

    Two replays succeeded before containment, releasing 412,000 axUSD on Arbitrum.

    Risk Index CCC → BB, +7.0 points

    Read section

Background

Axeline Bridge delivers messages between Optimism, Avalanche and Arbitrum. Applications choose a verifier in the VerifierRegistry; most use the AttestationVerifier, where a threshold of attesters signs a digest of each message and the Executor delivers it to the receiving application on the destination chain.

The bridge’s own token application, axUSD, is deployed at the same address on every chain. That convenience is harmless as long as a message can only ever be valid on one destination.

Architecture

An Endpoint on each chain assigns nonces to outgoing messages and records delivered ones. The AttestationVerifier recovers attester signatures over the message digest and checks the threshold; the VerifierRegistry tells the endpoint which verifier an application uses; the Executor submits verified messages, subject to a per-asset outflow limiter.

The boundary that matters in this case is the digest: whatever it does not include, the attesters never approved.

Figure 1Axeline Bridge: components and paths involved in this case

Axeline Bridge: architecture relevant to this case7 elements and 6 connections. Highlighted components are directly involved in the case; dashed red arrows are attack paths, dotted brass arrows are control paths.MessageSentsigned digestreceiveMessage()verify(digest, sigs)verifierOf(app)receiveMessage() on ArbitrumEndpointsource: OptimismAttestersthreshold signaturesAttestationVerifierThreshold signatures fromattestersExecutorDelivers verified messagesand retries failuresEndpointdestinations: Avalanche,ArbitrumVerifierRegistryPer-application verifierconfigurationSender relaying its ownmessagesresubmits deliveredattestations
Protocol componentInvolved in this caseExternal partyAttack pathControl path

Elements:

  • Endpoint (protocol component): source: Optimism
  • Attesters (external party): threshold signatures
  • AttestationVerifier (protocol component, involved in this case): Threshold signatures from attesters
  • Executor (protocol component): Delivers verified messages and retries failures
  • Endpoint (protocol component, involved in this case): destinations: Avalanche, Arbitrum
  • VerifierRegistry (protocol component): Per-application verifier configuration
  • Sender relaying its own messages (adversary): resubmits delivered attestations

Connections:

  • Endpoint to Attesters: MessageSent (data or call flow)
  • Attesters to AttestationVerifier: signed digest (data or call flow)
  • Executor to Endpoint: receiveMessage() (data or call flow)
  • Endpoint to AttestationVerifier: verify(digest, sigs) (data or call flow)
  • Endpoint to VerifierRegistry: verifierOf(app) (control path)
  • Sender relaying its own messages to Endpoint: receiveMessage() on Arbitrum (attack path)

Components in the Axeline Bridge profile

  • Endpoint(involved in this case)

    Sends and receives messages with nonces

  • VerifierRegistry

    Per-application verifier configuration

  • AttestationVerifier(involved in this case)

    Threshold signatures from attesters

  • Executor

    Delivers verified messages and retries failures

Security challenge

On 22 July 2026 the cross-chain replay rule raised a critical alert: the digest of message 48211, a 206,000 axUSD transfer from Optimism delivered on Avalanche the previous evening, had just been accepted by the Arbitrum endpoint. Ninety seconds later a second Avalanche message, 48202, was replayed the same way, and axUSD outflows on Arbitrum reached 118% of the window limit.

Both messages had been sent by the same account, from Optimism to its own address on Avalanche. Relaying the attestations to Arbitrum credited it a second time.

The response had to stop further replays, establish whether attester keys were compromised or the protocol was accepting signatures it should not, and identify every message that was exposed.

Attack surface

The investigation listed every input the destination endpoint trusted when accepting a message.

Attack surface examined in Axeline Bridge
SurfaceExposureControl
Digest fieldsv1 digest omitted the destination chain ID and destination endpointv2 digest commits to source chain, destination chain, endpoint address and nonce
Delivered-message recordKept per endpoint, so another endpoint never sees itSufficient once the digest is destination-bound
Receiver addressaxUSD deployed at the same address on every chainDestination check in the application as defence in depth
Outflow limiterConfigured to alert, not blockBlocking mode on every destination
Attester keysCompromise would allow arbitrary messagesUnchanged; no compromise found

Technical investigation

The first question was whether the attesters had signed anything unusual. They had not: the signatures accepted on Arbitrum were byte-for-byte the signatures produced for the Avalanche deliveries. The sender had simply relayed its own delivered messages to another endpoint.

Rebuilding the v1 digest showed why that worked. It hashed the source chain ID, nonce, sender, receiver and payload. The destination chain was carried in the payload envelope but not in the signed digest, and each endpoint recorded only the digests it had delivered itself.

Because axUSD has the same address on Avalanche and Arbitrum, the Arbitrum endpoint resolved the same receiver, the registry returned the same verifier, and the threshold check passed. Any attested message to an application deployed on more than one destination could be replayed once on each of the others.

Operations paused the executor on Arbitrum seven minutes after the first alert. Fourteen further replay attempts against other Avalanche deliveries reverted while it was paused. A scan of earlier deliveries found no replays before 22 July.

Figure 2Replay of an attested message on a second destination

Replay of an attested message on a second destinationSender (Optimism)AttestersEndpoint (Avalanche)Endpoint (Arbitrum)Monitoring1. send 206,000 axUSD to self on Avalanche (nonce 48211)2. receiveMessage(msg, sigs)verified, credited3. receiveMessage(msg, sigs)same attestations; digest unseen on Arbitrum4. credit 206,000 axUSD again5. R-13: digest delivered on two destinations6. replay nonce 4820288 s later
Ordinary stepAttack stepDefensive step
  1. Sender (Optimism) (adversary) to Attesters: send 206,000 axUSD to self on Avalanche (nonce 48211) (normal step)
  2. Attesters (off-chain) to Endpoint (Avalanche): receiveMessage(msg, sigs). verified, credited (normal step)
  3. Sender (Optimism) (adversary) to Endpoint (Arbitrum): receiveMessage(msg, sigs). same attestations; digest unseen on Arbitrum (attack step)
  4. Endpoint (Arbitrum) (on-chain) to Sender (Optimism): credit 206,000 axUSD again (attack step)
  5. Endpoint (Arbitrum) (on-chain) to Monitoring: R-13: digest delivered on two destinations (defensive step)
  6. Sender (Optimism) (adversary) to Endpoint (Arbitrum): replay nonce 48202. 88 s later (attack step)
Messages replayed on Arbitrum before the executor was paused. Total replayed: 412,000 axUSD.
Source nonceOriginal delivery (Avalanche)Replay (Arbitrum)Amount (axUSD)
482112026-07-21 19:402026-07-22 11:02206,000
48202Earlier on 2026-07-212026-07-22 11:04206,000
Fields committed to by the attestation digest
FieldVerifier v1Verifier v2
Source chain IDCommittedCommitted
NonceCommittedCommitted
Sender and receiverCommittedCommitted
Payload hashCommittedCommitted
Destination chain IDNot committedCommitted (struct and EIP-712 domain)
Destination endpoint addressNot committedCommitted (EIP-712 verifyingContract)
Verifier versionNot committedCommitted (EIP-712 version)
Monitoring events recorded during the incident, with the rule that matched and its threshold
Time (UTC)RuleObservationMeasuredAlert
2026-07-21 19:40:33R-16Activity above baselineMessage 48211 delivered on AvalancheInformational1 (threshold 3 × 30-day median)Logged
2026-07-22 11:02:37R-13Cross-chain message replayDigest of message 48211 delivered on a second destinationCriticalCondition matchedAlert
2026-07-22 11:04:05R-13Cross-chain message replaySecond replay of message 48202 executedCriticalCondition matchedAlert
2026-07-22 11:05:50R-14Bridge outflow against rate limitaxUSD outflow at 118% of the window limitHigh118 (threshold 80 % of window limit)Alert
2026-07-22 11:09:12R-15Pause state changedExecutor paused on ArbitrumMediumCondition matchedAlert
2026-07-22 12:40:00R-13Cross-chain message replay14 further replay attempts rejectedHighCondition matchedAlert
2026-07-22 14:30:22R-09Protocol parameter changeVerifier v2 made the default for every applicationHighRecorded for the timelineLogged
2026-07-23 09:00:41R-15Pause state changedExecutor unpaused after verifier migrationMediumCondition matchedAlert

Timeline

  1. Message 48211 delivered on Avalanche

  2. Detection

    Digest of message 48211 accepted on Arbitrum; R-13 alert

  3. Exploitation

    Message 48202 replayed on Arbitrum

  4. Containment

    Executor paused on Arbitrum

  5. Fix

    Verifier v2 made the default for every application

  6. Verification

    Receivers confirmed on v2; delivery resumed

Root cause

Attestations authorised a message, not a message on a particular chain at a particular contract. Replay protection was correct within one endpoint and absent across endpoints. The same class of defect appears whenever a signed payload omits the domain it is valid in, which is why EIP-712 separates the domain from the message.

The alert-only outflow limiter turned a correctness defect into a second replay: the limiter saw the outflow exceed its window and did nothing to stop it.

Class
Signature replay
Severity
Critical
Components involved
AttestationVerifierEndpoint
AttestationVerifier v1 and Endpoint (pre-incident)solidity
function digest(Message calldata m) public pure returns (bytes32) {    // The destination chain and endpoint are not part of the signed digest.    return keccak256(abi.encode(m.srcChainId, m.nonce, m.sender, m.receiver, keccak256(m.payload)));} function receiveMessage(Message calldata m, bytes[] calldata sigs) external {    bytes32 d = verifier.digest(m);    require(!delivered[d], "delivered");          // recorded on this endpoint only    require(verifier.verify(d, sigs), "bad sigs");    delivered[d] = true;    IReceiver(m.receiver).onMessage(m.srcChainId, m.sender, m.payload);}

Risk evaluation

During the incident the replay is modelled as an open critical finding, and Operational Security is reduced for the alert-only limiter. Axeline still carries open items from its research assessment, so the after state is its current profile rather than a perfect score.

64.5CCCHigh risk
During the caseCCC64.5
71.5BBElevated risk
Current profileBB71.5
CodeSecurityOperationalSecurityGovernanceRiskEconomicSecurityInfrastructureResilienceTransparencyDuring the case, Code Security: 50During the case, Operational Security: 66During the case, Governance Risk: 70During the case, Economic Security: 76During the case, Infrastructure Resilience: 70During the case, Transparency: 74Current profile, Code Security: 68Current profile, Operational Security: 72Current profile, Governance Risk: 70Current profile, Economic Security: 76Current profile, Infrastructure Resilience: 70Current profile, Transparency: 78
During the caseCurrent profile

The score moves from 64.5 (CCC) to 71.5 (BB), a change of +7.0 points. The largest single contribution is Code Security: restoring it from 50.0 to 68.0 alone moves the score from 64.5 to 69.9.

Basis for the case state: Replay possible on every destination; outflow limiter in alert-only mode.
ComponentWeightRangeDuring caseTodayScore points
Code Security30%50.068.0+5.4
Operational Security20%66.072.0+1.2
Governance Risk15%70.070.00.0
Economic Security15%76.076.00.0
Infrastructure Resilience10%70.070.00.0
Transparency10%74.078.0+0.4
Quantix Risk Index100%64.571.5+7.0
During the caseCurrent profileBasis for the case state: Replay possible on every destination; outflow limiter in alert-only mode.

Scores use the six weighted components of the Quantix Risk Index. Open findings reduce Code Security by a fixed penalty per severity; the current profile is the protocol’s entry in Quantix Intelligence.

Mitigation strategy

Verifier v2 signs EIP-712 typed data. The domain binds the destination chain ID and the destination endpoint, the message struct repeats the destination fields, and the endpoint rejects messages not addressed to block.chainid. v2 became the default for every application on the afternoon of the incident.

Delivery resumed the next morning once every receiver was confirmed on v2. The outflow limiter now blocks rather than alerts, and axUSD added its own destination check as defence in depth.

  1. 01Pause the executor on the affected destination
  2. 02Make verifier v2, with destination-bound EIP-712 digests, the default for every application
  3. 03Reject messages whose destination does not match the executing chain
  4. 04Switch the outflow limiter from alert to block
  5. 05Confirm every receiver on v2 before resuming delivery
AttestationVerifier v2 (verified)diff
-function digest(Message calldata m) public pure returns (bytes32) {-    return keccak256(abi.encode(m.srcChainId, m.nonce, m.sender, m.receiver, keccak256(m.payload)));-}+bytes32 constant MESSAGE_TYPEHASH = keccak256(+    "Message(uint64 srcChainId,address srcEndpoint,uint64 nonce,address sender,uint64 dstChainId,address receiver,bytes32 payloadHash)"+);++// EIP-712 domain: name, version "2", chainId = this chain, verifyingContract = this endpoint+function digest(Message calldata m) public view returns (bytes32) {+    return _hashTypedDataV4(keccak256(abi.encode(+        MESSAGE_TYPEHASH, m.srcChainId, m.srcEndpoint, m.nonce, m.sender, m.dstChainId, m.receiver, keccak256(m.payload)+    )));+}  function receiveMessage(Message calldata m, bytes[] calldata sigs) external {+    require(m.dstChainId == block.chainid, "wrong destination");     bytes32 d = verifier.digest(m);

Security outcome

Two replays succeeded before containment, releasing 412,000 axUSD on Arbitrum. No further replay succeeded, no attester key was compromised, and the replayed amount was reconciled against the axUSD supply accounting on both chains.

The cross-chain replay rule and the outflow rule remain in the monitoring rule set, and the limiter now blocks at its window limit.

Detection to containment
7 min
Detection to first fix
3 h 28 min
Detection to verified fix
21 h 58 min
Risk Index change
CCC → BB (+7.0)

Lessons learned

  1. 01

    A signature must bind every dimension of the context it is valid in: chain, contract and version.

  2. 02

    Deterministic addresses across chains turn a missing domain into a replay.

  3. 03

    Rules that correlate events across chains catch failures that single-chain rules cannot see.

  4. 04

    A safety limit that only alerts is a dashboard, not a control.

Have a problem like this one?

Tell us what you are building and where it runs. We will reply with the review plan we would use for your architecture.