Confidential Fungible Token
The Confidential Fungible Token is a standard fungible token implementation that is similar to ERC20
, but built from the ground up with confidentiality in mind. All balance and transfer amounts are represented as cypher-text handles, ensuring that no data is leaked to the public.
While the standard is built with inspiration from ERC-20, it is not ERC-20 compliant—the standard takes learning from all tokens built over the past 10 years (ERC-20, ERC-721, ERC-1155, ERC-6909 etc) and provides a functional interface for maximal functionality.
Usage
Transfer
The token standard exposes eight different transfer functions. They are all permutations of the following options:
-
transfer
andtransferFrom
:transfer
moves tokens from the sender whiletransferFrom
moves tokens from a specifiedfrom
address. See operator. -
With and without
inputProof
: AninputProof
can be provided to prove that the sender knows the value of the cypher-textamount
provided. -
With and without an
ERC1363
style callback: The standard implements callbacks, see the callback section for more details.
Select the appropriate transfer function and generate a cypher-text using fhevm-js. If the cypher-text is a new value, or the sender does not have permission to access the cypher text, an input-proof must be provided to show that the sender knows the value of the cypher-text.
Operator
An operator is an address that has the ability to move tokens on behalf of another address by calling transferFrom
. If Bob is an operator for Alice, Bob can move any amount of Alice’s tokens at any point in time. Operators are set using an expiration timestamp—this can be thought of as a limited duration infinite approval for an ERC20
. Below is an example of setting Bob as an operator for Alice for 24 hours.
const alice: Wallet;
const expirationTimestamp = Math.round(Date.now()) + 60 * 60 * 24; // Now + 24 hours
await tokenContract.connect(alice).setOperator(bob, expirationTimestamp);
Operators do not have allowance to reencrypt/decrypt balance handles for other addresses. This means that operators cannot transfer full balances and can only know success after a transaction (by decrypting the transferred amount). |
Setting an operator for any amount of time allows the operator to take all of your tokens. Carefully vet all potential operators before giving operator approval. |
Callback
The token standard exposes transfer functions with and without callbacks. It is up to the caller to decide if a callback is necessary for the transfer. For smart contracts that support it, callbacks allow the operator approval step to be skipped and directly invoke the receiver contract via a callback.
Smart contracts that are the target of a callback must implement IConfidentialFungibleTokenReceiver
. After balances are updated for a transfer, the callback is triggered by calling the onConfidentialTransferReceived
function. The function must either revert or return an ebool
indicating success. If the callback returns false, the token transfer is reversed.
Examples
Privileged Minter/Burner
Here is an example of a contract for a confidential fungible token with a privileged minter and burner.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {TFHE, einput, ebool, euint64} from "fhevm/lib/TFHE.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ConfidentialFungibleToken} from "@openzeppelin/confidential-contracts/token/ConfidentialFungibleToken.sol";
contract ConfidentialFungibleTokenMintableBurnable is ConfidentialFungibleToken, Ownable {
using TFHE for *;
constructor(
address owner,
string memory name,
string memory symbol,
string memory uri
) ConfidentialFungibleToken(name, symbol, uri) Ownable(owner) {}
function mint(address to, einput amount, bytes memory inputProof) public onlyOwner {
_mint(to, amount.asEuint64(inputProof));
}
function burn(address from, einput amount, bytes memory inputProof) public onlyOwner {
_burn(from, amount.asEuint64(inputProof));
}
}
Swaps
Swapping is one of the most primitive use-cases for fungible tokens. Below are examples for swapping between confidential and non-confidential tokens.
Swap ERC20
to ConfidentialFungibleToken
Swapping from a non-confidential ERC20
to a confidential ConfidentialFungibleToken
is simple and actually done within the ConfidentialFungibleTokenERC20Wrapper
. See the excerpt from the wrap
function below.
function wrap(address to, uint256 amount) public virtual {
// take ownership of the tokens
SafeERC20.safeTransferFrom(underlying(), msg.sender, address(this), amount - (amount % rate()));
// mint confidential token
_mint(to, (amount / rate()).toUint64().asEuint64());
}
The ERC20
token is simply transferred in, which would revert on failure. We then transfer out the correct amount of the ConfidentialFungibleToken
using the internal _mint
function, which is guaranteed to succeed.
Swap ConfidentialFungibleToken
to ConfidentialFungibleToken
Swapping from a confidential ConfidentialFungibleToken
to another confidential ConfidentialFungibleToken
is a bit more complex although quite simple given the usage of the TFHE
library. For the sake of the example, we will swap from fromToken
to toToken
with a 1:1 exchange rate.
function swapConfidentialForConfidential(
IConfidentialFungibleToken fromToken,
IConfidentialFungibleToken toToken,
einput amountInput,
bytes calldata inputProof
) public virtual {
require(fromToken.isOperator(address(this), msg.sender));
euint64 amount = TFHE.asEuint64(amountInput, inputProof);
TFHE.allowTransient(amount, address(fromToken));
euint64 amountTransferred = fromToken.confidentialTransferFrom(msg.sender, address(this), amount);
TFHE.allowTransient(amountTransferred, address(toToken));
toToken.confidentialTransfer(msg.sender, amountTransferred);
}
}
The steps are as follows:
-
Check operator approval
-
Allow the
fromToken
to accessamount
-
Transfer from
from
to this contract foramount
-
Allow the
toToken
to accessamountTransferred
-
Transfer
amountTransferred
tomsg.sender
Swap ConfidentialFungibleToken
to ERC20
Swapping from a confidential token to a non-confidential token is the most complex since the decrypted data must be accessed to accurately complete the request. Decryption in our example will be done off-chain and relayed back using Zama’s Gateway. Below is an example of a contract doing a 1:1 swap from a confidential token to an ERC20 token.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {TFHE, einput, euint64} from "fhevm/lib/TFHE.sol";
import {Gateway} from "fhevm/gateway/lib/Gateway.sol";
import {GatewayCaller} from "fhevm/gateway/GatewayCaller.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IConfidentialFungibleToken} from "@openzeppelin/confidential-contracts/interfaces/IConfidentialFungibleToken.sol";
contract SwapConfidentialToERC20 is GatewayCaller {
using TFHE for *;
error SwapConfidentialToERC20InvalidGatewayRequest(uint256 requestId);
mapping(uint256 requestId => address) private _receivers;
IConfidentialFungibleToken private _fromToken;
IERC20 private _toToken;
constructor(IConfidentialFungibleToken fromToken, IERC20 toToken) {
_fromToken = fromToken;
_toToken = toToken;
}
function swapConfidentialToERC20(einput encryptedInput, bytes memory inputProof) public {
euint64 amount = encryptedInput.asEuint64(inputProof);
amount.allowTransient(address(_fromToken));
euint64 amountTransferred = _fromToken.confidentialTransferFrom(msg.sender, address(this), amount);
uint256[] memory cts = new uint256[](1);
cts[0] = euint64.unwrap(amountTransferred);
uint256 requestID = Gateway.requestDecryption(
cts,
this.finalizeSwap.selector,
0,
block.timestamp + 1 days, // Max delay is 1 day
false
);
// register who is getting the tokens
_receivers[requestID] = msg.sender;
}
function finalizeSwap(uint256 requestID, uint64 amount) public virtual onlyGateway {
address to = _receivers[requestID];
require(to != address(0), SwapConfidentialToERC20InvalidGatewayRequest(requestID));
delete _receivers[requestID];
if (amount != 0) {
SafeERC20.safeTransfer(_toToken, to, amount);
}
}
}