Admin Commands
Administrative commands for managing bridge contracts. These commands require private keys and submit transactions.
Usage
bridge-cli admin <command> [options]
View all admin commands:
bridge-cli admin --help
Ethereum Commands
register-token
Register an ERC-20 token on MultiSigLocker.
bridge-cli admin register-token \
--erc20 <address> \
--casper-hash <hash> \
--network <network>
Options:
| Option | Required | Description |
|---|---|---|
--erc20 <address> |
Yes | ERC-20 token address |
--casper-hash <hash> |
Yes | Casper CEP-18 contract hash |
-n, --network <network> |
No | EVM network (defaults to config) |
Example:
bridge-cli admin register-token \
--erc20 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 \
--casper-hash hash-62d62d5cb43d095b9096ecfe3a3f2bd4d2a1df4020cd9cda94594edb77fedf38 \
--network sepolia
update-token-mapping
Update the Casper hash for an already registered ERC-20 token.
bridge-cli admin update-token-mapping \
--erc20 <address> \
--casper-hash <hash> \
--network <network>
deploy-wrapped-token
Deploy a wrapped ERC-20 token on Ethereum for a Casper-native CEP-18 token.
bridge-cli admin deploy-wrapped-token \
--casper-hash <hash> \
--name <name> \
--symbol <symbol> \
--decimals <decimals> \
--network <network>
Example:
bridge-cli admin deploy-wrapped-token \
--casper-hash hash-abc123... \
--name "Wrapped CSPR Test Token" \
--symbol "wTEST" \
--decimals 9 \
--network sepolia
replace-wrapped-token
Replace an existing zero-supply Casper-native wrapper with the V13 bridge-only-burn implementation. This recovery command is intended for testnet wrappers deployed before the bridge-only-burn hardening; it does not migrate balances and refuses to run unless the locker is fully paused and the old wrapper's total supply is exactly zero.
bridge-cli admin replace-wrapped-token \
--casper-hash hash-<64-hex-character-CEP-18-hash> \
--expected-old-wrapper 0x<approved-legacy-wrapper> \
--network sepolia
The command is hard-blocked outside Sepolia, Base Sepolia, and Polygon Amoy. It
binds the operation to the exact approved old-wrapper address and validates that
the wrapper still exposes the legacy direct-burn API, plus V13, admin ownership,
full pause, zero supply, mappings, and enumeration before submission. After
confirmation it verifies the replacement
event, bytecode, both mappings, enumeration, immutable bridge/hash values, and
that direct burn(uint256) plus non-bridge burnFrom(address,uint256) calls
revert while locker-authorized zero-amount burnFrom succeeds. Use --yes only
for controlled non-interactive testnet operations.
After replacement, retire the old reverse mapping and install the new Casper TokenFactory mapping for the same EVM chain ID while the locker remains fully paused:
bridge-cli admin casper-unregister-wrapped-token \
--wrapped <old-wrapper> \
--chain-id <testnet-chain-id> \
--expected-cep18 hash-<64-hex-character-CEP-18-hash>
bridge-cli admin casper-update-native-token-mapping \
--cep18 hash-<64-hex-character-CEP-18-hash> \
--chain-id <testnet-chain-id> \
--wrapped <new-wrapper>
# Exact read-back: old reverse mapping must be absent; both new directions must agree.
bridge-cli casper TokenFactory get_native_token <old-wrapper> <testnet-chain-id>
# Expected: (not registered as wrapped token)
bridge-cli casper TokenFactory get_wrapped_token hash-<CEP-18-hash> <testnet-chain-id>
# Expected: <new-wrapper>
bridge-cli casper TokenFactory get_native_token <new-wrapper> <testnet-chain-id>
# Expected: <CEP-18-hash>
Keep the locker fully paused until those three reads and relayer configuration
resolve the replacement. The unregister command itself requires casper-test, an
approved EVM testnet chain ID, and an exact live reverse-mapping match; a stale or
absent mapping aborts before confirmation.
This procedure is authorized for one designated zero-supply Casper-native wrapper on testnet only; never use it for mainnet or a second/repeated replacement.
Record a real round trip with the production CLI after unpausing under the approved procedure:
# Capture Casper source and EVM destination balances before each operation.
bridge-cli admin lock-native --cep18 hash-<CEP-18-hash> --amount <amount> \
--recipient <evm-recipient> --network <testnet-network>
# Assert NativeTokenLocked identifies the CEP-18, recipient, amount, nonce, and chain ID.
# Assert the EVM balance and totalSupply of <new-wrapper> each increase by <amount>.
bridge-cli admin burn-wrapped --wrapped <new-wrapper> --amount <amount> \
--recipient <casper-recipient> --network <testnet-network>
# Assert exactly one WrappedBurned identifies <new-wrapper>, recipient type/hash,
# amount, nonce, and chain ID. Assert EVM balance/supply decrease by <amount> and
# the Casper CEP-18 balance increases by <amount> after unlock.
Preserve both source transaction IDs, both destination transaction/deploy IDs, exact decoded event fields, and all four before/after balance and supply values. The old wrapper remains deployed but is no longer recognized by the locker.
lock-tokens
Lock ERC-20 tokens on EVM chain to mint CEP-18 on Casper.
bridge-cli admin lock-tokens \
--erc20 <address> \
--route <route-id> \
--amount <amount> \
--recipient <casper-hash> \
--network <network> \
[--skip-fee]
Options:
| Option | Required | Description |
|---|---|---|
--erc20 <address> |
Yes | ERC-20 token address; must be the route's origin asset |
--route <route-id> |
On exposure-control lockers | The lock-mint route (32-byte id) |
--amount <amount> |
Yes | Amount in smallest unit (wei) |
--recipient <hash> |
Yes | Casper recipient (account-hash-xxx or hash-xxx) |
-n, --network <network> |
No | Target EVM network |
--skip-fee |
No | Skip bridge fee (testing) |
On an exposure-control locker the command reads the route's policy, the domain's ceilings and the pause flags first, and stops with the contract's reason (AmountOutOfRange, CustodyCeilingExceeded, RouteAllocationExceeded, RepresentationCeilingExceeded, CommitmentsNotActive, CommitmentsPaused) before sending anything. The contract admits the amount the locker actually receives, so when the requested amount is refused only by an upper bound (a token that charges a transfer fee delivers less), the command asks the contract with a simulated call and proceeds if it admits the transfer. On success it prints the transfer id, the route's source sequence and the gateway sendId. Deployed V17 lockers take the legacy token-based lock without --route.
Example:
bridge-cli admin lock-tokens \
--erc20 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 \
--route 0x4f5e...7ea8 \
--amount 100000000 \
--recipient account-hash-abc123... \
--network sepolia
update-relayer-set
Update the relayer set on MultiSigLocker.
bridge-cli admin update-relayer-set \
--relayers <addr1,addr2,...> \
--threshold <n>
Example:
bridge-cli admin update-relayer-set \
--relayers 0xabc...,0xdef...,0x123...,0x456...,0x789... \
--threshold 3
get-relayer-set
Query current relayer configuration from MultiSigLocker.
# Single network
bridge-cli admin get-relayer-set --network sepolia
# All configured networks
bridge-cli admin get-relayer-set --all
Source limits
On exposure-control lockers, minimum and maximum commitment amounts are part of each route's policy, set with configurePolicy by the governance Safe. set-min-lock-amount and set-token-min-lock refuse with that path. They still apply to deployed V17 lockers:
bridge-cli admin set-min-lock-amount --amount <token-units> --network <network>
bridge-cli admin set-token-min-lock --token <address> --amount <decimal> --network <network>
Casper Commands
deploy-token
Deploy/register an ERC-20 token mapping on TokenFactory.
bridge-cli admin deploy-token \
--erc20 <address> \
--name <name> \
--symbol <symbol> \
--decimals <decimals> \
[--skip-decimals-check]
Before submitting deploy_token, the CLI verifies --decimals against the
source ERC-20's live on-chain decimals(). A mismatch aborts the
deploy - a wrong decimals value would cause systematic over/under-crediting
on every future mint for that mapping, and it cannot be corrected after
registration. Use --skip-decimals-check only for nonstandard tokens without
a decimals() view or when the source RPC is intentionally unavailable.
get-cep18
Get the CEP-18 address for a registered ERC-20 token.
bridge-cli admin get-cep18 \
--erc20 <address> \
--chain <chainId>
Example:
bridge-cli admin get-cep18 \
--erc20 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 \
--chain 11155111
burn-tokens
Burn CEP-18 tokens on Casper to unlock ERC-20 on EVM.
bridge-cli admin burn-tokens \
--route <route-id> \
--amount <amount> \
--recipient <evm-address> \
[--cep18 <hash>] [--chain <chain-id>] [--skip-fee]
On an exposure-control TokenFactory --route names the burn-unlock route. The command reads the route first: it burns the domain's representation token toward the route's chain, and shows both before submitting. --cep18 and --chain are optional checks; if either disagrees with the route, or the route is unknown or cannot be read, the command stops before sending anything. The bridge fee must be exact. Pre-exposure TokenFactory deployments take the legacy token-based burn with --cep18, --chain (Sepolia by default) and no --route.
Example:
bridge-cli admin burn-tokens \
--route 0x9b1c...04e2 \
--amount 100000000 \
--recipient 0x1234567890abcdef1234567890abcdef12345678
Casper-Native Token Commands
lock-native
Lock CEP-18 tokens on Casper to mint wrapped ERC-20 on EVM.
bridge-cli admin lock-native \
--route <route-id> \
--amount <amount> \
--recipient <evm-address> \
[--cep18 <hash>] [--chain <chain-id>] [--skip-fee]
On an exposure-control TokenFactory --route names the lock-mintWrapped route. The command reads the route first, shows the asset it locks and the chain it targets, approves TokenFactory on that asset, then commits with the exact bridge fee. --cep18 and --chain are optional checks; if either disagrees with the route, or the route is unknown or cannot be read, the command stops before approving or sending anything. Route bounds, ceilings, status and pause refusals come back with their contract names (AmountOutOfRange, CustodyCeilingExceeded, RouteAllocationExceeded, RepresentationCeilingExceeded, CommitmentsNotActive, CommitmentsPaused). Pre-exposure TokenFactory deployments take the legacy token-based lock with --cep18, --chain (Sepolia by default) and no --route.
burn-wrapped
Burn wrapped ERC-20 tokens on EVM to unlock CEP-18 on Casper.
bridge-cli admin burn-wrapped \
--wrapped <address> \
--route <route-id> \
--amount <amount> \
--recipient <casper-hash> \
[--skip-fee]
On an exposure-control locker --route names the burnWrapped-unlock route and --wrapped must be the domain's representation token. The command checks the route's status (ACTIVE or DEPOSIT_PAUSED), bounds and pause flags first, then burns and prints the transfer id. Deployed V17 lockers take the legacy burn without --route.
inbound-status
Show an inbound transfer's status on an exposure-control locker, as the locker's transferStatus reports it from current conditions: unknown, unconfigured, paused, held, queued_waiting_delay, queued_waiting_capacity, queued_behind_head, queued_ready, recovery_held, executed or terminal. A held transfer shows the condition holding it now; once that has cleared it says so and shows the reason stored at the hold. The output also shows the domain's queue, its release limiter and any recovery shortfall.
bridge-cli admin inbound-status --transfer <transfer-id> [--network <network>]
complete-queued-release
Complete the head of a backing domain's release queue. Anyone may call it; it charges no bridge fee.
bridge-cli admin complete-queued-release --domain <backing-domain-id> [--network <network>]
Stops with QueueEmpty or QueuedTransferNotReady (with the ready time) before sending. If the release limiter cannot cover the head yet, the transaction succeeds without paying out and keeps any debt repayment it made; run it again after refill. A head whose recipient the asset refuses moves to held, and the next entry becomes the head. A head marked terminal by a transfer-specific repair upgrade is skipped at once, whatever its ready time, and nothing is paid for it. The command reports only what its own mined transaction did, from that transaction's events (executed, held, skipped terminal), so a head another caller completed in the same block is never reported as this transaction's work. A transaction without one of those events returned because the release limiter could not cover the head it ran against; the command says so without naming a transfer, since later transactions in the block may already have moved the queue. Run inbound-status for the current state.
retry-held
Retry a held inbound transfer once its condition clears. Anyone may call it; it charges no bridge fee.
bridge-cli admin retry-held --transfer <transfer-id> [--network <network>]
Stops with TransferNotHeld, or with TransferStillHeld and the remaining reason, before sending. A backing release then executes or joins its domain's queue like a new arrival. A recipient refusal shows only at payout, so a refused release above the large-release threshold re-queues on retry even if the recipient still cannot receive; it is held again when it reaches the head, and the queue moves on.
restore-backing
Return backing to an EVM-origin domain after an emergency withdrawal created a recovery shortfall. Anyone may call it. The locker takes at most the shortfall and reduces it by the amount it actually received.
bridge-cli admin restore-backing --domain <backing-domain-id> --amount <raw-amount> [--network <network>]
Approve the locker for the asset first. Stops with NoRecoveryShortfall, or with the missing balance or allowance, before sending. Restoring backing never pays a transfer and never adds custody capacity. Releases held by the shortfall execute through retry-held and complete-queued-release once it is zero and governance has unpaused the domain.
casper-inbound-status
Show an inbound transfer's live state on an exposure-control TokenFactory, computed with the contract's own rules: unknown, unconfigured, paused, held (with the reason blocking it now, or that it can be retried), queued_behind_head, queued_waiting_delay, queued_waiting_capacity, queued_ready, recovery_held, executed or terminal. A pause, status or recovery shortfall outranks the queue states. Also prints the domain's queue and any recovery shortfall.
bridge-cli admin casper-inbound-status --transfer <transfer-id>
casper-complete-queued-release
Complete the head of a backing domain's release queue on Casper. Anyone may call it; it charges no bridge fee.
bridge-cli admin casper-complete-queued-release --domain <backing-domain-id>
Stops with QueueEmpty, QueuedTransferNotReady (with the ready time) or ReleaseNotExecutable (paused, not executing or a recovery shortfall) before sending. A head marked TERMINAL by a repair upgrade is sent whatever its ready time; the command then reports the skip, never a payout. Reports what its own deploy did, from that deploy's events: the release it executed, or that it paid nothing because the release limiter cannot cover the head yet (any debt repayment it made persists; run it again after refill). Another caller may complete the same queue at the same time, so the head shown afterwards is the current state, not this deploy's result. If the deploy's events cannot be read or verified, the command says so rather than guessing either way; check the transaction or run casper-inbound-status.
casper-retry-held
Retry a held inbound transfer on Casper once its condition clears. Anyone may call it; it charges no bridge fee.
bridge-cli admin casper-retry-held --transfer <transfer-id>
Stops with TransferNotHeld before sending, and the contract refuses with TransferStillHeld while the condition remains. A release then executes or joins its domain's queue like a new arrival; the command reports which, from its own deploy's events.
casper-restore-backing
Return backing to a Casper-origin domain after an emergency withdrawal created a recovery shortfall. Anyone may call it.
bridge-cli admin casper-restore-backing --domain <backing-domain-id> --amount <raw-amount>
Stops before sending when the domain is not a registered Casper-origin domain or has no shortfall. Approves TokenFactory on the domain's CEP-18 for at most the shortfall, then calls restore_backing and reports the amount received and the remaining shortfall from its own deploy's BackingRestored event. Restoring never pays a transfer and never adds custody capacity; held releases execute through casper-retry-held and casper-complete-queued-release once the shortfall is zero and governance has unpaused the domain.
Casper Contract Administration
casper-pause
Pause TokenFactory on Casper.
bridge-cli admin casper-pause
casper-unpause
Unpause TokenFactory on Casper.
bridge-cli admin casper-unpause
casper-propose-admin / casper-accept-admin
Transfer admin on TokenFactory (two-step). The current admin proposes; the proposed admin accepts with their own key.
# Step 1: current admin key
bridge-cli admin casper-propose-admin --address <account-hash>
# Step 2: proposed admin key
bridge-cli admin casper-accept-admin
casper-register-token-mapping
Register a token mapping on TokenFactory.
bridge-cli admin casper-register-token-mapping \
--erc20 <address> \
--cep18 <hash> \
--chain-id <chainId>
casper-update-native-token-mapping
Update native token mapping on TokenFactory.
bridge-cli admin casper-update-native-token-mapping \
--cep18 <hash> \
--chain-id <chainId> \
--wrapped <address>
casper-set-eth-client
Set EthClient address on TokenFactory.
bridge-cli admin casper-set-eth-client --hash <contract-hash>
casper-set-erc20-locker
Set the ERC20Locker address on TokenFactory for a specific source EVM chain (per-chain mapping since v7 / BRIDGE-E2E-1).
bridge-cli admin casper-set-erc20-locker --chain-id <source-chain-id> --address <evm-address>
Both options are required. The contract entry point takes source_chain_id and erc20_locker.
Post-upgrade pitfall: an in-place
upgrade_packageto v7 leaves theerc20_lockersmapping empty (it is seeded only in the constructor path). Every mint from an unseeded chain reverts withErc20LockerNotSet(60045). Seed each supported chain explicitly after upgrading.
Casper EthClient Administration
casper-add-relayer
Add a relayer to EthClient on Casper.
bridge-cli admin casper-add-relayer --address <eth-address>
casper-remove-relayer
Remove a relayer from EthClient on Casper.
bridge-cli admin casper-remove-relayer --address <eth-address>
casper-set-threshold
Set signature threshold on EthClient.
bridge-cli admin casper-set-threshold --threshold <n>
casper-propose-ethclient-admin / casper-accept-ethclient-admin
Transfer admin on EthClient (two-step - same propose/accept pattern as TokenFactory).
# Step 1: current admin key
bridge-cli admin casper-propose-ethclient-admin --address <account-hash>
# Step 2: proposed admin key
bridge-cli admin casper-accept-ethclient-admin
Relayer State Recovery
replay-lock
Re-queue a real, unminted EVM lock into a stopped relayer's persisted state so the normal 3-of-5 path mints it. For locks outside every automatic window (pruned logs, wiped PVC). Offline: edits the state file, no relayer endpoint involved.
bridge-cli admin replay-lock --chain 46630 --tx 0x… --dry-run # verify only
bridge-cli admin replay-lock --chain 46630 --tx 0x… --yes # write
| Option | Description |
|---|---|
--chain <chainId> |
EVM source chain id (must be configured on this relayer) |
--tx <hash> |
Lock transaction hash |
--state-file <path> |
Explicit evm2casper-addr-<last8>.json; default derives from PERSISTENCE_DIR + the relayer key |
--dry-run |
Run every check, write nothing |
--yes |
Skip the "relayer is stopped" confirmation |
Refuses (exit 2) when the receipt is missing or reverted, the tx did not call the configured locker, there is no single Locked log from it, the event's chain id disagrees, the lock is already pending, the block is incomplete, or Casper already minted it (TokenFactory.processed_events). A Casper RPC failure is an error (exit 1), never a pass. Full procedure for EKS: docs/runbooks/replay-lock.md.
Environment Requirements
Admin commands require private keys configured in .env:
# For EVM operations
ETH_PRIVATE_KEY=0x...
# For Casper operations
CASPER_PRIVATE_KEY_PATH=./keys/secret_key.pem
Common Errors
# Not admin
Error: Caller 0x... is not admin (0x...)
# Token not registered
Error: Token is not registered on MultiSigLocker
# Insufficient fee
Error: Insufficient bridge fee sent
# Missing key
Error: ETH_PRIVATE_KEY not configured in .env