Read this in other languages: English, 日本語.
It is under development.
流通やその価値が管理された内部流通トークンの例として、ポイントとして利用できるトークンの実装を想定して開発したものである。ここでいうポイントとは、ショッピングをしたときに付与されるポイントのようなものを意味します。
このトークンはCryptocurrencyのSmart Contractおよび関連技術の仕組みを使って実現しています。
このトークンはショッピングなどで利用されるポイントを想定し、Smart Contract内で実現するための基本機能を実装したものです。Ethereum上のDappsとして動作するもので、Solidity言語で開発しました。
また、Ethereum上のトークン送金時に発生する手数料(GAS)に関しては、ポイント付与されたユーザーがETHを持つ必要がないよう工夫しています。
実現した場合のメリット
- Cryptocurrencyの取引所で利用できない性質を持つ(ユーザー間で送金できない)
- トークンの配布や交換などにSmart Contract技術を利用することができる
- EthereumのPublic Chainに記録されるため、不正防止や透明性が担保される
- トークンの価値(価格)を固定化できる
- ユーザーに手数料が発生しないため利用しやすい
ショッピングのポイントをERC20トークンで実現することを考える時、ユーザー同士がトークンを送金し合うことができてしまうことなど、そのままでは導入しにくい。
そこで、ERC20 トークンのような使い勝手でありながら、ユーザー間では送金できないようにするなど、いくつかの制約を検討した。
トークンの保有者を次の3つの役割で定義する
- オーナー:このスマートコントラクトのオーナー
- 配布者:オーナーから割り当てられたトークンを実際にユーザーへ配布する
- ユーザー:ポイントとしてトークンを得られ、ポイントを消費することができる
トークンのおおまかな流れはこのようになる。
- 配布者はオーナーによって決定される
- 配布者にはオーナーからトークンを割り当てられる
- 配布者はユーザーへ任意のトークンを配布する
- ユーザーは保有するトークンの照会
balanceOf()
やトークンの移動の記録Transfer()
を ERC20 トークンと同じ仕組みで確認できる - ユーザーはオーナーにトークンの交換(ポイントの消費)を申請できる・・・(A)
- オーナーはユーザーが申請した数量のトークンを受け取ることができる・・・(B)
(A)から(B)への情報伝達はオフチェーンにより行うが、実装例は後ほど記載する。
トークンのインターフェース
pragma solidity ^0.5.0;
interface InternalDistributionTokenInterface {
// Required methods
// @title Is the ETH address of the argument the distributor of the token?
// @param _account
// @return bool (true:owner false:not owner)
function isDistributor(address _account) external view returns (bool);
// @title A function that adds the ETH address of the argument to the distributor list of the token
// @param _account ETH address you want to add
// @return bool
function addToDistributor(address _account) external returns (bool success);
// @title A function that excludes the ETH address of the argument from the distributor list of the token
// @param _account ETH address you want to delete
// @return bool
function deleteFromDistributor(address _account) external returns (bool success);
// @title A function that accepts a user's transfer request (executed by the contract owner)
// @param bytes memory _signature
// @param address _requested_user
// @param uint256 _value
// @param string _nonce
// @return bool
function acceptTokenTransfer(bytes calldata _signature, address _requested_user, uint256 _value, string calldata _nonce)
external
returns (bool success);
// @title A function that generates a hash value of a request to which a user sends a token (executed by the user of the token)
// @params _requested_user ETH address that requested token transfer
// @params _value Number of tokens
// @params _nonce One-time string
// @return bytes32 Hash value
// @dev The user signs the hash value obtained from this function and hands it over to the owner outside the system
function requestTokenTransfer(address _requested_user, uint256 _value, string calldata _nonce) external view returns (bytes32);
// @title Returns whether it is a used signature
// @params _signature Signature string
// @return bool Used or not
function isUsedSignature(bytes calldata _signature) external view returns (bool);
// Events
// token assignment from owner to distributor
event Allocate(address indexed from, address indexed to, uint256 value);
// tokens from distributor to users
event Distribute(address indexed from, address indexed to, uint256 value);
// tokens from distributor to owner
event BackTo(address indexed from, address indexed to, uint256 value);
// owner accepted the token from the user
event Exchange(address indexed from, address indexed to, uint256 value, bytes signature, string nonce);
event AddedToDistributor(address indexed account);
event DeletedFromDistributor(address indexed account);
}
インターフェースでは定義できない仕様を以下に記す
配布者の追加や抹消はオーナーだけが実行できるように実装する。 配布者の抹消は、配布者がトークンを保有していない場合に実行ができるように実装する。
// @title A function that adds the ETH address of the argument to the distributor list of the token
// @param _account ETH address you want to add
// @return bool
function addToDistributor(address _account) external onlyOwner returns (bool success) {
// `_account` is a correct address
require(_account != address(0), "Correct EOA address is required");
// `_account` is necessary to have no token
require(_balances[_account] == 0, "This EOA address has a token");
// `_account` is not an owner or a distributor
require(this.isOwner(_account) == false && this.isDistributor(_account) == false, "This EOA address can not be a distributor");
// `_account` is not a contract address
require(_account.isContract() == false, "Contract address can not be specified");
// Add to distributor
Distributors.add(_account);
emit AddedToDistributor(_account);
return true;
}
// @title A function that excludes the ETH address of the argument from the distributor list of the token
// @param _account ETH address you want to delete
// @return bool
function deleteFromDistributor(address _account) external onlyOwner returns (bool success) {
// `_account` is a correct address
require(_account != address(0), "Correct EOA address is required");
// `_account` is necessary to have no token
require(_balances[_account] == 0, "This EOA address has a token");
// Delete from distributor
Distributors.remove(_account);
emit DeletedFromDistributor(_account);
return true;
}
ユーザーはオーナーに送金したいトークンの数量を指定する。
requestTokenTransfer()
を実行することにより、次の情報をハッシュ化する。
- 送金をリクエストしているユーザーの EOA アドレス
- 送金したい数量
- 送金のリクエストを識別する文字列
_nonce
送金したいトークンの数量を次の実装により得るが、その際の _nonce
は重複しないものを与える必要がある。
requestTokenTransfer()
の実行時にはトランザクションが発生しないため、連番などのカウントをトークンの内部で行うことができない。
そのため、関数を実行する際に引数として与えて上げる必要があるが、一例としては Nano ID のような推測が難しい文字列を使用するとよいと思われる。
また、得られた得られた署名、はブロックチェーン以外の方法でオーナーへ引き渡す。
// @title A function that generates a hash value of a request to which a user sends a token (executed by the user of the token)
// @params _requested_user ETH address that requested token transfer
// @params _value Number of tokens
// @params _nonce One-time string
// @return bytes32 Hash value
// @dev The user signs the hash value obtained from this function and hands it over to the owner outside the system
function requestTokenTransfer(address _requested_user, uint256 _value, string calldata _nonce) external view returns (bytes32) {
return keccak256(abi.encodePacked(address(this), bytes4(0x8210d627), _requested_user, _value, _nonce));
}
オーナーはユーザーより次の値を受け取り、acceptTokenTransfer()
関数を実行する。
- 署名文字列
- 送金したいユーザーの EOA アドレス
- 送りたいトークンの数
- 署名時の
_nonce
の値
// @title A function that accepts a user's transfer request (executed by the contract owner)
// @param bytes _signature
// @param address _requested_user
// @param uint256 _value
// @param string _nonce
// @return bool
function acceptTokenTransfer(bytes calldata _signature, address _requested_user, uint256 _value, string calldata _nonce)
external
onlyOwner
returns (bool success)
{
// argument `_signature` is not yet used
require(usedSignatures[_signature] == false);
// Recalculate hash value
bytes32 hashedTx = this.requestTokenTransfer(_requested_user, _value, _nonce);
// Identify the requester's ETH Address
address _user = hashedTx.recover(_signature);
require(_user != address(0), "Unable to get EOA address from signature");
// the argument `_requested_user` and
// the value obtained by calculation from the signature are the same ETH address
//
// If they are different, it is judged that the user's request has not been transmitted correctly
require(_user == _requested_user, "EOA address mismatch");
// user has the amount of that token
require(this.balanceOf(_user) >= _value, "Insufficient funds");
_balances[_user] = _balances[_user].sub(_value);
_balances[msg.sender] = _balances[msg.sender].add(_value);
// Record as used signature
usedSignatures[_signature] = true;
// Execute events
emit Transfer(_user, msg.sender, _value);
emit Exchange(_user, msg.sender, _value, _signature, _nonce);
return true;
}
〜現在トークン交換の申請として実装されている署名方式について〜
web3.eth.sign()
は廃止の予定があるため、EIP-712による署名検証を採用する予定ですが、
本体には組み込まず、ÐAppsを別のリポジトリで実装の実験を行っています。
-
Signing message with eth.sign never finishes · Issue #1530 · MetaMask/metamask-extension MetaMask/metamask-extension#1530
-
Signature verification implementation for EIP712 https://github.com/godappslab/signature-verification
しばらくの間はweb3.eth.sign()
が動作しますが、EIP712 署名へ切り替えていく予定です。
Truffle Suite を利用したテストスクリプトで動作確認を行う。
ただし、署名の処理について、正しく機能しないため、その部分についてはブラウザを使ったテストを行う。
トークンの実装は GitHub にて公開する。
https://github.com/godappslab/internal-distribution-token
ウェブサイトからも、このトークンを操作することができる。(現在は Ropsten Test Network のみ利用可能)
https://lab.godapps.io/points/
Standards
- ERC-20 Token Standard. https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
Issues
- ERC865: Pay transfers in tokens instead of gas, in one transaction #865 ethereum/EIPs#865