> ## Documentation Index
> Fetch the complete documentation index at: https://developers.circle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How-to: Manage TokenManager roles

> Transfer ownership, replace the operator, and update the pauser on a CCTP for non-USDC TokenManager.

A `TokenManager` is a per-token, per-domain contract that controls crosschain
transfers for a custom token registered on CCTP for non-USDC. Each domain where
your token is active has its own independent `TokenManager`, and three roles
govern it: the owner (highest privilege, can change all roles), the operator
(sets rate limits and transfer caps), and the pauser (can pause and `unpause`
the token on this domain). You need to manage these roles when rotating keys,
delegating operator duties to a separate address, or responding to a security
incident.

## Prerequisites

Before you begin, ensure that you've:

* Registered a custom token using `registerCustomToken` or
  `deployCrossChainToken`, and noted the `tokenId`. See
  [Configure a custom token](/cctp/expanded-assets/quickstarts/configure-custom-token)
* Noted the `CrossChainTokenService` address for each domain where you want to
  change roles. See
  [Contract addresses](/cctp/expanded-assets/references/contract-addresses)
* Verified access to the wallet that currently holds the role you want to
  change: the owner wallet for ownership, operator, or pauser changes; the
  pauser wallet to call `pause` or `unpause`

## Steps

### Step 1: Find the TokenManager address

Every token has a separate `TokenManager` per domain. Resolve its address from
the `CrossChainTokenService` before making any role change.

```typescript TypeScript theme={null}
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains"; // e.g., Ethereum Sepolia

const publicClient = createPublicClient({
  chain: sepolia,
  transport: http(),
});

const serviceAbi = [
  {
    name: "resolveTokenManager",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "tokenId", type: "bytes32" }],
    outputs: [{ name: "tokenManager", type: "address" }],
  },
] as const;

// Replace with the CrossChainTokenService address for this domain
const CCTS_ADDRESS = "0xYourCrossChainTokenServiceAddress" as `0x${string}`;

// Replace with your token's bytes32 tokenId
const tokenId = "0xYourTokenId" as `0x${string}`;

const tokenManager: `0x${string}` = await publicClient.readContract({
  address: CCTS_ADDRESS,
  abi: serviceAbi,
  functionName: "resolveTokenManager",
  args: [tokenId],
});

console.log("TokenManager address:", tokenManager);
```

Repeat this on each domain where you want to change roles. Each domain's
`TokenManager` is an independent contract with its own role state.

### Step 2: Transfer ownership

Ownership transfer is a two-step operation. The current owner calls
`transferOwnership` to initiate. The `TokenManager` enters a pending
state—ownership has not changed yet. The new owner must then call
`acceptOwnership` from their address to complete the transfer.

<Warning>
  Transferring ownership to an address you do not control is irreversible. If
  the new owner never calls `acceptOwnership`, the current owner retains
  control—but there is no cancel mechanism, and the pending transfer remains
  indefinitely. Verify the new address carefully before sending the transaction.
</Warning>

Both transactions require a private key. Keep keys out of version control—load
them from environment variables or a secrets manager.

**Initiate the transfer (current owner):**

```typescript TypeScript theme={null}
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

// Current owner wallet—initiates the transfer
const currentOwnerAccount = privateKeyToAccount(
  process.env.OWNER_PRIVATE_KEY as `0x${string}`,
);

const ownerWalletClient = createWalletClient({
  account: currentOwnerAccount,
  chain: sepolia,
  transport: http(),
});

const tokenManagerAbi = [
  {
    name: "transferOwnership",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "newOwner", type: "address" }],
    outputs: [],
  },
] as const;

// Replace with the address of the incoming owner
const newOwnerAddress = "0xNewOwnerAddress" as `0x${string}`;

// Current owner calls transferOwnership—ownership is NOT transferred yet
const transferHash = await ownerWalletClient.writeContract({
  address: tokenManager,
  abi: tokenManagerAbi,
  functionName: "transferOwnership",
  args: [newOwnerAddress],
});

console.log("transferOwnership tx:", transferHash);
```

**Complete the transfer (new owner):**

```typescript TypeScript theme={null}
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

// New owner wallet—must call acceptOwnership to complete the transfer
const newOwnerAccount = privateKeyToAccount(
  process.env.NEW_OWNER_PRIVATE_KEY as `0x${string}`,
);

const newOwnerWalletClient = createWalletClient({
  account: newOwnerAccount,
  chain: sepolia,
  transport: http(),
});

const acceptOwnershipAbi = [
  {
    name: "acceptOwnership",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [],
    outputs: [],
  },
] as const;

// New owner calls acceptOwnership—ownership transfers at this point
const acceptHash = await newOwnerWalletClient.writeContract({
  address: tokenManager,
  abi: acceptOwnershipAbi,
  functionName: "acceptOwnership",
  args: [],
});

console.log("acceptOwnership tx:", acceptHash);
```

This applies only to the domain where you call it. Repeat on each domain's
`TokenManager` independently.

### Step 3: Replace the operator

Unlike ownership, operator replacement is a single-step operation. The current
owner calls `transferOperatorship` and the change takes effect immediately—no
acceptance step is required.

```typescript TypeScript theme={null}
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

const ownerAccount = privateKeyToAccount(
  process.env.OWNER_PRIVATE_KEY as `0x${string}`,
);

const ownerWalletClient = createWalletClient({
  account: ownerAccount,
  chain: sepolia,
  transport: http(),
});

const transferOperatorshipAbi = [
  {
    name: "transferOperatorship",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "newOperator", type: "address" }],
    outputs: [],
  },
] as const;

// Replace with the address of the new operator
const newOperatorAddress = "0xNewOperatorAddress" as `0x${string}`;

// Owner calls transferOperatorship—takes effect immediately
const operatorHash = await ownerWalletClient.writeContract({
  address: tokenManager,
  abi: transferOperatorshipAbi,
  functionName: "transferOperatorship",
  args: [newOperatorAddress],
});

console.log("transferOperatorship tx:", operatorHash);
```

The operator can call `setRateLimit`, `setMaxTransferAmount`, and
`setRateLimitWindow` on the `TokenManager`. For details on configuring those
values, see
[Configure rate limits and caps](/cctp/expanded-assets/howtos/configure-rate-limits-and-caps).

### Step 4: Replace the pauser

The pauser role defaults to the operator address at deployment time. If you
never call `updatePauser`, the operator is also the pauser. The current owner
can replace the pauser by calling `updatePauser`.

```typescript TypeScript theme={null}
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

const ownerAccount = privateKeyToAccount(
  process.env.OWNER_PRIVATE_KEY as `0x${string}`,
);

const ownerWalletClient = createWalletClient({
  account: ownerAccount,
  chain: sepolia,
  transport: http(),
});

const updatePauserAbi = [
  {
    name: "updatePauser",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [{ name: "newPauser", type: "address" }],
    outputs: [],
  },
] as const;

// Replace with the address of the new pauser
const newPauserAddress = "0xNewPauserAddress" as `0x${string}`;

// Owner calls updatePauser—takes effect immediately
const pauserHash = await ownerWalletClient.writeContract({
  address: tokenManager,
  abi: updatePauserAbi,
  functionName: "updatePauser",
  args: [newPauserAddress],
});

console.log("updatePauser tx:", pauserHash);
```

The pauser can call `pause()` to block all crosschain transfers of this token on
this domain, and `unpause()` to restore them. These calls affect only this token
on this domain—they are not protocol-wide.

```typescript TypeScript theme={null}
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

const pauserAccount = privateKeyToAccount(
  process.env.PAUSER_PRIVATE_KEY as `0x${string}`,
);

const pauserWalletClient = createWalletClient({
  account: pauserAccount,
  chain: sepolia,
  transport: http(),
});

const pauseAbi = [
  {
    name: "pause",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [],
    outputs: [],
  },
  {
    name: "unpause",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [],
    outputs: [],
  },
] as const;

// Pauser calls pause—blocks all crosschain transfers of this token on this domain
const pauseHash = await pauserWalletClient.writeContract({
  address: tokenManager,
  abi: pauseAbi,
  functionName: "pause",
  args: [],
});

console.log("pause tx:", pauseHash);

// Pauser calls unpause—restores crosschain transfers
const unpauseHash = await pauserWalletClient.writeContract({
  address: tokenManager,
  abi: pauseAbi,
  functionName: "unpause",
  args: [],
});

console.log("unpause tx:", unpauseHash);
```
