57018 sc high handler address format mismatch causes digest divergence and unexecutable messages

Submitted on Oct 22nd 2025 at 17:19:22 UTC by @Rhaydden for Audit Comp | Folks Finance: Wormhole NTT on Algorand

  • Report ID: #57018

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/Folks-Finance/algorand-ntt-contracts/blob/main/ntt_contracts/transceiver/TransceiverManager.py

  • Impacts:

    • Permanent freezing of funds

Description

Issue description

There are two incompatible handler formats in play here.

Result: a message attested under one handler representation (padded app ID bytes) can fail execution if the handler expects an app address (32 bytes), or vice versa. Because the VAA is consumed first, the message becomes permanently stuck.

Impact

Proof of Concept

The included tests demonstrate the digest divergence and execution failure when switching the handler representation.

Test added in: tests/poc/HandlerAddressMismatch.test.ts (see links in report)

Example test excerpt:

test("calculate_message_digest differs between padded app id bytes vs app address bytes", async () => {
  const base = getRandomMessageToSend({
    destinationChainId: Number(getRandomUInt(MAX_UINT16)),
    sourceAddress: getRandomBytes(32),
    userAddress: getRandomBytes(32),
  });

  const paddedHandler = convertNumberToBytes(handlerAppId, 32);
  const appAddressBytes = getApplicationAddress(handlerAppId).publicKey;
  const sourceChainId = Number(getRandomUInt(MAX_UINT16));

  const digestPadded = await transceiverManagerClient.calculateMessageDigest({
    args: [{
      id: base.id,
      userAddress: base.sourceAddress,
      sourceChainId,
      sourceAddress: base.sourceAddress,
      handlerAddress: paddedHandler,
      payload: base.payload,
    }],
  });

  const digestAddress = await transceiverManagerClient.calculateMessageDigest({
    args: [{
      id: base.id,
      userAddress: base.sourceAddress,
      sourceChainId,
      sourceAddress: base.sourceAddress,
      handlerAddress: appAddressBytes,
      payload: base.payload,
    }],
  });

  expect(Uint8Array.from(digestPadded)).not.toEqual(Uint8Array.from(digestAddress));
});

As shown by the PoC,:

  • executeMessage fails when handlerAddress is a 32-byte padded uint64 app ID (handler expects app address bytes).

  • calculate_message_digest differs when only handlerAddress changes (padded app ID vs app address bytes).

Logs

Test output
 PASS  tests/poc/HandlerAddressMismatch.test.ts
  PoC: handler address format mismatch
    ✓ executeMessage fails when handlerAddress is 32-byte padded app id (Handler expects app address bytes) (39 ms)
    ✓ calculate_message_digest differs between padded app id bytes vs app address bytes (11 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.139 s
Ran all test suites matching /tests\/poc\/HandlerAddressMismatch.test.ts/i.

References

  • TransceiverManager.attestation_received: https://github.com/Folks-Finance/algorand-ntt-contracts//blob/99dfaec5420fa22cfcfaef756f96bfaa5b79701b/ntt_contracts/transceiver/TransceiverManager.py#L189-L218

  • BytesUtils.safe_convert_bytes32_to_uint64: https://github.com/Folks-Finance/algorand-ntt-contracts//blob/99dfaec5420fa22cfcfaef756f96bfaa5b79701b/folks_contracts/library/BytesUtils.py#L12-L16

  • MessageHandler.execute_message: https://github.com/Folks-Finance/algorand-ntt-contracts//blob/99dfaec5420fa22cfcfaef756f96bfaa5b79701b/ntt_contracts/transceiver/MessageHandler.py#L35-L65

  • TransceiverManager.calculate_message_digest: https://github.com/Folks-Finance/algorand-ntt-contracts//blob/99dfaec5420fa22cfcfaef756f96bfaa5b79701b/ntt_contracts/transceiver/TransceiverManager.py#L229-L238

  • WormholeTransceiver._receive_message: https://github.com/Folks-Finance/algorand-ntt-contracts//blob/99dfaec5420fa22cfcfaef756f96bfaa5b79701b/ntt_contracts/transceiver/WormholeTransceiver.py#L138-L184

Was this helpful?