メインコンテンツまでスキップ

· 約2分
Thurendous

Hello! Everybody!

SafeERC20 の使い方 - What is and how to use SafeERC20

OpenZeppelin の SafeERC20 というコントラクトがあります。このコントラクトの趣旨は、ERC20 トークンには基準があるものの、基準に沿っていない有名なトークンがあるからです。例えば、BNB や USDT は bool 値を返さない transfer 関数を持っています。このようなトークンを扱うときに、SafeERC20 を使うことで、bool 値を返さない関数を使っても、失敗した場合には revert することができます。

では正しく使うにはどうしたらよいか

SafeERC20 の使用方法はよく誤解されることがあります。

SafeERC20 は、トークンを安全にするために使用する ERC20 の拡張ではありません(OpenZeppelin の ERC20 はすでに安全ですがw)。他人の ERC20 トークンとのやり取りを安全にするための補助機能です。

この補助機能が行うことは、

  • ERC20 操作のブール値の戻り値をチェックし、失敗した場合にトランザクションを revert させます。
  • 同時に、ブール値の戻り値を持たない非標準の ERC20 トークンもサポートします。
  • さらに、攻撃 を緩和するために、承認額を増減させるための補助機能も提供します。

例として、トークンとやり取りする必要がある契約を作成しましょう。それは、人々が一定の価格でトークンを販売できるようにし、後で購入者がそれらから購入できるようにするものです。いくつかのトークンの転送を実行する必要があるため、それらに SafeERC20 を使用します。IERC20 の型の値で安全な操作を使用できるように、行「using SafeERC20 for IERC20」に注意してください。例:tradingToken.safeTransferFrom。

contract FixedPriceMarket {
using SafeERC20 for IERC20;

IERC20 tradingToken = 0x1234...;
uint256 price = 128;

mapping (address => uint256) selling;

function sell(uint256 tokenValue) {
tradingToken.safeTransferFrom(msg.sender, this, tokenValue);
selling[msg.sender] = selling[msg.sender].add(tokenValue);
}

function buy(address seller, uint256 tokenValue) {
require(msg.value == tokenValue.mul(price));
selling[seller] = selling[seller].sub(tokenValue);
tradingToken.safeTransfer(msg.sender, tokenValue);
seller.transfer(msg.value);
}
}

Reference

OpenZeppelin Forum
SafeERC20 について

· 約8分
Thurendous

Hello, everybody!

Never Give Up

以前書いた時から、EVM について理解できてきているので、当時は読みづらかった技術系の文章もすらすら読めるようになりました。そこで EVM のことあるいは他のなにかで頑張っているあなたももしかして何かができずに悩んでいるのかもしれません。

ここで行っておきますが、僕もそうだったので、安心してください。そのうち理解できるようになるだろう。

ちなみに、こちらの画像をみてごらん。

本当に経験した人しかわからないと思いますし、経験したとしてももう一度経験すると、やはり信じられない気持ちになると思うよね。

何がどうなっても、続けることが一番大切。

  • 遅くてもいい

  • 理想と現実

  • 「続けてても意味ない」

  • 最初の第一歩が一番むずいけどね

  • 三日坊主 VS 続ける


今回は、メモリーについて説明する。

Part 1 では、EVM がどのようにバイトコードのどこを狙って run させるかを見てきた。 それは、外部の calldata を入力してきてコントラクトのどの関数を呼んでいるかを判別して run すべきバイトコードの箇所を決めていることがわかった。

これを理解することで、関数の署名・call stack・calldata・EVM のオペコード について理解が進んだと思う。

Part 2 では、EVM におけるメモリーについて色々見ていこう。

Memory

Part1 のコードを思いだしてみて。1_Storage.solというコントラクトがあった。

pragma solidity >=0.7.0 <0.9.0;

contract Storage {
uint256 number;

function store(uint256 num) public {
number = num;
}

function retrieve() public view returns (uint256) {
return number;
}
}

これのバイトコードを生成すると、こうなっている。

608060405234801561001057600080fd....

今回はこの部分にフォーカスして説明したいと思う。一番最初の 5bytes だ。

6080604052
60 80                       =   PUSH1 0x80
60 40 = PUSH1 0x40
52 = MSTORE

これが聞いたことあるがかもしれないけど、いわゆる「free memory pointer」だ。

これを理解するには、まずはコントラクトのメモリーを理解しなければならない。

メモリのデータ構造

コントラクトのメモリはシンプルにいうと、byte の array だ。32bytes(256bit)あるいは 1byte(8bit)の単位として保存される。読まれるときには、32bytes(256bit)単位ごとになる。下記の画像は話した内容を具現化したもの。

この機能は3つの opcode に左右されている。

  • MSTORE(x, y): 32 byte(256 bit) の値 y をメモリのロケーションの x に保存
  • MLOAD(x): 32 byte(256 bit) のメモリの場所をロケーション x から読み出し、スタックに入れる
  • MSOTRE8(x, y): 1 byte (8 bit)の値 y をメモリロケーションの x に保存する

メモリロケーションのことはどこからメモリを読み取る/書き込むかを決めていると考えてよい。もし、1byte より多く読み取る/書き込むことをしたいのであれば、そのまま継続すればよいだけの話になる。

EVM playground

この EVM playground はあなたの理解の手助けができるだろう。Run をクリックして、右上の矢印をクリックしてみよう。矢印ボタンはコードの稼働プロセスをたどっていってくれる。スタック、メモリはどうなっているのかもわかる。すごく直感的なツールだと感心する。

// MSTORE 32 bytes 0x11...1 at memory location 0
PUSH32 0x1111111111111111111111111111111111111111111111111111111111111111
PUSH1 0x00
MSTORE

// MSTORE8 1 byte 0x22 at memory location 32 (0x20 in hex)
PUSH1 0x22
PUSH1 0x20
MSTORE8

// MSTORE8 1 byte 0x33 at memory location 33 (0x21 in hex)
PUSH1 0x33
PUSH1 0x21
MSTORE8

// MLOAD 32 bytes to the call stack from memory location 0 ie 0-32 bytes of memory
PUSH1 0x00
MLOAD

// MLOAD 32 bytes to the call stack from memory location 32 ie 32-64 bytes of memory
PUSH1 0x20
MLOAD

// MLOAD 32 to the call stack from memory location 33 ie 33-65 bytes of memory
PUSH1 0x21
MLOAD

opcode についても、英語だが説明文がついている。

やってみて変だなと思うことはないのか? まず、MSTORE8 を使って 1 byte の 0x22 をメモリの 0x20 に書き込んでいるつもりだが、何故かメモリはここから、

こう変わるんだ。

この余分なゼロってなんだと思うだろう。

メモリの拡張

コントラクトがメモリへなにかを書き込む際、byte 数に従ってガス代を支払っている。これが opcode のコストと言う。もし、これまでメモリへ書き込んだことがない場所へ書き込む場合、追加的にメモリ拡張のコストがかかる。

これまで書き込んだことがない箇所へ書き込む場合、メモリは 32bytes(256bit)拡張されることになる。

メモリのコストの増え方に関しては、最初の 724bytes は線形的に増加するが、それ以降は二次指数関数的になる。

上の例では、まず 0x00 のロケーションへ 32 bytes 書き込んだが、そこからさらに書き込むとなると、メモリ拡張をしなければならず、結果的に、メモリは 64 bytes になった。

メモリにある保存領域の最初のデフォルト値はゼロ。だから 2200000000000000000000000000000000000000000000000000000000000000 がメモリに追加された。

メモリは byte の Array だから

次に注意しないといけないのは、メモリロケーションの 33(0x21)から MLOAD したときに、下記の値が返された。

3300000000000000000000000000000000000000000000000000000000000000

32 bytes ずつ読み込んでいなくても、読み込めるということであった。

memory はただの byte array なので、どこの場所からでも読み込めることを覚えて下さい。32bytes の制限は特になく、どこからでも byte 単位から読み込める。

関数内でのみ新たにメモリを作成することができます。それは新たにインスタンス化された複雑な型(例えば new int[...] など)であったり、ストレージ参照変数からコピーされたものであったりします。

現在、我々はデータ構造について理解できたので、free memory の話へ戻ろう。

Free Memory Pointer

Free Memory Pointer とは、簡単にいうとメモリロケーションのポインターであり、Free Memory がどこからスタートすべきなのかを記録している。言い換えると、メモリのロケーションはどこまで書き込まれていて、どこから書き込むべきなのかを記録している。

これはコントラクトがメモリを上書きするのを防ぐためでもある。

変数がメモリへ書き込むときに、コントラクトはまず Free Memory Pointer を参照し、どこから書き込むべきかを決めないといけない。それから Free Memory Pointer をアップデートし、最新のメモリの「境界値」あるいは「オフセット」を記録する。

  newFreeMemoryPointer = freeMemoryPointer + dataSizeBytes

Bytecode

先程にも言及したように、Free Memory Pointer は最初の runtime bytecode のこの部分によって定義される。

60 80                       =   PUSH1 0x80
60 40 = PUSH1 0x40
52 = MSTORE

これは基本的に何を言っているかというと、Free Memory Pointer はメモリロケーションの 0x40(64 in decimal)で、値は 0x80(128 in decimal)となる。

すぐに疑問に思うのは、なぜこの数字なのということだろう。

答え:

0x00 - 0x3f (64 bytes): scratch space

0x40 - 0x5f (32 bytes): free memory pointer

0x60 - 0x7f (32 bytes): zero slot

0x40 は solidity が定義した一番最初の Free Memory Pointer のメモリロケーションだ。値 0x80 は単に最初の 4 つの 32 bytes のポジションを記録している。

これらの予約されたメモリについて

  • Scrach space: inline assembly のハッシングメソッドとして使っても良い
  • Free memroy pointer: 今のメモリのサイズ、free memory のスタートロケーション、最初は 0x80
  • The zero slot: 動的な Memory Array の初期値であり、どんなときにでも書き込まれることはない

コントラクトのメモリ

リアルなコントラクトのメモリについて見ていくとしようか。

非常にシンプルなコントラクトを作成した。こいつの名は MemoryLane。たった一個の関数を持っており、2つの Array を持っている。それから b[0]に値 1 を付与する。

3 行程度のコードだけど、かなり多くのことが起きているよ。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.3;

contract MemoryLane {
function memoryLane() public pure {
bytes32[5] memory a;
bytes32[5] memory b;
b[0] = bytes32(uint256(1));
}
}

上記のコードをもう一度 remix へ投入。

それから、コンパイルし、デプロイしてください。

memoryLane()関数を run して、debugging mode へ入ってください。

opcode を少しずつ見ていこう。

少し簡単なバージョンをEVM playgroundへ入れている。その opcode はそのようになっている。

この簡単なバージョンは opcode を順番に構成し、JUMP などのメモリ操作と関連しないことを除外した。いろいろコメントもしているので、参照してほしい。

このコードは 6 部分から構成されていて、これから dive in する。

play ground を使うことを強くおすすめする。

Free Memory Pointer Initialisation (EVM Playground Lines 1-15)

まず、“free memory pointer initialisation”からやっていく。上記でもあったように、0x80(128 in decimal)をスタックに入れる。これは free memory pointer の値として保存される。Solidity の memory のレイアウトを決めている。

この段階では、まだメモリにはなにもない。

続いて free memory pointer のロケーションの 0x40(64 in decimal)をプッシュする。

最後に、MSTORE を使って、0x40 ロケーションに 0x80 の値を記録。

この後、スタックには何もなく、メモリになにかを残せたことになる。メモリは 16 進数になっており、数字ごとに 4bit のデータを記録している。

今となって、192 の 16 進数の数字となっている(数字 2 桁 = 1 byte = 8 bit)。

先程の内容を思い出してください。最初の 64bytes は scrach space で、次の 32bytes は free memory pointer になる。これはまさに下記の状態。

Memory Allocation Variable “a” & Free Memory Pointer Update (EVM Playground Lines 16-34)

残りの部分については、簡単にするために一旦最後の飛ばしてハイレベルの概要を見てみよう。

次のメモリに関して、変数a(bytes32[5])のことを配置して free memory pointer をアップデートするということをしなければならないのがわかる。

コンパイラは array size によってどれくらいのスペースが必要か決定する。

メモリ array の要素の占める領域は 32 bytes の倍数になる。これは bytes1[]に対しても通用する。ただし、bytesstring は違う。

array のサイズ ×32bytes でどれくらいのメモリを食うかわかる。

この場合、5 * 32 bytes で、0xa0 (160)になるのだ。したがってその値は stack にプッシュされ、現在の free memory pointer に保存された。

この後、前の free memory pointer の値と足し算をして、値 0x120 (288)が生成された。この値が free memory pointer に保存された。

stack には変数aメモリをロケーション 0x80 にキープして後から参照できるようにしている。0xffffは JUMP のロケーションを意味しており、関係ないので当面は無視してよい(stack の underflow を防ぐためにある)。

Memory Initialisation Variable “a” (EVM Playground Lines 35-95)

今ではメモリに保存されて free memory pointer がアップデートされた。ここからは変数aのメモリにおける初期化をする。変数は宣言されて値代入していないので、ゼロ値になっているはず。

これをするためには、EVM の CALLDATACOPY を使って実施する。この opcode は3つの引数が必要:

  • memoryOffset(メモリロケーションへコピーする場所)
  • calldataOffset(calldata における byte offset のこと)
  • size(コピーする byte size)

今回の場合、memoryOffset はaのメモリロケーション 0x80。

calldataOffset は実際の calldata のサイズ、なぜならそもそも calldata の copy がいらない。メモリをゼロ値で初期化したい。結果的にサイズが 0xa0 あるいは 160 bytes になる。

ここで、メモリが 288 bytes になったとわかる(zero slot も含めて)。stack は今もう一度変数のメモリロケーションと JUMP ロケーションを持っている状態。

(その間に細かい操作については、何度も pop しているのがあるが、個人的にはまだなぞで、わかる方なら教えて下さい)

Memory Allocation Variable “b” & Free Memory Pointer Update (EVM Playground Lines 96-112)

ここでの操作は上記の流れと非常に似ているが、ただし、今回はbytes32[2] memory bとなる。

free memory pointer を 0x160(352 in decimal)にアップデートする(前の free memory pointer 288 + 新しい変数のサイズ 64 bytes)。

注意としては、free memory pointer はメモリにて 0x160 にアップデートされ、そこで現在は変数bのメモリロケーション(0x120)をスタックに入れてある。

Memory Initialisation Variable “b” (EVM Playground Lines 113-162)

続いて変数b初期化について説明する。

現在、メモリは 352bytes へ拡張された。スタックにはメモリロケーションを 2 個持っている。

Assign Value to b[0] (EVM Playground Lines 163-207)

やっと最後の段階に来れた。正直にいって難しかった。

ここでは、array b の index 0に値を付与したい。

コードでは b[0] = 1 となっている。

まず、この値が stack に突っ込まれる。その後、bit shift が起きるが、これ移動したのは 0 で何も変わりはないことを意味する。

次に array の index には 0x00 へ書き込まれ、0 positon へ書き込むことを意味する。その際に、array length の 2 を超えていないことをチェック。もし条件をクリアできない場合、異なるバイトコードのポジションへ飛び、エラーハンドリングの部分へ飛ばされる。

MUL、ADD opcode はメモリいにおいて値が書き込まれる場所を決めている。

0x20 (32 in decimal) * 0x00 (0 in decimal) = 0x00

メモリの array は 32 byte 単位であることを思い出してほしい。こちらは 0x00 になるので、offset は不要でそのまま 0 のポジションから書着込む。

0x00 + 0x120 = 0x120 (288 in decimal)

ADD は offset の値をメモリにある変数 b のロケーションを定義するように使われた。offset は今回 0 なので、そのまま、free memory pointer の位置に書き込むこととなる。

一番最後に、MSTORE を使って値 0x01 をメモリロケーションの 0x120 へ書き込む。そして、スタックにあるすべての要素を pop し、終了。

これでメモリはb[0] = 1となっている。下から 3 行目のところに 1 という数字が入っているのがわかる。

今回の記事はここまで!Bye!

Reference

EVM Deep Dives: The Path to Shadowy Super Coder 🥷 💻 - Part 2

· 約4分
Thurendous

Hello, everybody!

First Principle という言葉を耳にされたことはないだろうか。それは奥深く物事の基礎を理解した上で、よりよい発想を生まれることという概念だ。

スマートコントラクトの世界では、EVM とその周りにあるアルゴリズム、データ構造に関しては、まさにこの First Principle である。Solidity あるいはスマートコントラクトはこの基礎の上に作る構造物なので、EVM のことを理解せずにはよい solidity dev と称するにはまだ早いと言ってよいだろう。

基礎:Solidity → Bytecode → Opcode

まず、基礎的な部分について一定程度の知識を有することを前提としている。

  • おさらい:
    • Bytecode & ABI: バイトコードとは EVM 上で実行可能なコードで、ABI とはこの EVM バイトコードとやり取りができるための interface。
    • Solidity コードはまずバイトコードへコンパイルしてから、イーサリアムブロックチェーンへ乗せるという流れになる。バイトコードは実は色々なオペコードを意味している。

今回は基本的な solidity コントラクトについてのバイトコードの一部を一緒にみていき、EVM がどのように関数を選んでいるかを見よう。

コントラクトのバイトコードはコントラクトからコンパイルされたもので、コントラクトにはいくつかもの関数があるだろう。

よくある質問は、デプロイした後、EVM はどのようにバイトコードのどの部分を実行すべきかがわかったのかというのがある。

1_Storage.sol コントラクト

pragma solidity >=0.7.0 <0.9.0;

contract Storage {
uint256 number;

function store(uint256 num) public {
number = num;
}

function retrieve() public view returns (uint256) {
return number;
}
}

今回のコントラクトは上記のものとなる。コントラクトには二個の関数がある。store()retrieve()

runtime のバイトコードは以下の通り:

608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea2646970667358221220404e37f487a89a932dca5e77faaf6ca2de3b991f93d230604b1b8daaef64766264736f6c63430008070033

今回フォーカスするのは以下の部分:

60003560e01c80632e64cec11461003b5780636057361d1461005957

この部分は全体のバイトコードから抜き取ったもの。

関数のセレクタのロジックが含まれている。

言い遅れたが、EVM のオペコードに関しては、ここから確認できる。

オペコードは 1 byte の長さになる。そうすると自然に 256 パターンがあると分ると思う。

しかし、実際には 140 個のオペコードしか存在しない。

以下はバイトコードを関係するオペコードへ変換したもの。

60 00                       =   PUSH1 0x00
35 = CALLDATALOAD
60 e0 = PUSH1 0xe0
1c = SHR
80 = DUP1
63 2e64cec1 = PUSH4 0x2e64cec1
14 = EQ
61 003b = PUSH2 0x003b
57 = JUMPI
80 = DUP1
63 6057361d = PUSH4 0x6057361d
14 = EQ
61 0059 = PUSH2 0x0059
57 = JUMPI

スマコンの関数呼び出しや Calldata

オペコードへダイブする前に、コントラクトの関数の呼び出しについて復習しよう。

関数の呼び出しの前に、calldata に、関数の署名、そして続いて引数も入れる。

Solidity のコードに表現してもらうと、こうなるだろう。

event FunctionCalldata(bytes);
bytes memory functionCalldata = abi.encodeWithSignature('store(uint256)', 10);
emit FunctionCalldata(functionCalldata);
address(storageContract).call(functionCalldata);

今回の場合、僕は store 関数を呼び、10 引数として代入したい。

0x6057361d000000000000000000000000000000000000000000000000000000000000000a

上の長い数字の配列は abi.encodeWithSignature(”store(uint256)”,10)の結果。

関数シグネチャーは4バイトの Keccak ハッシュ値によって定義されている「0x6057361d」。

keccak256(“store(uint256)”) →  first 4 bytes = 6057361d

keccak256(“retrieve()”) → first 4 bytes = 2e64cec1

先程の calldata を見ると、全部で 36bytes あった。最初の 4bytes: 6057361dは関数のセレクタ関連で、store(uint256)と関係する。

ご自身ではッシュしてみて →ここ

6057361d = function signature (4 bytes)

000000000000000000000000000000000000000000000000000000000000000a = uint256 input (32 bytes)

以上、calldata の準備だった。

オペコード&スタック

それでは、EVM レベルへダイブしていこう。スタックについて理解しておく必要がある。わからない場合はこれを見て。

先程のオペコードはこれ:

60 00                       =   PUSH1 0x00
35 = CALLDATALOAD
60 e0 = PUSH1 0xe0
1c = SHR
80 = DUP1
63 2e64cec1 = PUSH4 0x2e64cec1
14 = EQ
61 003b = PUSH2 0x003b
57 = JUMPI
80 = DUP1
63 6057361d = PUSH4 0x6057361d
14 = EQ
61 0059 = PUSH2 0x0059
57 = JUMPI

PUSH1 は 1byte のデータをスタックへ入れることを意味する。そうすると、スタックはこうなる

PUSH1 0x00    | 0 |

続いて CALLDATALOAD は最初の stack(0)の値をポップさせる。この 0 値を input として使用し、offset として使う。スタックアイテムのサイズは 32bytes なのに対し、今回の calldata は 36bytes になっている。プッシュする値は msg.data[i : i+32]で、i は今回の input 値となる。これは毎回プッシュする値が 32bytes になることを保証できる。同時に、どの部分にもアクセルできる。

今回の場合、offset はなかったので、32bytes の calldata の値をスタックに push した。さきほど用意した calldata はこれ。やっと出番がきた。

0x6057361d000000000000000000000000000000000000000000000000000000000000000a

というのは、最後の0000000aが除外された 32bytes が今回の入力となる。

CALLDATALOAD    | 0x6057361d0...00 |

次に PUSH1 を使って hex value の 0xe0 をスタックに入れる。これは十進数だと 224。

PUSH1 0xe0      |       224        |
| 0x6057361d0...00 |

SHR を使ってライトシフトさせる。今回は最初のアイテムである 224 を取り出し、input として扱う。スタックにある二番目のアイテムをどれくらい右へライトシフトさせるかを定義している。256 - 244 = 32 bit とわかるように、最後には 4bytes のセレクタが call stack に残る。

SHR    | 0x6057361d |

DUP1 を使ってスタックの一番上の値をコピーする。

DUP1    | 0x6057361d |
| 0x6057361d |

PUSH4 を使って 4byte の関数のシグネチャーのretrieve() (0x2e64cec1) をスタックにプッシュする。

PUSH4 0x2e64cec1    | 0x2e64cec1 |
| 0x6057361d |
| 0x6057361d |

EQ は二個の値をスタックから出し、イコールなのかどうかをチェックする。もしイコールなら 1(true) をスタックにプッシュ、そうでない場合は 0(false) をプッシュする。

EQ          |      0     |
| 0x6057361d |

次に PUSH2 を使って二個の値をプッシュ。(0x003b, 十進数だと 59)

ここでは、59 が出たのはプログラムカウンターがバイトコードに次の実行コマンドはどこにあるのかを確認しているから。この 59 は retrieve()がスタート地点は 59 を意味している。

PUSH2 0x003b    |     59     |
| 0 |
| 0x6057361d |

JUMPI は”jump if”を意味する。二個の値をポップさせ、一個目の値は 59 で、二個目は 0。二個目の値は bool 値でこの jump を実行すべきかを確定している。1 = true, 0 = false。

もし true の場合、プログラムカウンターはアップデートされ実行はそちらへ jump する。今回は false なので、スキップ。

JUMPI    | 0x6057361d |

DUP1 again

DUP1    | 0x6057361d |
| 0x6057361d |

PUSH4 は 4byte の値をスタックにプッシュする

PUSH4 0x6057361d    | 0x6057361d |
| 0x6057361d |
| 0x6057361d |

また EQ して、今回は true なので。シグネチャーが合った。

EQ    |      1     |
| 0x6057361d |

JUMPI、今回は true なんで jump を実行する。プログラムカウンターは 89 で、バイトコードの違う場所へ移動。

PUSH2 0x0059    |     89     |
| 1 |
| 0x6057361d |

この場所に JUMPDEST オペコードがある。これがないと失敗する。

JUMPI    | 0x6057361d |

そこで終わり。これでオペコードの実行はstore(uint156)の場所へ移動できた。

今回は二個の関数しかないものの、たとえ 20 個の関数があったとしても、プロセスは一緒。

このリンクは非常におすすめ。触ってみると吉。

EVM のスタック・メモリ・ストレージをシミュレートしてくれる。

Reference

EVM Deep Dives: The Path to Shadowy Super Coder 🥷 💻 - Part 1

· 約2分
Thurendous
Polymetis

Hello, everybody!

EVM のインタラクションについては、外部のコントラクトを呼び出すときに、主導権は全部外部のコントラクトが握ることとなり、危険がケースがある。もしその外部のコントラクトは悪意のあるコントラクトの場合、悪用されるリスクにさらされる。

EVM(Ethereum Virtual Machine) の世界では、Re-entrancy の攻撃というのがある。

まさにこの攻撃手法が取られることがある。

攻撃のコントラクトはコントラクトに何度も入り込んで、チェックする条件が反映される前になんども同じ操作をされてしまうというやり口である。本来なら一度のみ呼ばれるものが何度もチェックを抜けて呼ばれると予期しない挙動になったりして、資産が盗まれたりする。

この攻撃手法は昔の The DAO 事件がやられた手法でもある。歴史に残る大事件だ。

他のソフトウェア環境では起こらないよう脆弱性だ。これは新しい開発者だとやりがちなミスになる。

このような状況において、Checks-Effects-Interactions を適用してください。

  • 外部のコントラクトを呼び出すのは避けられない
  • Re-entrancy 攻撃を避けたい

実装

  • まず、check

関数を呼び出してまず実行する条件をチェック(例えば十分な資金をもっているか)。

  • 次、effect

その次にすべての状態変数を更新すべき

  • 最後、interaction

最後に外部のコントラクトとのインタラクションをすべき。内部の状態が完全に更新された後に、初めて外部のコントラクトを呼び出す。

この順番さえ守れば、Re-entrancy 攻撃は最初の関門をくぐりぬけれなくなります。

もし、最後に状態の変更をするとなると、状態変更される前に何回も同じ関数の部分が呼び出されてしまい、Re-entrancy 攻撃の罠にハマってしまう。

実例

pragma solidity 0.8.13;


contract reentrancyVictim{
mapping(address => uint256) public balances;
uint256 public contractBalance;

function payIn() public payable{
balances[msg.sender] += msg.value;
}

function withdraw() public payable{
require(balances[msg.sender] > 0, "Insufficiant balance");
payable(msg.sender).call{value: balances[msg.sender]}("");
balances[msg.sender] = 0;
}

function updateContractBalance() public{
contractBalance = address(this).balance;
}
}

このコードは一見して大丈夫なように見えるが、withdraw()関数の 2 行目に十分な資産があるかを確認している。次にアセットをユーザーへ送付している。最後にバランスを更新している。

これは check-effect-interaction のルールを守っていない。

  • check: require(balances[msg.sender] > 0, "Insufficiant balance");
  • interaction: payable(msg.sender).call{value: balances[msg.sender]}("");
  • effect: balances[msg.sender] = 0;

となっており、effect と interaction の順番が逆。

その結果、Re-entrancy 攻撃が可能となる。

具体的なスキームはこんなかんじ:

adjust funds の前に、何回も何回も資産が抜き出されて、最後に資金がそこをつくことになるだろう。

Attacker のコードは以下:

pragma solidity 0.8.13;


contract reentrancyAttack{

address public victim;
uint256 public amount;
uint256 public counter;

constructor(address _victim) payable{
victim = _victim;
amount = msg.value;
}

receive() external payable{ // イーサ受け取り用のreceive関数
counter++;
attack();
}

function payIn() public returns (bool success){
(bool success, bytes memory data) = payable(victim).call{value: amount}(abi.encodeWithSignature("payIn()"));
}

function withdrawAttack() public{
if(counter < 4){
// もう一度withdrawを呼ぶ用
payable(victim).call(abi.encodeWithSignature("withdraw()"));
}
}
}

ソリューション

その対策としては、実はものすごくシンプルにwithdraw()の第 3, 4 行を逆転させて、修正を加える。

 function withdraw() public payable{
// check
require(balances[msg.sender] > 0, "Insufficiant balance");
// effect
uint256 payBalance = balances[msg.sender];
balances[msg.sender] = 0;
// interaction
(bool success, bytes memory data) = payable(msg.sender).call{value: payBalance}("");
if(!success){ // catch the case where the send was unsuccesful
balances[msg.sender] = payBalance;
}
}

もし、success が false の場合、バランスを元通りに戻す操作をしている。 // interaction の行において reenter されても、すでにバランスはゼロなので、revert され、問題はなくなった。

THE END

· 約3分
Thurendous
Polymetis

Hi Hi, never give up!

ここでは、opcode(オペレーションコード)について語るには EVM についても語る必要がある。

他のプログラミング言語と同じように、solidity はハイレベルなプログラミング言語であり、人間が読みやすい一方で、コンピューターは理解できない。私達が geth をインストールすると、それに加えて Ethereum Virtual Machine がつきものとしてついてくる。EVM とは軽量級の OS みたいなもので、スマートコントラクトをランする環境として用意されている。

Solidity のコードを solc コンパイラを使って compile するときに、コードをバイトコードに変換される。EVM はバイトコードが理解できる。

下記のスマートコントラクトの例を見てみよう。

pragma solidity ^0.4.11;
contract MyContract {
uint i = (10 + 2) * 2;
}

この場合、remix browser で走らせた場合、コントラクトの details を見てみてください。そうすると、以下の情報が見えてくる。

このケースでは、コンパイルされたコードは:

60606040525b600080fd00a165627a7a7230582012c9bd00152fa1c480f6827f81515bb19c3e63bf7ed9ffbb5fda0265983ac7980029

このデータはヘクサデシマルデータとして現れ、コントラクトの最終の形となっている。バイトコードと呼ばれているものだ。

実際にコントラクトをデプロイするときには、このバイトコードをデプロイしていることとイコール。

ウォレットあるいはアドレスの前にある"0x"とは何を意味するのだろうか。"0x"からスタートするものはこれは EVM と会話をするには、EVM はどんなデータでも 16 進数のデータとして扱うのがデフォルトだからである。

opcode もある。

PUSH1 0x60 PUSH1 0x40 MSTORE PUSH1 0x18 PUSH1 0x0 SSTORE CALLVALUE ISZERO PUSH1 0x13 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST JUMPDEST PUSH1 0x36 DUP1 PUSH1 0x21 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x60 PUSH1 0x40 MSTORE JUMPDEST PUSH1 0x0 DUP1 REVERT STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 SLT 0xc9 0xbd STOP ISZERO 0x2f LOG1 0xc4 DUP1 0xf6 DUP3 PUSH32

opcode の情報については、yellow paper に書かれている。

EVM はスタックマシンである。

簡単に理解するために、以下を参考にしてください。

まずこのルールは伝統的なパソコンの LIFO と同じである。

通常の計算の場合:

10 + 2 * 2
// 答えは14

// stack machineの場合
2 2 * 10 +

上記はスタックマシンの場合で、まず2をいれて次の2を入れて、次に乗法記号の*をいれて、計算し 4 になり、次に 10、最後にプラス記号で加法計算して、14 になる。

これで 14 の結果になる。

スタックに入れる命令はPUSHといい、スタックから除外する命令はPOPという。一番良く見るパターンとしては、PUSH1があり、それは 1byte のデータをスタックにいれる

PUSH1 0x60

これは 1byte のデータとして0x60をスタックに入れることを意味する。そして、PUSH1の 16 進数表示は偶然に0x60と同じ。ここで、0x を除外すると、6060になる。

PUSH1 0x60 PUSH1 0x40 MSTORE

MSTORE は2つの入力が求められ、output なし。なので、上記の解釈をすると、

  1. PUSH1(0x60): put 0x60 in the stack
  2. PUSH1(0x40): put 0x40 in the stack
  3. MSTORE(0x52): allocate 0x60 of memory space and move to the 0x40 position

実はいつも同じようなマジックナンバー6060604052を見るのは、solidity bytecode の誘導の始まりだからだ。

また、実は 16 進数ということは、10 進数で考えると、40 は 64 で 60 は 96 になっている。

“PUSH1 0x60 PUSH1 0x40 MSTORE”がやっているのは、96bytes のメモリーを allocate し、ポインターを 64 個目のバイトの開始位置に移動することになる。

これで、64byte のスクラッチスペースと 32byte の一時的なメモリを確保することができた。

EVM において、3 種類のデータ保存領域がある。

  • 今紹介したスタックにある場所
  • RAM のメモリー領域で、MSTORE の opcode のところ
  • SSTORE の永久保存領域(とてもガス代がかかる)

そして、Assembly Language を使って、スマートコントラクトをかける。これによって opcode を使うことができる。結構難しいけど、有用なときもある。solidity のみではできないようなこともできてしまう。

THE END

Bernard さんの記事を参考に抜粋・翻訳したりしている。

· 約2分
Thurendous
Polymetis

Hello, everybody!

ゼロ知識証明はシンプルに言うと人に秘密をバラすことなく、知っていることを証明できること。

妙な言い方をしていて、自分もなかなかとっつきにくいことだと思いました。事例を通して理解すると、ピンとくることもあるのかもしれない。

ゼロ知識証明は非常に重要で、これを使うことで、プライバシーの妥協をすることなく、なにかをすることができる。

  • ウェブサイトへログイン:パスワードを入力するかわりに、自分がそれを知っていることを証明すればよい
  • アイデンティティの認証:本名をバラすかわりに、プルーフを送るだけで済む
  • プライベートブロックチェーンのトランザクション:お金をパブリックなビットコインブロックチェーン上で送るよりも、プルーフで検証することで済む。例えば、Zcash はこの仕組である。

例1

Alice and Bob は絵本にいる Waldo の場所を探すのに競っているとしよう。

Alice は Waldo の居場所は見つかったと言い張っている。 Alice は実際の居場所をバラすことなく、伝えたくて色々考えた。

Proof1

Alice は Waldo の破片をそのまま切り取って Bob に見せる。Bob は Alice の切り取った破片、あるいは切り取った後の絵本を参考に答えを探すことができる。

Proof2

Alice は別の紙を絵本にかぶせて、Bob に Waldo だけ穴を開けてを見せるとしよう。この場合、Waldo が周りの正確な風景にいるかは不明で、Alice はその風景を正しく証明できれば証明できる。

健全性、完全性、ゼロ知識の属性

この二種類のソリューションはゼロ知識証明の3つの属性である健全性、完全性、ゼロ知識を満たしていると言えよう。

  1. 健全性:証明できたら、本当になる。Alice がもし Waldo の居場所を探せなかったら、ランダムなピースになったに違いなく、すぐに見つかっていないとばれるだろう。
  2. 完全性:証明できた場合は必ずプルーフがある。Waldo を探せた場合、必ず Waldo を見せなければならない。
  3. ゼロ知識:証明されたことが見せられていて、事実そのものは見せていない。唯一見せた事実は Waldo は見つかったよということだけで、どの場所にいるかとかの情報は開示していない。

例 2

もう一つ例を見てみよう。Alice は色盲で、Bob はそうではない。でも Alice は Bob が自分は色盲でないことを信用できておらず、Bob に証明してほしいと思っている。

そこで暗号専門家の彼女はうまく考えて実験をしだした。

Alice は青いボールとレッドのボールを持っていて、体の後ろにもっていき、シャッフルしてから Bob に見せつつ、ボールが入っている手が変わったのかについて尋ねる。 これを 20 回くらい繰り返すことで、もし Bob が全部正解していたら、たまたま Bob が色盲でありつつ、正解できた確率は 1/1048576 くらいで無視できるので、ほぼ信頼に値するだろうと Alice は考えた。

THE END

0xSage さんの記事を参考に抜粋・翻訳したりしている。

· 約8分
Thurendous
Polymetis

BYAC

TL;DR

  • ERC721 は一種の NFT に対応しているのに対し、ERC1155 は複数の NFT に対応しているイメージです。
  • ERC1155 の ERC721 との違い
    • token id は種類を規定し、id ごとに amount という数量を定義した
    • バッチ処理を実装した
  • 今回は ERC1155 のコード解説をした

EIP1155

ERC20 にしろ ERC721 にしろ、コントラクトごとに一種のコインにしか対応していないです。例えば、我々がポケモンのゲームを作ろうとすると、ERC21 あるいは ERC721 を使うと装備ごとにコントラクトをデプロイしないと行けないことになります。一千種類のアイテムがあるとすると、一千個のコントラクトを作らなければなりません。これはとんでもないことになってしまいます。課題を解決すく、イーサリアムの EIP1155 では、一個のコントラクトに複数の FT あるいは NFT を含めることにしました。特に GameFi のケースでは非常に有用です。

シンプルに言うと、ERC1155 は前に紹介した NFT の token スタンダード ERC721 と似ている:ERC721 では、token ごとにtokenIdを持っており、この id がユニークです。tokenIdは一個の token を代表している。それに対して、ERC1155 の場合は、token ごとに id がユニークだが、id ごとに数量が定義されている。これで、複数の種類の token は同じコントラクト内で管理することができる様になりました。

種類ごとに URI が存在していて、matadata を保存しています。ERC721 の URI と類似するが、以下のように、ERC1155 のメタデータインターフェイスのコントラクト:

/**
* @dev ERC1155のオプションインターフェイス、URI()でmetadataを返す
*/
interface IERC1155MetadataURI is IERC1155 {
/**
* @dev idのURIを返す
*/
function uri(uint256 id) external view returns (string memory);

では、どのようにトークンは FT か NFT を区別するのか?答え簡単だ、id の数量が 1 の場合ですと、これは NFT であり、ERC721 と似ている。もし id に対応する token の数量が 1 より大きい場合、それは FT となる。同じ id を共有しているので、ERC20 に類似します。

IERC1155 インターフェイス

ERC1155 インターフェイスは EIP1155 の実現すべき機能を定義しています。その中で、4 つのイベントと 6 個の関数を定義しています。ERC721 との違いとしては、ERC1155 は複数の種類の token を含まれます。また、バッチトランスファ、バッチバランスチェックの機能が追加されました。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "https://github.com/AmazingAng/WTF-Solidity/blob/main/40_ERC1155/IERC1155.sol";

/**
* @dev ERC1155のインターフェイス、EIP1155で求められた機能を定義
* 詳細:https://eips.ethereum.org/EIPS/eip-1155[EIP].
*/
interface IERC1155 is IERC165 {
/**
* @dev 単一な種類のtokenのトランスファイベント
* `value`個の`id`種類のtokenが`operator`によって`from`から`to`へトランスファ
*/
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);

/**
* @dev バッチトランスファイベント
* ids, valuesはトランスファするtokenの種類、数量の配列
*/
event TransferBatch(
address indexed operator,
address indexed from,
address indexed to,
uint256[] ids,
uint256[] values
);

/**
* @dev バッチアプルーブ
* `account`がすべての権限を`operator`に移譲するときに放出
*/
event ApprovalForAll(address indexed account, address indexed operator, bool approved);

/**
* @dev `id`のtokenのURIが変更となったとき、放出。`value`は新たなURI
*/
event URI(string value, uint256 indexed id);

/**
* @dev バランスを返す,`account`が持っている`id`のtokenの残高を返す
*/
function balanceOf(address account, uint256 id) external view returns (uint256);

/**
* @dev バッチで複数バランスを返す。`accounts`の配列と`ids`配列のlengthがイコールでないといけない
*/
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
external
view
returns (uint256[] memory);

/**
* @dev バッチで複数アプルーブ,callerのtokenの権限を`operator`に渡す
* {ApprovalForAll}イベントを放出
*/
function setApprovalForAll(address operator, bool approved) external;

/**
* @dev バッチでアプルーブをチェックし、boolを返す。`operator`が`account`によってアプルーブされた場合,`true`を返す
*/
function isApprovedForAll(address account, address operator) external view returns (bool);

/**
* @dev セーフトランスファ、`amount`数量の`id`種類のtoken
* {TransferSingle}イベントを放出
* 条件:
* - callerがownerではない場合、権限もっていないと使えない
* - `from`は十分なtokenをもっている
* - 送り先がコントラクトの場合,`IERC1155Receiver`の`onERC1155Received`関数を実装していないと通らない
*/
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes calldata data
) external;

/**
* @dev バッチセーフトランスファ
* {TransferBatch}イベントを放出
* 条件:
* - `ids`、`amounts`のlengthが同じ
* - 送り先がコントラクトの場合,`IERC1155Receiver`の`onERC1155Received`関数を実装していないと通らない
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata amounts,
bytes calldata data
) external;
}

ERC1155 のイベント

  • TransferSingle event: 単一な種類のトランスファイベント、トランスファが起きたときに放出
  • TransferBatch event: バッチトークントランスファのイベント、バッチのトランスファが起きたときに放出
  • ApprovalForAll event: バッチアプルーブのイベント、バッチアプルーブが起きたときに放出
  • URI event: metadata のアドレス変更のイベント、uri変更時に放出

IERC1155

  • balanceOf(): 単一な種類の残高をチェックする。accountの持っているid種類の token のバランス
  • balanceOfBatch(): 多種類のバランスをチェック。チェックするaccountsidsとの length が同じである必要がある
  • setApprovalForAll(): バッチアプルーブ、caller の token を operator に権限を移譲する
  • isApprovalForAll(): バッチアプルーブの情報をチェックする。operatoraccountによって権限をもらっている場合はtrueを返す
  • safeTransferFrom(): セーフな単一の token のトランスファ。amount数量のid種類の token をfromから、toへ送る。toがコントラクトの場合、onERC1155BatchReceived()関数の実装があるかをチェックされる。

ERC1155 を受け取るために、用意するコントラクトの形

ERC721と同じように、NFT を送ってロックされてしまうことを避けるために、ERC1155は受け取る側のコントラクトにIERC1155Receiverコントラクトを継承し、2 つの関数を実装しなければならない。

  • onERC1155Recieved(): 単一な種類の token を受け取るための関数。ERC1155 のセーフトランスファであるsafeTransferFrom関数からのトランスファを受けるためには、自分自身の selector である0xf23a6e61を返す
  • onERC1155BatchReceived(): 複数種類の token を受け取る用の関数。ERC1155のセーフトランスファsafeBatchTransferFromからのトランスファを受け取るために、自分自身で selector0xbc197c81を返す必要がある。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/IERC165.sol";

/**
* @dev ERC1155を受け取るためのコントラクト、ERC1155のtokenを受け取るにはこれを実装しないといけない
*/
interface IERC1155Receiver is IERC165 {
/**
* @dev ERC1155の`safeTransferFrom`のトランスファを受ける
* 0xf23a6e61 あるいは `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`を返す
*/
function onERC1155Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes calldata data
) external returns (bytes4);

/**
* @dev ERC1155の`safeBatchTransferFrom`を受ける
* 0xbc197c81 あるいは `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`を返す
*/
function onERC1155BatchReceived(
address operator,
address from,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
) external returns (bytes4);
}

ERC1155 のメインコントラクト

メインコントラクトはIERC1155のインターフェイス規定の関数を実装しました。また、単一な種類/複数の種類の token のミント、バーン関数も実装しました。

状態変数は4つ

  • name: token 名
  • symbol: token シンボル
  • _balances: token バランスのマッピング。idの owner のアドレスのバランスを記録
  • _operatorApprovals: バッチでアプルーブをするためのマッピング。残高を持っているアドレスのアプルーブ状態を記録

ERC1155 関数

全部で 16 個の関数を持っています。 もちろん、ERC1155規定の関数を実装しています。

  • constructor: 引数は name, symbol
  • supportsInterface(): ERC165スタンダードを実現、support するインターフェイスを返す。他のコントラクトがチェックする用に準備する
  • balanceOf(): IERC1166の残高をチェックする関数。ERC721と違うのは、引数はaccount及びid
  • balanceOfBatch(): バッチで複数のバランスを返す関数
  • setApprovalForAll(): バッチで複数のアプルーブをする関数。ApprovalForAllイベントを放出する
  • isApprovedForAll(): 全部の権限を持っているアドレスなのかの確認をする関数。
  • safeTransferFrom(): 単一な種類の token のトランスファをする関数。TransferSingleイベントを放出。ERC721と違うのは、引数はfrom, to, id以外にも、amountというのが必要となる。
  • safeBatchTransferFrom(): 複数種類の token をトランスファする関数。TransferBatchを放出。
  • _mint(): 一種類の token を鋳造
  • _mintBatch(): 複数種類の token を鋳
  • _burn(): 一種類の token を burn する
  • _burBatch(): 複数種類の token を burn する
  • doSafeTransferAcceptanceCheck(): 一種類の token のトランスファのセーフチェック。safeTransferFrom()によって使われる。onERC1155received()関数を実装しているかをチェックする。
  • uri(): ERC1155idの種類の metadata のリンクを返す。ERC721tokenURIと似ている。
  • baseURI(): baseURIを返す。uri はbaseURIidを接続するので、通常は開発者が書き換える必要がある。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IERC1155.sol";
import "./IERC1155Receiver.sol";
import "./IERC1155MetadataURI.sol";
import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/Address.sol";
import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/String.sol";
import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/IERC165.sol";

/**
* @dev ERC1155スタンダードのimplementation
* 詳細はhttps://eips.ethereum.org/EIPS/eip-1155
*/
contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI {
using Address for address; // library Address
using Strings for uint256; // library Strings
// Token名
string public name;
// Tokenシンボル
string public symbol;
// token種類のid → account → balances のマッピング、残高を記録する用
mapping(uint256 => mapping(address => uint256)) private _balances;
// address → adderss の全権移譲の記録をするマッピング
mapping(address => mapping(address => bool)) private _operatorApprovals;

/**
* コンストラクタ、初期化の値`name` 、`symbol`
*/
constructor(string memory name_, string memory symbol_) {
name = name_;
symbol = symbol_;
}

/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return
interfaceId == type(IERC1155).interfaceId ||
interfaceId == type(IERC1155MetadataURI).interfaceId ||
interfaceId == type(IERC165).interfaceId;
}

/**
* @dev バランスを返す関数、IERC1155のbalanceOf。accountのid種類のtoken数を返す
*/
function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
require(account != address(0), "ERC1155: address zero is not a valid owner");
return _balances[id][account];
}

/**
* @dev 複数のバランスを返す
* 条件:
* - `accounts` 、 `ids` のlengthが同じでなければならない.
*/
function balanceOfBatch(address[] memory accounts, uint256[] memory ids)
public view virtual override
returns (uint256[] memory)
{
require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch");
uint256[] memory batchBalances = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; ++i) {
batchBalances[i] = balanceOf(accounts[i], ids[i]);
}
return batchBalances;
}

/**
* @dev 複数のアプルーブをする関数。callerはoperatorに全権移譲をする
* {ApprovalForAll}オベントを放出
* 条件:msg.sender != operator
*/
function setApprovalForAll(address operator, bool approved) public virtual override {
require(msg.sender != operator, "ERC1155: setting approval status for self");
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}

/**
* @dev バッチ確認、複数
*/
function isApprovedForAll(address account, address operator) public view virtual override returns (bool) {
return _operatorApprovals[account][operator];
}

/**
* @dev セーフトランスファ,`amount`数量の`id`tokenを`from`から`to`へ送る関数
* {TransferSingle} イベントを放出
* 条件:
* - to ゼロアドレスでないこと
* - fromアドレスは十分なtoken数を持っており、callerは権限を持っていること
* - to がスマートコントラクトの場合、IERC1155Receiver-onERC1155Receivedをサポートしていること
*/
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) public virtual override {
address operator = msg.sender;
// ownerか、権限をもらっているかのチェック
require(
from == operator || isApprovedForAll(from, operator),
"ERC1155: caller is not token owner nor approved"
);
require(to != address(0), "ERC1155: transfer to the zero address");
// fromは十分なtoken数を持っているかチェック
uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
// バランスを更新
unchecked {
_balances[id][from] = fromBalance - amount;
}
_balances[id][to] += amount;
// イベント放出
emit TransferSingle(operator, from, to, id, amount);
// セーフなトランスファのチェック
_doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
}

/**
* @dev バッチで複数のトランスファ,`amounts`という数量の配列、`ids`というtoke種類の
* 配列を使って`from`から`to`へ送る
* {TransferSingle} イベント
* 条件:
* - to ゼロアドレスでない
* - fromアドレスは十分なtoken数を持っており、callerは権限を持っていること
* - to がスマートコントラクトの場合、IERC1155Receiver-onERC1155Receivedをサポートしていること
* - ids、amountsの配列のlengthが同じである
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) public virtual override {
address operator = msg.sender;
// ownerか、権限をもらっているかのチェック
require(
from == operator || isApprovedForAll(from, operator),
"ERC1155: caller is not token owner nor approved"
);
// 配列の長さが一緒である
require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
// ゼロアドレスでないこと
require(to != address(0), "ERC1155: transfer to the zero address");

// for loopでバランスを更新
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids[i];
uint256 amount = amounts[i];

uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
unchecked {
_balances[id][from] = fromBalance - amount;
}
_balances[id][to] += amount;
}

emit TransferBatch(operator, from, to, ids, amounts);
// セーフなコントラクトのチェック
_doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);
}

/**
* @dev mint鋳造関数
* {TransferSingle} イベントを放出
*/
function _mint(
address to,
uint256 id,
uint256 amount,
bytes memory data
) internal virtual {
require(to != address(0), "ERC1155: mint to the zero address");

address operator = msg.sender;

_balances[id][to] += amount;
emit TransferSingle(operator, address(0), to, id, amount);

_doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data);
}

/**
* @dev バッチで鋳造
* 释放 {TransferBatch} 事件.
*/
function _mintBatch(
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal virtual {
require(to != address(0), "ERC1155: mint to the zero address");
require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");

address operator = msg.sender;

for (uint256 i = 0; i < ids.length; i++) {
_balances[ids[i]][to] += amounts[i];
}

emit TransferBatch(operator, address(0), to, ids, amounts);

_doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data);
}

/**
* @dev バーン関数
*/
function _burn(
address from,
uint256 id,
uint256 amount
) internal virtual {
require(from != address(0), "ERC1155: burn from the zero address");

address operator = msg.sender;

uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
unchecked {
_balances[id][from] = fromBalance - amount;
}

emit TransferSingle(operator, from, address(0), id, amount);
}

/**
* @dev バーン関数:複数を同時に実行
*/
function _burnBatch(
address from,
uint256[] memory ids,
uint256[] memory amounts
) internal virtual {
require(from != address(0), "ERC1155: burn from the zero address");
require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");

address operator = msg.sender;

for (uint256 i = 0; i < ids.length; i++) {
uint256 id = ids[i];
uint256 amount = amounts[i];

uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
unchecked {
_balances[id][from] = fromBalance - amount;
}
}

emit TransferBatch(operator, from, address(0), ids, amounts);
}

// @dev ERC1155のセーフトランスファのチェック
function _doSafeTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) private {
if (to.isContract()) {
try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
if (response != IERC1155Receiver.onERC1155Received.selector) {
revert("ERC1155: ERC1155Receiver rejected tokens");
}
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1155: transfer to non-ERC1155Receiver implementer");
}
}
}

// @dev ERC1155の複数セーフトランスファのチェック
function _doSafeBatchTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) private {
if (to.isContract()) {
try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns (
bytes4 response
) {
if (response != IERC1155Receiver.onERC1155BatchReceived.selector) {
revert("ERC1155: ERC1155Receiver rejected tokens");
}
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1155: transfer to non-ERC1155Receiver implementer");
}
}
}

/**
* @dev ERC1155のidのuriを返す、metadata、ERC721のtokenURIに同じ
*/
function uri(uint256 id) public view virtual override returns (string memory) {
string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, id.toString())) : "";
}

/**
* {uri}のBaseURIを返す,uriはbaseURI、tokenIdをつないだもの
*/
function _baseURI() internal view virtual returns (string memory) {
return "";
}
}

remix で作成

今回 remix で作るのは ERC1155 版の BAYC。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "./ERC1155.sol";

contract BAYC1155 is ERC1155{
uint256 constant MAX_ID = 10000;
// コンストラクタ
constructor() ERC1155("BAYC1155", "BAYC1155"){
}

//BAYCのbaseURIはこれ:ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
function _baseURI() internal pure override returns (string memory) {
return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/";
}

// ミント関数
function mint(address to, uint256 id, uint256 amount) external {
// id は10,000を超えてはならない
require(id < MAX_ID, "id overflow");
_mint(to, id, amount, "");
}

// バッチでミント
function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) external {
// id は10,000を超えてはならない
for (uint256 i = 0; i < ids.length; i++) {
require(ids[i] < MAX_ID, "id overflow");
}
_mintBatch(to, ids, amounts, "");
}

}

実際の remix については、また今度の機会に書きますので、今回は割愛。

最後に

今回はERC1155について学習しました。いかがでしょうか。このスタンダードは同じコントラクトに複数の NFT や FT の共存を許したコントラクトを可能にしたので、個人的には大きなステップだと思っています。また、BYAC の改造をして無理やりERC1155にしました。ほんじゃ、またね。

THE END

· 約3分
Thurendous
Polymetis

Hello, everybody!

TL;DR:

結論を言っておきますと、秘密をオンチェーンに乗せるな。private という修飾子も秘密を保持するのに、意味をなさない。

勘違いしていませんか

solidity に触ると、private という修飾子にであうことがあるかと思うんですが、それは時にはプライベートという修飾子をつけてあるので、より秘密性を持っている変数でパスワードとかを保存すればよくね?と勘違いをすると思います。結論からいうと、ブロックチェーンというものは全くと言っていいくらいプライバシーがありませんので、秘密をオンチェーンに保存するのは論外だということです。

プライベートな変数を呼び出してみる

さて、実演を伴って行きたいので、コードを実際に動かしてみて行きましょうか。

今回の使うレポジトリはこちらです。

レポジトリの結果を見たい方はこのようにしましょう。

git clone https://github.com/thurendous/private-var
yarn install
yarn hardhat test

これで以下のような結果になります。

imageOfTerminalwithResult

今回使ったディレクトリーはtestcontractsのみです。

その詳細を説明します。

今回はこんなコントラクトを作ってみました。 中身は至ってシンプルです。

  • LoginContractというコントラクトを宣言
  • private な変数の userName, password を作りました。 この2つの変数をコンストラクタで初期化しています。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Uncomment this line to use console.log
// import "hardhat/console.sol";

contract LoginContract {
bytes32 private userName;
bytes32 private password;

constructor(bytes32 _username, bytes32 _password) {
userName = _username;
password = _password;
}
}

コントラクトのストレージの単位は slot と言われます。このスロットを実は何らかの方法で読み取ることができるのです。

続いて、testフォルダを見てください。

以下のコードが描かれていると思います。

  • テストのフォルダは書いたコントラクトのテストをするためのフォルダ

今回はテストをするというか、ストレージのスロットを読み取ることにします。

const {
time,
loadFixture,
} = require('@nomicfoundation/hardhat-network-helpers')
const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs')
const { expect } = require('chai')
const { ethers } = require('hardhat')

describe('LoginContract', function () {
// this is the test we run to reveal the private variable
it('should show us private variable data', async function () {
const LoginContract = await ethers.getContractFactory('LoginContract')

const userName = ethers.utils.formatBytes32String('calvin')
const password = ethers.utils.formatBytes32String(
'passwordIsMyBigSecret'
)

const loginContract = await LoginContract.deploy(userName, password)

await loginContract.deployed() // wait for it to be deployed

const slot0 = await ethers.provider.getStorageAt(
loginContract.address,
0
)
const slot1 = await ethers.provider.getStorageAt(
loginContract.address,
1
)
const formatUsername = await ethers.utils.parseBytes32String(slot0)
const formatPassword = await ethers.utils.parseBytes32String(slot1)

console.log('userName should be ->', formatUsername)
console.log('password should be ->', formatPassword)
})
})

結構たくさん書かれていますが、少しずつご説明します。

テストの大きな単位、「LoginContract というテストをするよ」ということを意味します。

describe('LoginContract', function () {

}

テストのより小さな単位、文字列の部分は自由に記載して OK。自分が書いたのは「プライベートな値を出してくれるよ」ということを意味する

 it('should show us private variable data', async function () {

}

コントラクトのクラスを作成します。

const LoginContract = await ethers.getContractFactory('LoginContract')

sername, password を作成しておきます。

const userName = ethers.utils.formatBytes32String('calvin')
const password = ethers.utils.formatBytes32String('passwordIsMyBigSecret')

コントラクトをインスタンス化します。そのときに、コントラクトのコンストラクタに入っている引数、先程作った username, password も代入します。

const loginContract = await LoginContract.deploy(userName, password)

コントラクトのデプロイを待ちます。

await loginContract.deployed() // wait for it to be deployed

続いて、肝心なところです。コントラクトのスロットを読み取ります。ポジションはそれぞれ 0, 1 です。これはuserName, passwordのポジションと同じです。

const slot0 = await ethers.provider.getStorageAt(loginContract.address, 0)
const slot1 = await ethers.provider.getStorageAt(loginContract.address, 1)

読み取った値を整形します。

const formatUsername = await ethers.utils.parseBytes32String(slot0)
const formatPassword = await ethers.utils.parseBytes32String(slot1)

ターミナルへ表示させます。

console.log('userName should be ->', formatUsername)
console.log('password should be ->', formatPassword)

これで先程の結果の画面が表示されて、読み取られていることがわかりますね。

  LoginContract
userName should be -> calvin
password should be -> passwordIsMyBigSecret
✔ should show us private variable data (668ms)


1 passing (670ms)

だから、private な変数だったとしても、秘密なことは何一つねえよ。

THE END

· 約7分
Thurendous
Polymetis

Hello, everybody!

TL;DR

  • NFT は非代替性トークンのこと
  • シンプルにいうとデジタル所有権のこと
  • コードで理解しましょう

BTC や ETH のようなトークンは FT と呼ばれ、代替性トークンのことです。特徴としては、お互いに交換可能で大きな違いはないことです(厳密には違いもありますが、ここではその議論をしない)。また、アート、コレクション、不動産などのようなお互い違いをかなり持っているようなものは非代替性トークンで代表されることがほとんどです。

イーサリアムでは EIP721 が提案されて、ERC721 のスタンダードが形成されました。

EIP, ERC とは

結論:EIP が ERC を含んでいる

まず、理解しなければならないのは、ERC721 です。この2つの間にはなんの関係があるのでしょうか。EIP とは、Ethereum Improvement Proposals のことで、イーサリアムコミュニティが提案したプロトコルなどを改善するための提案のことです。EIP はイーサリアムの中の任意の分野の改善で、例えば新たな機能、ERC、プロトコル改善などがあります。

ERC とは、Ethereum Request For Comment のことで、イーサリアム上の各種アプリケーションのプロトコルとスタンダードのこととなっています。典型的な ERC20, ERC721、あるいは URI のスタンダード ERC67、あるいはウォレットのフォーマット EIP75, EIP85 などがある。

ERC スタンダードはイーサリアムの発展における重要な構成要素で、ERC20, ERC721, ERC223, ERC777 などのスタンダードがイーサリアムのエコシステムに多大な影響を与えていました。

ERC165

まず ERC165 について理解しましょう。 スマートコントラクトはインターフェースを宣言して他のスマートコントラクトがチェックするためにやっているのが ERC165 のことです。

シンプルにいうと、ERC165 を通してとあるコントラクトが ERC721, ERC1155 をサポートしているかチェックできるという仕組みです。

interface IERC165 {
/**
* @dev コントラクトが当該スタンダードの`interfaceId`を実装していればtrueを返す
* 詳細はこちら:https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
*
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

次に実際には ERC721 がどのように supportsInterface()を実現したのか見てみましょう。

function supportsInterface(bytes4 interfaceId) external pure override returns (bool)
{
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC165).interfaceId;
}

この関数supportsInterfaceは IERC721 あるいは IERC165 の interfaceId が引数として入力された場合に、trueを返し、そうでない場合はfalseを返します。

IERC721

IERC721の中身を見てみましょう。 IERC721ERC721のインターフェーススタンダードのコントラクトで、ERC721 が実現すべき一般的な関数を定義してます。tokenIdを使って非代替性トークンを代表しています。アプルーブあるいはトランスファに際して、tokenIdは必ず出番があります。しかし、ERC20 はトランスファにおける数量だけを定義すればよくて、tokenId はありません。

/**
* @dev ERC721スタンダードのインターフェース
*/
interface IERC721 is IERC165 {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

function balanceOf(address owner) external view returns (uint256 balance);

function ownerOf(uint256 tokenId) external view returns (address owner);

function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes calldata data
) external;

function safeTransferFrom(
address from,
address to,
uint256 tokenId
) external;

function transferFrom(
address from,
address to,
uint256 tokenId
) external;

function approve(address to, uint256 tokenId) external;

function setApprovalForAll(address operator, bool _approved) external;

function getApproved(uint256 tokenId) external view returns (address operator);

function isApprovedForAll(address owner, address operator) external view returns (bool);
}

IERC721 の event

3 つの event を含まれています。TransferApprovalは ERC20 にも存在します。

  • Transfer: トークントランスファのときに放出。送り元from、送り先totokenId
  • Approvel: アプルーブするときに放出。アプルーブする側のアドレスowner、アプルーブした先のアドレスapprovedtokenId
  • ApprovalForAll: 一気に大量にアプルーブするときに放出するイベント。アプルーブする側のアドレスowner、アプルーブされる側のアドレスoperator、アプルーブする場合approvedは true で、逆に権限を剥奪する場合、approvedは false にする

IERC721 関数

  • balanceOf: とあるアドレスの持っている NFT の数量を返す
  • ownerOf: tokenId のオーナーを返す
  • transferFrom: 普通のトランスファ、引数は from は送り元で、to は送り先、tokenId も必要
  • safeTransferFrom:安全なトランスファ、もし受ける側はコントラクトの場合、ERC721Receiverの実装が求められる。引数は送り元のfrom、送り先のtotokenId
  • approve: 他のアドレスにあなたの NFT を使用する権利を渡す。権利を付与するアドレスはto、そしてtokenIdも引数に
  • getApproved: tokenIdがどのアドレスに権限を付与したのか確認する
  • setApprovalFroAll: 自分の持っているこのコントラクトのすべての NFT をとあるアドレスに対して全権移譲する
  • isApprovedForAll: 全権移譲しているアドレスがあるか確認する
  • safeTransferFrom: 安全なトランスファ関数のオーバーライド関数、引数にdata`が含まれている

IERC721Receiver

コントラクトがもし ERC721 の実現をしていない場合、送られてきた NFT はブラックホールに送ったように、永遠に取り出せなくなります。これを防止するために、ERC721 はsafetransferFrom()関数を実装している。ターゲットコントラクトがIERCReceiverインターフェースを実装している場合のみ、ERC721 トークンを受け取ることができます。そうでない場合はrevertされます。IERC721Receiver インターフェースは一個のonERC721Receiver()関数しかありません。

// ERC721Receiverのインターフェース: コントラクトはこれを実装して安全なトランスファを受けることができる
interface IERC721Receiver {
function onERC721Received(
address operator,
address from,
uint tokenId,
bytes calldata data
) external returns (bytes4);
}

次にERC721がどうやって_checkOnERC721Receivedを使ってコントラクトがonERC721Receiver()関数を実装していることを確認しているのを見てみましょう。

function _checkOnERC721Received(
address from,
address to,
uint tokenId,
bytes memory _data
) private returns (bool) {
if (to.isContract()) {
return
IERC721Receiver(to).onERC721Received(
msg.sender,
from,
tokenId,
_data
) == IERC721Receiver.onERC721Received.selector;
} else {
return true;
}
}

IERC721Metadata

IERC721Metadata は ERC721 の拡張インターフェイス。3つの metadata 用関数を用いている。

  • name():トークン名を返す
  • symbol():トークン符号
  • tokenURI():tokenId を使って metadata の url をとってくる。ERC721 特有の関数
interface Ierc721Metadata is IERC721 {
function name() external view returns (string memory);

function symbol() external view returns (string memory);

function tokenURI(uint256 tokenId) external view returns (string memory);
}

ERC721 メインコントラクト

ERC721 メインコントラクトは IERC721, IERC165, IERC721Metadata のすべての機能を定義した。4 つの状態変数、17 個の関数を含まれている。シンプルに実装されている。詳細はコメントを読んでください。

// SPDX-License-Identifier: MIT
// by 0xAA
pragma solidity ^0.8.4;

import "./IERC165.sol";
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./IERC721Metadata.sol";
import "./Address.sol"; // library
import "./String.sol"; // library

contract ERC721 is IERC721, IERC721Metadata{
using Address for address; // Address libraryを使う(isContractを使うため)
using Strings for uint256; // String library

// Token名
string public override name;
// Tokenシンボル
string public override symbol;
// tokenId -> owner address のマッピング
mapping(uint => address) private _owners;
// address -> バランスのマッピング
mapping(address => uint) private _balances;
// tokenID -> アプルーブされたアドレスのマッピング
mapping(uint => address) private _tokenApprovals;
// owner -> operatorアドレス -> bool(権限渡した場合はtrue)
mapping(address => mapping(address => bool)) private _operatorApprovals;

/**
* constructor, name, symbolを初期化する
*/
constructor(string memory name_, string memory symbol_) {
name = name_;
symbol = symbol_;
}

// ERC165のsupportsInterface関数
function supportsInterface(bytes4 interfaceId)
external
pure
override
returns (bool)
{
return
interfaceId == type(IERC721).interfaceId ||
interfaceId == type(IERC165).interfaceId ||
interfaceId == type(IERC721Metadata).interfaceId;
}

// IERC721のbalanceOf関数、_balancesを使って調べたいアドレスの残高を返す
function balanceOf(address owner) external view override returns (uint) {
require(owner != address(0), "owner = zero address");
return _balances[owner];
}

// IERC721のownerOf関数、_owners変数を使ってtokenIdのownerを返す
function ownerOf(uint tokenId) public view override returns (address owner) {
owner = _owners[tokenId];
require(owner != address(0), "token doesn't exist");
}

// IERC721のisApprovedForAll、_operatorApprovals変数を
// 使ってownerがoperatorに権限を移譲したかどうかをチェックする。
// 権限移譲した場合はtrueを返す
function isApprovedForAll(address owner, address operator)
external
view
override
returns (bool)
{
return _operatorApprovals[owner][operator];
}

// IERC721のsetApprovalForAllを実装。持っているトークンをすべてoperatorに権限を渡す(true),
// あるいはoperatorの権限を剥奪する(false)。_setApprovalForAll関数を呼び出す。
function setApprovalForAll(address operator, bool approved) external override {
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}

// 实现IERC721的getApproved,利用_tokenApprovals变量查询tokenId的授权地址。
function getApproved(uint tokenId) external view override returns (address) {
require(_owners[tokenId] != address(0), "token doesn't exist");
return _tokenApprovals[tokenId];
}

// _approve関数。_tokenApprovalsを書き換えて,toアドレスに tokenIdをいじる権限を渡す。
// Approvalイベントを放出。
function _approve(
address owner,
address to,
uint tokenId
) private {
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}

// IERC721のapprove関数,tokenIdの権限を to アドレスに渡す。
// 条件:toはownerではないこと,かつmsg.senderはownerあるいはapproveされたアドレス。
// _approve関数を呼び出す
function approve(address to, uint tokenId) external override {
address owner = _owners[tokenId];
require(
msg.sender == owner || _operatorApprovals[owner][msg.sender],
"not owner nor approved for all"
);
_approve(owner, to, tokenId);
}

// spenderアドレスがtokenIdを使う権限があるかないかを調べる。(あるのはownerか
// approveされたかのいずれだ。approveされた場合は通常のapproveあるいは
// setApprovalForAllの2パターン)
function _isApprovedOrOwner(
address owner,
address spender,
uint tokenId
) private view returns (bool) {
return (spender == owner ||
_tokenApprovals[tokenId] == spender ||
_operatorApprovals[owner][spender]);
}

/*
* トランスファ関数。_balances、_ownerのバランスを調整して tokenId を from から toに
* トランスファする。同時にTransferイベントを放出。
* 前提条件:
* 1. tokenId は from によって所有されている
* 2. to はゼロアドレスでない
* 条件を満たさない場合はrevert
*/
function _transfer(
address owner,
address from,
address to,
uint tokenId
) private {
require(from == owner, "not owner");
require(to != address(0), "transfer to the zero address");

_approve(owner, address(0), tokenId); // トランスファするので権限をリセットする

_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;

emit Transfer(from, to, tokenId);
}

// IERC721のtransferFrom関数の実装,セーフトランスファではないので、
// この関数を使うのは推奨されていない。
function transferFrom(
address from,
address to,
uint tokenId
) external override {
address owner = ownerOf(tokenId);
require(
_isApprovedOrOwner(owner, msg.sender, tokenId),
"not owner nor approved"
);
_transfer(owner, from, to, tokenId);
}

/**
* セーフトランスファ,安全にtokenIdをfromからtoへトランスファする。スマートコントラクトが
* erc721に対応しているかどうかをチェックした上で、トランスファをするので、NFTが永遠に
* ロックされる実態を回避する。_transfer、_checkOnERC721Received関数を呼び出す。
* 条件:
* from はゼロアドレスではない
* to はゼロアドレスではない
* tokenId が存在してしかもfromアドレスが所有
* toがスマートコントラクトの場合、必ずIERC721Receiver-onERC721Receivedをサポートされる
* ことが求められる
*/
function _safeTransfer(
address owner,
address from,
address to,
uint tokenId,
bytes memory _data
) private {
_transfer(owner, from, to, tokenId);
require(_checkOnERC721Received(from, to, tokenId, _data), "not ERC721Receiver");
}

/**
* IERC721のsafeTransferFromを実装。セーフトランスファ関数,_safeTransfer関数を呼び出している
*/
function safeTransferFrom(
address from,
address to,
uint tokenId,
bytes memory _data
) public override {
address owner = ownerOf(tokenId);
require(
_isApprovedOrOwner(owner, msg.sender, tokenId),
"not owner nor approved"
);
_safeTransfer(owner, from, to, tokenId, _data);
}

// safeTransferFromのオーバライド関数、引数が違う
function safeTransferFrom(
address from,
address to,
uint tokenId
) external override {
safeTransferFrom(from, to, tokenId, "");
}

/**
* mint関数。_balances、_ownersのバランスをいじることで、tokenIdをtoへトランスファする。
* 同時にTransferイベントを放出する。
* 現在の状態では、誰でもミントできるので、開発者は普通この関数を書き換える
* 条件:
* 1. tokenIdがまだ存在しない
* 2. toはゼロアドレスでない
*/
function _mint(address to, uint tokenId) internal virtual {
require(to != address(0), "mint to zero address");
require(_owners[tokenId] == address(0), "token already minted");

_balances[to] += 1;
_owners[tokenId] = to;

emit Transfer(address(0), to, tokenId);
}

// バーン関数,_balances、_owners変数を調整してtokenIdをバーンする。同時にTransferイベントを放出
// 条件:tokenId存在。
function _burn(uint tokenId) internal virtual {
address owner = ownerOf(tokenId);
require(msg.sender == owner, "not owner of token");

_approve(owner, address(0), tokenId); // 権限を更新

_balances[owner] -= 1;
delete _owners[tokenId];

emit Transfer(owner, address(0), tokenId);
}

// _checkOnERC721Received:IERC721Receiver-onERC721Received関数, 送り先はERC721互換かどうかをチェックするため
function _checkOnERC721Received(
address from,
address to,
uint tokenId,
bytes memory _data
) private returns (bool) {
if (to.isContract()) {
return
IERC721Receiver(to).onERC721Received(
msg.sender,
from,
tokenId,
_data
) == IERC721Receiver.onERC721Received.selector;
} else {
return true;
}
}

/**
* IERC721MetadataのtokenURI関数,metadataを返す
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_owners[tokenId] != address(0), "Token Not Exist");

string memory baseURI = _baseURI();
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
}

/**
* {tokenURI}のBaseURI。tokenURIはbaseURI、tokenIdをつないでできたもの。
* 開発者がこの関数を書きかえる
* 例えばBAYCのbaseURIはipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
*/
function _baseURI() internal view virtual returns (string memory) {
return "";
}
}

free mint の APE を作ろう

それでは、free mint の APE を作りましょうか。totalSupply を 10000 個にして、mint 関数や baseURI を書き換えるだけで済みます。

baseURI()の設定を BAYS と全く同じようにすることで、BAYC の猿が表示されるはずです。


// SPDX-License-Identifier: MIT
// by 0xAA
pragma solidity ^0.8.4;

import "./ERC721.sol";

contract OmaenoApe is ERC721{
uint public MAX_APES = 10000; // 総数

// コンストラクタ
constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){
}

//BAYCのbaseURIはipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
function _baseURI() internal pure override returns (string memory) {
return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/";
}

// mint関数
function mint(address to, uint tokenId) external {
require(tokenId >= 0 && tokenId < MAX_APES, "tokenId out of range");
_mint(to, tokenId);
}
}

それでは remix を開いてコードを書きましょう。

ERC721 を発行しよう

ERC721 スタンダードがあれば、ブロックチェーンにて NFT を発行することは非常にシンプルになります。 今、上記のコードができたので、remix にて発行しましょう。

実際の手順はまたの機会で書きます。

ERC165 と ERC721

NFT を NFT についてコントロールできないコントラクトへ送付してしまうと、永遠に消失してしまうため、これを防ぐためにERC721TokenReceiverインターフェイスの実装が求められます。

interface ERC721TokenReceiver {
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

実はこのインターフェイスとは、この onERC721REceived 関数のことです。この関数を実装していれば、NFT を処理する能力があるという宣言になります。

THE END

· 約4分
Thurendous
Polymetis

Hello, everybody!

Web3 流行語

「gm って何」とよく聞かれるので、Web3 の流行語を日本語でまとめて簡単に調べられるようにして、Web3 の言語文化を広めていってわかりやすくなったらいいなと思って作りました。 web3 業界へのハードルが下がって、もっとたくさんの人が参加できるようになって盛り上げていければ良いなと思っています。

A

ABI: Application Binary Interface は、API と似ていて 2 つのバイナリプログラムモジュール間のインターフェイスのこと

address: ウォレット/コントラクトのアドレス

Alpha:アルファ、すごい収益のチャンスのこと。

AMA:Ask Me Anything、「なんでも聞いてください」のイベントで、プロジェクト側が実施する一般参加者の質問への回答を行うイベントのこと

AMM:Autonomous Market Making の略で、自動的に行われるマーケットメイキング、分散型取引所における共通技術

ape: All in すること、すべてを賭けてやる

ATH:All Time High、史上最高値のこと

B

Blockchain:ブロックチェーン

Bridge:クロスチェーンのブリッジ、異なるパブリックチェーン上のアセットを接続するためのアプリケーション。複雑さがゆえに脆弱性がよく発見される

Buy the fucking dip:仮想通貨を安い値段で拾う(買う)こと

C

CeFi:Centralized Finance の略で、DeFi の逆

Cex: Centralized Exchange(セントラル・エクスチェンジ)で、Binance、Coincheck などの取引所

ctf: Capture The Flag、サイバーセキュリティの技術コンテスト。

chad: すごい人(一般的には男性)

D

DAO: Decentralized Autonomous Organization(分散型自律組織)

Dapp:分散型アプリケーション、Decentralized Application の略

DeFi: Decentralized Finance(分散型金融)の略

degen:ギャンブル中毒者でやたらと ape in する人

delist: リスティングから除外すること

DeSci: Decentralized Science、分散型サイエンス

DeSoc:分散型社会、Decentralized Society

Dex:Decentralized Exchange、分散型取引所

Diamond hands:ダイヤモンドハンド、トークン/NFT を長期に渡って保有できる人

DID: Decentralized Identity(分散型アイデンティティ)

discord:コミュニティのための共通情報発信プラットフォーム(ラインににているがコミュニティに特化している)

dm: ダイレクトメッセージ、プライベートメッセージ

DYOR:Do Your Own Research(自分で調べて自分で責任をとれ)

Due diligence:デューディリジェンス(投資を行うにあたって投資対象となる企業や投資先の価値やリスクなどを調査する適正な努力のこと)

E

EOA: Externally Owned Accounts、イーサリアムネットワークの個々のユーザーで、スマートコントラクトのアドレスではないこと、普通のウォレットのこと

ERC20:Ether 上のトークンを均質化するための規格

ERC721: Ether 上の非均質なトークン規格。

Ethereum:分散型オープンソースブロックチェーンネットワークであるイーサ、およびそのネイティブトークンの名称。

EVM:Ethereum Virtual Machine の略で、ブロックチェーン開発者がイーサリアムブロックチェーン上に分散型アプリケーション(Dapp)を展開するために使用するソフトウェアアプリケーションです。

F

FT:ファンジブルトークン

fam:家族のこと

floor price:フロアプライス、スタートの価格のこと

fren:friend,友達、友

Fomo:Fear of Missing Out、機会を逃すことが怖くて盲目的に ape in すること

Fud:Fear(恐怖)、Uncertainty(不確実性)、Doubt(疑い)で、市場の悪い面を煽る情報

FYI:for your information(ご参考までに)

Full Send:パワー全開で

G

gas:ガス、ブロックチェーン上で取引を完了させるために必要なガスのこと

gas fee:ガス代、ブロックチェーン上で取引を完了するために必要なガス代(単位は ETH)→ (gas fee) = gas × (gas price)

gas limit:1 回の取引で消費できるガスの最大値

gas price:ブロックチェーンにおけるガスの単位あたりの価格

gas war:人気プロジェクトによるガス代の高騰。

gm:おはようございます。web3 における日常的な挨拶

gn:おやすみなさい

gg:グッドゲーム、ゲーム仲間がよく使う挨拶

H

hacked: ハッキングされた

HODL:hodl on dear love、ダイヤモンドハンド、長期に渡ってホールドすること

I

IRL:In Real Life の略

ifykyk: わかる人にはわかる

L

layer2: 既存のパブリックチェーンの上に構築されたレイヤー 2 のフレームワークで、通常はレイヤー1より高速で安価なトランザクションを発行でき、より UX がよい

LFG:Let's Fucking Go!Ape in すること

LGTM:Looking good to me, 見た感じはいいね

liquidity: 流動性

LMAO:クソ笑った

M

maxi:マクシ、何かをものすごい信仰している人

meme:ミーム、ネタ画像など

Mempool: トランザクションバッファプール。トランザクションが送信された後に mempool には表示されるが、まだチェーン上にのっていない状態

Merkle Tree:ブロックチェーンが大規模なデータセットを安全に検証・集約するために使用するデータ構造。

MEV:Miner Extractable Value または Maximal Extractable Value の略で、ブロック内のトランザクションを含めたり、除外したり、並び替えたりすることでマイナーが得ることのできる利益のこと

Mint:Mint NFT/ERC20、ミントする、鋳造する

Moon:価格が急騰して月まで飛ぶ、to the moon

N

nfa: Not Financial Advice、「フィナンシャルアドバイスではありません」

NFT: Non-Fungible Token、ノンファンジブルトークンです。

ngmi: not gonna make it, 「だめだ、うまくいかない」

Noob:ルーキー、新人、くそ素人

Nonce: 重複を防ぐためにイーサリアム取引に関連付けられた番号で、各取引に 1 つずつ追加される。各 Nonce は 1 度だけ使用できる

O

OG:Original Gangster、古参の人

on-chain:オンチェーン、ブロックチェーンにのっている

off-chain:オンチェーンではなく、オフチェーン、ブロックチェーンにのっていない

optimistic rollups: イーサリアムの layer2 のソリューションの一つ

oracle:オラクル、非中央集権的にデータをオンチェーンに持ってこれる何か

P

paper hands: ダイヤモンドの手の反対語で、全くホールドできない人

pfp: プロフィール画像

PoS:Proof of Stake の略で、参加者は POW のコンピューティングパワーの代わりにトークンの staking によってブロックチェーンネットワークの正常な機能を維持させる

PoW:Proof of Work(プルーフ・オブ・ワーク)の略で、参加者がコンピューティングパワーとエネルギーを消費してブロックチェーン・ネットワークの正常な機能を維持させる

prolly nothing: Probbably Nothing、「何も起こらないかもだけど」

R

ReFi:Regenerative Finance、再生可能・持続可能な金融。

rekt:大破、終了、破産、ゲームオーバー

roadmap:ロードマップ

RT:Twitter をリツイートする

rug:ラグプール、逃げる/ゼロになる、プロジェクト側が全ての資金を抜き取って終了する

S

Sandwich Attack:サンドイッチ攻撃、mempool でトランザクションは誰でも確認することができることから、被害者はオンチェーンで購入注文を出すと、攻撃者はそれを利用して被害者の購入注文の前後に取引を仕掛けて被害者に効率の悪い取引を強いて、利益を取ろうとする攻撃のこと。こういうことがあることから、オンチェーンの世界は弱肉強食のブラックフォレストとも呼ばれている

SBT:Soulbound Token、魂が宿ったトークン、つまりあなたの魂とバンドルとなって取引できない NFT のこと、ただし、自分は自分の SBT を burn できる

shilling: マーケティングする、営業する

Smart Contracts:ブロックチェーン上で動作するスマートコントラクトのことで、通常は一旦デプロイすると、止められることはない

solidity: イーサリアム上でスマートコントラクトを記述するための言語

Send it: 了解

T

Tokenomics:トークンエコノミー

tradfi: 伝統的な金融システムのこと

TVL: Total Value Locked、プロトコルにロックされた資金の合計

U

Up Only:価格が上がり続ける(大体は皆の理想な願望なだけ)

W

wagmi:We are gonna make it, 私達は必ず成功する。 また、多くの共通フックを含んだ web3 react フロントエンドライブラリの名前でもある → これはおもしろい

wallet:ウォレット

web3.0: Web1.0 や Web2.0 とは対照的なもので、分散化、非中央集権化とデジタル所有権によって特徴づけられるインターネットのこと

wen:いつ?

wen token:いつエアドロすんの?

WETH:Wrapped Ethereum、イーサリアムトークンの ERC20 バージョン

whale:くじら、大量のお金・トークンを保有する人

WL:White list、ホワイトリスト。特権のあるユーザー群のこと

Z

zk: zero-knowledge proof, ゼロ知識証明のこと、誰かがある秘密をしっていることを、その秘密自体を明かさずに証明できる

· 約9分
Thurendous
Polymetis

Crypto is the future

結論から言います。ぼくはクリプトが未来だと考えています。

その理由を語るには、結構な労力と時間が必要なのかもしれませんが、言語化することによって、自分の頭の整理にもなるので、ここで僕はそれに挑戦したいと思います。

少し想像してみてくださいね。

現実世界のどんなものでも(サービス、不動産、債権、キャッシュ、買い物、国債、ゴールド、ホテルのいち日泊まる権利、などなどありとあらゆるもの)、また、バーチャル世界にあるどんなものでも(ゲームアイテム、音楽、バーチャル世界の土地、ゲームキャラのスキンなどなど)、インターネット上でボーダレスにミドルマンなしで、オープンに公平に取引ができる時代

国境など関係なしにスムーズに多文化多言語協力関係の組織を築けて、そして報酬を直接的に自分のアカウントでもらえる。なにか没頭できるようなことに関してグローバルに募集して組織を一瞬に作って何かをやったり、収益化できたり

これが僕が考えたブロックチェーンの近未来です。もちろん、様々な壁やハードルが立ちはだかっているのは間違いないです。けれど、それが何年かかるかわかりませんが、近いうちそれに近い形態になると思います。なぜなら、そういったインフラはすでにできてきて改善もどんどんされていっている時代なので、そうなっていくのは目に見えたからです。

また、自分が考えた世界に比べて実際にはもっと進んでいる可能性が結構高いと思います。なぜかというと、人間は線形的に物事を考えがちであり、今の状況がよりよい状況になるという想像しかできません。実際には今では考えたことのない状況になったりするのもあり得ます。自分が想像力に欠けるのも無理はありません。ただ、現時点に基づいて予測をしないと何も始まりませんよね。

結果を言ったのはいいけど、なんでそうなるんだ?と思うと思いますので、ここから述べていきます。言いたいことが山程あるので、記事はシリーズ化します。

リアルからバーチャルへ

大きな理由としては、人間社会はリアルからバーチャルへ遷移していることが大きな理由です。

Metaverse

最近だと、メタバースというワードがかなり耳に入ってくる様になったのではないでしょうか。メタバースと聞くと、わけがわからないのかもしれませんが、自分の理解ですと、このワードはまだ意味が定まっていないと思います。あえて意見を言わせてもらうと、メタバースはいわゆるバーチャル世界そのものの定義になります。誰でも作れますし、すでにあるインターネットそのものも広義的にメタバースなのです。

例えば、すでに僕達のグーグルアカウントあるいはイーサリアムウォレットのアカウントとかがメタバースの僕達の分身と理解してもよいでしょう。ただ、このメタバースはまだまだ初歩的な段階なので、本当に使えるバーチャル世界と言えるのかというところが確かにあります。

"Software is eating the world"

英語圏ではとても有名な言葉があります。それはこの「Software is eating the world. (ソフトウェアは世界を飲み込んでいる)」という言葉です。

これは 2011 年に A16Z というとても先進的な視野を持つ VC のファンダーが話した言葉です。その後の世界はご存知の通り、彼が言った通りになり、ソフトウェアは世界を飲み込んでいきました。

Companies' market cap ranking

上の画像でわかるように、その過程では世界時価総額ランキングの上位はほとんどインターネット企業、いわゆる FANG または GAFA と呼ばれる企業に独占されるようになりました。

一昔前日本企業があれだけ独占していたランキングですが、日本がインターネットの波を完全に逃したせいで、今やランキングの下位となっています。アメリカの企業がほとんどで、たまに中国や韓国の企業が現れて、31 位になって初めてトヨタが出てきています。世の中の時流はものづくり時代からインターネット時代になったということです。

こういった企業のサービスを使ったことがある方はわかると思いますが、とても役に立っていて、一度使うとなかなかそのエコシステムから抜け出せなくなります。このように、ソフトウェアが世界を飲み込んでいます。しかし、これでは終わらないのです。

これは見方を変えると、人類がバーチャル世界へ進んだ第一歩に過ぎず、まだまだ道のりは長いです。現在こういった大企業が作っている初歩的な「メタバース」の世界はこれはこれでインフォメーションをデジタル化することに成功していますが、まだお金というものは現実世界から切り離した形でデジタル化できていません。(例えば paypay とかはお金と同等なように思われますが、独立した形でお金というものではなく、結局は日本円の分身であり、paypay の運用会社に大きく依存した形です → 運用会社はその世界の神様です)

神様のいない(非中央集権的な)世界

ブロックチェーンはこれを解決してくれます。ブロックチェーンは今のインターネットにおけるなんでもコピーできてしまう問題を解決し、バーチャルなものにリアル世界と同じような誰にも関与されない希少性、改ざん耐性を与えて、初めてバーチャル世界を「世界」として成り立たせることができるのです。先程、神様といったのは例えばあなたの生きる世界に神様がどこかの人間になると、怖くありませんか?その人の決定で何もかも変わる可能性があります。極端な話、生存の権利さえも奪われるリスクが潜んでいます。そんな世界ってディストピアで嫌ですよね?

すでに気づいてるかと思いますが、今僕達はまさにこの神様がいる(中央集権的な)インターネット世界に住んでいます。youtuber さんはわけがなくチャンネル停止されるし、アマゾンでは明確な理由が与えられず出店停止処分になったりします。ツイッターだってトランプさんのアカウントがバンされたことで有名でしたね。

もちろん、悪さをしてバンされたらそれはそれで仕方ないのですが、ただ会社側の一任ですべてが決定されてしまうのは果たして正義なのかどうかは議論の余地があるのではないでしょうか?

実際にイーサリアムの創始者であるビタリック氏はワールドオブワークラフトの MMO ゲーム内の好きなキャラクタが弱体化されたことで嫌気が差したのがイーサリアムというブロックチェーンの始まりだとも言われています。神様のいない世界は人類史上なかったので、さぞかしビタリック氏は作りたかったんだろうね。

ブロックチェーンであれば、神様のいない世界が作れるという点が非常に大きいです。これによって万人平等で同じルールに則った世界が構築できる様になったといえます。

ただ、今はこの大きな課題解決の現在進行系であり、あれやこれの不具合や満足の行かない点がたくさんあるでしょう。

そういったときには、僕の意見としては未来志向がいいです。未来志向は何かというと、クレーマーにならず暖かく見守ってあげて一緒に成長するのがベストだと思います。クレーマーになるのは簡単であり、課題を解決する側に回って色々考えて行動することこそ社会が進化していく力となっていくので、結局はみんなウィンウィンになるわけです。

話がそれましたが、人間社会はリアルからバーチャルへという大きな方向性は間違いがなく、すでに自分たちの活動はたくさんインターネットへ移植しています。これが加速していきます。

Human society's GDP

出典

人類の歴史における 一人当たりの GDP は上のグラフを見て下さい。わかるかと思いますが、指数関数的に増加していることがわかります。これの主な要因には、一つとしてリアルな世界ではリアルな資源の開発ができる部分がたくさんあったことと、技術的な進歩が爆発的に増加したと思います。産業革命以後、人類はパンドラのボックスを開いたように、知の集合体として大きく成長し、もはや産業革命のときみたいに一種の技術進歩で大きく人類が前進するという状況ではなくなっています。

今現在だけでも、自分の認識している範囲ですと、5G、ブロックチェーン、AI、Iot など、様々な技術が雨後の筍のように現れてきて、同時に進化して融合しつつ、次の産業革命として人類社会を爆発的に前進させるでしょう。想像を絶するような世界がまた 10 年 20 年後には現れて人間社会のこれまでの知識、経験、哲学や価値観をすべて考え直さなければならないのかもしれません。今はこれだけ進化の速度が早くて、毎日が新たな歴史のページをめくっているようなものです。

Why human will go virtual

また、なぜバーチャルが良いのかという疑問もあろうかと思います。

昔マルクスの人口論でも有名ですが、人類の発展はいずれ現実世界の限界を迎えてしまい、減速してしまうでしょうという理論です。自分は人間社会の GDP はますます増加していくことは間違いないと思っています。なぜなら現実世界は制約がありますが、バーチャル空間は制約が少ないからです。

人間は大航海時代以来、影響範囲を拡大しようと、地球をたくさん探索し続け開発してきました。今や宇宙へ行こうとしています。ただ、宇宙開発はそう簡単にできるわけではありません。宇宙の開発を突破するには時間と空間というとてつもなく困難な壁がありました。そこで、この制限がかかっていないバーチャル空間の開発が容易なのではないかと普通は考えます。バーチャルな世界にいると、瞬間移動もできてスペースの問題も解決されます。

Web3 が来ると人類のバーチャル化が更に加速していきます。価値が容易にインターネット内でやり取りできるようになり、機械と機械がミドルマンなしで価値のやり取りができるようになります。少し予言しますが、将来はこの機械間の価値の交換がメインになるでしょう。いわゆるお金については、ますますプログラマブルマネー(コーディングで制御できるお金)になっていくわけです。

バーチャルな世界で生きている人間にとっては当然バーチャル世界ネイティブな資産が生まれてもおかしくないでしょう。それがクリプト(仮想通貨)です。

Why people cannot understand this trend?

この話はめちゃめちゃ重要で、なぜ気づいていない人々が多いのか?という疑問を抱くと思います。これについては、まあいろいろな理由があるでしょう。でもこれを知ってもらったあなたにはこのトレンドのことを覚えてほしいです。

技術というものは歴史において毎回何かを一新して、既得権益者の利益に触れます。そうすると必ず反発を受けます。

だけど、短期的には既得権益のほうが勝つかもしれませんが、長期的には勝てません。時代のトレンドというのは逆らうことがほぼ不可能なのです。

英語ではよく言われる言葉として以下があります。

First they ignore you, then they laugh at you, then they fight you, then you win.

これはクリプトの業界でもめちゃくちゃぴんとくる言葉です。「最初に彼らあなたを無視し、それからあざ笑わい、それから抗い、最終的にはあなたの勝利」というような感じで、歴史においては様々なトレンドがたどってきた運命なのです。

車の歴史もそうですし、発明された当初、めちゃくちゃ効率が悪くデザインも良くなかったので、馬車のほうが全然いいと思っていた人が少なからずいました。また、そのあとのインターネットも発明された当初は誰にも理解されず、ビル・ゲイツがテレビでインターネットを紹介していたときには観客と司会者の方から大いに笑われていました。今だから言えるのですが、当初のビル・ゲイツが想像した世界よりも遥かに進んだインターネット社会ができています。

"History Doesn’t Repeat Itself, but It Often Rhymes" - Mark Twain

Mark Twain 氏が言っている通りです。

そこで、今度はブロックチェーンの番です。「ディスられる、怒られる、軽蔑される、無視される、それでも進化し続けて社会を変えていき、我々の生き方も変えてくれる」、これがクリプトです。

だらだらと書いてしまいましたが、普段友人に聞かれて説明すると結構な時間をかけて説明できたりしますが、いざ書くとなるとうまく書けないんですね。まだ書ききれないほどありますので、また次回をお楽しみに!

免責事項

本 website にかかれている内容はあくまで個人的な見解であり、Financial advice ではありません。

· 約6分
Thurendous
Polymetis

あなたは、「希少性がある」「希少性が無い」ということを考えたことがありますか?

「希少性」とは何でしょう?

「希少性」は、欲求に対して量や数が極めて少ない、ある意味客観的事実です。

また、そのために生じる価値のことを「希少価値」といいます。

普段の生活においては我々は実は無意識のうちに価値の判断を毎日しています。「これは私にとって価値がある」、「それは価値がない」などと色々考えて行動をして、人間の一つ一つの小さな行動パターンが市場を決めているのです。しかし、我々の感覚的な価値の判断は実は少し立ち戻って考えると、多少ずれた感覚であり、あまりにも短視的に物事を見ていることがわかると思います。

今回は希少性の話をしたいと思います。結論をいきなり言ってしまうとすごく疑問に思うかもしれませんので、ゆっくり話を読んでもらえれば、私の主張が当たり前のように思えてくるでしょう。

希少性を考える

世の中で何に希少性があるかということはよくよく考えてください。もちろん、代替できないものが一番希少性があるでしょう。その最たるものはあなたの命です。それは自分自身にとってはかけがえのない価値を持っているのは自明です。しかし、よく考えるとそれは赤の他人にとってどれくらいの価値があるのか?あまりないのかもしれませんし、結構ある場合もあるでしょう。

こういう主観によって価値が変わるものが世の中には存在します。例えば画家による絵画がそうです。ブロックチェーンの世界でいうと NFT に相当します。人によってはものすごい価値を持つし、価値がゼロの場合もあるでしょう。

しかし、今回議論したいのはこういう類のものではなく、普遍的に皆さんにとって価値があり、尚且つ交換しやすいものの希少性を考えてほしいのです。

まず言っておきたいことがあります。前提として、もしあなたが崇拝している資産があれば、その考えを捨ててほしいです。先入観なしで平等に客観的に判断をして初めて希少性について気付くことができると思います。

物事の値段というのは伝統的な経済の理論の中で需要と供給のバランスで決まるという前提があり、希少性と不可分です。希少価値があるか否かの判断基準としては、需要があまり減らず、供給もあまり増えないことだと考えています。希少価値があるものは高価格である、あるいは将来的に高価格になりうるものと言えます。

それでは、希少性のあるものについて一個ずつ考えましょう。

Japanese Yen

価値というものについて考えた時に一番最初に思い浮かぶものは日本円でしょう。しかし、日本円は果たしてどれくらい希少性があるのか。

日本円は政府の信頼に基づいて発行されており、政府は信頼されてきちんと円の希少性を担保してくれると信じている方が少なくありませんが、実際には未だに日本円は続々と刷られて市場へ放出されています。これがいわゆる普段耳にする「金融緩和」という言葉です。シンプルにいうとお金を大量に刷っているのです。

こんなものに希少性があるのか甚だ疑問です。

Real Estate

不動産というのは、大衆的に特に中国あるいは昔の日本のような急成長した国では神話のような資産として語られがちです。そういう国では、永遠に価値が上がるものとして見られてきました。何十年も事実として成り立っているものは神話となってしまいます。それもいずれ終焉を迎えるのです。

不動産を需要と供給のバランスの観点から話すと、決して希少性のある資産ではありません。なぜなら、まず需要は人口や景気に比例し、供給は不動産の開発規模に依存するからです。基本的に不動産開発の規模は、もし需要が増えれば増加させることで価格のバランスを取ることができます。

不動産開発の規模はある程度までは無限に増長できます。極端な話ですが、地球上に土地がなくなっても宇宙開発をする人間社会は、例えば火星へ引っ越しして不動産開発をすれば、供給は一気に増えるでしょう。ましてや現在の人間社会はリアル世界からバーチャル世界へ進んでいることから、不動産に代表されるリアル世界の経済規模というのは長期的に見ると、どこまでも無限に増長可能なバーチャル世界と比較して、伸びしろが縮小する傾向にあります。

少なくとも不動産にはあまり希少性がありません。

Stocks

株は会社が発行しているものです。いくらでも再発行や追加発行でき、しかも倒産のリスクもあります。当然ですが、あまり希少性はありません。

GOLD

続いては歴史が長いゴールドについて話をします。

こういう貴金属には希少性があると思うでしょう。ゴールドは長い歴史の中で供給もあまり増えず、なかなかレアなものであり、お金として見られてきたので、たしかに希少性があるように思えてしまいます。

しかし、需要と供給の話をすると、需要は長い歴史の中で形成されてきたのであるのは間違いありません。ですが供給側でいうと、自然な鉱物だとすると、鉱山をいっぱい発見すれば供給がそれだけ増えてしまいます。

一方でもし需要がたくさんある場合、鉱山を見つけるためのインセンティブが働いてたくさん鉱山が見つかるでしょう。また、さらに宇宙進出をしている人間が宇宙でたくさんのゴールドを手に入れたら、ゴールドの希少性にまつわる神話も終わりを告げるでしょう。

一定条件のもとでは、ゴールドは希少性がありますが、長期的に考えるとあまり希少性はありません。

ゴールドでさえも希少性があまりないというなら、何に希少性があるんだよと思うかもしれないです。

ではここで重要な結論を話します。ブロックチェーンの元祖であるビットコインです。

ここでいうのは代表格の Bitcoin を言っていますが、別にその他のアルトコインに希少性が無いという意味ではないと理解してください。

Bitcoin

ビットコインはハードキャップ(上限となる発行量)である 2100 万個という人類社会のコンセンサスがあることから、人工的に作ったあまり変えられない数の供給が成り立っています。これは1国家だろうがだれであろうが、このハードキャップに対するコンセンサスを動かすのは結構なハードルがあります。

また、需要面からいうと、ブロックチェーンのリテラシーが増加の一途をたどっており、今やWeb3は日本の国家戦略とかになったりしていて、そのインフラであることからますます多くの人々がブロックチェーンへの理解ができるようになるでしょう。また、人間社会がリアル世界からバーチャル世界へいくのは目に見えているし、現実世界の価値のアンカー(基礎)はゴールドだとすると、バーチャル世界の価値のアンカーはほぼビットコイン一択です。需要が増えることも間違いないでしょう。

したがって、筆者はこれまで色々見てきた普遍的な価値を持つものの中で、bitcoin のような人工的なものだけが長期に渡って絶対的な希少性が成立します。

PS

正直、希少性のある普遍的なものというのはあまりありません。どんな物質であろうが頑張れば手に入るか、あるいは量を増やせると思います。今までのインターネットにおいてはデータはコピーできるというチート技を使えば希少性はありませんでした。

免責事項

本 website にかかれている内容はあくまで個人的な見解であり、Financial advice ではありません。

· 約3分
Thurendous
Polymetis

Preface

こんにちわ!Thurendous です。

そもそも本 Website を作ろうと思ったのは、自分は 2017 年からブロックチェーンに燃え始めてから、周りの方たちは興味を示すどころか、疑義の念を抱いている方が少なからずいたことがきっかけで、それから悶々となにかをやってやろうじゃないかと色々思いを抱いてきました。

今現在に至るまで、だいぶ時間が経ちましたし、色々な考えも成熟してきているかと思います。勉強も続けてきており、それが終わりのないマラソンのようですが、絶えず続けていく所存です。本 Website はブロックチェーン素人のあなたにとってのすべての疑問に答え、長年に渡って追ってきたブロックチェーン業界の観察者である自分自身がこの業界の将来に対するビジョンを共有できれば幸いだと考えております。

本ウェブサイトをきっかけに少しでもブロックチェーンの理解の促進、Web3 の理解、メタバースへの興味を持ち始めてもらえると嬉しいです。

ひいては熱狂的に興味を持ってもらえるような人が生まれてきて、Web3 業界にて起業したり社会を変革させたり、これまでの固定観念、常識を取り壊し、新たな歩み、富を創造して、人々を自由を与え、社会を開放していくとか、そういうことは願ってやみません!

Big Trend

自分はもともと海外出身かつ理系出身ですが、文系も大好きでした。それが原因なのかわかりませんが、ブロックチェーンに出会ってからその考え方が理系でもできて文系でもできるところ、また深く考えれば考えるほど、楽しくなっていくことが大好きでした。

ブロックチェーンを考えるときに、投資の世界ですと、どうしても短期的に物事を価格的、あるいは投機的な視点で見がちですが、自分はどちらかというと、少し原点に立ち戻って大きな視点から見るのが好きです。

個人として行動を起こす上で、大きな方向として間違えなければ、何か方向性を疑うような事件が起きたとしても個人的には許容範囲なのかなって思ってます。ブロックチェーンに対して疑義の念を抱いてる人が結構いますが、大きなトレンドとして分析すると、色々明らかになることが多いように思います。

これから、大きなトレンドの話をいくつか記事に分けて書いていこうと考えています。こういった記事を読んでいただければ、ブロックチェーン業界を長年観察してきて、なぜブロックチェーンに熱狂的に自分はなったのか、なぜブロックチェーン業界はすごいのかといった質問に答えることができるのではないかと思います。

もちろん、ブロックチェーン業界に対して疑義の念をあなたは抱いているかもしれませんが、そこはゆっくり吟味していただいて、私は素人でありながらも、普通の人が考えたことのない、あるいは知ろうとしないことを長い年月と経験を経て多言語的に情報を収集し勉強してきて今に至っています。

ブロックチェーンについてはものすごい楽観視しているのはそれなりの理由や背景知識を頑張って手に入れたからです。根拠があって今の認識があるわけです。ゆっくりとご覧になって考えてみてほしいです。

一度に書くと書ききれないことですので、自分としてもゆっくり整理して複数の記事にして全部書いていきたいと考えています。何かお役に立ったのであれば幸いです!

What to Write

小難しいことを書くと多分理解されませんので、ある意味自分がブロックチェーンを理解してきた中で、実生活の事例とか、普段の感覚とかそういったものを使いつつ、直感的にわかってもらえるように内容を書いていければと思っています。というのも自分はそういった経緯をたどった経験がありましたので、そうしようと考えたのです。

  • マクロな視点から見た blockchain

    • 歴史の話から、お金の話から、インターネットの歴史(バーチャルへ)
      • software is eating the world
    • 技術の進歩についての現状
    • 世界経済の規模
      • 仮想通貨は値上がりするの?
    • 将来の経済発展の割合
    • 社会の変貌(非中央集権へ)
  • Web3 ってなに

  • DeFi ってなに

  • スマートコントラクトってなに

  • 仮想通貨は本質的な価値がないとよく言われるので大丈夫か

  • 仮想通貨は投機ばかりですよね?

  • 希少性の話

  • 人間の本性

  • 国境がない世界

  • blockchain の仕組み

  • 人口分布の関係

  • 技術系関連の solidity などの記事