Guides
Ethereum - ERC7984 Tokens
Overview
ERC7984 is a token standard for confidential fungible tokens on Ethereum. Unlike ERC20, balances and transfer
amounts are encrypted on-chain using Fully Homomorphic Encryption (FHE), powered by the
Zama Protocol. Token contracts do not
expose ERC20's balanceOf/transfer/approve functions. Instead they expose their own, distinctly named functions —
confidentialBalanceOf, confidentialTransfer/confidentialTransferFrom, and a time-limited setOperator in place
of approve — whose amounts are opaque encrypted handles (bytes32 pointers) rather than plaintext integers. Only
parties holding the right decryption key can learn the actual amount behind a handle.
Note: ERC7984 tokens are supported on multisig and MPC wallets, but not on wallet version 6 or later (v6+). A v6+ wallet cannot enable or hold ERC7984 tokens.
How ERC7984 differs from ERC20
ERC7984 tokens share a wallet with their parent Ethereum wallet the same way ERC20 tokens do (no separate keychains, addresses, or wallet objects). However, the confidentiality model changes several behaviors that developers relying on standard ERC20 flows need to account for:
| Behavior | ERC20 | ERC7984 |
|---|---|---|
| Balances / transfer amounts | Plaintext, readable directly from the contract | Encrypted on-chain; BitGo must decrypt them via a viewing key before they can be displayed |
| Token enablement | Not required | Requires an on-chain delegation transaction, submitted separately per address, before BitGo can decrypt balances/transfers for that address |
| Balance & transfer visibility | Available immediately once indexed | Available asynchronously, after BitGo completes decryption |
| Consolidation | Automatic via forwarders | Manual only, and only from receive addresses that have already delegated |
| Recipients per transaction | 1 | 1 |
| Chains supported | Ethereum + most EVM chains BitGo supports | Ethereum only, currently |
Confidential balances and encrypted amounts
An ERC7984 contract's confidentialBalanceOf and transfer functions return an encrypted handle, not a plaintext
value. To show a usable balance or transfer amount, BitGo decrypts these handles using a per-enterprise viewing key.
This means:
- Balances on a wallet's confidential tokens are only meaningful once BitGo has decrypted them; consumers of the API should expect balance/amount fields to populate asynchronously rather than immediately after a transfer is seen on-chain.
- Decryption is scoped to the enterprise's viewing key — only BitGo can read the actual amounts. The chain itself never reveals plaintext balances.
Token enablement (delegation)
ERC20 tokens require no wallet-level setup before use. ERC7984 tokens do: before BitGo can decrypt balances or
transfers for an address, that address's owner must call enableToken to submit an on-chain delegation
transaction that grants BitGo's viewing key the right to decrypt that address's encrypted data on the token's ACL
contract. This follows the same token enablement pattern used for other
enablement-required assets (for example SOL, HBAR, and ALGO) — a normal transaction that must be built, signed, and
sent before the token is usable — except that for ERC7984 tokens, "enabling" the token means delegating decryption
rights rather than creating an account or trustline.
This is a one-time setup per address, per ERC7984 token. Clients must call enableToken for an address before
attempting deposits or transfers of that token to or from it — until that address is enabled, BitGo cannot decrypt
its balances or transfer amounts.
Delegation is scoped per address, not per wallet. Enabling the token on a wallet's base address does not delegate decryption rights for its receive addresses — each receive address must have its own delegation transaction submitted before BitGo can decrypt balances or transfers held at that address. This also means consolidation will not pick up a receive address's confidential token balance until that address has delegated; consolidation attempts against a non-delegated receive address will not succeed.
The delegation transaction for an address can only be sent once that address's forwarder contract has been deployed. The forwarder is deployed the first time the address receives any asset — a receive address must have previously received a deposit (ETH or any token) before an enable-token/delegation transaction can be built and sent for it.
If a confidential transfer is received at an address while delegation is not active for it (for example, before the
address delegated, or if delegation was later revoked), the transfer cannot be decrypted immediately. It is surfaced
as a pending, "awaiting delegation" transaction. To resolve it, the address owner calls enableToken for that
address — once the resulting delegation transaction confirms on-chain, BitGo decrypts the stuck transfer and it
finalizes normally.
Building confidential transfers
Building, signing, and sending a confidential transfer uses the same API flow as an ERC20 transfer, and, as with ERC20, only a single recipient is supported per transaction. The transfer amount still needs to be encrypted before it's submitted on-chain, but BitGo handles that step internally when building the transaction — there's nothing different an API consumer needs to do to build a confidential transfer.
Consolidation
For ERC20 tokens, deposits are auto-flushed from the receiving address to the wallet's base address with no client
action needed. ERC7984 deposits stay on the receiving address until the client explicitly triggers consolidation by
calling POST /api/v2/:coin/wallet/:walletId/consolidateAccount/build, and only after that receive address has
completed its own delegation transaction — consolidation cannot decrypt or move a confidential balance from an
address that has not delegated.
Webhooks
Register a webhook with type: pendingDecryption (via Add Wallet Webhook) on the
wallet's base Ethereum coin to be notified when a confidential deposit can't be decrypted because the receiving
address hasn't delegated yet. The payload looks like:
{
"event": "erc7984_deposit_awaiting_delegation",
"walletId": "<walletId>",
"coin": "<token coin, e.g. eth:cusdc>",
"data": {
"walletName": "<wallet label>",
"address": "<receive address awaiting delegation>",
"txHashes": ["<txHash>"],
"receivedAt": "<ISO 8601 timestamp>"
}
}
No transfer record exists yet at this point, so calling Get Transfer for a txHash
from this payload will 404. Once the address delegates and BitGo completes decryption, the deposit is recorded as a
normal transfer and the standard transfer webhook fires — at that point, use Get Transfer with the token's coin name
to retrieve the decrypted amount and current state.