Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
tip: 135 
title: Shielded TRC-20 Contract 
author: federico<federico.zhen@tron.network>, leo<leo.hou@tron.network>
discussions-to: https://github.com/tronprotocol/tips/issues/135
status: Final
type: Standards Track
category: TRC
created: 2020-03-04

Simple Summary

This TIP provides the contract implementation of transforming public TRC-20 token to shielded token, which can guarantee the privacy of token ownership and transactions.

Abstract

The shielded TRC-20 contract has three core modules: mint, transfer and burn. mint is used to transform the public TRC-20 token to shielded token, which makes token ownership invisible. transfer is used for shielded token transactions, which can hide the source address, the destination address, and the transaction amount. burn is used to transform the shielded token to the public TRC-20 token. The technical implementation is based on zk-SNARK proof system, which is secure and efficient.

Motivation

TRC-20 token contract allows users to issue and transfer tokens, but it can not guarantee the privacy since it leaks the token ownership. When transferring the token, the source address, destination address, and the token amount are public. The shielded TRC-20 contract aims to solve this problem and provides users better privacy of token ownership and transactions.

Specification

The following defines the implementation of the shielded TRC-20 contract.

Preliminaries

NoteCommitment

The TRC-20 token is transformed into shielded token in the form of commitment by the cryptographic method, which is defined as:

note-commitment = NoteCommitmentr(v)

The v value is hidden by note_commitment with the blinding factor r.

SpendDescription

The SpendDescription includes zk-SNARK proof related data, which is used as the input of the shielded token transaction.

message SpendDescription { 
  bytes value_commitment = 1; // value commitment
  bytes anchor = 2; // merkle root
  bytes nullifier = 3; // used for check double spend
  bytes rk = 4; // used for check spend spend authority signature
  bytes zkproof = 5; // zk-SNARK proof
  bytes spend_authority_signature = 6; //spend authority signature 
}

ReceiveDescription

The ReceiveDescrion also includes zk-SNARK proof related data, which is used as the output of shielded token transaction.

message ReceiveDescription { 
  bytes value_commitment = 1; // value commitment used for binding signature
  byte  note_commitment = 2;  // note commitment used for hiding the value
  bytes epk = 3;  // ephemeral public key used for encryption
  bytes c_enc = 4;  // encryption for note plaintext
  bytes c_out = 5; // encryption for audit
  bytes zkproof = 6; // zk-SNARK proof 
}

For more details about note commitment, SpendDescription and ReceiveDescripton, please refer the TRONZ shielded transaction protocol.

Methods

constructor

The constructor method binds the TRC-20 token address to the shielded TRC-20 contract when it is deployed. The scalingFactorExponent is used as the scaling factor between the TRC-20 value and the shielded token value, since the type of prior is uint256 while the rear is unit64. For example, if scalingFactorExponent is 2, it means 100 (102) TRC-20 tokens can be transformed to a shielded token.

    constructor (address trc20ContractAddress, uint256 scalingFactorExponent) public {
        require(scalingFactorExponent < 77, "The scalingFactorExponent is out of range!");
        scalingFactor = 10 ** scalingFactorExponent;
        owner = msg.sender;
        trc20Token = TokenTRC20(trc20ContractAddress);
        require(approveSelf(), "approveSelf failed!");
    }

approveSelf() is used to approve the shielded TRC-20 contract address for itself, which can replace the transfer function by transferFrom function since the transfer function in some TRC-20 contract doesn't have the returned value.

    function approveSelf() public returns (bool) {
        return address(trc20Token).safeApprove(address(this), uint256(- 1));
    }

mint

mint method is to transform the public TRC-20 token with amount value to shielded token in the form of note_commitment. The method input parameters also include zk-SNARK proof related data.

    // output: cm, cv, epk, proof
    function mint(uint256 rawValue, bytes32[9] calldata output, bytes32[2] calldata bindingSignature, bytes32[21] calldata c) external {
        // step 1: transfer the TRC-20 Tokens from the sender to this contract
        address sender = msg.sender;
        bool transferResult = address(trc20Token).safeTransferFrom(sender, address(this), rawValue);
        require(transferResult, "safeTransferFrom failed!");
        
        // step 2: check the value and the note commitments
        require(noteCommitment[output[0]] == 0, "Duplicate noteCommitments!");
        
        // step 3: Scale the raw value 
        uint64 value = rawValueToValue(rawValue);
        
        // step 4: check the zk-SNARK proof 
        bytes32 signHash = sha256(abi.encodePacked(address(this), value, output, c));
        (bytes32[] memory ret) = verifyMintProof(output, bindingSignature, value, signHash, frontier, leafCount);
        uint256 result = uint256(ret[0]);
        require(result == 1, "The proof and signature have not been verified by the contract!");
        
        //step 5: store the note_commitment in the Merkle tree
        uint256 slot = uint256(ret[1]);
        uint256 nodeIndex = leafCount + 2 ** 32 - 1;
        tree[nodeIndex] = output[0];
        if (slot == 0) {
            frontier[0] = output[0];
        }
        for (uint256 i = 1; i < slot + 1; i++) {
            nodeIndex = (nodeIndex - 1) / 2;
            tree[nodeIndex] = ret[i + 1];
            if (i == slot) {
                frontier[slot] = tree[nodeIndex];
            }
        }
        
        //step 6: store the latest root in the contract
        latestRoot = ret[slot + 2];
        roots[latestRoot] = latestRoot;
        noteCommitment[output[0]] = output[0];
        leafCount ++;
        
        //step 7: emit the event
        emit MintNewLeaf(leafCount - 1, output[0], output[1], output[2], c);
        emit TokenMint(sender, rawValue);
    }

The mint includes the seven steps:

(1) transfer the TRC-20 token to the shielded contract by safeTransferFrom function.

(2) check the value is positive and prevent duplicate note commitments.

(3) Scale the TRC-20 value to shielded token value by rawValueToValue function. The rawValue must be the integer multiples of scalingFactor, which is initialized in the constructor function.

    function rawValueToValue(uint256 rawValue) private view returns (uint64) {
        require(rawValue > 0, "Value must be positive!");
        require(rawValue.mod(scalingFactor) == 0, "Value must be integer multiples of scalingFactor!");
        uint256 value = rawValue.div(scalingFactor);
        require(value < INT64_MAX);
        return uint64(value);
    }

(4) verify the proof to check the validity of note_commitment by verifyMintProof function. Here, the input of the signed hash includes shielded contract address this , the scaled value, output and c. output includes note_commitment, value_commitment, epk and zkproof. c includes c_enc and c_out.

bytes32 signHash = sha256(abi.encodePacked(address(this), value, output, c));

(5) store the note_commitment in the merkle tree tree, which is a mapping data structure.

mapping(uint256 => bytes32) public tree;

(6) store the latest root in the roots, which is used for merkle path proof.

mapping(bytes32 => bytes32) public roots; 

(7) emit the related data ( position, note_commitment, value_commitment, epk, c_enc, c_out) as event to make it convenient for shielded note scanning.

event MintNewLeaf(uint256 position, bytes32 cm, bytes32 cv, bytes32 epk, bytes32[21] c);

Note: c includes c_enc and c_out.

transfer

transfer method is used for shielded token transfer.

    //input: nf, anchor, cv, rk, proof
    //output: cm, cv, epk, proof
    function transfer(bytes32[10][] calldata input, bytes32[2][] calldata spendAuthoritySignature, bytes32[9][] calldata output, bytes32[2] calldata bindingSignature, bytes32[21][] calldata c) external {
        // step 1: check the parameters 
        require(input.length >= 1 && input.length <= 2, "Input number must be 1 or 2!");
        require(input.length == spendAuthoritySignature.length, "Input number must be equal to spendAuthoritySignature number!");
        require(output.length >= 1 && output.length <= 2, "Output number must be 1 or 2!");
        require(output.length == c.length, "Output number must be equal to c number!");
        
        for (uint256 i = 0; i < input.length; i++) {
            require(nullifiers[input[i][0]] == 0, "The note has already been spent!");
            require(roots[input[i][1]] != 0, "The anchor must exist!");
        }
        for (uint256 i = 0; i < output.length; i++) {
            require(noteCommitment[output[i][0]] == 0, "Duplicate noteCommitment!");
        }
        
        //step 2: check the proof 
        bytes32 signHash = sha256(abi.encodePacked(address(this), input, output, c));
        (bytes32[] memory ret) = verifyTransferProof(input, spendAuthoritySignature, output, bindingSignature, signHash, 0, frontier, leafCount);
        uint256 result = uint256(ret[0]);
        require(result == 1, "The proof and signature have not been verified by the contract!");
        
        //step 3: store the output note_commitments in the merkle tree 
        uint256 offset = 1;
        //ret offset
        for (uint256 i = 0; i < output.length; i++) {
            uint256 slot = uint256(ret[offset++]);
            uint256 nodeIndex = leafCount + 2 ** 32 - 1;
            tree[nodeIndex] = output[i][0];
            if (slot == 0) {
                frontier[0] = output[i][0];
            }
            for (uint256 k = 1; k < slot + 1; k++) {
                nodeIndex = (nodeIndex - 1) / 2;
                tree[nodeIndex] = ret[offset++];
                if (k == slot) {
                    frontier[slot] = tree[nodeIndex];
                }
            }
            leafCount++;
        }
        
        //step 4: store the latest root and nullifier in the contract
        latestRoot = ret[offset];
        roots[latestRoot] = latestRoot;
        for (uint256 i = 0; i < input.length; i++) {
            bytes32 nf = input[i][0];
            nullifiers[nf] = nf;
        }
        
        //step 5: emit the event
        for (uint256 i = 0; i < output.length; i++) {
            noteCommitment[output[i][0]] = output[i][0];
            emit TransferNewLeaf(leafCount - (output.length - i), output[i][0], output[i][1], output[i][2], c[i]);
        }
    }

The transfer method includes five steps:

(1) check the size of input and output to be in the range [1,2] , which corresponds to the input and output number in the UTXO model; check nullifiers to prevent the double spend. nullifiers are defined as a mapping structure in the contract. check the notecommitment to prevent the duplicate note commitments.

mapping(bytes32 => bytes32) public nullifiers; 

(2) verify the proof by verifyTransferProof function check the validity of the note_commitments. The input of the signed hash includes shielded contract address this , input, output and c. The input and output both includes one or two arrays. An array of input includes nullifier, anchor, value_commitment, rk and zkproof. An array of output includes note_commitment, value_commitment, epk and proof. c includes c_enc and c_out.

bytes32 signHash = sha256(abi.encodePacked(address(this), input, output, c));

(3) store the output note_commitments in the Merkle tree tree.

(4) store the latest root in roots and the input nullifier in the nullifiers .

(5) emit the related data ( position, note_commitment, value_commitment, epk, c_enc, c_out) as an event to make it convenient for shielded note scanning.

burn

burn method is to transform shielded token note_commitment to the public TRC-20 token with amount value. burn can have zero or one shielded output.

    //input: nf, anchor, cv, rk, proof
    //output: cm, cv, epk, proof
    function burn(bytes32[10] calldata input, bytes32[2] calldata spendAuthoritySignature, uint256 rawValue, bytes32[2] calldata bindingSignature, address payTo, bytes32[3] calldata burnCipher, bytes32[9][] calldata output, bytes32[21][] calldata c) external {
        //step 1: scale the rawValue to value
        uint64 value = rawValueToValue(rawValue);
        bytes32 signHash = sha256(abi.encodePacked(address(this), input, output, c, payTo, value));
        
        //step 2: check the validity of parameters
        bytes32 nf = input[0];
        bytes32 anchor = input[1];
        require(nullifiers[nf] == 0, "The note has already been spent!");
        require(roots[anchor] != 0, "The anchor must exist!");
        
        require(output.length <= 1, "Output number cannot exceed 1!");
        require(output.length == c.length, "Output number must be equal to length of c!");
        
        //step 3: check the zk-SNARK proof 
        if (output.length == 0) {
            (bool result) = verifyBurnProof(input, spendAuthoritySignature, value, bindingSignature, signHash);
            require(result, "The proof and signature have not been verified by the contract!");
            
        } else {
            transferInBurn(input, spendAuthoritySignature, value, bindingSignature, signHash, output, c);
        }

        //step 4: store nullifier in the contract
        nullifiers[nf] = nf;
        
        //step 5: transfer TRC-20 token from this contract to the nominated address
        bool transferResult = address(trc20Token).safeTransferFrom(address(this), payTo, rawValue);
        require(transferResult, "safeTransferFrom failed!");
        emit TokenBurn(payTo, rawValue, burnCipher);
    }

The burn includes the five steps:

(1) Scale the shielded token value to TRC-20 value by scalingFactor. The input of the signed hash includes shielded contract address this , input, output, c, payTO and the scaled value. input includes nullifier, anchor, value_commitment, rk, zkproof. output includes note_commitment, value_commitment,epk, zkproof. c includes c_enc and c_out. payTo is the recipient address.

bytes32 signHash = sha256(abi.encodePacked(address(this), input, output, c, payTo, value));

(2) check the validity of value, nullifier and anchor.

(3) verify the proof to check the validity of note_commitment. There are two situations. If there is no shielded output, it calls the verifyBurnProof instruction. If there is one shielded output, it calls the transferInBurn function.

    function transferInBurn(bytes32[10] memory input, bytes32[2] memory spendAuthoritySignature, uint64 value, bytes32[2] memory bindingSignature, bytes32 signHash, bytes32[9][] memory output, bytes32[21][] memory c) private {
        bytes32 cm = output[0][0];
        require(noteCommitment[cm] == 0, "Duplicate noteCommitment!");
        bytes32[10][] memory inputs = new bytes32[10][](1);
        inputs[0] = input;
        bytes32[2][] memory spendAuthoritySignatures = new bytes32[2][](1);
        spendAuthoritySignatures[0] = spendAuthoritySignature;
        (bytes32[] memory ret) = verifyTransferProof(inputs, spendAuthoritySignatures, output, bindingSignature, signHash, value, frontier, leafCount);
        uint256 result = uint256(ret[0]);
        require(result == 1, "The proof and signature have not been verified by the contract!");

        uint256 slot = uint256(ret[1]);
        uint256 nodeIndex = leafCount + 2 ** 32 - 1;
        tree[nodeIndex] = cm;
        if (slot == 0) {
            frontier[0] = cm;
        }
        for (uint256 i = 1; i < slot + 1; i++) {
            nodeIndex = (nodeIndex - 1) / 2;
            tree[nodeIndex] = ret[i + 1];
            if (i == slot) {
                frontier[slot] = tree[nodeIndex];
            }
        }
        latestRoot = ret[slot + 2];
        roots[latestRoot] = latestRoot;
        noteCommitment[cm] = cm;
        leafCount ++;
        
        emit BurnNewLeaf(leafCount - 1, cm, output[0][1], output[0][2], c[0]);
    }

(4) store the nullifier in the contract.

(5) transfer the TRC-20 token from the shielded TRC-20 contract to payTo address by safeTransferFrom function.

getPath

In order to make it convenient for users to construct zk-SNARK proof, the shielded TRC-20 contract provides getPath function to return the latest root and merkle tree path for the given note_commitment with position parameter.

    function getPath(uint256 position) public view returns (bytes32, bytes32[32] memory) {
        ...
    }

For more technical details of Merkle tree implementation, please refer timber.

Privacy Protection

The shielded TRC-20 contract aims to provide users better privacy of token ownership and transactions. The transfer function can completely hide the source address, the destination address, and the amount for shielded token transactions. But it still has some limitations. For mint(burn) function, the TRC-20 source (destination) address and amount are public, it may leak some information. Furthermore, triggering the contract involves the public address, which may leak the ownership of note_commitment. To solve the problem, triggering the contract can be delegated to a third party (refer EIP-1077) or GSN (Gas Station Network, refer EIP-1613).

Rationale

Transaction privacy protection is quite important for blockchain. Providing the shielded TRC-20 contract will help to attract more users because it can provide better privacy for token transactions and promotes the widespread adoption of TRC-20 token and related DApps.

Implementation