VRC25 is the official standard for fungible tokens in Viction ecosystem.
Abstract
The following standard allows for the implementation of a standard API for tokens within smart contracts. This standard provides basic functionality to transfer tokens, as well as allow tokens to be approved so they can be spent by another on-chain third party. This standard also defines how fee should be managed to prevent abuse of the feature.
Motivation
For EVM-based blockchain, ERC20 has became the standard for fungible tokens. This standard works perfectly and had been proven for a long time. However, due to the way smart-contract works in blockchain, it's somewhat difficult for new users to understand, especially web2 users.
VRC25 is developed in effort to simplify the way a token works by eliminate the need of gas when using the token. It means that users won't need to keep native token when they transfer, approve or any other actions available on the token. Instead the fee for the network can be paid by the token itself.
Returns the amount which spender is still allowed to withdraw from owner.
issuer: Returns the address of the token issuer.
functionissuer() externalviewreturns (address);
The method returns the address of the token issuer. This is to ensure that only the issuer has the right to decide in regard to paying fees of token-holder transactions to the token contract in terms of the token itself. The method is to verify that no one else is able to change the token contract, except the issuer.
estimateFee: Calculate the transaction fee in terms of the token that the transaction makers will have to pay. Transaction fee will be paid to the issuer of the VRC25 token contract.
Ideally, the function will return the transaction fee based on the value (the number of tokens) that the transaction maker wants to transfer. Transaction fee for allowance the function will be estimated if the input parameter value = 0. The way fees are computed is not standardized. Token issuers can fully customize the implementation of the function.
This function will also be called by user wallets to evaluate fees the user must pay.
transfer: Transfers value amount of tokens to address recipient, and MUST fire the Transfer and Fee event.
The function will call estimateFee function to compute the transaction fee. The function SHOULD throw if the message caller’s account balance does not have enough tokens to spend and to pay transaction fees. Once succeeded, the token balance of the sender should be reduced by value plus the computed transaction fee, the balance of recipient should be increasing value, while the balance of the token issuer should be increased by the computed transaction fee.
Allows spender to withdraw from your account multiple times, up to the value amount. If this function is called again it overwrites the current allowance with value. This function also calls estimateFee with input parameter 0 in order to compute the transaction fee in terms of tokens that the transaction sender must pay to the token issuer.
This is the same as approve function with the different that caller is not necessary the owner. Instead, the caller must provide a valid signature signed by the owner.
Events
Transfer
eventTransfer(addressindexed from, addressindexed to, uint256 amount);
This event MUST be emitted when tokens are transferred in functions transfer and transferFrom.
This event MUST be emitted on any successful call to approve function.
Fee
eventFee(addressindexed from, addressindexed to, addressindexed issuer, uint256 amount);
This event MUST be emitted when tokens are transferred in functions transfer and transferFrom in order for clients/DApp/third-party wallets to notify their users about the paid transaction fee in terms of tokens.
Requirement
For a contract to meet VRC25 requirements, it must satisfy the following conditions:
Implement IVRC25 interface in the specification.
Have 3 first storage slots in the contracts as follow:
Implement Permit extension, as defined in EIP-2612. Permit acts as a fallback for any gas-less protocol to support your token properly, in the case that your token isn't registered for [VIC ZeroGas](../integration/VIC ZeroGas-integration.md).
This standard will need some basic information to track and
_balances: record the balance of each token holder
_minFee: the minimum fee in terms of tokens that the transaction sender must pay. Ideally, minFee will be paid when the approve function is called or when the transaction fails.
_owner: the address of the token issuer who will receive transaction fees from token holders in terms of the token, but will pay transaction fees to masternodes by means of VIC.
The implementation also defines some additional functions as follows:
issuer: Returns the address of the issuer of the token.
minFee: Returns the minimum fee for any transaction.
/** * @title Base VRC25 implementation * @notice VRC25 implementation for opt-in to gas sponsor program. This replace Ownable from OpenZeppelin as well. */abstractcontractVRC25isIVRC25, IERC165 {usingAddressforaddress;usingSafeMathforuint256;// The order of _balances, _minFeem, _issuer must not be changed to pass validation of gas sponsor applicationmapping (address=>uint256) private _balances;uint256private _minFee;addressprivate _owner;addressprivate _newOwner;mapping (address=>mapping (address=>uint256)) private _allowances;stringprivate _name;stringprivate _symbol;uint8private _decimals;uint256private _totalSupply;eventFeeUpdated(uint256 fee);eventOwnershipTransferred(addressindexed previousOwner, addressindexed newOwner);constructor(stringmemory name,stringmemory symbol,uint8 decimals_) internal { _name = name; _symbol = symbol; _decimals = decimals_; _owner = msg.sender; }/** * @dev Throws if called by any account other than the owner. */modifieronlyOwner() {require(_owner == msg.sender,"VRC25: caller is not the owner"); _; }/** * @notice Name of token */functionname() publicviewreturns (stringmemory) {return _name; }/** * @notice Symbol of token */functionsymbol() publicviewreturns (stringmemory) {return _symbol; }/** * @notice Returns the number of decimals used to get its user representation. * For example, if `decimals` equals `2`, a balance of `505` tokens should * be displayed to a user as `5,05` (`505 / 10 ** 2`). */functiondecimals() publicviewoverridereturns (uint8) {return _decimals; }/** * @notice Returns the amount of tokens in existence. */functiontotalSupply() publicviewoverridereturns (uint256) {return _totalSupply; }/** * @notice Returns the amount of tokens owned by `account`. * @param owner The address to query the balance of. * @return An uint256 representing the amount owned by the passed address. */functionbalanceOf(address owner) publicviewoverridereturns (uint256) {return _balances[owner]; }/** * @notice Returns the remaining number of tokens that `spender` will be * allowed to spend on behalf of `owner` through {transferFrom}. This is * zero by default. * * This value changes when {approve} or {transferFrom} are called. */functionallowance(address owner,address spender) publicviewoverridereturns (uint256){return _allowances[owner][spender]; }/** * @notice Owner of the token */functionowner() publicviewreturns (address) {return _owner; }/** * @notice Owner of the token */functionissuer() publicviewoverridereturns (address) {return _owner; }/** * @dev The amount fee that will be lost when transferring. */functionminFee() publicviewreturns (uint256) {return _minFee; }/** * @notice Calculate fee needed to transfer `amount` of tokens. */functionestimateFee(uint256 value) publicviewoverridereturns (uint256) {if (address(msg.sender).isContract()) {return0; } else {return_estimateFee(value); } }/** * @notice Moves `amount` tokens from the caller's account to `recipient`. * * Returns a boolean value indicating whether the operation succeeded. * * Emits a {Transfer} event. */functiontransfer(address recipient,uint256 amount) externaloverridereturns (bool) {uint256 fee =estimateFee(amount);_transfer(msg.sender, recipient, amount);_chargeFeeFrom(msg.sender, recipient, fee);returntrue; }/** * @notice Sets `amount` as the allowance of `spender` over the caller's tokens. * * Returns a boolean value indicating whether the operation succeeded. * * IMPORTANT: Beware that changing an allowance with this method brings the risk * that someone may use both the old and the new allowance by unfortunate * transaction ordering. One possible solution to mitigate this race * condition is to first reduce the spender's allowance to 0 and set the * desired value afterwards: * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 * * Emits an {Approval} event. */functionapprove(address spender,uint256 amount) externaloverridereturns (bool) {uint256 fee =estimateFee(0);_approve(msg.sender, spender, amount);_chargeFeeFrom(msg.sender,address(this), fee);returntrue; }/** * @notice Moves `amount` tokens from `sender` to `recipient` using the * allowance mechanism. `amount` is then deducted from the caller's * allowance. * * Returns a boolean value indicating whether the operation succeeded. * * Emits a {Transfer} event. */functiontransferFrom(address sender,address recipient,uint256 amount) externaloverridereturns (bool) {uint256 fee =estimateFee(amount);require(_allowances[sender][msg.sender] >= amount.add(fee),"VRC25: amount exeeds allowance"); _allowances[sender][msg.sender] = _allowances[sender][msg.sender].sub(amount).sub(fee);_transfer(sender, recipient, amount);_chargeFeeFrom(sender, recipient, fee);returntrue; }/** * @notice Remove `amount` tokens owned by caller from circulation. */functionburn(uint256 amount) externalreturns (bool) {uint256 fee =estimateFee(0);_burn(msg.sender, amount);_chargeFeeFrom(msg.sender,address(this), fee);returntrue; }/** * @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,"VRC25: 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),"VRC25: new owner is the zero address"); _newOwner = newOwner; }/** * @notice Set minimum fee for each transaction * * Can only be called by the current owner. */functionsetFee(uint256 fee) externalvirtualonlyOwner { _minFee = fee;emitFeeUpdated(fee); }/** * @dev Returns true if this contract implements the interface defined by * `interfaceId`. See the corresponding * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] * to learn more about how these ids are created. * */functionsupportsInterface(bytes4 interfaceId) publicviewoverridevirtualreturns (bool) {return interfaceId == type(IVRC25).interfaceId; }/** * @notice Calculate fee needed to transfer `amount` of tokens. */function_estimateFee(uint256 value) internalviewvirtualreturns (uint256);/** * @dev Transfer token for a specified addresses * @param from The address to transfer from. * @param to The address to transfer to. * @param amount The amount to be transferred. */function_transfer(address from,address to,uint256 amount) internal {require(from !=address(0),"VRC25: transfer from the zero address");require(to !=address(0),"VRC25: transfer to the zero address");require(amount <= _balances[from],"VRC25: insuffient balance"); _balances[from] = _balances[from].sub(amount); _balances[to] = _balances[to].add(amount);emitTransfer(from, to, amount); }/** * @dev Set allowance that spender can use from owner * @param owner The address that authroize the allowance * @param spender The address that can spend the allowance * @param amount The amount that can be allowed */function_approve(address owner,address spender,uint256 amount) internal {require(owner !=address(0),"VRC25: approve from the zero address");require(spender !=address(0),"VRC25: approve to the zero address"); _allowances[owner][spender] = amount;emitApproval(owner, spender, amount); }/** * @dev Internal function to charge fee for gas sponsor function. Won't charge fee if caller is smart-contract because they are not sponsored gas.
* NOTICE: this function is only a helper to transfer fee from an address different that msg.sender. Other validation should be handled outside of this function if necessary.
* @param sender The address that will pay the fee * @param recipient The address that is destination of token transfer. If not token transfer should be address of contract
* @param amount The amount token as fee */function_chargeFeeFrom(address sender,address recipient,uint256 amount) internal {if (address(msg.sender).isContract()) {return; }if(amount >0) {_transfer(sender, _owner, amount);emitFee(sender, recipient, _owner, amount); } }/** * @dev Internal function that mints an amount of the token and assigns it to * an account. This encapsulates the modification of balances such that the * proper events are emitted. * @param to The account that will receive the created tokens. * @param amount The amount that will be created. */function_mint(address to,uint256 amount) internal {require(to !=address(0),"VRC25: mint to the zero address"); _totalSupply = _totalSupply.add(amount); _balances[to] = _balances[to].add(amount);emitTransfer(address(0), to, amount); }/** * @dev Internal function that burns an amount of the token * This encapsulates the modification of balances such that the * proper events are emitted. * @param from The account that token amount will be deducted. * @param amount The amount that will be burned. */function_burn(address from,uint256 amount) internal {require(from !=address(0),"VRC25: burn from the zero address");require(amount <= _balances[from],"VRC25: insuffient balance"); _totalSupply = _totalSupply.sub(amount); _balances[from] = _balances[from].sub(amount);emitTransfer(from,address(0), amount); }}
/** * @title VRC25Permit * @notice Approval via signature for VRC25. Must be included for token contract. */abstractcontractVRC25PermitisVRC25, EIP712, IVRC25Permit { bytes32 private constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
mapping(address=>uint256) private _nonces;constructor() publicEIP712("VRC25", "1") { }/** * @dev See {IERC20Permit-DOMAIN_SEPARATOR}. */// solhint-disable-next-line func-name-mixedcasefunctionDOMAIN_SEPARATOR() externaloverrideviewreturns (bytes32) {return_domainSeparatorV4(); }/** * @dev Returns an the next unused nonce for an address. */functionnonces(address owner) publicviewvirtualoverride(IVRC25Permit) returns (uint256) {return _nonces[owner]; }/** * @dev See {IERC20Permit-permit}. */ function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external override {
require(block.timestamp <= deadline,"VRC25: Permit expired");bytes32 structHash =keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value,_useNonce(owner), deadline));bytes32hash=_hashTypedDataV4(structHash);address signer = ECDSA.recover(hash, v, r, s);require(signer == owner,"VRC25: Invalid permit");uint256 fee =estimateFee(0);_approve(owner, spender, value);_chargeFeeFrom(owner,address(this), fee); }/** * @dev Consumes a nonce. * * Returns the current value and increments nonce. */function_useNonce(address owner) internalreturns (uint256) {// For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be// decremented or reset. This guarantees that the nonce never overflows.// It is important to do x++ and not ++x here.return _nonces[owner]++; }}
The following example demonstrates a token using the VRC25 standard with a custom fee.
The easiest way to implement VRC25 specification is to let first inheritance of your contract to be VRC25 or VRC25Permit.
contractSampleVRC25isVRC25Permit {usingAddressforaddress;eventHello(address sender);constructor() publicVRC25("Example Fungible Token", "EFT", 0) EIP712("VRC25Permit", "1") { }/** * @notice Calculate fee required for action related to this token * @param value Amount of fee */function_estimateFee(uint256 value) internalviewoverridereturns (uint256) {return value +minFee(); }functionsayHello() public {_chargeFeeFrom(msg.sender,address(0),estimateFee(0));emitHello(msg.sender); }functionsupportsInterface(bytes4 interfaceId) publicviewoverridereturns (bool) {return interfaceId == type(IVRC25).interfaceId || super.supportsInterface(interfaceId); }/** * @notice Issues `amount` tokens to the designated `address`. * * Can only be called by the current owner. */functionmint(address recipient,uint256 amount) externalonlyOwnerreturns (bool) {_mint(recipient, amount);returntrue; }}
Once you have deployed a VRC25 compatible contract. You will also need to register for VIC ZeroGas to enable gas-less transaction for your contract. Please refer to VIC ZeroGas page for more information.