LoginSignup
9
3

OCamlでEthereumのスマートコントラクトを開発できるようにするプロジェクトの紹介

Last updated at Posted at 2024-01-12

株式会社 proof ninja 技術顧問の池渕です.
proof ninja では,関数型プログラミング言語 OCaml で Ethereum 上のスマートコントラクトを書けるようにするため,OCaml コードを Ethereum VM (EVM) のバイトコードへ変換するコンパイラ ocaml2evm を開発中です.
まだまだ開発途上ではありますが,この記事では ocaml2evm の現在の状況・使い方について紹介します.

ocaml2evm のソースコード,環境準備

ソースコードは以下:

ビルド等のために必要なもの:

  • OCaml バージョン 4.14 以上
  • dune (OCaml のビルドシステム)
  • yojson, digestif (OCaml ライブラリ)
  • solc (Yulコード(後述)のコンパイルのため)
  • npm (出力したバイトコードが動くことのテストのため.ビルド自体には不要)

使い方:
以下のようなディレクトリ構造があるとします.

ocaml2evm/
└─ dir/
   ├─ contracts/
   └─ src/
      ├─ foo.ml
      ...
      └─ bar.ml

(ディレクトリ名 dir, src は自由ですが contracts は固定です.)
ocaml2evm ディレクトリ下で

$ dune exec ocamyul dir/src/foo.ml

を実行すると foo.ml がコンパイルされ,dir/contracts/(モジュール名).json が出力されます.この JSON ファイルには ABI (Application Binary Interface,コントラクトの関数を呼び出すために必要な情報)とバイトコードが含まれており,これを使うことでコントラクトを Ethereum ネットワーク上にデプロイできます.

たとえば,サンプルコード ocaml2evm/sample/src/erc20.ml のコンパイル結果が ocaml2evm/sample/contracts/ERC20.json です.

サンプルコード

簡単なコントラクトの実装

ocaml2evm/sample/src ディレクトリ下にいくつかサンプルの OCaml コードがあります.
たとえば,ocaml2evm/sample/src/simple_storage.ml は以下です.

module SimpleStorage : sig
  type storage

  val set : int -> storage -> unit * storage
  val get : unit -> storage -> int * storage
  val incr : unit -> storage -> unit * storage
  val twice : int -> storage -> int * storage
  val anormaltest : int -> storage -> int * storage
end = struct
  type storage = int

  let set n _ = ((), n)
  let get () s = (s, s)
  let incr () s = ((), s + 1)
  let twice n s = (n * 2, s)

  let anormaltest n s =
    let m =
      let l = s * 2 in
      let o = n * 3 in
      l + o
    in
    (m, m)
end

ここでは,SimpleStorage というモジュールを作り,その中で setget などの簡単な関数を定義しています.
まず,SimpleStorage ではストレージ(複数の関数呼び出しで共有されるデータの領域)は整数型の値を持つという意味で,

type storage = int

のように storage 型を定義しています.

各関数は値とストレージを受け取り,値と更新後のストレージのペアを返します.つまり,型は以下の形になっています.

'a -> storage -> 'b * storage

たとえば set 関数の定義

  let set n _ = ((), n)

は整数 n を取り,それをストレージに格納し,返り値はなし( = unit 型のコンストラクタ () を返す)ということを意味します.

その他,

  • get 関数は「ストレージの値 s を取り出して返し,ストレージの変更はなし」,
  • incr 関数は「ストレージの値をインクリメントして更新する」,
  • twice関数は「整数 n を取ってその二倍を返し,ストレージの変更はなし」
    という定義になっています.

関数定義の中では let ... in 式が使用でき,関数 anormaltest で例を示しています.
anormaltest の定義にあるように,ネストした let ... in 式も扱えます.

前述したように,このファイルは ocaml2evm ディレクトリの中で

$ dune exec ocamyulc sample/src/simple_storage.ml

を実行することでコンパイルでき,その結果が sample/contracts/SimpleStorage.json として出力されます.
JavaScript コード sample/contract_test/SimpleStorage.test.js でこの SimpleStorage コントラクトをローカルネットワーク上でテストしています.
テストは sample ディレクトリ下で

$ npm test

で実行できます.

ERC20 の実装

sample/src/erc20.ml で ERC20 を実装しています.

open OCamYul.Primitives

module ERC20 : sig
  type storage
  type mut_storage

  val total_supply : unit -> storage -> int * storage
  val balance_of : address -> storage -> mut_storage -> int * storage
  val allowance : address * address -> storage -> mut_storage -> int * storage
  val mint : int -> storage -> mut_storage -> unit * storage
  val burn : int -> storage -> mut_storage -> unit * storage
  val transfer : address * int -> storage -> mut_storage -> unit * storage
  val approve : address * int -> storage -> mut_storage -> unit * storage

  val transfer_from :
    address * address * int -> storage -> mut_storage -> unit * storage
end = struct
  type storage = int

  type mut_storage =
    (address, int) Hashtbl.t * (address, (address, int) Hashtbl.t) Hashtbl.t

  let total_supply () total = (total, total)

  let balance_of account total (balance, _) =
    (Hashtbl.find balance account, total)

  let allowance (owner, allowed_address) total (_, allow) =
    (Hashtbl.find (Hashtbl.find allow owner) allowed_address, total)

  let mint amount total (balance, _) =
    let from_address = caller () in
    let from_balance = Hashtbl.find balance from_address in
    Hashtbl.replace balance from_address (from_balance + amount);
    ((), total + amount)

  let burn amount total (balance, _) =
    let from_address = caller () in
    let from_balance = Hashtbl.find balance from_address in
    Hashtbl.replace balance from_address (from_balance - amount);
    ((), total - amount)

  let transfer (to_address, amount) total (balance, _) =
    let from_address = caller () in
    let from_balance = Hashtbl.find balance from_address in
    let to_balance = Hashtbl.find balance to_address in
    Hashtbl.replace balance from_address (from_balance - amount);
    Hashtbl.replace balance to_address (to_balance + amount);
    ((), total)

  let approve (allowed_address, amount) total (_, allow) =
    Hashtbl.replace (Hashtbl.find allow (caller ())) allowed_address amount;
    ((), total)

  let transfer_from (from_address, to_address, amount) total (balance, allow) =
    let from_address_allow = Hashtbl.find allow from_address in
    let allowed_balance = Hashtbl.find from_address_allow (caller ()) in
    Hashtbl.replace from_address_allow (caller ()) (allowed_balance - amount);
    let from_balance = Hashtbl.find balance from_address in
    let to_balance = Hashtbl.find balance to_address in
    Hashtbl.replace balance from_address (from_balance - amount);
    Hashtbl.replace balance to_address (to_balance + amount);
    ((), total)
end

SimpleStorage と違う点の一つとして,ここでは storage の他に mut_storage という型を定義しています.
storage は整数値のようにイミュータブルな値を格納することを想定しており,一方 mut_storage はハッシュテーブルのようなミュータブルな値を格納するために使います.
(OCaml でいうハッシュテーブルは,Solidity でいう mapping に対応します.)

erc20.ml では mut_storage は二つのハッシュテーブルから成り,一つ目である (address, int) Hashtbl.t は口座のアドレスをキー,その口座の残高を値として持つハッシュテーブルを表します.
二つ目である (address, (address, int) Hashtbl.t) Hashtbl.t は,キー(口座アドレス) a に対して,「それぞれの口座 b について ab からいくら預金を移してよいかを表すテーブル」を値に持つようなテーブルです.
また,storage はこれまでに発行されたトークン全体の量を表す整数を格納します.

たとえば関数 allowance

  let allowance (owner, allowed_address) total (_, allow) =
    (Hashtbl.find (Hashtbl.find allow owner) allowed_address, total)

は二つの口座アドレス owner, allowed_address のペアを受け取り,ownerallowed_address の口座から移せるトークンの量を返しています.

また,関数 transfer_from

  let transfer_from (from_address, to_address, amount) total (balance, allow) =
    let from_address_allow = Hashtbl.find allow from_address in
    let allowed_balance = Hashtbl.find from_address_allow (caller ()) in
    Hashtbl.replace from_address_allow (caller ()) (allowed_balance - amount);
    let from_balance = Hashtbl.find balance from_address in
    let to_balance = Hashtbl.find balance to_address in
    Hashtbl.replace balance from_address (from_balance - amount);
    Hashtbl.replace balance to_address (to_balance + amount);
    ((), total)

from_address から to_adressamount の量だけトークンを移す,という動作をします.7,8行目の Hashtbl.replacefrom_address/to_address の口座残高を amount だけ減らし/増やしています.
また,この移動に伴い4行目で関数の呼び出し元(caller ())が from_address から移してよいトークン量を減らしています.

内部のこと:コンパイルの流れ

コンパイルは,OCaml コードから直接バイトコードを出力しているわけではなく,Yul という中間言語を介しています.
ocaml2evm のメインの部分は OCaml コードを Yul コードに変換しており,生成された Yul コードは Solidity コンパイラである solc の機能を使ってバイトコードに変換しています.

今後など

ocaml2evm コンパイラプロジェクトはまだまだ始動したばかりで,現在は OCaml のごく小さなサブセットしか扱えないのですが,ERC20 を実装できる程度にはなっています.
たとえばまだレコード型や高階関数などは扱えません.
これから扱える機能を増やしていくのはもちろんのこと,今後の展開としては定理証明支援系 Coq を使った検証ができるようにすることも目標の一つです.

ということで,開発中である ocaml2evm を紹介してみました.OCaml と Coq でハッピー・スマートコントラクト・ディヴェロッピングを目指そう!!

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3