この記事は筆者のソロ Advent Calendar 2022 24日目の記事です。
引き続きflowブロックチェーン上にスマートコントラクトを実装するためのCadenceという言語について公式ドキュメントのチュートリアルをやってみたのでその備忘録です。
前回はNon-Fungible Token(NFT)を学びましたが、今回はFungible Token(FT)についてのチュートリアルになります!
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Hello World編]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[リソース編]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[capability リンクの参照とスクリプト]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Non Fungible Tokens(NFT)編1]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Non Fungible Tokens(NFT)編2]
NFT特化flowブロックチェーンに入門するためにcadence言語を学ぶ[Fungible Tokens(FT)編] <- 今ここ
チュートリアルplaygroundはこちら
従来のSolidityで書かれたようなファンジブルトークンの実装は、中央台帳的に管理されており、ファンジブルトークン用のコントラクトで管理されていることが多いと思います。Cadenceではファンジブルトークンの実装にリソース指向言語の特徴を活かすことで以下のような利点があります。
(公式ドキュメントより)
- 所有権は分散化され、中央台帳に依存しない
- バグとエクスプロイトにより、ユーザーにとってのリスクと攻撃者にとってのチャンスが減少する
- 整数のアンダーフローやオーバーフローが発生する危険性はありません
- 資産の複製が不可能であり、紛失、盗難、破壊が非常に起こりにくい。
- コードはコンポーザブルにできる
- ルールは不変であることができる
- コードが意図せず公開されることはない
Fungible Tokenコントラクトの実装
チュートリアルplaygroundのアカウント0x01タブを開くとBasicToken.cdcというファイルが存在しており、この中身はFungibleトークンをアカウントに保存し、他のユーザーとトークンをやり取りするためのコア機能を提供しています。
pub contract BasicToken {
pub resource Vault {
// keeps track of the total balance of the account's tokens
pub var balance: UFix64
// initialize the balance at resource creation time
init(balance: UFix64) {
self.balance = balance
}
pub fun withdraw(amount: UFix64): @Vault {
self.balance = self.balance - amount
return <-create Vault(balance: amount)
}
pub fun deposit(from: @Vault) {
self.balance = self.balance + from.balance
destroy from
}
}
pub fun createVault(): @Vault {
return <-create Vault(balance: 30.0)
}
init() {
let vault <- self.createVault()
self.account.save(<-vault, to: /storage/CadenceFungibleTokenTutorialVault)
}
}
このコントラクトではVaultという名前の残高を管理するresourceを定義しており、このリソースでは残高と出金・預金関数が定義されています。コントラクトにはリソースを作成して返すファクトリ関数と初期化処理でアカウントストレージにリソースを保存する処理が書かれています。
Solidityとの比較
以下のSolidityコードはCadenceの公式ドキュメント記載のERC20トークンの実装例です。
contract ERC20 {
// Maps user addresses to balances, similar to a dictionary in Cadence
mapping (address => uint256) private _balances;
function _transfer(address sender, address recipient, uint256 amount) {
// ensure the sender has a valid balance
require(_balances[sender] >= amount);
// subtract the amount from the senders ledger balance
_balances[sender] = _balances[sender] - amount;
// add the amount to the recipient’s ledger balance
_balances[recipient] = _balances[recipient] + amount
}
}
上記のようにSolidityの場合はトークンコントラクトに全てのアカウントと残高を紐付け中央台帳的に管理をする必要がありますが、Cadenceではこのような中央台帳的な管理を使用しない異なるアプローチをしています。そして、それを可能としているのはリソース指向言語としてのCadenceの特徴的な機能です。
このような違いはセキュリティの面でも優位に働き、Solidityの場合はユーザーがトークンで何かをしようとするたびにトークンコントラクトと対話する必要があり、このトークンコントラクトが悪用された場合全てのアカウントが危険に晒される可能性があります。
しかし、Cadenceの場合、代わりにリソースオブジェクトを自身のアカウントストレージに保存することで残高の管理を行い、ユーザー同士で中央のトークンコントラクトと対話することなくトークンのやり取りを可能としています。
また、Solidityの場合、整数のオーバーフローやアンダーフローが発生する危険性がありますが、Cadenceではこういったオーバーフローなどの保護機能が組み込まれており、そのような危険性はありません。
トークンの転送
前述の通りCadenceにおいてトークンの転送を実行する場合、Solidityとは異なるアプローチをとっています。
以下のような出金関数を前述のトークンコントラクトのVaultリソースに定義していましたが、トークンを別のアカウントに転送する場合、まずこの関数を呼び、残高を減らすのに加え新しいリソースオブジェクトを作成します。
pub fun withdraw(amount: UFix64): @Vault {
self.balance = self.balance - amount
return <-create Vault(balance: amount)
}
取得したリソースオブジェクトを引数に今度は以下のような預金関数を別のアカウントから呼び出すことで残高を増やします。
pub fun deposit(from: @Vault) {
self.balance = self.balance + from.balance
destroy from
}
Cadenceのリソースは必ず一つの場所にしか存在しないことをプログラミング言語レベルで補償されており、預金関数で使用したリソースは破棄する必要があります。
トークン転送のトランザクションを実行する
実際にトークンの転送を実行してみます。実行する前にplaygroundの0x01アカウントタブを開きデプロイします。これにより0x01アカウントはトークンコントラクトを持つ唯一のアカウントとなります。デプロイが成功すると初期化関数により30の残高を持つVaultオブジェクトのインスタンスをストレージに格納しています。
Deployment Deployed Contract To: 0x01
デプロイが完了したらplaygroundのBasic Transferタブを開きます。
// Basic Transfer
import BasicToken from 0x01
// This transaction is used to withdraw and deposit tokens with a Vault
transaction {
prepare(acct: AuthAccount) {
// withdraw tokens from your vault by borrowing a reference to it
// and calling the withdraw function with that reference
let vaultRef = acct.borrow<&BasicToken.Vault>(from: /storage/CadenceFungibleTokenTutorialVault)
?? panic("Could not borrow a reference to the owner's vault")
let temporaryVault <- vaultRef.withdraw(amount: 10.0)
// deposit your tokens to the Vault
vaultRef.deposit(from: <-temporaryVault)
log("Withdraw/Deposit succeeded!")
}
}
このトランザクションは引き出したトークン残高を預け戻すだけの処理を実行します。signerを0x01アカウントにしてトランザクションを実行してみます。
Basic Transfer "Withdraw/Deposit succeeded!"
成功しました。このトランザクションでは直接ストレージの参照を取得し関数を実行しています。これにより、よりコストの高いload関数を使用することなくトランザクションを実行することができています。
Interfaceの安全な利用
Cadenceのインターフェースは型定義以外にも特定の動作に関するルールを強制することができます。先の例で利用したVaultリソースに実装するようなインターフェースは以下のように作成することができます。
// Interface that enforces the requirements for withdrawing
// tokens from the implementing type
//
pub resource interface Provider {
pub fun withdraw(amount: UFix64): @Vault {
post {
result.balance == amount:
"Withdrawal amount must be the same as the balance of the withdrawn Vault"
}
}
}
// Interface that enforces the requirements for depositing
// tokens into the implementing type
//
pub resource interface Receiver {
// There aren't any meaningful requirements for only a deposit function
// but this still shows that the deposit function is required in an implementation.
pub fun deposit(from: @Vault)
}
// Balance
//
// Interface that specifies a public `balance` field for the vault
//
pub resource interface Balance {
pub var balance: UFix64
}
残高と出金関数、預金関数を定義しているだけですが出金関数にはpost
ブロックを使用し関数の戻り値であるVaultリソースの残高と引数で渡ってきたamountが等しくなることを強制しています。
また、各関数の修飾子にpub
がついていることで外部にも公開できるようにしていることがわかります。しかし、Cadenceでは修飾子を何もつけないとprivateとなり必要のないものはできるかぎりprivateにすることを推奨しています。これは辞書型や配列などを不用意に公開してしまうと悪意を持って変更されてしまう可能性があるためです。
Interfaceの実装
前述したようなInterfaceをトークンコントラクトに実装してみます。playgroudのアカウント0x02タブを開いてみます。
pub contract ExampleToken {
// Total supply of all tokens in existence.
pub var totalSupply: UFix64
pub resource interface Provider {
pub fun withdraw(amount: UFix64): @Vault {
post {
// `result` refers to the return value of the function
result.balance == UFix64(amount):
"Withdrawal amount must be the same as the balance of the withdrawn Vault"
}
}
}
pub resource interface Receiver {
pub fun deposit(from: @Vault) {
pre {
from.balance > 0.0:
"Deposit balance must be positive"
}
}
}
pub resource interface Balance {
pub var balance: UFix64
}
pub resource Vault: Provider, Receiver, Balance {
// keeps track of the total balance of the account's tokens
pub var balance: UFix64
// initialize the balance at resource creation time
init(balance: UFix64) {
self.balance = balance
}
pub fun withdraw(amount: UFix64): @Vault {
self.balance = self.balance - amount
return <-create Vault(balance: amount)
}
pub fun deposit(from: @Vault) {
self.balance = self.balance + from.balance
destroy from
}
}
pub fun createEmptyVault(): @Vault {
return <-create Vault(balance: 0.0)
}
pub resource VaultMinter {
pub fun mintTokens(amount: UFix64, recipient: Capability<&AnyResource{Receiver}>) {
let recipientRef = recipient.borrow()
?? panic("Could not borrow a receiver reference to the vault")
ExampleToken.totalSupply = ExampleToken.totalSupply + UFix64(amount)
recipientRef.deposit(from: <-create Vault(balance: amount))
}
}
init() {
self.totalSupply = 30.0
let vault <- create Vault(balance: self.totalSupply)
self.account.save(<-vault, to: /storage/CadenceFungibleTokenTutorialVault)
// Create a new MintAndBurn resource and store it in account storage
self.account.save(<-create VaultMinter(), to: /storage/CadenceFungibleTokenTutorialMinter)
self.account.link<&VaultMinter>(/private/Minter, target: /storage/CadenceFungibleTokenTutorialMinter)
}
}
これはBasicTokenコントラクトにinterfaceの実装を加えているのと新たにVaultMinterというリソースを定義しています。新たにトークンをmintする処理をこのVaultMinterを利用することでより限定的にしています。これに伴い、createVault
関数はcreateEmptyVault
関数に変更することで新たにトークンをmintするにはVaultMinterを使用するよう強制しています。
ではVaultMinterリソースを詳しく見てみましょう。
pub resource VaultMinter {
pub fun mintTokens(amount: UFix64, recipient: Capability<&AnyResource{Receiver}>) {
let recipientRef = recipient.borrow()
?? panic("Could not borrow a receiver reference to the vault")
ExampleToken.totalSupply = ExampleToken.totalSupply + UFix64(amount)
recipientRef.deposit(from: <-create Vault(balance: amount))
}
}
mintTokens関数の第二引数が重要なポイントとなっています。第二引数にはCapabilityを指定しますが型パラメーターで&AnyResourceを指定しており、{}内にはReceiverインターフェースを指定しています。{}内にはインターフェースのみを指定することができExampleToken.Receiver
インターフェースを実装した任意のリソースのCapabilityを引数に取ることを指定しています。Receiverインターフェースはdeposit関数を持っているため、mintTokens関数内でdeposit関数を呼ぶことが出来ます。
では、このコントラクトをデプロイし以下のCreate Linkトランザクションをsignerをアカウント0x02にして実行してみます。
// Create Link
import ExampleToken from 0x02
// This transaction creates a capability
// that is linked to the account's token vault.
// The capability is restricted to the fields in the `Receiver` interface,
// so it can only be used to deposit funds into the account.
transaction {
prepare(acct: AuthAccount) {
// Create a link to the Vault in storage that is restricted to the
// fields and functions in `Receiver` and `Balance` interfaces,
// this only exposes the balance field
// and deposit function of the underlying vault.
//
acct.link<&ExampleToken.Vault{ExampleToken.Receiver, ExampleToken.Balance}>(/public/CadenceFungibleTokenTutorialReceiver, target: /storage/CadenceFungibleTokenTutorialVault)
log("Public Receiver reference created!")
}
post {
// Check that the capabilities were created correctly
// by getting the public capability and checking
// that it points to a valid `Vault` object
// that implements the `Receiver` interface
getAccount(0x02).getCapability<&ExampleToken.Vault{ExampleToken.Receiver}>(/public/CadenceFungibleTokenTutorialReceiver)
.check():
"Vault Receiver Reference was not created correctly"
}
}
prepareブロックでReceiverとBalanceインタフェースのみを公開するVaultリソースのCapabilityを作成しストレージに保存します。
postブロックはトランザクションの実行後に特定の条件が満たされているかを確認するためのものです。アカウント0x02のpublicストレージからVautlリソースへの参照を取得できることを確認しています。ここではReceiverインタフェースを実装したCapabilityを取得できることを確認しています。
実行結果
"Public Receiver reference created!"
トークンのmintと転送
では実際にトークンの転送を実行していきます。アカウント0x03に10トークンを送信するトランザクションを実行します。トークンを移動するにはアカウント0x02アカウントから取得したCapabilityのwithdraw関数を呼び出し、トークンを移動させるための一時的なリソースを作成し、アカウント0x03のCapabilityからdeposit関数を呼び出し、残高を増やします。
このように、Cadenceではトークンを受け取る準備が出来ていないアカウントにはトークンを転送できないようになっており、誤って別のアカウントに送金するなどのリスクがなくなり安全です。
アカウント0x03がトークンを受け取れるようにまず以下のトランザクションを0x03のアカウントで実行します。
// Setup Account
import ExampleToken from 0x02
// This transaction configures an account to store and receive tokens defined by
// the ExampleToken contract.
transaction {
prepare(acct: AuthAccount) {
// Create a new empty Vault object
let vaultA <- ExampleToken.createEmptyVault()
// Store the vault in the account storage
acct.save<@ExampleToken.Vault>(<-vaultA, to: /storage/CadenceFungibleTokenTutorialVault)
log("Empty Vault stored")
// Create a public Receiver capability to the Vault
let ReceiverRef = acct.link<&ExampleToken.Vault{ExampleToken.Receiver, ExampleToken.Balance}>(/public/CadenceFungibleTokenTutorialReceiver, target: /storage/CadenceFungibleTokenTutorialVault)
log("References created")
}
post {
// Check that the capabilities were created correctly
getAccount(0x03).getCapability<&ExampleToken.Vault{ExampleToken.Receiver}>(/public/CadenceFungibleTokenTutorialReceiver)
.check():
"Vault Receiver Reference was not created correctly"
}
}
空のVaultリソースを作成し、アカウントストレージに保存するのとそのCapabilityをpublicストレージに保存するようなトランザクションになっています。
アカウント0x03がトークンを受け取る準備ができたら次にMint Tokensトランザクションのタブを開き実行します。
// Mint Tokens
import ExampleToken from 0x02
// This transaction mints tokens and deposits them into account 3's vault
transaction {
// Local variable for storing the reference to the minter resource
let mintingRef: &ExampleToken.VaultMinter
// Local variable for storing the reference to the Vault of
// the account that will receive the newly minted tokens
var receiver: Capability<&ExampleToken.Vault{ExampleToken.Receiver}>
prepare(acct: AuthAccount) {
// Borrow a reference to the stored, private minter resource
self.mintingRef = acct.borrow<&ExampleToken.VaultMinter>(from: /storage/CadenceFungibleTokenTutorialMinter)
?? panic("Could not borrow a reference to the minter")
// Get the public account object for account 0x03
let recipient = getAccount(0x03)
// Get their public receiver capability
self.receiver = recipient.getCapability<&ExampleToken.Vault{ExampleToken.Receiver}>
(/public/CadenceFungibleTokenTutorialReceiver)
}
execute {
// Mint 30 tokens and deposit them into the recipient's Vault
self.mintingRef.mintTokens(amount: 30.0, recipient: self.receiver)
log("30 tokens minted and deposited to account 0x03")
}
}
prepareブロックでアカウント0x02のVaultMinterリソースのCapabilityを取得するのとアカウント0x03のパブリックアカウントからVaultリソースのCapabilityを取得しています。
そして、executeブロックで取得していたVaultMinterの参照を利用しmintTokens関数を実行します。mintTokens関数の引数には送金する量とアカウント0x03のCapabilityを渡します。
エラーなく実行が完了すればmintされた30トークンがアカウント0x03に転送されているはずです。mintTokens関数はコントラクトの関数として実装することもできますがその場合、関数の実行者が実行権限を持ったアカウントなのかを検証する必要が出てきてしまいます。
mint関数を持ったVaultMinterリソースを実装しprivateストレージに格納することでアカウント0x02が唯一mintできるアカウントになります。mintする権限を他のアカウントにも与えたければVaultMinterの参照をpublicストレージに格納すればいいですし、コントラクト作成した後にmint自体をさせたくなければmint関数を実装しなければ良いです。
このようにCadenceではリソースとCapabilityを利用することで安全かつ柔軟なコントラクト実装をすることが可能となっています。
次にアカウント0x03に本当にトークンが転送されているかGet Balancesスクリプトタブを開いて実行してみます。
// Get Balances
import ExampleToken from 0x02
// This script reads the Vault balances of two accounts.
pub fun main() {
// Get the accounts' public account objects
let acct2 = getAccount(0x02)
let acct3 = getAccount(0x03)
// Get references to the account's receivers
// by getting their public capability
// and borrowing a reference from the capability
let acct2ReceiverRef = acct2.getCapability(/public/CadenceFungibleTokenTutorialReceiver)
.borrow<&ExampleToken.Vault{ExampleToken.Balance}>()
?? panic("Could not borrow a reference to the acct2 receiver")
let acct3ReceiverRef = acct3.getCapability(/public/CadenceFungibleTokenTutorialReceiver)
.borrow<&ExampleToken.Vault{ExampleToken.Balance}>()
?? panic("Could not borrow a reference to the acct3 receiver")
// Read and log balance fields
log("Account 2 Balance")
log(acct2ReceiverRef.balance)
log("Account 3 Balance")
log(acct3ReceiverRef.balance)
}
実行結果
"Account 1 Balance"
30
"Account 2 Balance"
30
Result > "void"
ちゃんとmintされた30トークンが転送されていることを確認することが出来ました。
最後にアカウント0x03からアカウント0x02に10トークン転送してみたいと思います。playgroundのTransfer Tokensタブを開き以下のトランザクションを実行します。
// Transfer Tokens
import ExampleToken from 0x02
// This transaction is a template for a transaction that
// could be used by anyone to send tokens to another account
// that owns a Vault
transaction {
// Temporary Vault object that holds the balance that is being transferred
var temporaryVault: @ExampleToken.Vault
prepare(acct: AuthAccount) {
// withdraw tokens from your vault by borrowing a reference to it
// and calling the withdraw function with that reference
let vaultRef = acct.borrow<&ExampleToken.Vault>(from: /storage/CadenceFungibleTokenTutorialVault)
?? panic("Could not borrow a reference to the owner's vault")
self.temporaryVault <- vaultRef.withdraw(amount: 10.0)
}
execute {
// get the recipient's public account object
let recipient = getAccount(0x02)
// get the recipient's Receiver reference to their Vault
// by borrowing the reference from the public capability
let receiverRef = recipient.getCapability(/public/CadenceFungibleTokenTutorialReceiver)
.borrow<&ExampleToken.Vault{ExampleToken.Receiver}>()
?? panic("Could not borrow a reference to the receiver")
// deposit your tokens to their Vault
receiverRef.deposit(from: <-self.temporaryVault)
log("Transfer succeeded!")
}
}
prepareブロックではアカウント0x03のVaultの参照を取得し、withdraw関数を実行して残高を減らし、一時的なVaultオブジェクトを変数に格納しています。
そして、executeブロックでアカウント0x02のパブリックアカウントを取得し、格納されているVaultリソースの参照を取得します。prepareブロックで格納しておいた一時的なVaultオブジェクトをdeposit関数に渡すことでアカウント0x02の残高を更新します。
実行完了したら再度Get Balancesスクリプトを実行し、転送できたことを確認します。
実行結果
"Account 2 Balance"
40
"Account 3 Balance"
20
Result > "void"
10トークン移動できていることが確認できました!
まとめ
今回は以下のことについて紹介しました。
- リソースを使用した基本的なFungible Tokenコントラクトの実装の仕組み
- 中央台帳的な管理であるSolidityとの違いについて
- Interfaceの安全な利用方法について
- InterfaceとCapabilityを利用した拡張性の高いトークンコントラクトの実装の仕組み
- mintと転送の基本的な仕組み
まだ、Cadenceのチュートリアルの続きもありますし、Cadenceについての基本的な文法やAPIなどまだまだ学ぶことは多いですが、今回までのチュートリアルでだいぶ雰囲気はつかめた気はします。もし、この記事でCadenceやflowに興味が湧いた方がいればぜひチュートリアルをやってみてください!ドキュメントもチュートリアルもかなりしっかり作られていますのでそんなにつまづくポイントもなくできると思います!今回は以上です!