Ton 区块链的官方 类ERC20-Token 智能合约代码-Transfer部分解析
作者:林冠宏 / 指尖下的幽灵。转载者,请: 务必标明出处。
掘金:https://juejin.im/user/1785262612681997
GitHub : https://github.com/af913337456/
出版的书籍:
Ton 区块链的官方 类ERC20-Token 智能合约代码-Transfer部分解析
最近在学习 Ton 链的智能合约,由于我之前的经验思维主要是集中在以太坊这条链的,即Solidity那套,所以带着长久偏向的思维去阅读 Ton 的合约时发现格格不入,Ton 的合约设计与EVM体系的属于天壤之别。
首先 Ton 的合约是分片的,遵循 Parent-Child 的规则,这里详细了解见:
其次是合约开发的语言,Ton 有三种,用得最多的是 FunC,这是一种完全的非主流语言,在 GitHub 上都没有特点标识的那种。
按照最快了解 Token 智能合约的方式,寻找到官方的合约代码项目。由于Ton 的经济 Token 代码目前还没有类似以太坊的各种模型协议,只能把对应以太坊ERC-20的那部分取下来进行阅读。
下面我将结合Token的转账核心操作的源码来对其整个调用链路 进行细致的分析讲解,所选代码片段也有注释。
先了解合约模式
- Ton 的合约是分片的,拿 Token 类型的合约做例子,其做法是将一份主合约,被称为 Master 或 Minter 的合约独一份进行部署,再将和 User 的子合约在转账进行时进行新建形式的一一对应部署。
- 比如说,发布一份名叫
NOT的 Token 合约,它的 Master 合约将被部署在链上,然后对于后续每一位收到 NOT token 的用户地址,若不存在就都会创建一份与该地址对应的子合约,称为 Wallet 合约。 - 在 Token 类型的合约中,Master 合约中存储了 Token 的公共信息,比如 Name,Metaurl,Supply 等,而Transfer 转账行为却都发生在各自的 Wallet 合约里面。
- 为 User 创建 Wallet 合约都要经过 Master 进行。
- 合约允许各自内部调用,A 合约调用 B 合约的函数。

客户端-发起转账 Token 的流程
例子取于 Golang 客户端项目代码。
func main() {
...
// 初始化自己的钱包
w, _ := wallet.FromSeed(api, words, wallet.V3R2)
// 根据该 Token 的 Master 合约地址初始化 Token
token := jetton.NewJettonMasterClient(api, address.MustParseAddr("EQD0vdS......"))
// 调用 Master 的合约函数获取转账者的 Wallet 合约
tokenWallet, _ := token.GetJettonWallet(ctx, w.WalletAddress())
tokenBalance, _ := tokenWallet.GetBalance(ctx)
amountTokens := tlb.MustFromDecimal("0.1", 9)
// 转账附带的信息
comment, _ := wallet.CreateCommentCell("Hello from tonutils-go!")
// 初始化收款者的地址,这不是 Wallet 地址
to := address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N")
// 在 BuildTransferPayloadV2 里指定了 OP = Transfer
transferPayload, _ := tokenWallet.BuildTransferPayloadV2(to, to, amountTokens, tlb.ZeroCoins, comment, nil)
// 构造链上请求的消息结构
msg := wallet.SimpleMessage(tokenWallet.Address(), tlb.MustFromTON("0.05"), transferPayload)
// 发送转账交易,然后结束
tx, _, _ := w.SendWaitTransaction(ctx, msg)
log.Println("transaction confirmed, hash:", base64.StdEncoding.EncodeToString(tx.Hash))
}
上述代码可以看到在发起转账的时候,收款地址并不是 User 的钱包地址,而是其对应的 Wallet 合约地址。这一点就和包括以太坊在内的绝大部分公链都不一样。
合约端对应的转账入口代码解析
内部消息的入口函数,根据 op 参数指定调用入口。
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
if (in_msg_body.slice_empty?()) { ;; ignore empty messages
return ();
}
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
if (flags & 1) {
on_bounce(in_msg_body);
return ();
}
slice sender_address = cs~load_msg_addr();
cs~load_msg_addr(); ;; skip dst
cs~load_coins(); ;; skip value
cs~skip_bits(1); ;; skip extracurrency collection
cs~load_coins(); ;; skip ihr_fee
int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; we use message fwd_fee for estimation of forward_payload costs
int op = in_msg_body~load_uint(32);
if (op == op::transfer()) { ;; outgoing transfer
;; sender_address 是一开始的转账者
;; msg_value 是改次转账中的 Ton 数额
send_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
return ();
}
if (op == op::internal_transfer()) { ;; incoming transfer
;; my_balance 是当前所执行的合约所有者的 Ton 余额
receive_tokens(in_msg_body, sender_address, my_balance, fwd_fee, msg_value);
return ();
}
if (op == op::burn()) { ;; burn
burn_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
return ();
}
throw(0xffff);
}
recv_internal是系统内置的函数入口,相当于 main;- 系统函数还有:
load_data与save_data,加载的是当前合约的数据,存储也是存储到当前合约。代码中的变量jetton_master_address地址永远是父合约地址。 - 转账发起时,指定 op 是 transfer,走到代码处理点
op == op::transfer,进入到send_tokens; send_tokens函数源码及其解析注释内容见下
() send_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure {
int query_id = in_msg_body~load_uint(64);
int jetton_amount = in_msg_body~load_coins();
slice to_owner_address = in_msg_body~load_msg_addr(); ;; 收款人
force_chain(to_owner_address);
;; owner_address 转账者,jetton_master_address Token主地址
(int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
;; msg_value 是改次转账中的 Ton 数额,不是 token 余额
balance -= jetton_amount; ;; balance 是转账者余额,jetton_amount 是要转的数额
throw_unless(705, equal_slices(owner_address, sender_address)); ;; 要求发起转账的人一致
throw_unless(706, balance >= 0); ;; 要求减去 jetton_amount 余额大于 0,防止超出
cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code);
slice to_wallet_address = calculate_jetton_wallet_address(state_init);
slice response_address = in_msg_body~load_msg_addr(); ;; 转账结束后要被通知到的地址
cell custom_payload = in_msg_body~load_dict();
int forward_ton_amount = in_msg_body~load_coins(); ;; 附属的要转的 Ton 的数额,可以是 0,客户端赋值
throw_unless(708, slice_bits(in_msg_body) >= 1);
slice either_forward_payload = in_msg_body;
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(to_wallet_address) ;; 走到收款人的合约处
.store_coins(0)
.store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
.store_ref(state_init); ;; 如果对方没被部署过 wallet 合约,那么这个消息会触发部署
var msg_body = begin_cell()
.store_uint(op::internal_transfer(), 32) ;; internal_transfer 引导到下一个 op
.store_uint(query_id, 64)
.store_coins(jetton_amount)
.store_slice(owner_address) ;; owner_address 转账者
.store_slice(response_address) ;; 转账结束后要被通知到的地址
.store_coins(forward_ton_amount)
.store_slice(either_forward_payload)
.end_cell();
msg = msg.store_ref(msg_body);
int fwd_count = forward_ton_amount ? 2 : 1;
throw_unless(709, msg_value >
forward_ton_amount +
;; 3 messages: wal1->wal2, wal2->owner, wal2->response
;; but last one is optional (it is ok if it fails)
fwd_count * fwd_fee +
(2 * gas_consumption() + min_tons_for_storage()));
;; universal message send fee calculation may be activated here
;; by using this instead of fwd_fee
;; msg_fwd_fee(to_wallet, msg_body, state_init, 15)
send_raw_message(msg.end_cell(), 64); ;; revert on errors
;; 如果 send_raw_message 没出错,那么下面就会完成最后一步
save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); ;; 存储转账者的 token 余额
}
- 在
send_tokens里面,还会进行一次内部合约调用,调用到收款人的 Wallet 合约,对应到 op 是internal_transfer,而internal_transfer的处理函数是receive_tokens receive_tokens函数源码及其解析注释内容见下
() receive_tokens (slice in_msg_body, slice sender_address, int my_ton_balance, int fwd_fee, int msg_value) impure {
;; NOTE we can not allow fails in action phase since in that case there will be
;; no bounce. Thus check and throw in computation phase.
(int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
int query_id = in_msg_body~load_uint(64);
int jetton_amount = in_msg_body~load_coins(); ;; token 代币余额
balance += jetton_amount; ;; token 代币余额累加
slice from_address = in_msg_body~load_msg_addr(); ;; 原始的 Token 转账者
slice response_address = in_msg_body~load_msg_addr(); ;; 转账结束后要被通知到的地址
;; sender_address 系统地址,意味着这个函数只能由系统内部调用,排除了外部调用
throw_unless(707,
equal_slices(jetton_master_address, sender_address)
|
equal_slices(calculate_user_jetton_wallet_address(from_address, jetton_master_address, jetton_wallet_code), sender_address)
);
int forward_ton_amount = in_msg_body~load_coins(); ;; 附属要转账 ton 数值
int ton_balance_before_msg = my_ton_balance - msg_value;
int storage_fee = min_tons_for_storage() - min(ton_balance_before_msg, min_tons_for_storage());
msg_value -= (storage_fee + gas_consumption());
if(forward_ton_amount) { ;; 附属要转账 ton,如果不是 0
msg_value -= (forward_ton_amount + fwd_fee);
slice either_forward_payload = in_msg_body;
var msg_body = begin_cell()
.store_uint(op::transfer_notification(), 32)
.store_uint(query_id, 64)
.store_coins(jetton_amount)
.store_slice(from_address)
.store_slice(either_forward_payload)
.end_cell();
var msg = begin_cell()
.store_uint(0x10, 6) ;; we should not bounce here cause receiver can have uninitialized contract
.store_slice(owner_address) ;; 当前 Token 收款人地址
.store_coins(forward_ton_amount) ;; 收款人加上这部分附属的 Ton。付款人减去
.store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_ref(msg_body);
send_raw_message(msg.end_cell(), 1);
}
if ((response_address.preload_uint(2) != 0) & (msg_value > 0)) {
var msg = begin_cell()
.store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000
.store_slice(response_address)
.store_coins(msg_value) ;; 超过的 Ton 手续费退款到这个地址
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(op::excesses(), 32)
.store_uint(query_id, 64);
send_raw_message(msg.end_cell(), 2);
}
;; 下面为收款人加上 Token
save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
}
- 走完
receive_tokens之后,整个转账行为就在链上闭环了。
注意:上面的源码实现部分比较绕,务必结合注释阅读。
Ton 区块链的官方 类ERC20-Token 智能合约代码-Transfer部分解析的更多相关文章
- (二)区块链的共识算法:PoS 及其 例子 代码 实现
作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:http://www.cnblogs.com/linguan ...
- 呼叫河马——搭建在NGK公链上的去中心化智能合约DAPP
基于区块链技术发展的DAPP是一种分布式应用生态系统.目前最受DAPP欢迎的区块链有以太坊.EOS.波场等公链. 但由于当前 EOS资源模型的局限性,使得其使用成本较高.尽管 EOS的DPOS共识机制 ...
- 详解支付体系颠覆者NGK公链:如何通过呼叫河马智能合约加速转账?
纵观全球加密货币市场,至今为止,全球已经发行的加密货币以及数字代币的数量已经超过了7000种,且未来还将会有更多的加密货币或数字代币出现.在众多加密货币项目中,投资者很难在众多的项目里甄别项目的好坏以 ...
- Genaro Network —— 区块链3.0缔造者
在2018年1月26日硅谷密探在美国旧金山艺术宫成功举办了“Blockchain Connect Conference”.在大会上,Genaro Network 联合CEO Jason Inch 携同 ...
- bimgotoblock-BIM要上区块链
比特币(BitCoin)的暴涨暴跌让参与者体验着过山车的感觉,有人一夜暴富.别墅靠海,也有人拍断大腿或者恨不得跳楼了此残生.世人也更多的听说了比特币底层的依托--区块链(BlockChain) 技术. ...
- android和java以太坊开发区块链应用使用web3j类库
如何使用web3j为Java应用或Android App增加以太坊区块链支持,教程内容即涉及以太坊中的核心概念,例如账户管理包括账户的创建.钱包创建.交易转账,交易与状态.智能合约开发与交互.过滤器和 ...
- AbelSu的区块链笔记
最近几年,像比特币.以太坊.ICO.区块链等概念突然成为互联网热门话题,今天写这篇博客,也是做一些笔记,大概说一下对这个的解释和其他相关内容. 区块链: 区块链是分布式数据存储.点对点传输.共识机制. ...
- IoT与区块链的机遇与挑战
区块链, 分布式账本技术的一种形式, 自从2014年或多或少地获得了大量的关注: 区块链和物联网, 区块链和安全, 区块链和金融, 区块链和物流, 凡是你能想到的,仿佛都可以应用区块链. 在本文中, ...
- 区块链学习笔记:DAY05 如何使用公有云区块链服务
这是最后一节课了,主要讲华为云在云区块链提供的服务,如何基于华为云BCS来构建应用 先来个简单的比喻: 1.有关BaaS的范围定义 包含物理主机.虚拟主机.容器服务.区块链.智能合约和服务 2.华为云 ...
- 区块链入门到实战(27)之以太坊(Ethereum) – 智能合约开发
智能合约的优点 与传统合同相比,智能合约有一些显著优点: 不需要中间人 费用低 代码就是规则 区块链网络中有多个备份,不用担心丢失 避免人工错误 无需信任,就可履行协议 匿名履行协议 以太坊(Ethe ...
随机推荐
- KubeVela 1.1 发布,开启混合环境应用交付新里程碑
简介: KubeVela 作为一个开箱即用.面向现代微服务架构的应用交付与管理平台,今天正式发布了 1.1 版本,以更加用户友好和完善的功能集,开启了"让混合环境应用交付更加简单高效&qu ...
- 深入理解云计算OpenAPI体系
简介: 就云计算的API来看,当前并没有类似POSIX这样的API标准,基本上各大厂商各自为政.当然,有一些业界主流标准例如OAS获得多数云厂商的支持,但云厂商本身的API却往往由于历史原因.技术路 ...
- [FAQ] web3js, Error: [number-to-bn] while converting number 0.1 to BN.js instance, error: invalid number value
我们在调用合约方法时,都可以传一些参数的,比如转账金额 value. value 的单位是 wei,这是一个很小的单位,所以一般数值很大. 如果误把 ether 当成 wei 传参,就会报标题中的错误 ...
- ES6中Promise方法详解
概要 Promise是个保存异步结果的容器(对象). Promise的状态有:pending(进行中),fulfilled(已成功),rejected(已失败),对应其异步操作的状态. 状态转换: p ...
- C# - 能否让 SortedSet.RemoveWhere 内传入的委托异步执行
TL;DR; 若想充分利用 RemoveWhere 带来的性能优势,建议传入判断是否删除元素的委托内采取同步操作.若一定要在该委托内使用异步操作,可以采用本文中绕行的方法,但摈弃了 RemoveWhe ...
- 七、Doris Colocation Join
Colocation Join 是在 Doris 0.9 版本中引入的新功能.旨在为某些 Join 查询提供本地性优化,来减少数据在节点间的传输耗时,加速查询. 1.基本理论 Join 的常见连接类型 ...
- 【停用词】NLP中的停用词怎么获取?我整理了6种方法
目录 一.停用词介绍 二.停用词应用场景 2.1 提取高频词 2.2 词云图 三.停用词获取方法 3.1 自定义停用词 3.2 用wordcloud调取停用词 3.3 用nltk调取停用词 3.3.1 ...
- java代码审计跨站脚本(XSS)--反射型
一.基础:跨站脚本(Xss) 一.原理: 恶意攻击者往web页面里插入恶意js代码,而在服务端没有对数据进行严格的过滤.当用户浏览页面时,js代码必须在该html页面中(hrml必须要存在这个而已艾玛 ...
- WEB服务与NGINX(14)-NGINX的压缩功能
1. nginx压缩功能 nginx支持对指定类型的文件进行压缩后再回传给客户端,而且压缩可以设置压缩比,压缩后的文件会明显变小,有助于降低出口带宽的利用率,但是会占用一定的CPU资源. nginx实 ...
- Oracle修改字段长度及属性
首发微信公众号:SQL数据库运维 原文链接:https://mp.weixin.qq.com/s?__biz=MzI1NTQyNzg3MQ==&mid=2247486117&idx=1 ...