DelegatableVerySimpleToken
ERC20トークンに代理トランザクション機能を追加
もんだい
ERC20トークンを配るのはいいけど、ほとんどのひとは Ether を持っていない
なので、ERC20トークンを使いようがない
この問題の1つの解決方法になるかもしれないやつの実験です。
つくったもの
トークン所有者がガスを支払わずに、第三者がガスを支払ってトークンを transfer するやつのプロトタイプ
できた(たぶん
第三者がトークンの transfer を実行できる delegateTransfer() メソッドを追加した。
「代理トランザクション」機能とでもしておこう
代理トランザクションの流れ
トークン所有者が、トークンのトランザクションデータ(独自フォーマット)に署名をして、トランザクションデータと署名(r,s,v)を、ガスを支払ってトランザクションを実行してくれる第三者に渡す。
第三者がガスを支払い、トランザクションデータと署名を引数にトークンのスマートコントラクトの関数を実行。
スマートコントラクト側では ecrecover で署名検証をして、ERC-20 トークンのトランザクションを実行。 リプレイアタックを防ぐために、nonce を導入してみた。 実際にやってみたこと
第三者がトークンの transfer を実行できる delegatedTransfer() メソッドを追加した。
このメソッドは、トークンのトランザクションデータと、そのトランザクションデータの署名を引数にとる。
アドレスA から、アドレスB に、15 トークンを送信してみた。
アドレスA 0x89c24a88bad4abe0a4f5b2eb5a86db1fb323832c
アドレスB 0x2890228d4478e2c3b0ebf5a38479e3396c1d6074
トランザクションを実行したのは、アドレスC 0x556e112b67d6ab826964bc9e247f3d4fbb8f33d3
ガスを消費したのもアドレスCのみ
手順
アドレスBに15 トークンを送信するトランザクションデータ(以下トークントランザクションとする)を作成
アドレスAの秘密鍵でトークントランザクションに署名
アドレスCが、トークントランザクションデータと署名を引数にして、トークンのスマートコントラクトの関数 delegatedTransfer() を実行。
トークントランザクションデータ
トークントランザクションデータの要素は、以下の通り
nonce : transfer を実行した回数
value : 送るトークンの量
to : トークンの送付先
tokenAddress : トークンのコントラクトアドレス
今回は、上記のデータを次のような独自フォーマットにして、コントラクト側でパースすることにした。
0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d02ab486cedc00002890228d4478e2c3b0ebf5a38479e3396c1d6074000000000000000000000000130eb8f4bb906d018e2ea85b74ab8ae57abc2bd4000000000000000000000000
署名
今回は、得られた署名(r,s,v)を次のような独自フォーマットにして、コントラクト側でパースすることにした。
0xd5ed9006fef09b7742b8e82e47e231587ba43f08060cd04ff2b7bd74b42918bb430001d3b8d4b540706a78226c3759e0f9992aea7e33c21051043da6820fb9871b00000000000000000000000000000000000000000000000000000000000000
最初の 32 byte : r
つぎの 32 byte : s
つぎの 1 byte : v
トランザクションデータと署名を得るコード
code:example.js
const EthUtil = require('ethereumjs-util')
const Web3 = require('web3')
const web3 = new Web3('wss://ropsten.infura.io/ws');
const NONCE = 0
const VALUE = Web3.utils.toWei('15')
const TO = '0x2890228d4478e2c3b0ebf5a38479e3396c1d6074'
const TOKEN_ADDRESS = '0x130eb8f4bb906d018e2ea85b74ab8ae57abc2bd4'
const PRIVATE_KEY_STRING = '0x61ce8b95ca5fd6f55cd97ac60817777bdf64f1670e903758ce53efc32c3dffeb'
// address: 0x89c24a88bad4abe0a4f5b2eb5a86db1fb323832c
// トークンを送信するためのトークントランザクションデータを作成します。
function createTokenTx(nonce, value, to, tokenAddress) {
const hexNonce = Web3.utils.padLeft(Web3.utils.numberToHex(nonce), 64)
const hexValue = Web3.utils.padLeft(Web3.utils.numberToHex(value), 64)
}
// ハッシュに署名をして独自フォーマットに整形します。
function createSignature(hash, privateKey) {
const sig = EthUtil.ecsign(hash, privateKey)
const r = EthUtil.bufferToHex(sig.r)
const s = EthUtil.bufferToHex(sig.s)
const v = Web3.utils.numberToHex(sig.v)
}
// トークントランザクションデータの作成
const tx = createTokenTx(NONCE, VALUE, TO, TOKEN_ADDRESS)
console.log(tx)
// => 0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d02ab486cedc00002890228d4478e2c3b0ebf5a38479e3396c1d6074000000000000000000000000130eb8f4bb906d018e2ea85b74ab8ae57abc2bd4000000000000000000000000
// 秘密鍵の文字列からバッファを取得
const privateKey = EthUtil.toBuffer(PRIVATE_KEY_STRING)
// トークントランザクションの Keccak256 ハッシュを取得
const hash = EthUtil.sha3(tx)
// 署名を取得
const sig = createSignature(hash, privateKey)
console.log(sig)
// => 0xd5ed9006fef09b7742b8e82e47e231587ba43f08060cd04ff2b7bd74b42918bb430001d3b8d4b540706a78226c3759e0f9992aea7e33c21051043da6820fb9871b00000000000000000000000000000000000000000000000000000000000000
スマートコントラクト
code:DelegatableVerySimpleToken.sol
pragma solidity 0.4.24;
contract DelegatableVerySimpleToken {
string public constant name = "DelegatableVerySimpleToken";
string public constant symbol = "VDST";
uint8 public constant decimals = 18;
uint256 public constant totalSupply = 1000000000000000000000; // 1000 DVST
mapping(address => uint256) balances;
mapping(address => uint256) nonces; // 追加
event Transfer(
address indexed from,
address indexed to,
uint256 value
);
constructor () public {
emit Transfer(0x0, msg.sender, totalSupply);
}
function balanceOf(address _owner) public view returns (uint256) {
}
// 追加
function nonceOf(address _owner) public view returns (uint256) {
}
function transfer(address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
balances_to = balances_to + _value; emit Transfer(msg.sender, _to, _value);
return true;
}
// 追加
function delegatedTransfer(bytes _tx, bytes _sig) public returns (bool) {
// 署名 _sig を分割
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(_sig, 32))
s := mload(add(_sig, 64))
v := byte(0, mload(add(_sig, 96)))
}
// ecrecover を使用して、署名者のアドレスを得る
bytes32 txHash = keccak256(bytes(_tx));
address signer = ecrecover(txHash, v, r, s);
require(signer != address(0));
// トランザクションデータ _tx を分割
uint256 nonce;
uint256 value;
address to;
address tokenAddress;
assembly {
nonce := mload(add(_tx, 32))
value := mload(add(_tx, 64))
to := mload(add(_tx, 84))
tokenAddress := mload(add(_tx, 116))
}
require(tokenAddress == address(this));
require(to != address(0));
require(nonce == noncessigner); require(value <= balancessigner); // nonce をインクリメント
// トークンを送信
balancesto = balancesto + value; emit Transfer(signer, to, value);
return true;
}
}
実際にデプロイしたコントラクト
トークンの詳細
考察
ほんとは、tx に sign する際に token のコントラクトのアドレスも含めるようにしないと、同じフォーマットを使うスマートコントラクトがあった場合にリプレイ攻撃が行われてしまうはず
コントラクト側でも、自分のコントラクト用に署名されたかチェックすべき
追加した。
ノリで書いてみたけど、よく考えたらすでに仕様とか策定されてそう..
メモ
通常の transfer で nonce を increment する意味は?
リプレイアタックを防ぐためだったらいらなくないか?