ERC7984
ERC7984
is a standard fungible token implementation that is similar to ERC-20, but built from the ground up with confidentiality in mind. All balance and transfer amounts are represented as ciphertext 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 an 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 ciphertextamount
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 ciphertext using fhevm-js. If the ciphertext is a new value, or the sender does not have permission to access the ciphertext, an input-proof must be provided to show that the sender knows the value of the ciphertext.
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 IERC7984Receiver
. 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 {FHE, externalEuint64, ebool, euint64} from "@fhevm/solidity/lib/FHE.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC7984} from "@openzeppelin/confidential-contracts/token/ERC7984/ERC7984.sol";
contract ERC7984MintableBurnable is ERC7984, Ownable {
using FHE for *;
constructor(
address owner,
string memory name,
string memory symbol,
string memory uri
) ERC7984(name, symbol, uri) Ownable(owner) {}
function mint(address to, externalEuint64 amount, bytes memory inputProof) public onlyOwner {
_mint(to, amount.fromExternal(inputProof));
}
function burn(address from, externalEuint64 amount, bytes memory inputProof) public onlyOwner {
_burn(from, amount.fromExternal(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 ERC7984
Swapping from a non-confidential ERC20
to a confidential ERC7984
is simple and actually done within the ERC7984ERC20Wrapper
. 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 ERC7984
using the internal _mint
function, which is guaranteed to succeed.
Swap ERC7984
to ERC7984
Swapping from a confidential ERC7984
to another confidential ERC7984
is a bit more complex although quite simple given the usage of the FHE
library. For the sake of the example, we will swap from fromToken
to toToken
with a 1:1 exchange rate.
function swapConfidentialForConfidential(
IERC7984 fromToken,
IERC7984 toToken,
externalEuint64 amountInput,
bytes calldata inputProof
) public virtual {
require(fromToken.isOperator(msg.sender, address(this)));
euint64 amount = FHE.fromExternal(amountInput, inputProof);
FHE.allowTransient(amount, address(fromToken));
euint64 amountTransferred = fromToken.confidentialTransferFrom(msg.sender, address(this), amount);
FHE.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 ERC7984
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.
Unresolved include directive in modules/ROOT/pages/token.adoc - include::api:example$SwapConfidentialToERC20.sol[]