Skip to main content

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

ContractAddressPurpose
USDC0x3600000000000000000000000000000000000000Native stablecoin with built-in blocklist
Memo0x9702466268ccF55eAB64cdf484d272Ac08d3b75bAttaches metadata to transfers; preserves msg.sender
Multicall3From0xEb7cc06E3D3b5F9F9a5fA2B31B477ff72bB9c8b6Batches 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:
StageWhen it appliesBehavior
Pre-mempoolTransaction submitted to RPC nodeIf the sender is blocklisted, the RPC node rejects the transaction. It never enters the mempool.
Post-mempool runtimeTransaction executes after entering mempoolIf the sender becomes blocklisted between submission and execution, the transaction reverts.
Runtime transfer checktransfer or transferFrom is calledIf 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:
CheckConditionAction
1. Direct sendertx.from is in blocklistFlag transaction—will be rejected pre-mempool or reverted at runtime
2. Transfer recipientTransfer event to is in blocklistFlag transaction—transfer/transferFrom will revert
3. Memo-routed sendertx.to is Memo contract AND tx.from is in blocklistFlag—CallFrom precompile will reject
4. Multicall3From-routed sendertx.to is Multicall3From AND tx.from is in blocklistFlag—CallFrom precompile will reject
5. Routed transfer recipienttx.to is Memo or Multicall3From AND any Transfer to is in blocklistFlag—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