Skip to main content
Transaction memos attach application metadata, such as an invoice or order reference, to a contract call on Arc. The predeployed Memo contract (0x5294E9927c3306DcBaDb03fe70b92e01cCede505) wraps the call and emits the metadata as events, so wallets, exchanges, and indexers can reconcile onchain transfers against offchain records without changing the target contract. To send a memo transfer end to end, see Send USDC with a transaction memo.

How a memo call works

The Memo contract wraps a target contract call. It routes the inner call through Arc’s CallFrom precompile, a system-level contract that executes a call on behalf of the original transaction sender. The transaction extension contracts use this precompile to keep the original externally owned account (EOA) wallet as msg.sender for the target call. A USDC transfer still sees your wallet as the sender, not the Memo contract. The contract exposes a single entry point:
function memo(
    address target,
    bytes calldata data,
    bytes32 memoId,
    bytes calldata memoData
) external;
target is the contract to call, data is the calldata to forward to it, memoId is a caller-supplied identifier for the memo, and memoData is the arbitrary memo bytes attached to the subcall. On success, Memo emits an ordered audit trail:
  1. BeforeMemo(memoIndex) before the inner call.
  2. The target contract events, such as a USDC Transfer.
  3. Memo(sender, target, callDataHash, memoId, memo, memoIndex) after the inner call.
The Memo contract supports nested memo calls. BeforeMemo events emit when each memo frame starts. Memo events unwind from the innermost frame back to the outermost frame.

Event schema

Use the emitted events as your audit trail and reconciliation source:
EventFieldTypeDescription
BeforeMemomemoIndexuint256 indexedSequential memo index reserved before the inner call starts.
Memosenderaddress indexedWallet that called Memo.memo(...). For a direct EOA call, this is the EOA preserved as msg.sender in the target call.
Memotargetaddress indexedContract called through the memo wrapper, such as the USDC ERC-20 interface.
MemocallDataHashbytes32keccak256 hash of the forwarded target calldata. Store the original calldata if you need to reconstruct the exact call.
MemomemoIdbytes32 indexedIdentifier that your application defines for lookup and reconciliation.
MemomemobytesMemo bytes that your application defines. Encode this value consistently in your application.
MemomemoIndexuint256Sequential index for the memo frame. Nested memos each receive their own index.
For reconciliation, query Memo events by indexed fields such as memoId, sender, and target. If you need to match a memo to a specific inner call, compare callDataHash with the hash of the calldata your application created.

Unsupported patterns and guardrails

The transaction memo flow has explicit guardrails:
  • Submit Memo.memo(...) from an EOA. Calls routed through an intermediary contract with a different msg.sender revert because sender spoofing is not allowed.
  • Do not call the CallFrom precompile directly from an EOA. Direct calls revert with unauthorized caller.
  • Do not rely on STATICCALL. Memo execution changes state by incrementing the memo index, so static execution is rejected.
  • Do not use DELEGATECALL into the Memo contract. The delegated context does not have the required authorization for CallFrom.
  • If the child call reverts, the outer transaction reverts. The memo index increment rolls back, and the child return data is wrapped in MemoFailed(bytes).
Before you use transaction memos in production, test your memo format and reconciliation query against Arc Testnet with the same wallet and indexing infrastructure you plan to use in your application.