fhenix-confidential-contracts is a Solidity package of confidential ERC-20 tokens built on FHE.sol. Balances and transfer amounts are stored as encrypted handles (euint64), so only the accounts the token grants access to can decrypt them. Read this page to pick a contract, install the package, and deploy a token that needs the shared library.
The source is public at FhenixProtocol/fhenix-confidential-contracts, under the MIT license. These docs cover version 0.4.0.
Which contract do you need?
The package ships two families. FHERC20 tokens hold only encrypted balances.ERC20Confidential is a normal public ERC-20 with an encrypted balance added next to it.
On an FHERC20 token,
balanceOf and totalSupply return an indicator value, not a balance, and transfer, transferFrom, approve, and allowance revert with FHERC20IncompatibleFunction. The indicator exists so wallets and explorers show activity without seeing amounts. On ERC20Confidential, the ERC-20 functions behave normally on the public balance.
Every contract above has an upgradeable variant with the Upgradeable suffix, which you set up from an initializer instead of a constructor. If your token already has its own ERC-20 implementation, ERC20ConfidentialCoreUpgradeable adds only the confidential layer and reaches your ledger through three hooks: _ledgerMint, _ledgerTransfer, and _ledgerBalanceOf.
What all of them have in common
- Amounts are 64-bit. Encrypted amounts are
euint64. The wrappers andERC20Confidentialcap the confidential side at 6 decimals and convert at a fixed rate, so an 18-decimal token moves in steps of 10^12 base units. - Operators replace allowances.
setOperator(operator, until)letsoperatormove any amount for you until theuntiltimestamp. There is no per-amount allowance, so keepuntilshort. - Two ways to pass an amount. A wallet calls the overload that takes
externalEuint64plusinputProof, which it gets from the SDK. Another contract calls the overload that takessharedEuint64, created withFHE.shareEuint64(amount, token). - A failed transfer moves zero instead of reverting. If the encrypted amount exceeds the balance, the transfer completes with an amount of zero. A revert would reveal that the balance was too low. Every transfer returns the amount that actually moved.
- Leaving the confidential side takes two transactions.
unshieldburns the encrypted amount and opens a claim. Once the amount is decrypted,claimUnshieldedpays out the public token.
Install the package
@fhenixprotocol/cofhe-contracts@0.2.0 exactly, plus @openzeppelin/contracts and @openzeppelin/contracts-upgradeable. Keep your own cofhe-contracts pin on the same version, or npm installs two copies of FHE.sol. See the compatibility page for the matching SDK and plugin versions.
Import contracts by their path inside the package:
ConfidentialToken.sol
FHERC20 takes a name, a symbol, the decimals, and a contract URI. It deploys like any other contract.
To send tokens from a wallet, encrypt the amount with the SDK and name the token as the consuming contract. ethers needs the full signature to choose between the transfer overloads:
externalEuint64 is bytes32 in the ABI, which is why the signature reads bytes32. See encrypting inputs for setting up client.
Link the shared library
The wrappers,ERC20Confidential, and their upgradeable variants call into ERC20ConfidentialLib, an external library. Keeping that logic in a separately deployed library is what holds these tokens under the 24 KB contract size limit. FHERC20 and FHERC20Upgradeable do not use it.
The library address is written into the token’s bytecode when you deploy, not passed to the constructor, so it cannot change afterwards. Deploy the library once on each chain, then link every token on that chain to it:
deploy.ts
contracts/, which only resolves inside that repository. If you forget the libraries option, getContractFactory throws missing links for the following libraries before anything is sent.
For an upgradeable token, the OpenZeppelin upgrades plugin refuses linked libraries unless you allow them:
Upgrading from 0.3
Version0.4.0 follows the 0.7 CoFHE release and changes every function that takes an encrypted amount:
InEuint64parameters are nowexternalEuint64plusbytes inputProof. On theAndCallfunctions,inputProofcomes beforedata.- Overloads that took a bare
euint64from another contract now takesharedEuint64, and every transfer returnssharedEuint64. A calling contract reads the result withFHE.receiveEuint64FromCall(result, token). IERC7984Receiver.onConfidentialTransferReceivedreceives asharedEuint64and returns asharedEbool.- Wrappers and
ERC20Confidentialmust be linked toERC20ConfidentialLib. - Claims are keyed by a claim ID, not by the ciphertext handle. Read the ID from
getUserClaimsbefore you callclaimUnshielded.
0.4.0, because sharedEuint64 has the same ABI encoding as euint64. It fails at runtime with NotShared. The changelog lists every change. For the client side, see migrating to 0.7.
Next steps
FHERC20
Tokens with encrypted balances only.
Wrap ERC-20 tokens and ETH
Shield an existing token into a confidential one, and claim it back.
ERC20Confidential
A public ERC-20 with an encrypted balance next to it.
Call a token from your contract
Pass encrypted amounts between your contract and a token.