使用Ink!開發(fā)Substrate ERC20智能合約
jasonruan 2020.07.14
1 環(huán)境搭建
1.1 安裝Substrate節(jié)點(diǎn)
$ git clone git@github.com:paritytech/substrate.git
$ cd substrate
(master)$ git checkout -b v2.0.0-rc4 v2.0.0-rc4
切換到一個(gè)新分支 'v2.0.0-rc4'
(v2.0.0-rc4)$ cargo build --release
1.2 安裝cargo contract插件
- 安裝命令
$ cargo install cargo-contract --vers 0.6.1 --force
- 幫助手冊(cè)
$ cargo contract --help
cargo-contract 0.6.1
Utilities to develop Wasm smart contracts
USAGE:
cargo contract <SUBCOMMAND>
OPTIONS:
-h, --help Prints help information
-V, --version Prints version information
SUBCOMMANDS:
new Setup and create a new smart contract project
build Compiles the smart contract
generate-metadata Generate contract metadata artifacts
test Test the smart contract off-chain
help Prints this message or the help of the given subcommand(s)
2 ERC20合約介紹
2.1 什么是ERC20標(biāo)準(zhǔn)
ERC20 通證標(biāo)準(zhǔn)(ERC20 Token Standard)是通過以太坊創(chuàng)建通證時(shí)的一種規(guī)范。按照 ERC20 的規(guī)范可以編寫一個(gè)智能合約,創(chuàng)建“可互換通證”。它并非強(qiáng)制要求亚茬,但遵循這個(gè)標(biāo)準(zhǔn)汞斧,所創(chuàng)建的通證可以與眾多交易所颜说、錢包等進(jìn)行交互哺壶,它現(xiàn)在已被行業(yè)普遍接受躲撰。
ERC20定義了一些標(biāo)準(zhǔn)的接口函數(shù):balanceOf
精偿、 totalSupply
泪酱、transfer
、transferFrom
还最、approve
和allowance
墓阀。 以及一些可選的字段,例如通證名稱拓轻、符號(hào)以及小數(shù)保留位數(shù)等斯撮。
詳見:https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
2.2 ERC20接口
contract ERC20 {
function totalSupply() constant returns (uint theTotalSupply);
function balanceOf(address _owner) constant returns (uint balance);
function transfer(address _to, uint _value) returns (bool success);
function transferFrom(address _from, address _to, uint _value) returns (bool success);
function approve(address _spender, uint _value) returns (bool success);
function allowance(address _owner, address _spender) constant returns (uint remaining);
event Transfer(address indexed _from, address indexed _to, uint _value);
event Approval(address indexed _owner, address indexed _spender, uint _value);
}
- 功能介紹:
函數(shù)名 | 功能 |
---|---|
totalSupply | 返回存在于流通中的通證(Token)總量 |
balanceOf | 返回指定賬戶地址的通證余額 |
transfer | 讓調(diào)用方將指定數(shù)量的通證發(fā)送到另一個(gè)地址,即轉(zhuǎn)賬 |
transferFrom | 允許智能合約自動(dòng)執(zhí)行轉(zhuǎn)賬流程并代表所有者發(fā)送給定數(shù)量的通證 |
approve | 調(diào)用方授權(quán)給定的地址可以從其地址中提款 |
allowance | 返回被允許轉(zhuǎn)移的余額數(shù)量 |
event Transfer | 事件通知扶叉,當(dāng)token被轉(zhuǎn)移時(shí)勿锅,必須調(diào)用觸發(fā),類似回調(diào)枣氧,當(dāng)事件發(fā)生時(shí)溢十,會(huì)得到通知 |
event Approval | 事件通知,當(dāng)任何成功調(diào)用approve 后达吞,必須調(diào)用觸發(fā) |
3 ERC20合約開發(fā)
3.1 創(chuàng)建合約工程
執(zhí)行命令后张弛,會(huì)生成2個(gè)文件,其中lib.rs
會(huì)包括一些基礎(chǔ)框架酪劫,我們可以在此基礎(chǔ)上開發(fā)我們的合約吞鸭。
$ cargo contract new erc20
Created contract erc20
$ tree erc20/
erc20/
├── Cargo.toml
└── lib.rs
3.2 合約存儲(chǔ)創(chuàng)建
#[ink(storage)]
struct Erc20 {
/// 代幣發(fā)行總量
total_supply: storage::Value<Balance>,
/// 用戶及余額映射
balances: storage::HashMap<AccountId, Balance>,
}
3.3 合約構(gòu)造方法創(chuàng)建
#[ink(constructor)]
fn new(&mut self, initial_supply: Balance) {
// 獲取合約創(chuàng)建者
let caller = self.env().caller();
// 設(shè)置發(fā)行總量
self.total_supply.set(initial_supply);
// 合約創(chuàng)建者擁有所有發(fā)行代幣
self.balances.insert(caller, initial_supply);
}
3.4 合約接口方法創(chuàng)建
(1)查詢代幣發(fā)行總量接口
#[ink(message)]
fn total_supply(&self) -> Balance {
*self.total_supply
}
(2)查詢用戶代幣余額接口
#[ink(message)]
fn balance_of(&self, owner: AccountId) -> Balance {
self.balance_of_or_zero(&owner)
}
// 工具方法:若用戶未被初始化,代幣余額置為0
fn balance_of_or_zero(&self, owner: &AccountId) -> Balance {
*self.balances.get(owner).unwrap_or(&0)
(3)轉(zhuǎn)賬接口
#[ink(message)]
fn transfer(&mut self, to: AccountId, value: Balance) -> bool {
// 獲取合約接口調(diào)用者地址
let from = self.env().caller();
// 給接收地址轉(zhuǎn)出指定金額代幣
self.transfer_from_to(from, to, value)
}
fn transfer_from_to(&mut self, from: AccountId, to: AccountId, value: Balance) -> bool {
// 獲取合約調(diào)用者賬戶余額
let from_balance = self.balance_of_or_zero(&from);
if from_balance < value {
return false
}
// 獲取合約接受者賬戶余額(代幣接收者賬戶可能未被初始化覆糟,通過此方法將其余額初始化為0)
let to_balance = self.balance_of_or_zero(&to);
// 發(fā)送者余額減少指定數(shù)量
self.balances.insert(from, from_balance - value);
// 接收者余額增加指定數(shù)量
self.balances.insert(to, to_balance + value);
true
}
我們注意到刻剥,在進(jìn)行余額的增減時(shí),并未像以太坊的solidity
智能合約滩字,使用額外的SafeMath
接口造虏,這是因?yàn)?code>ink!提供了內(nèi)置防溢出保護(hù),通過在Cargo.toml
配置文件中麦箍,添加如下配置來提供該安全機(jī)制:
[profile.release] panic = "abort" <-- Panics shall be treated as aborts: reduces binary size lto = true <-- enable link-time-optimization: more efficient codegen opt-level = "z" <-- Optimize for small binary output overflow-checks = true <-- Arithmetic overflow protection
(4)授權(quán)轉(zhuǎn)賬——授權(quán)接口
通過授權(quán)轉(zhuǎn)賬漓藕,調(diào)用方可以授權(quán)指定賬戶,從其地址中安全的消費(fèi)指定數(shù)量的代幣内列。
需完善合約存儲(chǔ):
#[ink(storage)] struct Erc20 { ...... // (代幣所有者, 代幣授權(quán)使用者) -> 代幣授權(quán)使用者可支配余額 allowances: storage::HashMap<(AccountId, AccountId), Balance>, }
#[ink(message)]
fn approve(&mut self, spender: AccountId, value: Balance) -> bool {
let owner = self.env().caller();
// 代幣所有者(owner)授權(quán)代幣使用者(spender)可支配余額(value)
self.allowances.insert((owner, spender), value);
true
}
(5)授權(quán)轉(zhuǎn)賬——余額查詢
獲取代幣授權(quán)使用者剩余被允許轉(zhuǎn)移的代幣數(shù)量撵术。
#[ink(message)]
fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance {
self.allowance_of_or_zero(&owner, &spender)
}
(6)授權(quán)轉(zhuǎn)賬——轉(zhuǎn)賬接口
允許智能合約自動(dòng)執(zhí)行轉(zhuǎn)賬流程并代表所有者發(fā)送給定數(shù)量的代幣
#[ink(message)]
fn transfer_from(&mut self, from: AccountId, to: AccountId, value: Balance) -> bool {
let caller = self.env().caller();
let allowance = self.allowance_of_or_zero(&from, &caller);
if allowance < value {
return false
}
self.allowances.insert((from, caller), allowance - value);
self.transfer_from_to(from, to, value)
}
fn allowance_of_or_zero(&self, owner: &AccountId, spender: &AccountId) -> Balance {
*self.allowances.get(&(*owner, *spender)).unwrap_or(&0)
}
3.5 合約事件創(chuàng)建
- 事件定義
#[ink(event)]
struct Transfer {
#[ink(topic)]
from: Option<AccountId>,
#[ink(topic)]
to: Option<AccountId>,
#[ink(topic)]
value: Balance,
}
#[ink(event)]
struct Approval {
#[ink(topic)]
owner: AccountId,
#[ink(topic)]
spender: AccountId,
#[ink(topic)]
value: Balance,
}
- 合約構(gòu)造事件
self.env().emit_event(Transfer {
from: None,
to: Some(caller),
value: initial_supply,
});
- 轉(zhuǎn)賬事件
self.env().emit_event(Transfer {
from: Some(from),
to: Some(to),
value,
});
- 授權(quán)事件
self.env().emit_event(Approval {
owner,
spender,
value,
});
3.6 單元測(cè)試用例編寫
#[test]
fn new_works() {
let contract = Erc20::new(777);
assert_eq!(contract.total_supply(), 777);
}
#[test]
fn balance_works() {
let contract = Erc20::new(100);
assert_eq!(contract.total_supply(), 100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 0);
}
#[test]
fn transfer_works() {
let mut contract = Erc20::new(100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
assert!(contract.transfer(AccountId::from([0x0; 32]), 10));
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 10);
assert!(!contract.transfer(AccountId::from([0x0; 32]), 100));
}
#[test]
fn transfer_from_works() {
let mut contract = Erc20::new(100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
contract.approve(AccountId::from([0x1; 32]), 20);
contract.transfer_from(AccountId::from([0x1; 32]), AccountId::from([0x0; 32]), 10);
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 10);
}
跑測(cè)試用例:
$ cargo +nightly test
image.png
3.7 完整代碼
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract(version = "0.1.0")]
mod erc20 {
use ink_core::storage;
#[ink(storage)]
struct Erc20 {
/// The total supply.
total_supply: storage::Value<Balance>,
/// The balance of each user.
balances: storage::HashMap<AccountId, Balance>,
/// Approval spender on behalf of the message's sender.
allowances: storage::HashMap<(AccountId, AccountId), Balance>,
}
#[ink(event)]
struct Transfer {
#[ink(topic)]
from: Option<AccountId>,
#[ink(topic)]
to: Option<AccountId>,
#[ink(topic)]
value: Balance,
}
#[ink(event)]
struct Approval {
#[ink(topic)]
owner: AccountId,
#[ink(topic)]
spender: AccountId,
#[ink(topic)]
value: Balance,
}
impl Erc20 {
#[ink(constructor)]
fn new(&mut self, initial_supply: Balance) {
let caller = self.env().caller();
self.total_supply.set(initial_supply);
self.balances.insert(caller, initial_supply);
self.env().emit_event(Transfer {
from: None,
to: Some(caller),
value: initial_supply,
});
}
#[ink(message)]
fn total_supply(&self) -> Balance {
*self.total_supply
}
#[ink(message)]
fn balance_of(&self, owner: AccountId) -> Balance {
self.balance_of_or_zero(&owner)
}
#[ink(message)]
fn approve(&mut self, spender: AccountId, value: Balance) -> bool {
let owner = self.env().caller();
self.allowances.insert((owner, spender), value);
self.env().emit_event(Approval {
owner,
spender,
value,
});
true
}
#[ink(message)]
fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance {
self.allowance_of_or_zero(&owner, &spender)
}
#[ink(message)]
fn transfer_from(&mut self, from: AccountId, to: AccountId, value: Balance) -> bool {
let caller = self.env().caller();
let allowance = self.allowance_of_or_zero(&from, &caller);
if allowance < value {
return false
}
self.allowances.insert((from, caller), allowance - value);
self.transfer_from_to(from, to, value)
}
#[ink(message)]
fn transfer(&mut self, to: AccountId, value: Balance) -> bool {
let from = self.env().caller();
self.transfer_from_to(from, to, value)
}
fn transfer_from_to(&mut self, from: AccountId, to: AccountId, value: Balance) -> bool {
let from_balance = self.balance_of_or_zero(&from);
if from_balance < value {
return false
}
let to_balance = self.balance_of_or_zero(&to);
self.balances.insert(from, from_balance - value);
self.balances.insert(to, to_balance + value);
self.env().emit_event(Transfer {
from: Some(from),
to: Some(to),
value,
});
true
}
fn balance_of_or_zero(&self, owner: &AccountId) -> Balance {
*self.balances.get(owner).unwrap_or(&0)
}
fn allowance_of_or_zero(&self, owner: &AccountId, spender: &AccountId) -> Balance {
*self.allowances.get(&(*owner, *spender)).unwrap_or(&0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_works() {
let contract = Erc20::new(777);
assert_eq!(contract.total_supply(), 777);
}
#[test]
fn balance_works() {
let contract = Erc20::new(100);
assert_eq!(contract.total_supply(), 100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 0);
}
#[test]
fn transfer_works() {
let mut contract = Erc20::new(100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
assert!(contract.transfer(AccountId::from([0x0; 32]), 10));
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 10);
assert!(!contract.transfer(AccountId::from([0x0; 32]), 100));
}
#[test]
fn transfer_from_works() {
let mut contract = Erc20::new(100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
contract.approve(AccountId::from([0x1; 32]), 20);
contract.transfer_from(AccountId::from([0x1; 32]), AccountId::from([0x0; 32]), 10);
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 10);
}
}
}
4 ERC20合約部署
4.1 啟動(dòng)substrate鏈
[Jason@RUAN:~/Blockchain/substrate] (v2.0.0-rc4)$ ./target/release/substrate purge-chain --dev
Are you sure to remove "/root/.local/share/substrate/chains/dev/db"? [y/N]: y
"/root/.local/share/substrate/chains/dev/db" did not exist.
[Jason@RUAN:~/Blockchain/substrate] (v2.0.0-rc4)$ ./target/release/substrate --dev --ws-external --rpc-external --rpc-cors=all
2020-07-13 23:07:17 Substrate Node
2020-07-13 23:07:17 ?? version 2.0.0-rc4-00768a1-x86_64-linux-gnu
2020-07-13 23:07:17 ?? by Parity Technologies <admin@parity.io>, 2017-2020
2020-07-13 23:07:17 ?? Chain specification: Development
2020-07-13 23:07:17 ?? Node name: ill-hen-8567
2020-07-13 23:07:17 ?? Role: AUTHORITY
2020-07-13 23:07:17 ?? Database: RocksDb at /root/.local/share/substrate/chains/dev/db
2020-07-13 23:07:17 ? Native runtime: node-254 (substrate-node-0.tx1.au10)
2020-07-13 23:07:17 ?? new validator set of size 1 has been elected via ElectionCompute::OnChain for era 0
2020-07-13 23:07:17 ?? Initializing Genesis block/state (state: 0xc720…bb8a, header-hash: 0x6ea2…1245)
2020-07-13 23:07:17 ?? Loading GRANDPA authority set from genesis on what appears to be first startup.
2020-07-13 23:07:17 ? Loaded block-time = 3000 milliseconds from genesis on first-launch
2020-07-13 23:07:17 ?? Creating empty BABE epoch changes on what appears to be first startup.
2020-07-13 23:07:17 ?? Highest known block at #0
2020-07-13 23:07:17 Using default protocol ID "sup" because none is configured in the chain specs
2020-07-13 23:07:17 ?? Local node identity is: 12D3KooWQUQtujJ5SGCdCcheuExioC81R5W4E3RFGhmhx3MT8iqy (legacy representation: QmX71wUqWKy7FQX8PEHKoQLaiBLLTfK8TL25mFXxKhMWGw)
2020-07-13 23:07:17 ? Prometheus server started at 127.0.0.1:9615
2020-07-13 23:07:17 ?? Starting BABE Authorship worker
2020-07-13 23:07:18 ?? Starting consensus session on top of parent 0x6ea2a97a8da973976a82f053a8b909aff5e0659ca6d51b6c9d6947b4dc3d1245
2020-07-13 23:07:18 ?? Prepared block for proposing at 1 [hash: 0x3b99b664d0a21fbc72bfed709700b5bba05564c8d62e9ddd677412896f25de31; parent_hash: 0x6ea2…1245; extrinsics (1): [0xdcda…fb8d]]
2020-07-13 23:07:18 ?? Pre-sealed block for proposal at 1. Hash now 0x3081484a5cbe82a9b4a4aea4d360fd69219a43d18182c6fd297e2ffac71feff2, previously 0x3b99b664d0a21fbc72bfed709700b5bba05564c8d62e9ddd677412896f25de31.
2020-07-13 23:07:18 ?? New epoch 0 launching at block 0x3081…eff2 (block slot 531550946 >= start slot 531550946).
2020-07-13 23:07:18 ?? Next epoch starts at slot 531551146
2020-07-13 23:07:18 ? Imported #1 (0x3081…eff2)
2020-07-13 23:07:21 ?? Starting consensus session on top of parent 0x3081484a5cbe82a9b4a4aea4d360fd69219a43d18182c6fd297e2ffac71feff2
2020-07-13 23:07:21 ?? Prepared block for proposing at 2 [hash: 0x346204e0b46b86dc4ec85b18cf2fdf0f0e818b24208e56217e6f44c135e3aef3; parent_hash: 0x3081…eff2; extrinsics (1): [0xfdbb…bdd0]]
2020-07-13 23:07:21 ?? Pre-sealed block for proposal at 2. Hash now 0x906f64c7a6139ad0819f6c31d776404573e72f3f155bab486a9aeca7c89df810, previously 0x346204e0b46b86dc4ec85b18cf2fdf0f0e818b24208e56217e6f44c135e3aef3.
2020-07-13 23:07:21 ? Imported #2 (0x906f…f810)
4.2 合約編譯
$ cargo contract build
[1/4] Collecting crate metadata
[2/4] Building cargo project
Finished release [optimized] target(s) in 0.05s
[3/4] Post processing wasm file
[4/4] Optimizing wasm file
wasm-opt is not installed. Install this tool on your system in order to
reduce the size of your contract's Wasm binary.
See https://github.com/WebAssembly/binaryen#tools
Your contract is ready. You can find it here:
./erc20/target/erc20.wasm
4.3 metadata生成
以便通過
polkadot.js.org
與合約進(jìn)行交互
$ cargo contract generate-metadata
Generating metadata
Updating git repository `https://github.com/paritytech/ink`
Updating crates.io index
Updating git repository `https://github.com/type-metadata/type-metadata.git`
Finished release [optimized] target(s) in 3.38s
Running `target/release/abi-gen`
Your metadata file is ready.
You can find it here:
./erc20/target/metadata.json
4.4 上傳WASM
4.5 部署合約
5 ERC20合約執(zhí)行
5.1 執(zhí)行合約
注:右下角開關(guān)
打開開關(guān):作為RPC調(diào)用發(fā)送,只能查看鏈上狀態(tài)
關(guān)閉開關(guān):作為交易發(fā)送话瞧,對(duì)鏈上狀態(tài)有更改