Describe the bug
TempoMethod._encode_transfer_with_memo validates the memo's length but not that it is hexadecimal, then concatenates it into the calldata:
memo_clean = memo[2:] if memo.startswith("0x") else memo
if len(memo_clean) != 64:
raise ValueError(f"memo must be exactly 32 bytes (64 hex chars), got {len(memo_clean)}")
return f"0x{selector}{to_padded}{amount_padded}{memo_clean.lower()}"
A 64-character non-hex memo passes that check and produces a transferWithMemo call no node can decode.
The two memo paths in this SDK disagree on the same input. The splits path goes through get_transfers and _parse_memo_bytes, which decodes the hex and raises VerificationError; the single-transfer path in TempoMethod.charge passes the raw string to the encoder. The memo arrives from the challenge's methodDetails, so it is server-supplied rather than caller-controlled.
This is not a security issue as far as I can tell — the calldata is malformed rather than misdirected, so the transaction is rejected rather than sent somewhere unintended. Filing it publicly on that basis.
Steps to reproduce
from mpp.methods.tempo.client import TempoMethod
encode = TempoMethod._encode_transfer_with_memo
address = "0x70997970c51812dc3a010c7d01b50e0d17dc79c8"
print(encode(None, address, 500000, "0x" + "zz" * 32))
Observed:
0x95777d59...07a120zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
Whitespace ("0x" + " a" * 32) and non-ASCII ("0x" + "é" * 64) are accepted the same way. The sibling path rejects the first of those:
>>> from mpp.methods.tempo.intents import _parse_memo_bytes
>>> _parse_memo_bytes("0x" + "zz" * 32)
VerificationError: Invalid memo hex: 0xzzzz...
Logs
No response
Platform(s)
Mac (Apple Silicon), Linux (x86)
Container Type
Not running in a container
What version/commit are you on?
main — 9793ef5 (also present in 0.10.0)
If you've built from source, provide the full command you used
No response
Suggested fix
Decode the memo and emit memo_bytes.hex(), which also normalizes the output to canonical lowercase. Keeping the length check first preserves its message and position, so existing callers see no change.
I have this ready with tests on mehmetkr-31:fix/validate-memo-hex-on-encode (commit 35a4857): non-hex, whitespace and non-ASCII memos rejected; a short memo still reports the length error; uppercase hex normalizes. 808 passed, 41 skipped; ruff clean.
I could not open it as a pull request — this repository currently limits opening pull requests to collaborators — so the branch is on my fork if it is useful. Happy to adjust the approach.
The Go SDK made the same change in tempoxyz/mpp-go#100, which rejects non-hexadecimal 32-byte memos in EncodeTransferWithMemo.
Disclosure: I used an AI assistant while investigating and preparing this; the analysis and conclusions are my own to defend.
Code of Conduct
Describe the bug
TempoMethod._encode_transfer_with_memovalidates the memo's length but not that it is hexadecimal, then concatenates it into the calldata:A 64-character non-hex memo passes that check and produces a
transferWithMemocall no node can decode.The two memo paths in this SDK disagree on the same input. The splits path goes through
get_transfersand_parse_memo_bytes, which decodes the hex and raisesVerificationError; the single-transfer path inTempoMethod.chargepasses the raw string to the encoder. The memo arrives from the challenge'smethodDetails, so it is server-supplied rather than caller-controlled.This is not a security issue as far as I can tell — the calldata is malformed rather than misdirected, so the transaction is rejected rather than sent somewhere unintended. Filing it publicly on that basis.
Steps to reproduce
Observed:
Whitespace (
"0x" + " a" * 32) and non-ASCII ("0x" + "é" * 64) are accepted the same way. The sibling path rejects the first of those:Logs
No response
Platform(s)
Mac (Apple Silicon), Linux (x86)
Container Type
Not running in a container
What version/commit are you on?
main — 9793ef5 (also present in 0.10.0)
If you've built from source, provide the full command you used
No response
Suggested fix
Decode the memo and emit
memo_bytes.hex(), which also normalizes the output to canonical lowercase. Keeping the length check first preserves its message and position, so existing callers see no change.I have this ready with tests on
mehmetkr-31:fix/validate-memo-hex-on-encode(commit35a4857): non-hex, whitespace and non-ASCII memos rejected; a short memo still reports the length error; uppercase hex normalizes. 808 passed, 41 skipped; ruff clean.I could not open it as a pull request — this repository currently limits opening pull requests to collaborators — so the branch is on my fork if it is useful. Happy to adjust the approach.
The Go SDK made the same change in tempoxyz/mpp-go#100, which rejects non-hexadecimal 32-byte memos in
EncodeTransferWithMemo.Disclosure: I used an AI assistant while investigating and preparing this; the analysis and conclusions are my own to defend.
Code of Conduct