56615 sc high inconsistent handler address representation in transceivermanager leads to permanent freezing of incoming transfers
Submitted on Oct 18th 2025 at 12:51:16 UTC by @Ambitious_DyDx for Audit Comp | Folks Finance: Wormhole NTT on Algorand
Report ID: #56615
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
Brief/Intro
The TransceiverManager contract incorrectly converts the handler_address (a 32-byte UniversalAddress) to a UInt64 app ID using BytesUtils.safe_convert_bytes32_to_uint64, assuming a padded UInt64 format, while the system consistently uses full 32-byte Algorand app addresses derived from sha512_256("appID" + itob(app_id)). This mismatch causes attestation failures during attestation_received (e.g., unsafe conversion errors or unknown handler assertions), preventing attestations from being recorded. As a result, incoming cross-chain messages never reach the required threshold for execution in MessageHandler, permanently freezing funds transferred to Algorand (tokens locked/burned on source chains but never minted/unlocked on Algorand).
Vulnerability Details
In the NTT framework, the MessageReceived.handler_address field is a UniversalAddress (Bytes32) representing the destination contract. For Algorand destinations, this should be the 32-byte app address (public key-like, derived via SHA512/256 hashing of the app ID). However, in TransceiverManager.attestation_received:
message_handler = BytesUtils.safe_convert_bytes32_to_uint64(message.handler_address.copy())This assumes handler_address is a 32-byte value with the last 8 bytes as the UInt64 app ID (padded with 24 zeros), allowing safe extraction. But the code populates handler_address with the full 32-byte hashed address:
In
NttManager._transfer:handler_address = ntt_manager_peer.peer_contract(set viaset_ntt_manager_peeras 32-byte peer address).source_address = UniversalAddress.from_bytes(Global.current_application_address.bytes)(32-byte address).
When receiving a message from a non-Algorand chain (where peers are set to 32-byte addresses), the conversion in attestation_received either fails with "Unsafe conversion of bytes32 to uint64" (if not padded) or yields an incorrect UInt64. Subsequent checks like _check_message_handler_known(message_handler) or _check_transceiver_configured(message_handler, transceiver) fail due to the invalid ID, halting attestation recording. The num_attestations box remains at 0, blocking threshold attainment.
Even if attestations were forced, MessageHandler.execute_message checks:
assert Address(message.handler_address.bytes) == Global.current_application_address, err.MESSAGE_HANDLER_ADDRESS_MISMATCHThis expects the full 32-byte address, so using padded IDs for peers would mismatch here, still preventing execution.
This breaks all incoming transfers to Algorand, as messages from other chains use 32-byte addresses.
Impact Details
Exploitation (or normal use) results in permanent freezing of funds: tokens are burned/locked on the source chain during outbound transfers, but incoming messages to Algorand cannot be attested or executed, preventing minting/unlocking on the destination. This affects all cross-chain inflows to Algorand-integrated NTT tokens, leading to total loss of transferred value (protocol insolvency for affected assets if unrecoverable). Matches the in-scope impact "Permanent freezing of funds" (Critical severity).
References
TransceiverManager.attestation_received: https://github.com/Folks-Finance/algorand-ntt-contracts/blob/main/contracts/transceiver/TransceiverManager.py (lines relevant to conversion and checks).
NttManager._transfer: https://github.com/Folks-Finance/algorand-ntt-contracts/blob/main/contracts/ntt_manager/NttManager.py (handler_address setting).
MessageHandler.execute_message: https://github.com/Folks-Finance/algorand-ntt-contracts/blob/main/contracts/transceiver/MessageHandler.py (address mismatch check).
UniversalAddress definition: https://github.com/Folks-Finance/algorand-ntt-contracts/blob/main/contracts/types.py.
Algorand app address derivation: https://developer.algorand.org/docs/get-details/dapps/smart-contracts/apps/specify/#application-address.
Proof of Concept
Proof of Concept
Add to algorand-ntt-contracts/tests/transceiver/TransceiverManager.test.ts:
test("POC: fails when handler_address is full 32-byte Algorand app address (conversion mismatch)", async () => {
// ensure there are configured transceivers
const added = await client.getHandlerTransceivers({ args: [messageHandlerAppId] });
expect(added.length).toBeGreaterThan(0);
const transceiverAppId = added[0];
// Build a messageReceived whose handlerAddress is the 32-byte Algorand app address bytes
const handlerAddress32 = getApplicationAddress(messageHandlerAppId).publicKey; // 32 bytes
const pocMessageReceived = getMessageReceived(
getRandomUInt(MAX_UINT16),
getRandomMessageToSend({ handlerAddress: handlerAddress32 }),
);
// Pre-calc digest to use for box references (optional but keeps boxes consistent with other tests)
const pocDigest = await client.calculateMessageDigest({ args: [pocMessageReceived] });
// Attempt to deliver the message from a configured transceiver.
// On the current (unpatched) implementation this fails earlier with:
// "Unsafe conversion of bytes32 to uint64"
// which is the actual runtime error observed.
await expect(
transceiverFactory.getAppClientById({ appId: transceiverAppId }).send.deliverMessage({
sender: user,
args: [pocMessageReceived],
appReferences: [appId],
boxReferences: [
// we pass the expected handler's boxes (for the correct app id) — the contract will
// still reject because it attempted an unsafe bytes32 -> uint64 conversion.
getHandlerTransceiversBoxKey(messageHandlerAppId),
getTransceiverAttestationsBoxKey(pocDigest, transceiverAppId),
getNumAttestationsBoxKey(pocDigest),
],
extraFee: (1000).microAlgos(),
}),
).rejects.toThrow("Unsafe conversion of bytes32 to uint64");
});Run:
npm test algorand-ntt-contracts/tests/transceiver/TransceiverManager.test.tsOutput:
Test Suites: 1 passed, 1 total
Tests: 51 passed, 51 total
Snapshots: 0 total
Time: 31.423 s, estimated 34 s
Ran all test suites matching /algorand-ntt-contracts\/tests\/transceiver\/TransceiverManager.test.ts/i.
@xxxxx ➜ /workspaces/codespaces-blank/algorand-ntt-contracts (main) $ The test passes by confirming the unsafe conversion error, proving attestation failure on realistic 32-byte handler addresses.
Recommendation
Refactor TransceiverManager to use a key derived from the full UniversalAddress (e.g., keccak256(handler_address)) for storage and checks, eliminating UInt64 conversion. Store configurations (e.g., transceivers, paused status) under this key. In attestation_received, compute the key from message.handler_address without conversion. Update peer setting/docs to ensure consistent 32-byte address use. If app ID is needed elsewhere, add a registry mapping addresses to IDs.
Was this helpful?