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 and transferFrom: transfer moves tokens from the sender while transferFrom moves tokens from a specified from address. See operator.

  • With and without inputProof: An inputProof can be provided to prove that the sender knows the value of the ciphertext amount 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:

  1. Check operator approval

  2. Allow the fromToken to access amount

  3. Transfer from from to this contract for amount

  4. Allow the toToken to access amountTransferred

  5. Transfer amountTransferred to msg.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[]