Documentation Index
Fetch the complete documentation index at: https://docs.arc.io/llms.txt
Use this file to discover all available pages before exploring further.
Arc enforces USDC blocklist restrictions at three levels: pre-mempool rejection,
post-mempool runtime revert, and per-transfer checks. The Memo and
Multicall3From contracts route transactions while preserving the original
msg.sender via the CallFrom precompile—your monitoring must attribute these to
the original sender. Subscribe to Blocklisted and UnBlocklisted events to
maintain a local copy of the blocklist.
Prerequisites
Before you begin:
- Access to an Arc RPC endpoint (
https://rpc.testnet.arc.network) or WebSocket
(wss://rpc.testnet.arc.network)
- Familiarity with Ethereum event log filtering and transaction tracing
- A local database or cache for storing blocklisted addresses
- Understanding of your regulatory obligations (AML/CFT screening requirements)
Contracts and addresses
| Contract | Address | Purpose |
|---|
| USDC | 0x3600000000000000000000000000000000000000 | Native stablecoin with built-in blocklist |
| Memo | 0x9702466268ccF55eAB64cdf484d272Ac08d3b75b | Attaches metadata to transfers; preserves msg.sender |
| Multicall3From | 0xEb7cc06E3D3b5F9F9a5fA2B31B477ff72bB9c8b6 | Batches multiple calls; preserves msg.sender |
Steps
Step 1. Understand the three enforcement stages
Arc enforces the USDC blocklist at every point in a transaction’s lifecycle:
| Stage | When it applies | Behavior |
|---|
| Pre-mempool | Transaction submitted to RPC node | If the sender is blocklisted, the RPC node rejects the transaction. It never enters the mempool. |
| Post-mempool runtime | Transaction executes after entering mempool | If the sender becomes blocklisted between submission and execution, the transaction reverts. |
| Runtime transfer check | transfer or transferFrom is called | If either the from or to address is blocklisted, the call reverts. |
The pre-mempool check means blocklisted addresses cannot submit any
transaction to Arc, not just USDC transfers. The runtime checks provide
defense-in-depth for edge cases where blocklist state changes between
submission and execution.
Step 2. Monitor blocklist events
Subscribe to the Blocklisted and UnBlocklisted events on the USDC contract
to maintain a real-time view of restricted addresses.
import { Contract, JsonRpcProvider } from "ethers";
const USDC_ADDRESS = "0x3600000000000000000000000000000000000000";
const provider = new JsonRpcProvider("https://rpc.testnet.arc.network");
const usdc = new Contract(
USDC_ADDRESS,
[
"event Blocklisted(address indexed account)",
"event UnBlocklisted(address indexed account)",
],
provider,
);
// Subscribe to blocklist changes
usdc.on("Blocklisted", (account: string) => {
console.log(`Address blocklisted: ${account}`);
addToLocalBlocklist(account);
});
usdc.on("UnBlocklisted", (account: string) => {
console.log(`Address unblocklisted: ${account}`);
removeFromLocalBlocklist(account);
});
Step 3. Include Memo and Multicall3From in your monitoring scope
The Memo and Multicall3From contracts use the CallFrom precompile to execute
calls on behalf of the original sender. The blocklist is still enforced (the
CallFrom precompile checks the original sender’s blocklist status), but
compliance monitors must attribute activity correctly.
If you only monitor direct from addresses in transaction receipts, you will
miss the true sender for transactions routed through Memo or Multicall3From.
You must inspect calls to these contracts and attribute them to the original
msg.sender.
import { Interface, JsonRpcProvider, Log } from "ethers";
const MEMO_ADDRESS = "0x9702466268ccF55eAB64cdf484d272Ac08d3b75b";
const MULTICALL3FROM_ADDRESS = "0xEb7cc06E3D3b5F9F9a5fA2B31B477ff72bB9c8b6";
const USDC_ADDRESS = "0x3600000000000000000000000000000000000000";
const TRANSFER_TOPIC =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
const provider = new JsonRpcProvider("https://rpc.testnet.arc.network");
const erc20Interface = new Interface([
"event Transfer(address indexed from, address indexed to, uint256 value)",
]);
async function checkTransactionCompliance(txHash: string): Promise<void> {
const tx = await provider.getTransaction(txHash);
if (!tx) return;
const receipt = await provider.getTransactionReceipt(txHash);
if (!receipt) return;
const originalSender = tx.from;
// Flag if the transaction is routed through Memo or Multicall3From
const isRoutedTransaction =
tx.to?.toLowerCase() === MEMO_ADDRESS.toLowerCase() ||
tx.to?.toLowerCase() === MULTICALL3FROM_ADDRESS.toLowerCase();
if (isRoutedTransaction) {
// Attribute all Transfer events in this transaction to the original sender
const transfers = receipt.logs.filter(
(log: Log) =>
log.address.toLowerCase() === USDC_ADDRESS.toLowerCase() &&
log.topics[0] === TRANSFER_TOPIC,
);
for (const log of transfers) {
const parsed = erc20Interface.parseLog({
topics: log.topics as string[],
data: log.data,
});
if (!parsed) continue;
// Screen the original sender, not the contract address
await screenAddress(originalSender, txHash);
await screenAddress(parsed.args.to as string, txHash);
}
}
}
Step 4. Build a transaction decision tree
Use the following logic to determine whether a transaction involves a
blocklisted address:
| Check | Condition | Action |
|---|
| 1. Direct sender | tx.from is in blocklist | Flag transaction—will be rejected pre-mempool or reverted at runtime |
| 2. Transfer recipient | Transfer event to is in blocklist | Flag transaction—transfer/transferFrom will revert |
| 3. Memo-routed sender | tx.to is Memo contract AND tx.from is in blocklist | Flag—CallFrom precompile will reject |
| 4. Multicall3From-routed sender | tx.to is Multicall3From AND tx.from is in blocklist | Flag—CallFrom precompile will reject |
| 5. Routed transfer recipient | tx.to is Memo or Multicall3From AND any Transfer to is in blocklist | Flag—runtime transfer check will revert |
interface ComplianceResult {
flagged: boolean;
reason?: string;
}
async function evaluateTransaction(txHash: string): Promise<ComplianceResult> {
const tx = await provider.getTransaction(txHash);
if (!tx) return { flagged: false };
// Check 1: Direct sender
if (await isBlocklisted(tx.from)) {
return { flagged: true, reason: "Sender is blocklisted" };
}
const receipt = await provider.getTransactionReceipt(txHash);
if (!receipt) return { flagged: false };
// Check 2-5: Inspect Transfer events for blocklisted recipients
const transfers = receipt.logs.filter(
(log: Log) =>
log.address.toLowerCase() === USDC_ADDRESS.toLowerCase() &&
log.topics[0] === TRANSFER_TOPIC,
);
for (const log of transfers) {
const parsed = erc20Interface.parseLog({
topics: log.topics as string[],
data: log.data,
});
if (!parsed) continue;
const to = parsed.args.to as string;
if (await isBlocklisted(to)) {
return { flagged: true, reason: `Recipient ${to} is blocklisted` };
}
}
return { flagged: false };
}
Step 5. Integrate compliance vendor APIs
Connect your monitoring pipeline to Elliptic or TRM Labs for automated risk
scoring and sanctions screening. These vendors provide Arc-compatible APIs for
real-time transaction analysis.
// Example: screen an address against a compliance vendor API
async function screenAddress(
address: string,
txHash: string,
): Promise<boolean> {
const response = await fetch(
"https://api.your-compliance-vendor.com/screen",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.COMPLIANCE_API_KEY}`,
},
body: JSON.stringify({
address,
chain: "arc",
transactionHash: txHash,
}),
},
);
const result = await response.json();
return result.risk_level === "high";
}
For vendor-specific integration details, see
Compliance vendors.
See also