Abstract

This ERC proposes a specification for an opaque token that enhances privacy by concealing balance information. Privacy is achieved by representing balances as off-chain data encapsulated in hashes, referred to as “baskets”. These baskets can be reorganized, transferred, and managed through token functions on-chain.

Motivation

Smart contract accounts serve as well-defined identities that can have reusable claims and attestations attached to them, making them highly useful for various applications. However, this strength also introduces a significant privacy challenge when these identities are used to hold tokens. Specifically, in the case of ERC-20 compatible tokens, where balances are stored directly on-chain in plain text, the transparency of these balances can compromise the privacy of the account holder. This creates a dilemma: while the reuse of claims and attestations tied to a smart contract account can be advantageous, it also increases the risk of exposing sensitive financial information, particularly when these well-defined identities are associated with publicly visible token holdings.

This proposal aims to conceal balances on-chain, allowing the use of smart contract accounts to hold tokens without compromising privacy or integrity.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

The concept revolves around representing token balances on-chain as hashed values, called baskets, which obscure the actual balance information. These baskets combine a random salt, a unique token ID, and the token’s value, making it impossible to derive the token’s value directly from the blockchain.

The token interface allows for creating, transferring, minting, and reorganizing (splitting and joining) these baskets. To prevent unauthorized changes and maintain integrity, oracle services verify that the total value of baskets remains consistent during reorganizations. Additionally, differential privacy techniques, such as overlaying noise and empty transfers, further protect privacy by making it difficult to trace token movements and determine actual transaction details.

Baskets

Balances are represented on-chain as hashes of the form:

keccak256(abi.encode(salt, tokenId, value))

// where  salt (bytes32)    - random 32bytes to increase the entropy and
//                            make brute-forcing the hash impossible
//        tokenId (bytes32) - a unique tokenId within token's smart contract instance
//        value (uint256)   - the value of the position

For the remainder of this document, we refer to these hashes as “baskets” because they conceal the balance information in an opaque manner, similar to how a covered basket hides its contents.

Token Interface

An opaque token MUST implement the following interface.

interface OpaqueToken {
  //
  // TYPES
  //

  struct SIGNATURE {
    uint8 v; bytes32 r; bytes32 s;
  }

  struct ORACLECONFIG {
    uint8 minNumberOfOracles; // min. number of oracle signatures required for reorg
    address[] oracles;        // valid oracles
  }

  //
  // EVENTS
  //

  event CreateToken(address initiatedBy, bytes32 tokenId, bytes32 totalSupplyBasket, bytes32 ref);
  event Mint(address initiatedBy, bytes32[] baskets, bytes32 ref);
  event ReorgHolderBaskets(address initiatedBy, bytes32[] basketsIn, bytes32[] basketsOut, bytes32 ref);
  event ReorgSupplyBaskets(address initiatedBy, bytes32[] basketsIn, bytes32[] basketsOut, bytes32 ref);
  event Transfer(address initiatedBy, address receiver, bytes32[] baskets, bytes32 ref);
  event Burn(address initiatedBy, bytes32[] baskets, bytes32 ref);

  //
  // FUNCTIONS
  //

  /**
   * @dev returns the configuration for this token
   */
  function oracleConfig() external view returns (ORACLECONFIG memory);

  /**
   * @dev returns the address of the basket owner
   */
  function owner(bytes32 basket) external view returns (address);

  /**
   * @dev returns the total supply for a `tokenId``
   * All token investors are allowed to fetch this value from the token operator's off-chain storage.
   */
  function totalSupply(bytes32 tokenId) external view returns (bytes32);

  /**
   * @dev returns the operator of this token, who is also responsible for providing the main
   * off-chain storage source.
   */
  function operator() external view returns (address);
  
  /**
   * @dev Allows the token operator to create a new token with the specified `tokenId` and an initial 
   * `totalSupplyBasket`. The `totalSupplyBasket` can be partitioned using {reorgSupplyBaskets} as needed 
   * when calling {mint}. The `ref` parameter can be used freely by the caller for any reference purpose.
   */
  function createToken(
      bytes32 tokenId,
      bytes32 totalSupplyBasket,
      bytes32 ref
  ) external;

  /**
   * @dev Allows the token operator to mint tokens by assigning `supplyBaskets` to a `receiver` which 
   * becomes the owner of these baskets. 
   */
  function mint(
      bytes32[] calldata supplyBaskets,
      address receiver,
      bytes32 ref
  ) external;
  
  /**
   * @dev transfers `baskets` to a `receiver` who becomes the new owner of these baskets. 
   */
  function transfer(
      bytes32[] calldata baskets,
      address receiver,
      bytes32 ref
  ) external;

  /**
   * @dev reorganizes a set of holder baskets (`basketsIn`) to a new set (`basketsOut`) having
   * the same value, i.e., the sum of all values from input baskets equals the sum of values
   * in output baskets. In order to ensure the integrity, external oracle service is required that
   * will sign the reorg proposal requested by the basket owner, which is passed as `reorgOracleSignatures`.
   * The minimum number of oracle signatures is defined in the oracle configuration.
   */
  function reorgHolderBaskets(
      SIGNATURE[] calldata reorgOracleSignatures,
      bytes32[] calldata basketsIn,
      bytes32[] calldata basketsOut,
      bytes32 ref
  ) external;

  /**
   * @dev same as {reorgHolderBaskets}, but for the available supply baskets.
   */
  function reorgSupplyBaskets(
      SIGNATURE[] calldata reorgOracleSignatures,
      bytes32[] calldata basketsIn,
      bytes32[] calldata basketsOut,
      bytes32 ref
  ) external;

  /**
   * @dev burns holder's `baskets` and returns them to available supply
   */
  function burn(
      bytes32[] calldata baskets,
      bytes32 ref
  ) external;
}

Off-chain Data Endpoints

  • The operator of the token (e.g., issuer or registrar) MUST provide the off-chain storage that implements the GET basket and PUT basket REST endpoints as described in this section.
  • The operator MUST ensure the availability of the basket data and will share it on need-to-know basis with all eligible holders, i.e., with all address that either were holding the basket in the past or are currently the holder of the basket.
  • To ensure data is only shared with and can be written by eligible holders, the operator MUST implement authentication for both endpoints. The concrete authentication schema is not specified here and my depend on the environment of the token operator.
  • The operator MUST allow an existing token holder to PUT basket
  • The operator MUST allow the current or historical basket holder to GET basket
  • Token holders SHOULD store a copy of the data about their own baskets in their own off-chain storage for the case that operator’s service is unavailable.

REST API Endpoints for creating and querying baskets:

  Endpoint: PUT baskets
  Description: will store baskets if the `basket` hash is matching `data`.
  PostData: 
  [
    {
      basket: keccak256(abi.encode(salt, tokenId, value)),
      data: {
        salt: <bytes32>,
        tokenId: <bytes32>,
        value: <uint256>
      }
    },
    ...
  ]

  Endpoint: GET baskets?basket-hash=<bytes32>
  Description: will return the list of baskets depending on the query parameters.
  Query Parameters:
    - basket-hash (optional): returns one basket matching the requested hash
    - if no query parameter is set, then the endpoint will return all baskets of the requestor
  Response:
  [
    {
      basket: keccak256(abi.encode(salt, tokenId, value)),
      data: {
        salt: <bytes32>,
        tokenId: <bytes32>,
        value: <uint256>
      }
    },
    ...
  ]

reorg Endpoint

To ensure the integrity of a reorg and avoid accidental or fraudulent mints or burns, an oracle services is required.

  • Oracles MUST provide a POST reorg REST Endpoint as described in this section
  • Oracles MUST sign any reorg proposal request where
    • the sum of values in input baskets grouped by tokenId is equal the sum of values of the output baskets grouped by tokenId.
    • item.basket hash matches keccak256(abi.encode(data.salt, data.tokenId, data.value))
  • The reorg endpoint MUST be stateless
  • Oracle MUST NOT persist data from the request for later analysis.
  • The reorg endpoint SHOULD NOT require authentication and can be used by anyone without restrictions.
Endpoint: POST reorg
PostData:
{
  in: [
    {
      basket: keccak256(abi.encode(salt, tokenId, value)),
      data: {
        salt: <bytes32>,
        tokenId: <bytes32>,
        value: <uint256>
      }
    },
    ...
  ],
  out: [
    {
      basket: keccak256(abi.encode(salt, tokenId, value)),
      data: {
        salt: <bytes32>,
        tokenId: <bytes32>,
        value: <uint256>
      }
    },
    ...
  ]
}

Response: {
    // hash is signed with oracles private key
    // basketsIn and basketsIn are bytes32[]
    signature: sign(keccak256(abi.encode(basketsIn, basketsOut)))
}

Example for valid reorg requests (salt and hashes are omitted for better readability):

in : (..., token1, 10), (..., token1, 30), (..., token2, 5), (..., token2, 95)
out: (..., token1, 40), (..., token2, 100)

in : (..., token1, 40), (..., token2, 100)
out: (..., token1, 10), (..., token1, 30), (..., token2, 5), (..., token2, 95)

Overlaying Noise (Differential Privacy)

To further enhance privacy and obscure transaction details, an additional layer of noise need to be introduced through reorgs and empty transfers. For example, received baskets can be reorganized into new baskets to prevent information leakage to the previous owner. Additionally, null-value baskets can be sent to random receivers (empty transfers), making it difficult for observers to determine who is transferring to whom.

Example with reorg and null-value basket transfers:

A owns basket-a1{..., value:10}
B owns basket-b1{..., value:5}, basket-b2{..., value:15}, ...
A: transfer basket-a1 to B
B: reorg [basket-a1, basket-b1, basket-b2]
      to [basket-b3{..., value:10}, basket-b4{..., value:10}, basket-b5:{..., value:10},
            basket-b6:{..., value:0}, basket-b7:{..., value:0}]
      where sum of inputs is the sum of outputs
B: transfer basket-b5{value:10} to C
B: transfer basket-b6{value:0}  to D
B: transfer basket-b7{value:0}  to E

If B would directly send basket-a1 to C, A would know what C is receiving, however, now that B has reorg’ed the baskets, A can not know anymore what has been sent to C.

Moreover, observers still see who is communicating with whom, but since there is noise introduced, they can not tell which of these transfers are actually transferring real values.

Rationale

Breaking the ERC-20 Compatibility

The transparency inherent in ERC-20 tokens presents a significant issue for reusable blockchain identities. To address this, we prioritize privacy over ERC-20 compatibility, ensuring the confidentiality of token balances.

Reorg Oracles

The trusted oracles and the minimum number of required signatures can be configured to achieve the desired level of decentralization.

The basket holder proposes the input and output baskets for the reorg, while the oracles are responsible for verifying that the sums of the values on both sides (input and output) are equal. This system allows for mutual control, ensuring that no single party can manipulate the process.

Fraudulent oracles can be tracked back on-chain, i.e., the system ensures weak-integrity at minimum.

To further strengthen the integrity, it would also be possible to apply Zero-Knowledge Proofs (ZKP) to provide reorg proofs, however, we have chosen to use oracles for efficiency and simplicity reasons.

Off-chain Data Storage

We have chosen the token operator, which in most cases will be the issuer or registrar, as the initial and main source for off-chain data. This is acceptable, since they must know anyway which investor holds which positions to manage lifecycle events on the token. While this approach may not be suitable for every use case within the broader Ethereum ecosystem, it fits well the financial instruments in the regulated environment of the financial industry, which rely on strict KYC and token operation procedures.

Backwards Compatibility

  • Opaque Token is not compatible with ERC-20 for reasons explained in the Rationale section.

Security Considerations

Fraudulent Oracles

Oracles Collecting Confidential Data

Confidential Data Loss

Copyright and related rights waived via CC0.