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
TheMemo 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:
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:
BeforeMemo(memoIndex)before the inner call.- The target contract events, such as a USDC
Transfer. Memo(sender, target, callDataHash, memoId, memo, memoIndex)after the inner call.
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:| Event | Field | Type | Description |
|---|---|---|---|
BeforeMemo | memoIndex | uint256 indexed | Sequential memo index reserved before the inner call starts. |
Memo | sender | address indexed | Wallet that called Memo.memo(...). For a direct EOA call, this is the EOA preserved as msg.sender in the target call. |
Memo | target | address indexed | Contract called through the memo wrapper, such as the USDC ERC-20 interface. |
Memo | callDataHash | bytes32 | keccak256 hash of the forwarded target calldata. Store the original calldata if you need to reconstruct the exact call. |
Memo | memoId | bytes32 indexed | Identifier that your application defines for lookup and reconciliation. |
Memo | memo | bytes | Memo bytes that your application defines. Encode this value consistently in your application. |
Memo | memoIndex | uint256 | Sequential index for the memo frame. Nested memos each receive their own index. |
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.
Wallet types
TheMemo contract must be invoked directly by an externally owned account
(EOA). Smart contract wallets aren’t supported as the direct caller.
Supported wallets
Any wallet that signs and broadcasts a standard EOA transaction works. This includes:- Browser and mobile wallets: MetaMask, Rabby, Coinbase Wallet, Rainbow
- Hardware wallets
- Server-side signers
- Circle developer-controlled wallets and user-controlled wallets configured as EOAs
CallFrom precompile preserves the signing EOA as msg.sender in the
target call, which is what the memo flow depends on.
Unsupported wallets
Smart contract wallets aren’t supported as the direct caller. This includes:- ERC-4337 smart accounts, including Circle modular wallets
- Circle developer-controlled wallets and user-controlled wallets configured as SCAs
- Safe and other multisig contract wallets
- Any other account-abstraction setup where the transaction originates from a bundler, entry point, or other intermediary contract
Memo.memo(...), the msg.sender reaching
the contract is the wallet contract or entry point, not the user’s EOA. The call
reverts because sender spoofing isn’t allowed.
If you need memo metadata from this flow, have the smart contract wallet trigger
a separate EOA-signed Memo.memo(...) transaction, or attach the metadata in
your application layer instead.
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 differentmsg.senderrevert because sender spoofing is not allowed. - Do not call the
CallFromprecompile directly from an EOA. Direct calls revert withunauthorized caller. - Do not rely on
STATICCALL. Memo execution changes state by incrementing the memo index, so static execution is rejected. - Do not use
DELEGATECALLinto theMemocontract. The delegated context does not have the required authorization forCallFrom. - 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).