VRC725 is the simplest form of Non-Fungible Token on Viction
VRC725 is the official standard for Non-fungible tokens in Viction ecosystem.
Abstract
The following standard allows for the implementation of a standard API for NFTs within smart contracts. This standard provides basic functionality to track and transfer NFTs.
We considered use cases of NFTs being owned and transacted by individuals as well as consignment to third party brokers/wallets/auctioneers (“operators”). NFTs can represent ownership over digital or physical assets. We considered a diverse universe of assets, and we know you will dream up many more:
Physical property — houses, unique artwork
Virtual collectibles — unique pictures of kittens, collectible cards
“Negative value” assets — loans, burdens and other responsibilities
In general, all houses are distinct and no two kittens are alike. NFTs are distinguishable and you must track the ownership of each one separately.
This standards also support gas-free protocol by enabling mechanism that those protocol can utilize while maintaince the security of the token itself.
Motivation
A standard interface allows wallet/broker/auction applications to work with any NFT on Viction. We provide for simple VRC725 smart contracts as well as contracts that track an arbitrarily large number of NFTs. Additional applications are discussed below.
VRC725 can work both with VictionZ with some limitation, which you can consider, or without VictionZ. The permit extension required by VRC725 is suffient for any gas-less protocol to work with.
Specification
VRC725 is based on ERC721. VRC725 includes IERC721Metadata extension to make it easier for use. Moreorver, it also includes two custom permit implementation inspired by EIP-4494 to support gas-free operation.
safeTransferFrom: Safely transfers tokenId token from from to to, checking first that contract recipients are aware of the ERC721 protocol to prevent tokens from being forever locked.
setApprovalForAll: Approve or remove operator as an operator for the caller. Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
minFee: The amount fee that will be lost when transferring.
functionminFee() publicviewreturns (uint256)
permit: Approve for one tokenId by using pre-signed signature, allow other users or smart contract to perform the transfer without owner calling approve first.
permitForAll: Approve for a spender by using pre-signed signature, allow other users or smart contract to perform the transfer without owner calling approve first.
eventTransfer(addressindexed from, addressindexed to, uintindexed tokenId);
Emitted when tokenId token is transferred from from to to. This event MUST be emitted when tokens are transferred in functions transfer ,transferFrom and safeTransferFrom.
Emitted when owner enables approved to manage the tokenId token. This event MUST be emitted on any successful call to approve function and permit function.
Emitted when owner enables or disables (approved) operator to manage all of its assets.This event MUST be emitted on any successful call to setApprovalForAll function and permitForAll function.
Requirement
For a contract to meet VRC725 requirements, it must satisfy the following conditions:
Implement IVRC725 which includes IERC721, IERC721Metadata.
Must implement 2 functions: permit as defined in EIP- 4494 and its custom variant permitForAll.
To implement VRC725, use can extend ERC721 contract from OpenZeppelin contract libraries or your existing NFT contract. The major differences are that permit and permitForAll functions are required, and the position in storage slots of _balances, _minFee, _owner.
Otherwise, you can inherit the VRC725 from our repository.
/** * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including * the Metadata extension, but not including the Enumerable extension, which is available separately as * {ERC721Enumerable}. */abstractcontractVRC725isERC165, IVRC725 {usingAddressforaddress;usingStringsforuint256;// Mapping owner address to token count// The order of _balances, _minFee, _issuer must not be changed to pass validation of gas sponsor applicationmapping (address=>uint256) private _balances; uint256 private _minFee; // minFee must always be 0 to ensure that VictionZ will work properly in the case you apply for it
addressprivate _owner;addressprivate _newOwner;bool isAlreadyInit;// EIP 712bytes32private _HASHED_NAME;bytes32private _HASHED_VERSION; bytes32 private constant _TYPE_HASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
// Permit type hash using for EIP712bytes32publicconstant PERMIT_TYPEHASH =keccak256('Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)' );// Permit for all type hash using for EIP712bytes32publicconstant PERMIT_FOR_ALL_TYPEHASH =keccak256('PermitForAll(address spender,uint256 nonce,uint256 deadline)' );mapping(uint256=>uint256) private _nonces;mapping(address=>uint256) private _noncesByAddress;// Token namestringprivate _name;// Token symbolstringprivate _symbol;// Mapping from token ID to owner addressmapping(uint256=>address) private _owners;// Mapping from token ID to approved addressmapping(uint256=>address) private _tokenApprovals;// Mapping from owner to operator approvalsmapping(address=>mapping(address=>bool)) private _operatorApprovals;eventFee(addressindexed from, addressindexed to, addressindexed issuer, uint256 value);eventOwnershipTransferred(addressindexed previousOwner, addressindexed newOwner);/** * @dev Init function called by child contract */function__VRC725_init(stringmemory name_,stringmemory symbol_,address owner_) internal {require(!isAlreadyInit,"VRC725: Contract already init");__ERC721_init(name_, symbol_);__EIP712_init(name_,"1"); _owner = owner_; isAlreadyInit =true; }/** * @dev Throws if called by any account other than the owner. */modifieronlyOwner() {require(_owner == msg.sender,"VRC725: caller is not the owner"); _; }/** * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection. */function__ERC721_init(stringmemory name_,stringmemory symbol_) private { _name = name_; _symbol = symbol_; _minFee =0; }/** * @notice Owner of the token */functionowner() publicviewreturns (address) {return _owner; }/** * @notice Owner of the token */functionissuer() publicviewreturns (address) {return _owner; }/** * @dev The amount fee that will be lost when transferring. */functionminFee() publicviewreturns (uint256) {return _minFee; }/** * @dev See {IERC165-supportsInterface}. */functionsupportsInterface(bytes4 interfaceId) publicviewvirtualoverride(ERC165,IERC165) returns (bool) {return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC721Metadata).interfaceId || interfaceId == type(IERC4494).interfaceId || interfaceId == type(IVRC725).interfaceId || super.supportsInterface(interfaceId); }/** * @dev See {IERC721-balanceOf}. */functionbalanceOf(address owner) publicviewvirtualoverridereturns (uint256) {require(owner !=address(0),"ERC721: address zero is not a valid owner");return _balances[owner]; }/** * @dev See {IERC721-ownerOf}. */functionownerOf(uint256 tokenId) publicviewvirtualoverridereturns (address) {address owner =_ownerOf(tokenId);require(owner !=address(0),"ERC721: invalid token ID");return owner; }/** * @dev See {IERC721Metadata-name}. */functionname() publicviewvirtualoverridereturns (stringmemory) {return _name; }/** * @dev See {IERC721Metadata-symbol}. */functionsymbol() publicviewvirtualoverridereturns (stringmemory) {return _symbol; }/** * @dev See {IERC721Metadata-tokenURI}. */functiontokenURI(uint256 tokenId) publicviewvirtualoverridereturns (stringmemory) {_requireMinted(tokenId);stringmemory baseURI =_baseURI();returnbytes(baseURI).length >0?string(abi.encodePacked(baseURI, tokenId.toString())) :""; }/** * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each * token will be the concatenation of the `baseURI` and the `tokenId`. Empty * by default, can be overridden in child contracts. */function_baseURI() internalviewvirtualreturns (stringmemory) {return""; }/** * @dev See {IERC4494-permit}. */functionpermit(address spender,uint256 tokenId,uint256 deadline,bytesmemory signature) externaloverride {require(deadline >= block.timestamp,'VRC725: Permit deadline expired');bytes32 digest =_getPermitDigest(spender, tokenId, _nonces[tokenId], deadline); (address recoverAddress,, ) = ECDSA.tryRecover(digest, signature);require(// if the recovered address is owner or approved on token id, recoverAddress !=address(0)&&_isApprovedOrOwner(recoverAddress, tokenId)// try to recover signature using SignatureChecker, which allows to recover signature made by contracts|| SignatureChecker.isValidSignatureNow(ownerOf(tokenId), digest, signature),"VRC725: Invalid permit signature" );_approve(spender, tokenId); }/** * @dev Permit for all */functionpermitForAll(address owner,address spender,uint256 deadline,bytesmemory signature) external {require(deadline >= block.timestamp,'VRC725: Permit deadline expired');bytes32 digest =_getPermitForAllDigest(spender, _noncesByAddress[owner], deadline); (address recoverAddress,, ) = ECDSA.tryRecover(digest, signature);require(// if the recovered address is owner, recoverAddress == owner || SignatureChecker.isValidSignatureNow(owner, digest, signature),"VRC725: Invalid permit signature" );_setApprovalForAll(owner, spender,true); _noncesByAddress[owner]++; }/** * @dev See {IERC721-approve}. */functionapprove(address to,uint256 tokenId) publicvirtualoverride {address owner = VRC725.ownerOf(tokenId);require(to != owner,"ERC721: approval to current owner");require( msg.sender == owner ||isApprovedForAll(owner, msg.sender),"ERC721: approve caller is not token owner or approved for all" );_approve(to, tokenId); }/** * @dev See {IERC721-getApproved}. */functiongetApproved(uint256 tokenId) publicviewvirtualoverridereturns (address) {_requireMinted(tokenId);return _tokenApprovals[tokenId]; }/** * @dev See {IERC721-setApprovalForAll}. */functionsetApprovalForAll(address operator,bool approved) publicvirtualoverride {_setApprovalForAll(msg.sender, operator, approved); }/** * @dev See {IERC721-isApprovedForAll}. */functionisApprovedForAll(address owner,address operator) publicviewvirtualoverridereturns (bool) {return _operatorApprovals[owner][operator]; }/** * @dev See {IERC721-transferFrom}. */functiontransferFrom(address from,address to,uint256 tokenId ) publicvirtualoverride {//solhint-disable-next-line max-line-lengthrequire(_isApprovedOrOwner(msg.sender, tokenId),"ERC721: caller is not token owner or approved");_transfer(from, to, tokenId); }/** * @dev See {IERC721-safeTransferFrom}. */functionsafeTransferFrom(address from,address to,uint256 tokenId ) publicvirtualoverride {safeTransferFrom(from, to, tokenId,""); }/** * @dev See {IERC721-safeTransferFrom}. */functionsafeTransferFrom(address from,address to,uint256 tokenId,bytesmemory data ) publicvirtualoverride {require(_isApprovedOrOwner(msg.sender, tokenId),"ERC721: caller is not token owner or approved");_safeTransfer(from, to, tokenId, data); }/** * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients * are aware of the ERC721 protocol to prevent tokens from being forever locked. * * `data` is additional data, it has no specified format and it is sent in call to `to`. * * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. * implement alternative mechanisms to perform token transfer, such as signature-based. * * Requirements: * * - `from` cannot be the zero address. * - `to` cannot be the zero address. * - `tokenId` token must exist and be owned by `from`. * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
* * Emits a {Transfer} event. */function_safeTransfer(address from,address to,uint256 tokenId,bytesmemory data ) internalvirtual {_transfer(from, to, tokenId);require(_checkOnERC721Received(from, to, tokenId, data),"ERC721: transfer to non ERC721Receiver implementer"); }/** * @dev Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist */function_ownerOf(uint256 tokenId) internalviewvirtualreturns (address) {return _owners[tokenId]; }/** * @dev Returns whether `tokenId` exists. * * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. * * Tokens start existing when they are minted (`_mint`), * and stop existing when they are burned (`_burn`). */function_exists(uint256 tokenId) internalviewvirtualreturns (bool) {return_ownerOf(tokenId) !=address(0); }/** * @dev Returns whether `spender` is allowed to manage `tokenId`. * * Requirements: * * - `tokenId` must exist. */function_isApprovedOrOwner(address spender,uint256 tokenId) internalviewvirtualreturns (bool) {address owner = VRC725.ownerOf(tokenId);return (spender == owner ||isApprovedForAll(owner, spender) ||getApproved(tokenId) == spender); }/** * @dev Safely mints `tokenId` and transfers it to `to`. * * Requirements: * * - `tokenId` must not exist. * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
* * Emits a {Transfer} event. */function_safeMint(address to,uint256 tokenId) internalvirtual {_safeMint(to, tokenId,""); }/** * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is * forwarded in {IERC721Receiver-onERC721Received} to contract recipients. */function_safeMint(address to,uint256 tokenId,bytesmemory data ) internalvirtual {_mint(to, tokenId);require(_checkOnERC721Received(address(0), to, tokenId, data),"ERC721: transfer to non ERC721Receiver implementer" ); }/** * @dev Mints `tokenId` and transfers it to `to`. * * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible * * Requirements: * * - `tokenId` must not exist. * - `to` cannot be the zero address. * * Emits a {Transfer} event. */function_mint(address to,uint256 tokenId) internalvirtual {require(to !=address(0),"ERC721: mint to the zero address");require(!_exists(tokenId),"ERC721: token already minted");_beforeTokenTransfer(address(0), to, tokenId,1);// Check that tokenId was not minted by `_beforeTokenTransfer` hookrequire(!_exists(tokenId),"ERC721: token already minted");unchecked {// Will not overflow unless all 2**256 token ids are minted to the same owner.// Given that tokens are minted one by one, it is impossible in practice that// this ever happens. Might change if we allow batch minting.// The ERC fails to describe this case. _balances[to] +=1; } _owners[tokenId] = to;emitTransfer(address(0), to, tokenId);_afterTokenTransfer(address(0), to, tokenId,1); }/** * @dev Destroys `tokenId`. * The approval is cleared when the token is burned. * This is an internal function that does not check if the sender is authorized to operate on the token. * * Requirements: * * - `tokenId` must exist. * * Emits a {Transfer} event. */function_burn(uint256 tokenId) internalvirtual {address owner = VRC725.ownerOf(tokenId);_beforeTokenTransfer(owner,address(0), tokenId,1);// Update ownership in case tokenId was transferred by `_beforeTokenTransfer` hook owner = VRC725.ownerOf(tokenId);// Clear approvalsdelete _tokenApprovals[tokenId];unchecked {// Cannot overflow, as that would require more tokens to be burned/transferred// out than the owner initially received through minting and transferring in. _balances[owner] -=1; }delete _owners[tokenId];emitTransfer(owner,address(0), tokenId);_afterTokenTransfer(owner,address(0), tokenId,1); }/** * @dev Transfers `tokenId` from `from` to `to`. * As opposed to {transferFrom}, this imposes no restrictions on msg.sender. * * Requirements: * * - `to` cannot be the zero address. * - `tokenId` token must be owned by `from`. * * Emits a {Transfer} event. */function_transfer(address from,address to,uint256 tokenId ) internalvirtual {require(VRC725.ownerOf(tokenId) == from,"ERC721: transfer from incorrect owner");require(to !=address(0),"ERC721: transfer to the zero address");_beforeTokenTransfer(from, to, tokenId,1);// Check that tokenId was not transferred by `_beforeTokenTransfer` hookrequire(VRC725.ownerOf(tokenId) == from,"ERC721: transfer from incorrect owner");// Clear approvals from the previous ownerdelete _tokenApprovals[tokenId];unchecked {// `_balances[from]` cannot overflow for the same reason as described in `_burn`:// `from`'s balance is the number of token held, which is at least one before the current// transfer.// `_balances[to]` could overflow in the conditions described in `_mint`. That would require// all 2**256 token ids to be minted, which in practice is impossible. _balances[from] -=1; _balances[to] +=1; } _owners[tokenId] = to;// increment nonce using for permit_incrementNonce(tokenId);emitTransfer(from, to, tokenId);_afterTokenTransfer(from, to, tokenId,1); }/** * @dev Approve `to` to operate on `tokenId` * * Emits an {Approval} event. */function_approve(address to,uint256 tokenId) internalvirtual { _tokenApprovals[tokenId] = to;emitApproval(VRC725.ownerOf(tokenId), to, tokenId); }/** * @dev Approve `operator` to operate on all of `owner` tokens * * Emits an {ApprovalForAll} event. */function_setApprovalForAll(address owner,address operator,bool approved ) internalvirtual {require(owner != operator,"ERC721: approve to caller"); _operatorApprovals[owner][operator] = approved;emitApprovalForAll(owner, operator, approved); }/** * @dev Reverts if the `tokenId` has not been minted yet. */function_requireMinted(uint256 tokenId) internalviewvirtual {require(_exists(tokenId),"ERC721: invalid token ID"); }/** * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address. * The call is not executed if the target address is not a contract. * * @param from address representing the previous owner of the given token ID * @param to target address that will receive the tokens * @param tokenId uint256 ID of the token to be transferred * @param data bytes optional data to send along with the call * @return bool whether the call correctly returned the expected magic value */function_checkOnERC721Received(address from,address to,uint256 tokenId,bytesmemory data ) privatereturns (bool) {if (to.isContract()) {tryIERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) {return retval == IERC721Receiver.onERC721Received.selector; } catch (bytesmemory reason) {if (reason.length ==0) {revert("ERC721: transfer to non ERC721Receiver implementer"); } else {/// @solidity memory-safe-assemblyassembly {revert(add(32, reason),mload(reason)) } } } } else {returntrue; } }/** * @dev Hook that is called before any token transfer. This includes minting and burning. If {ERC721Consecutive} is * used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1. * * Calling conditions: * * - When `from` and `to` are both non-zero, ``from``'s tokens will be transferred to `to`. * - When `from` is zero, the tokens will be minted for `to`. * - When `to` is zero, ``from``'s tokens will be burned. * - `from` and `to` are never both zero. * - `batchSize` is non-zero. * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */function_beforeTokenTransfer(address from,address to,uint256,/* firstTokenId */uint256 batchSize ) internalvirtual {if (batchSize >1) {if (from !=address(0)) { _balances[from] -= batchSize; }if (to !=address(0)) { _balances[to] += batchSize; } } }/** * @dev Hook that is called after any token transfer. This includes minting and burning. If {ERC721Consecutive} is * used, the hook may be called as part of a consecutive (batch) mint, as indicated by `batchSize` greater than 1. * * Calling conditions: * * - When `from` and `to` are both non-zero, ``from``'s tokens were transferred to `to`. * - When `from` is zero, the tokens were minted for `to`. * - When `to` is zero, ``from``'s tokens were burned. * - `from` and `to` are never both zero. * - `batchSize` is non-zero. * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */function_afterTokenTransfer(address from,address to,uint256 firstTokenId,uint256 batchSize ) internalvirtual {}/** * @dev Builds the permit digest to sign * @param spender the address to approve * @param tokenId the index of the NFT to approve the spender on * @param nonce the nonce to make a permit for * @param deadline a timestamp expiry for the permit */ function _getPermitDigest(address spender, uint256 tokenId, uint256 nonce, uint256 deadline) internal view returns (bytes32) {
return_hashTypedDataV4(keccak256(abi.encode( PERMIT_TYPEHASH, spender, tokenId, nonce, deadline )) ); }/** * @dev Builds the permit for all digest to sign * @param spender the address to approve * @param nonce the nonce to make a permit for * @param deadline a timestamp expiry for the permit */function_getPermitForAllDigest(address spender,uint256 nonce,uint256 deadline) internalviewreturns (bytes32) {return_hashTypedDataV4(keccak256(abi.encode( PERMIT_FOR_ALL_TYPEHASH, spender, nonce, deadline )) ); }/** * @dev Helper to easily increment a nonce for a given tokenId */function_incrementNonce(uint256 tokenId) internal { _nonces[tokenId]++; }/** * @dev See {IERC4494-DOMAIN_SEPARATOR}. */functionDOMAIN_SEPARATOR() externalviewoverridereturns(bytes32) {return_domainSeparatorV4(); }/** * @dev See {IERC4494-nonces}. */functionnonces(uint256 tokenId) externalviewoverridereturns(uint256) {require(_exists(tokenId),"ERC721: invalid token ID");return _nonces[tokenId]; }/** * @dev Find nonce by address */functionnonceByAddress(address owner) externalviewreturns(uint256) {return _noncesByAddress[owner]; }/// ------------------------------------- Ownable -------------------------------------/** * @dev Accept the ownership transfer. This is to make sure that the contract is * transferred to a working address * * Can only be called by the newly transfered owner. */functionacceptOwnership() external {require(msg.sender == _newOwner,"VRC725: only new owner can accept ownership");address oldOwner = _owner; _owner = _newOwner; _newOwner =address(0);emitOwnershipTransferred(oldOwner, _owner); }/** * @dev Transfers ownership of the contract to a new account (`newOwner`). * * Can only be called by the current owner. */functiontransferOwnership(address newOwner) externalvirtualonlyOwner {require(newOwner !=address(0),"VRC725: new owner is the zero address"); _newOwner = newOwner; }/// ------------------------------------- EIP-712 -------------------------------------/* solhint-enable var-name-mixedcase *//** * @dev Initializes the domain separator and parameter caches. * * The meaning of `name` and `version` is specified in * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]: * * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol. * - `version`: the current major version of the signing domain. * * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart * contract upgrade]. */function__EIP712_init(stringmemory name_,stringmemory version) private {__EIP712_init_unchained(name_, version); }function__EIP712_init_unchained(stringmemory name_,stringmemory version) internal {bytes32 hashedName =keccak256(bytes(name_));bytes32 hashedVersion =keccak256(bytes(version)); _HASHED_NAME = hashedName; _HASHED_VERSION = hashedVersion; }/** * @dev Returns the domain separator for the current chain. */function_domainSeparatorV4() internalviewreturns (bytes32) {return_buildDomainSeparator(_TYPE_HASH,_EIP712NameHash(),_EIP712VersionHash()); }function_buildDomainSeparator(bytes32 typeHash,bytes32 nameHash,bytes32 versionHash ) privateviewreturns (bytes32) {returnkeccak256(abi.encode(typeHash, nameHash, versionHash, block.chainid,address(this))); }/** * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this * function returns the hash of the fully encoded EIP712 message for this domain. * * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: * * ```solidity * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( * keccak256("Mail(address to,string contents)"), * mailTo, * keccak256(bytes(mailContents)) * ))); * address signer = ECDSA.recover(digest, signature); * ``` */function_hashTypedDataV4(bytes32 structHash) internalviewvirtualreturns (bytes32) {return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); }/** * @dev The hash of the name parameter for the EIP712 domain. * * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs * are a concern. */function_EIP712NameHash() internalvirtualviewreturns (bytes32) {return _HASHED_NAME; }/** * @dev The hash of the version parameter for the EIP712 domain. * * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs * are a concern. */function_EIP712VersionHash() internalvirtualviewreturns (bytes32) {return _HASHED_VERSION; }}
VRC725 is not required to apply for VictionZ. Zero-gas operation will be applied through TransferHelper (click here for example) which is integrated VRC25.
In the case you need to apply for VictionZ to support more Zero-gas operations in your token, VRC725 is still compatible with VictionZ. Please refer to VIC ZeroGas page for instruction.