logo

TONのコントラクトを触ってみた

〜次はどこに怒られるんだろう〜
profile photo
TaniguchiAkira

そもそもTONとは

Telegramが開発したブロックチェーンプラットフォーム。
基軸通貨はToncoinで、Ethrerumのようにコントラクトを記述することができる。
利用ユーザ数は8億人を超えるらしく、アカウント数=ウォレット数となることを強みとして、TONは躍進していくのだ(予定)。

準備

typescript
npm create ton@latest
任意のフォルダでこのコマンドを実行すると開発環境が構築できる。
Image without caption
任意のプロジェクト名とコントラクト名を入力したら、テンプレートが選択できるようになる。
ここでは「A simple counter contract(FunC)」を選択してみる。すると、
Image without caption
blueprintというコマンドが実行できるようになる。

動作検証

想定通り、exampleのコントラクトやテストコードが生成された。
指示された通り、buildコマンドを実行してみる
typescript
akira@taniguchieinoMacBook-Pro-2 test % npx blueprint build Using file: TestContract Build script running, compiling TestContract ✅ Compiled successfully! Cell BOC result: { "hash": "61689f0431f0bead3490202a24110f59f58ac3c23f5e4ad4168923eee237287a", "hashBase64": "YWifBDHwvq00kCAqJBEPWfWKw8I/XkrUFokj7uI3KHo=", "hex": "b5ee9c7241010a010089000114ff00f4a413f4bcf2c80b0102016202070202ce0306020120040500671b088831c02456f8007434c0cc1c6c244c383c0074c7f4cfcc4060841fa1d93beea6f4c7cc3e1080683e18bc00b80c2103fcbc20001d3b513434c7c07e1874c7c07e18b46000194f842f841c8cb1fcb1fc9ed54802016e0809000db5473e003f0830000db63ffe003f0850cfa4a3cd" } ✅ Wrote compilation artifact to build/TestContract.compiled.json
どうやら成功したらしい。Solidityと同じく、バイトコードが出来上がっていると思われる。
同様にtestコマンドを実行してみる
typescript
akira@taniguchieinoMacBook-Pro-2 test % npx blueprint test 省略 PASS tests/TestContract.spec.ts TestContract ✓ should deploy (210 ms) ✓ should increase counter (156 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 1.565 s
成功したらしい。

コードを読む

コントラクト

まずはコントラクトからみてみる。
typescript
#include "imports/stdlib.fc";
ライブラリをインポートしている。
標準ライブラリを利用できるようにするために、おまじないのようにこれを記述してるっぽい。
ここだけみると、Cっぽい。だからFunCって言うんだろうけど。
typescript
const op::increase = "op::increase"c;
定数を作っている。独自オペコードを定義してるっぽい。
あとで使ってる。
plain text
global int ctx_id; global int ctx_counter;
ストレージ領域での変数宣言。
これがSolidityでいうところのコントラクトのプロパティに位置すると思われる。
TONのアドレスはコントラクトの初期状態に依存するらしく、そのためのidを用意して、アドレスの重複を回避したりするらしい。
plain text
() load_data() impure { var ds = get_data().begin_parse(); ctx_id = ds~load_uint(32); ctx_counter = ds~load_uint(32); ds.end_parse(); }
ストレージからデータを読み込み、グローバル変数に保存してるっぽい。
「get_data()」でストレージを読み込み、「.begin_parse()」でHDDのシーク的な雰囲気で読み込み位置を最初に持ってきてる気がする。
で、load_uint(32)を実行するたびにこれまたシークを移動するイメージで順にストレージを読み込み、end_parseで終了処理という感じかな。
impureという修飾子は関数内でストレージを変更したり、メッセージ(ユーザー、アプリケーション、スマートコントラクト間で送信されるデータのパケット)を送信したり、例外を投げたりする可能性がある時に付与する必要があるらしい。
plain text
() save_data() impure { set_data( begin_cell() .store_uint(ctx_id, 32) .store_uint(ctx_counter, 32) .end_cell() ); }
ストレージへの保存処理。
set_dataが保存処理のメイン関数で、グローバル変数に保存されている値を多分デコードかなんかして保存してるっぽい。
連続した保存領域を使ってるから、store_uintを連続して実行させる感じで、こういう記述をしてるんやろなと予想してる。
plain text
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { if (in_msg_body.slice_empty?()) { ;; ignore all empty messages return (); } slice cs = in_msg_full.begin_parse(); int flags = cs~load_uint(4); if (flags & 1) { ;; ignore all bounced messages return (); } load_data(); ;; here we populate the storage variables int op = in_msg_body~load_uint(32); ;; by convention, the first 32 bits of incoming message is the op int query_id = in_msg_body~load_uint(64); ;; also by convention, the next 64 bits contain the "query id", although this is not always the case if (op == op::increase) { int increase_by = in_msg_body~load_uint(32); ctx_counter += increase_by; save_data(); return (); } throw(0xffff); ;; if the message contains an op that is not known to this contract, we throw }
この「recv_internal」と言う関数がコントラクトのメイン関数的な位置付けらしい。
他のコントラクトからメッセージを受信した時に呼び出されるっぽい。
引数は複数のパターンがあるらしく、任意のものを選べる。
処理の内容としては
  1. メッセージを読み取り、条件によって処理を終了している。
  1. 処理対象だった場合、ストレージからデータを読み取り、オペコードがincreaseだった場合にインクリメント処理を実行する。
  1. オペコードが意図しないものだった場合、エラーを投げている。
とのこと。
慣例として、in_msg_bodyの最初の32ビットはオペコードであり、次の64ビットはクエリIDらしい。
必要なデータはその次にセットされているものらしく、そこから32ビット読み取って、増加数を取得している。
plain text
int get_counter() method_id { load_data(); return ctx_counter; } int get_id() method_id { load_data(); return ctx_id; }
getメソッド。他のVMとは異なり、別コントラクトからgetメソッドは呼び出せないらしいが、
method_idを付与しておくと、エクスプローラーから呼び出せたりするらしい。
Solidityでいうところのviewみたいなもんか。

テストコード

次にテストコードを見る
typescript
let code: Cell; beforeAll(async () => { code = await compile('TestContract'); });
実行前にコントラクトのバイトコードを取得してるっぽい。
コントラクトのbegin_cellを見た時にも思ったが、どうやらtonのコントラクトはバイトコードをcellと表現してるらしい。
typescript
beforeEach(async () => { blockchain = await Blockchain.create(); testContract = blockchain.openContract( TestContract.createFromConfig( { id: 0, counter: 0, }, code ) ); deployer = await blockchain.treasury('deployer'); const deployResult = await testContract.sendDeploy(deployer.getSender(), toNano('0.05')); expect(deployResult.transactions).toHaveTransaction({ from: deployer.address, to: testContract.address, deploy: true, success: true, }); });
各test実行前にtest用のブロックチェーンを作成して、idとcounter、およびコントラクトのコードを使って、おそらくここでコントラクトアドレスを確定させている。
seedから適当に資産を持ったアドレスを作成し、そのウォレットからデプロイして、正常に完了したかチェックしている。
typescript
it('should increase counter', async () => { const increaseTimes = 3; for (let i = 0; i < increaseTimes; i++) { console.log(`increase ${i + 1}/${increaseTimes}`); const increaser = await blockchain.treasury('increaser' + i); const counterBefore = await testContract.getCounter(); console.log('counter before increasing', counterBefore); const increaseBy = Math.floor(Math.random() * 100); console.log('increasing by', increaseBy); const increaseResult = await testContract.sendIncrease(increaser.getSender(), { increaseBy, value: toNano('0.05'), }); expect(increaseResult.transactions).toHaveTransaction({ from: increaser.address, to: testContract.address, success: true, }); const counterAfter = await testContract.getCounter(); console.log('counter after increasing', counterAfter); expect(counterAfter).toBe(counterBefore + increaseBy); } });
testコード。3回インクリメントして実行結果を検証している。
getCounterはコントラクトのgetメソッドを呼び出し、callしている。
変数の参照の仕方が、どことなくcairoというかStarkNetのライブラリっぽいなと思う。
sendIncreaseの中でコントラクトの呼び出しを行なっており、コントラクトのメッセージ処理と同様、オペコード、クエリID、実際の増加値と順番にデータを構築している。

所感

ライブラリも充実しており、まともに動作している雰囲気がする。
ドキュメントもしっかりしているので、何かあったら見ればわかると思うし、サンプル通りに動作していると感じる(極めて重要)。
TONのトランザクションは単一のスマートコントラクトでのみ実行されているらしく、EVMのように複数のコントラクトが一つのトランザクションで動作することがないらしい。それらのメリットはまだ理解していないが、EVMとは毛色が違くため、NFTマーケットプレイスなどのDEXを作る際は、また違ったノウハウが必要になってくるかも。

参考リンク

Introduction | The Open Network
Smart contract creation, development, and deployment on TON Blockchain leverages the FunC programming language and TON Virtual Machine (TVM).
Introduction | The Open Network
Message Overview | The Open Network
TON is an asynchronous blockchain with a complex structure very different from other blockchains. Because of this, new developers often have questions about low-level things in TON. In this article, we will have a look at one of these related to message delivery.
Message Overview | The Open Network
TON Hello World part 2: Step by step guide for writing your first smart contract
A smart contract is simply a computer program running on TON Blockchain - or more exactly its TVM (TON Virtual Machine). The contract is made of code (compiled TVM instructions) and data (persistent state) that are stored in some address on TON Blockchain.
Functions | The Open Network
FunC program is essentially a list of function declarations/definitions and global variable declarations. This section covers the first topic.
Functions | The Open Network
Related posts
post image
Arbitrum
Rust
contract
WASM
Arbitrum Stylusのすゝめ
〜SolidityHouseの名前はどうなるの?〜
post image
foundry
Solidity
test
deploy
Foundry 1.0.0が出るよ
1.0の壁
post image
Solidity
Hardhat
test
zkSync
Optimism
hardhat-upgrades
Hardhat3が出るよ
予定は未定
Powered by Notaku