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
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.
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.
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.
The following defines the implementation of the shielded TRC-20 contract.
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.
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.
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).
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.