New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zkERC20: Confidential Token Standard #1724

Open
zac-williamson opened this Issue Jan 25, 2019 · 16 comments

Comments

Projects
None yet
@zac-williamson
Copy link

zac-williamson commented Jan 25, 2019

eip: 1724
title: Confidential Token Standard
author: AZTEC
discussions-to: https://github.com/ethereum/EIPs/issues/1724
status: Draft
type: Standards Track
category: ERC
created: 2019-01-25
requires: 1723

Simple Summary

This EIP defines the standard interface and behaviours of a confidential token contract, where ownership values and the values of transfers are encrypted.

Abstract

This standard defines a way of interacting with a confidential token contract. Confidential tokens do not have traditional balances - value is represented by notes, which are composed of a public owner and an encrypted value. Value is transferred by splitting a note into multiple notes with different owners. Similarly notes can be combined into a larger note. Note splitting is analogous to the behaviour of Bitcoin UTXOs, which is a good mental model to follow.

These "join-split" transactions must satisfy a balancing relationship (the sum of the values of the old notes must be equal to the sum of the values of the new notes) - this can be proven via a zero-knowledge proof.

This EIP was modelled on the zero-knowledge proofs enabled by the AZTEC protocol. However this specification is not specific to AZTEC and alternative technologies can be used to implement this standard, such as Bulletproofs or a zk-SNARK-based implementation

Motivation

The ability to transact in confidentiality is a requirement for many kinds of financial instruments. The motivation of this EIP is to establish a standard that defines how these confidential assets are constructed and traded. Similar to an ERC20 token, if confidential tokens conform to the same interface then this standard can be re-used by other on-chain applications, such as confidential decentralized exchanges or confidential escrow accounts.

The zkERC20 token interface is designed such that the economic beneficiary of any transaction is completely divorced from the transaction sender. This is to facilitate the use of one-time stealth addresses to "own" zero-knowledge notes. Such addresses will not easily be fundable with gas to pay for transactions (without leaking information). Creating a clear separation between the transaction sender and the economic beneficiary allows third party service layers to be tasked with the responsibility to sign transactions.

Specification

An example zkERC20 token contract

interface zkERC20 {
    event LogCreateConfidentialNote(address indexed _owner, bytes _metadata);
    event LogDestroyConfidentialNote(address indexed _owner, bytes32 _noteHash);

    function cryptographyEngine() external view returns (address);
    function confidentialIsApproved(address spender, bytes32 noteHash) external view returns (bool);
    function confidentialTotalSupply() external view returns (uint256);
    function publicToken() external view returns (address);
    function supportsProof(uint16 _proofId) external view returns (bool);
    function scalingFactor() external view returns (uint256);

    function confidentialApprove(bytes32 _noteHash, address _spender, bool _status, bytes _signature) public;
    function confidentialTransfer(bytes _proofData) public;
    function confidentialTransferFrom(uint16 _proofId, bytes _proofOutput) public;
}

The token contract must implement the above interface to be compatible with the standard. The implementation must follow the specifications described below.

The fundamental unit of 'value' in a zk-ERC20: the zero-knowledge note

Unlike traditional balances, value is represented via an UXTO-style model represented by notes. A note has the following public information:

  • A public key, that contains an encrypted representation of the note's value
  • The Ethereum address of the note's 'owner'
  • Note metadata - additional data required by the note owner, but is not used in any smart-contract logic

A note has the following private information:

  • A viewing key, which can be used to decrypt the note
  • A spending key
  • A value - a representation of the number of tokens this note contains

Public notes, private values: rationale behind the note construct

In order to enable cross-asset interoperability, we can hide the notionals in a given transaction, however what is being transacted is public, as well as the Ethereum addresses of the transactors.

This is to enable a high degree of interoperability between zero-knowledge assets - it is difficult to design a zero-knowledge DApp if one cannot identify the asset class of any given note.

The owner field of a note is public for ease-of-use as we want traditional Ethereum private keys to be able to sign against zero-knowledge notes, and zero-knowledge spending proofs. One can use a Monero-style stealth address protocol to ensure that the Ethereum address of a note's owner contains no identifying information about the note's true owner.

The zero-knowledge note registry

A token that conforms to the zkERC20 standard must have a method of storing the token's set of unspent zero-knowledge notes. The Cryptography Engine identifies notes with the following tuple:

  1. A bytes32 noteHash variable, a keccak256 hash of a note's encrypted data
  2. A address owner variable, an address that defines a note's owner
  3. A bytes notedata variable, the notedata is a combination of the note's public key and the note metadata. When implemented using the AZTEC protocol, secp256k1 and bn128 group elements that allows a note owner to recover and decrypt the note.

An example implementation of zkERC20 represents this as a mapping from noteHash to owner: mapping(bytes32 => address) noteRegistry;. The metadata is required for logging purposes only, the noteHash and owner variables alone are enough to define a unique note.

View Functions

cryptographyEngine

function cryptographyEngine() view returns (address)

This function returns the address of the smart contract that validates this token's zero-knowledge proofs. For the specification of the Cryptography Engine, please see this ERC.

returns: address of the cryptography engine that validates this token's zero-knowledge proofs

publicToken

function publicToken() view returns (address)

This function returns the address of the public token that this confidential token is attached to. The public token should conform to the ERC20 token standard. This link enables a user to convert between an ERC20 token balance and confidential zkERC20 notes.

If the token has no public analogue (i.e. it is a purely confidential token) this method should return 0.

returns: address of attached ERC20 token

supportsProof

function supportsProof(uint16 _proofId) view returns (bool)

This function returns whether this token supports a specific zero-knowledge proof ID. The Cryptography Engine can support a number of zero-knowledge proofs. The token creator may wish to only support a subset of these proofs.

The rationale behind using a uint16 variable is twofold:

  1. The total number of proofs supported by the engine will never grow to be larger than 65535
  2. The purpose of the engine is to define a "grammar" of composable zero-knowledge proofs that can be used to define the semantics of confidential transactions and the total set will be quite small. Using an integer as a proofID allows for a simple bit-filter to validate whether a proof is supported or not (TODO put somewhere else).

returns: boolean that defines whether a proof is supported by the token

confidentialTotalSupply

function confidentialTotalSupply() view returns (uint256);

This function returns the total sum of tokens that are currently represented in zero-knowledge note form by the contract. This value must be equal to the sum of the values of all unspent notes, which is validated by the Cryptography Engine. Note that this function may leak privacy if there's only one user of the zkERC20 contract instance.

returns: the combined value of all confidential tokens

scalingFactor

function scalingFactor() view returns (uint256)

This function returns the token scalingFactor. The range of integers that can be represented in a note is likely smaller than the native word size of the EVM (~30 bits vs 256 bits). As a result, a scaling factor is applied when converting between public tokens and confidential note form. An ERC20 token value of 1 corresponds to an zkERC20 value of scalingFactor.

returns: the multiplier used when converting between confidential note values and public tokens

Approving addresses to transact zero-knowledge notes

For confidential transactions to become truly useful, it must be possible for smart contracts to transact notes on behalf of their owners. For example a confidential decentralized exchange or a confidential investment fund. These transactions still require zero-knowledge proofs that must be constructed on-chain, but they can be constructed on behalf of note owners and validated against ECDSA signatures signed by note owners.

To this end, a confidentialApprove method is required to delegate.

confidentialApprove

function confidentialApprove(bytes32 _noteHash, address _spender, bool _status, bytes _signature)

This function allows a note owner to approve the address approved to "spend" a zero-knowledge note in a confidentialTransferFrom transaction.

parameters
_noteHash: the hash of the note being approved
_sender: the address of the entity being approved
_status: defines whether approved is being given permission to spend a note, or if permission is being revoked
_signature: ECDSA signature from the note owner that validates the confidentialApprove instruction

Confidential Transfers

The action of sending confidential notes requires a zero-knowledge proof to be validated by the Cryptography Engine that a given zk-ERC20 contract listens to. The semantics of this proof will vary depending on the proof ID. For example, the zero-knowledge proof required to partially fill an order between two zero-knowledge assets and the zero-knowledge proof required for a unilateral "join-split" transaction are different proofs, with different validation logic. Every proof supported by the Cryptography Engine will share the following common feature:

  • A balancing relationship has been satisfied - the sum of the values of the notes "to be created" equals the sum of the values of the notes "to be spent"

To validate a zero-knowledge proof, the token smart contract must call the Cryptography Engine's validateProof(uint16 _proofId, bytes _proofData) public returns (bytes32[] _destroyedNotes, Note[] _createdNotes, address _publicOwner, int256 _publicValue) function. This method will throw an error if the proof is invalid. If the proof is valid, the following data is returned:

createdNotes: the array of notes the proof wishes to create
destroyedNotes: the array of notes the proof wishes to destroy
publicOwner: if a public conversion is required, this is the address of the public token holder
publicValue: if a public conversion is required, this is the amount of tokens to be transferred to the public token holder. Can be negative, which represents a conversion from the public token holder to the zkERC20 contract

The structure of Note is the following:

struct Note {
    address owner;
    bytes32 noteHash;
    bytes noteData;
}

The above information can be used by the zkERC20 token to validate the legitimacy of a confidential transfer.

Direct Transactions

Basic "unilateral" transfers of zero-knowledge notes are enabled via a "join-split"-style transaction, accessed via the confidentialTransfer method.

confidentialTransfer

function confidentialTransfer(bytes proofData)

This function is designed as an analogue to the ERC20 transfer method.

To enact a confidentialTransfer method call, the token contract must check and perform the following:

  1. Successfully execute cryptographyEngine.validateProof(1, proofData)
    • If this proof is valid, then for every note being consumed in the transaction, the note owner has provided a satisfying ECDSA signature
  2. Examine the output of cryptographyEngine.validateProof (createdNotes, destroyedNotes, publicOwner, publicValue) and validate the following:
    1. Every Note in destroyedNotes exists in the token's note registry
    2. Every Note in createdNotes does not exist in the token's note registry

If the above conditions are satisfied, the following steps must be performed:

  1. If publicValue < 0, call erc20Token.transferFrom(publicOwner, this, uint256(-publicValue)). If this call fails, abort the transaction
  2. If publicValue > 0, call erc20Token.transfer(publicOwner, uint256(publicValue))
  3. Update the token's total confidential supply to reflect the above transfers
  4. For every Note in destroyedNotes, remove Note from the token's note registry and emit LogDestroyConfidentialNote(Note.owner, Note.noteHash)
  5. For every Note in createdNotes, add Note to the token's note registry and emit LogCreateConfidentialNote(Note.owner, Note.metadata)
  6. Emit the ConfidentialTransfer event.

Autonomous Transactions

For more exotic forms of transfers, mediated by smart contracts, the confidentialTransferFrom method is used.

confidentialTransferFrom

function confidentialTransferFrom(uint16 _proofId, bytes _proofOutput);

This function enacts a confidential transfer of zero-knowledge notes. This function is designed as an analogue to the ERC20 transferFrom method, to be called by smart contracts that enact confidential transfers.

Instead of supplying a zero-knowledge proof to be validated, this method is supplied with a transfer instruction that was generated by the Cryptography Engine that this asset listens to. This is to aid in preventing redundant validation of zero-knowledge proofs - some types of proof produce multiple transfer instructions (e.g. a bilateral-swap style proof in the Cryptography Engine standard).

The bytes _proofOutput variable MUST conform to the specification of a 'proof output' from the Cryptography Engine standard. A valid _proofOutput will contain the following data: bytes inputNotes, bytes outputNotes, address publicOwner, int256 publicValue

To enact a confidentialTransferFrom method call, the token contract must check and perform the following:

  1. The proofId must correspond to a proof supported by the token
  2. Construct the bytes32 proofHash, a keccak256 hash of bytes _proofOutput
  3. Call cryptographyEngine.validateProofByHash(proofId, proofHash, msg.sender)
  4. If validateProofByHash returns false the transaction MUST throw
  5. If validateProofByHash returns true, the following MUST be validated
    1. Every Note in inputNotes exists in the token's note registry
    2. Every Note in outputNotes does not exist in the token's note registry
    3. For every Note in outputNotes, confidentialIsApproved(noteHash, owner) returns true

If the above conditions are satisfied, the following steps must be performed:

  1. If publicValue < 0, call erc20Token.transferFrom(publicOwner, this, uint256(-publicValue)). If this call fails, abort the transaction
  2. If publicValue > 0, call erc20Token.transfer(publicOwner, uint256(publicValue))
  3. Update the token's total confidential supply to reflect the above transfers
  4. For every Note in destroyedNotes, remove Note from the token's note registry and emit LogDestroyConfidentialNote(Note.owner, Note.noteHash)
  5. For every Note in createdNotes, add Note to the token's note registry and emit LogCreateConfidentialNote(Note.owner, Note.metadata)
  6. Emit the LogConfidentialTransfer event.

Events

LogCreateConfidentialNote

event LogCreateConfidentialNote(address indexed _owner, bytes_metadata)

An event that logs the creation of a note against the note owner and the note metadata.

parameters
_owner: The Ethereum address of the note owner
_metadata: Data required by the note owner to recover and decrypt their note

LogDestroyConfidentialNote

event LogDestroyConfidentialNote(address indexed owner)

An event that logs the destruction of a note against the note owner and the note metadata.

parameters
_owner: The ethereum address of the note owner
_noteHash: The hash of the note. Note metadata can be recovered from the LogDestroyConfidentialNote event that created this note

Implementation

Head to the AZTEC monorepo for a work in progress implementation. Many thanks to @PaulRBerg, @thomas-waite, @ArnSch and the @AztecProtocol team for their contributions to this document.

Copyright

Work released under LGPL-3.0.

@ligi

This comment has been minimized.

Copy link
Member

ligi commented Jan 29, 2019

nice! Just a nit - there is a missing closing (or one too many opening) bracket in this:

This function returns the address of the smart contract that validates this token's zero-knowledge proofs. For the specification of the [AZTEC Cryptography Engine, please see this ERC.

@davidp94

This comment has been minimized.

Copy link

davidp94 commented Jan 29, 2019

Nice job!

Could there be multiple crytographic engine to achieve "zk" ERC20?

If so, I would not name it zkERC20 - as we have a aztecCryptographyEngine interface.

@markusj1201

This comment has been minimized.

Copy link

markusj1201 commented Jan 29, 2019

@zac-williamson

This comment has been minimized.

Copy link
Author

zac-williamson commented Jan 29, 2019

nice! Just a nit - there is a missing closing (or one too many opening) bracket in this:

This function returns the address of the smart contract that validates this token's zero-knowledge proofs. For the specification of the [AZTEC Cryptography Engine, please see this ERC.

Thanks for spotting that! Fixed.

@markusj1201

This comment has been minimized.

Copy link

markusj1201 commented Jan 29, 2019

@fubuloubu

This comment has been minimized.

Copy link
Member

fubuloubu commented Jan 30, 2019

Why do LogCreateConfidentialNote and LogDestroyConfidentialNote both start with Log? Can Log be removed?

I'm also curious if Confidential is strictly necessary to differentiate the two events from each other?

@ligi

This comment has been minimized.

Copy link
Member

ligi commented Jan 30, 2019

Name suggestions?

zkatERC20

@aimanbaharum

This comment has been minimized.

Copy link

aimanbaharum commented Jan 30, 2019

ZERC20

@adamdossa

This comment has been minimized.

Copy link

adamdossa commented Jan 30, 2019

re. starting events with Log - IIRC this was the advice previously to distinguish function calls from emitting events. Now that we have the emit keyword I don’t really think this is necessary anymore.

Changing aztecCryptographyEngine to just cryptographyEngine would make the standard a little more general.

Some questions / thoughts:

  1. function publicToken() implies that this will always be a wrapper around a non-confidential token. Will this always be the case, or would it be possible to have a token where all transfers are confidential and / or a token which extends both ERC20 and ERC1724 to provide confidential and non-confidential functionality in a single contract?

  2. Should confidentialTotalSupply() just be totalSupply()? AFAICT this isn’t confidential and is just the balance of the publicToken held by the confidential token wrapper? Not sure how / why this would be validated via a ZK proof?

  3. Would it be possible to approve only a balance up to a certain amount confidentially using a public/private-range proof? Should the standard support this?

  4. Should LogCreateConfidentialNote explicitly log out the _noteHash? I note that in the Specification LogDestroyConfidentialNote includes _noteHash whilst this is not included in the Event description further down.

@PaulRBerg

This comment has been minimized.

Copy link
Contributor

PaulRBerg commented Jan 30, 2019

Could there be multiple crytographic engine to achieve "zk" ERC20?

If so, I would not name it zkERC20 - as we have a aztecCryptographyEngine interface.

Yes, there could be multiple methodologies (cryptography engines) validating the zero-knowledge-ness of the token standards, that is, zkSNARKs or Bulletproofs. This is why we'd keep zkERC20.

Why do LogCreateConfidentialNote and LogDestroyConfidentialNote both start with Log? Can Log be removed?

I'm also curious if Confidential is strictly necessary to differentiate the two events from each other?

Regarding the Log prefix, we followed the ConsenSys smart contract best practices. Keeping Confidential makes the nomenclature more descriptive.

Changing aztecCryptographyEngine to just cryptographyEngine would make the standard a little more general.

Sounds good! For this and all other questions raised by @adamdossa, @zac-williamson will write a reply soon.

@adamdossa

This comment has been minimized.

Copy link

adamdossa commented Jan 30, 2019

It's a very minor point, but for the Log... events, the Consensys best practices only applies to Solidity versions earlier than 0.4.21 as I read it (before emit was introduced):
"Differentiate functions and events (Solidity < 0.4.21)"

Obviously standards don't really include a Solidity version, so a bit of grey area.

@fubuloubu

This comment has been minimized.

Copy link
Member

fubuloubu commented Jan 30, 2019

cc @maurelian ^

@maurelian

This comment has been minimized.

Copy link
Contributor

maurelian commented Jan 30, 2019

Ya, the “emit” keyword makes the Log prefix unnecessary, thanks for the ping @fubuloubu

@HarryR

This comment has been minimized.

Copy link

HarryR commented Jan 30, 2019

This is very 'AZTEC' specific, and doesn't account for any other protocols.

What are the options to support Bulletproofs, tokens like Miximus and zkDAI (which use zkSNARK proofs), mixers like Möbius etc.?

It's not really a standard if it enforces one specific algorithm and is unusable by any others... because of the semantics imposed by it being over-fitted for your project.

How can this be made more agnostic to the underlying mechanisms? To be implementation agnostic...

I see no reason why the ERC20 interfaces (or ERC-223, ERC-721 and subsequent improvements etc.) can't be used to implement the basic functionality of anonymity tokens. If anything, the ERC-721 interface is probably the more viable candidate to implement an anonymity token, but this proposed standard doesn't take that into account.

Can you please re-consider your proposal, and see how you can implement this using existing standards with a wide range of wallet and tooling support (e.g. ERC-20, ERC-721)?

@zac-williamson

This comment has been minimized.

Copy link
Author

zac-williamson commented Jan 31, 2019

This is very 'AZTEC' specific, and doesn't account for any other protocols.

What are the options to support Bulletproofs, tokens like Miximus and zkDAI (which use zkSNARK proofs), mixers like Möbius etc.?

It's not really a standard if it enforces one specific algorithm and is unusable by any others... because of the semantics imposed by it being over-fitted for your project.

How can this be made more agnostic to the underlying mechanisms? To be implementation agnostic...

I see no reason why the ERC20 interfaces (or ERC-223, ERC-721 and subsequent improvements etc.) can't be used to implement the basic functionality of anonymity tokens. If anything, the ERC-721 interface is probably the more viable candidate to implement an anonymity token, but this proposed standard doesn't take that into account.

Can you please re-consider your proposal, and see how you can implement this using existing standards with a wide range of wallet and tooling support (e.g. ERC-20, ERC-721)?

Hi HarryR,

You're right that the language of the EIP leans heavily on AZTEC - we used AZTEC's zero-knowledge proof system as the scaffolding around which to construct this EIP, but the proposal is actually quite general and can accomodate implementations that use a variety of zero-knowledge technologies. Any zk-proof system that can validate 'join-split' style transfers involving zero-knowledge notes can be used to build a Cryptography Engine and implement this standard. I have changed the language of the EIP to reflect this.

To answer your question about the unique interface - the goal was to create an interface that is very similar to the ERC20 interface but still distinct. This is because a digital asset can have both an ERC20 representation and a confidential representation that conforms to this EIP. The EIP adds 'backwards-compatible' confidentiality - zkERC20 representations of existing tokens can be created.

The goal of this EIP is to define a fungible confidential token - where the values being transacted are encrypted, but the addresses of the transactors are not neccesarily hidden (this can be added on-top of the standard through anonymous addresses or note mixing). An ERC-721-style interface does not suit this purpose due to the non-fungible nature of an ERC-721 token. Similarly, existing token interfaces are predicated on knowing the values within a transaction, making them incompatible with such a confidential token.

@banteg

This comment has been minimized.

Copy link

banteg commented Jan 31, 2019

consider changing the name to something like zk-note, it would reflect how it works much better and would remove unnecessary confusion about compatibility with erc-20. it's a very different interface and it deserves a distinct name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment