一 背景介绍
在智能合约的开发和应用过程中,向合约转账是非常常见的应用场景。实现这一功能的方式不止一种,不同的转账方式最初的目标也不一样。随着TRON的发展以及Solidity版本的演进,有些转账方式已经不再是最佳方案,甚至存在潜在的风险。由于开发者们在TRON开发智能合约的经验不同,对TVM理解程度也不一样,因此在开发给智能合转账功能的过程中,开发者们采用的实现方式也不一样,不正确的实现方式必然会导致达不到预期的结果,此类问题在开发者社区里屡见不鲜。本文的目的是给读者介绍如何正确的给已部署的合约转入TRX或者TRC10。文章首先对智能合约的一些基本知识进行了简单的介绍,接着文章重点介绍给已部署合约转账的正确方式,最后文章对系统合约转账方式和TIP37以及他所带来的影响进行进一步的阐述,希望读者通过此文能对波场中给智能合约转账方式有更完整的认识。
二 预备知识
给智能合约转账涉及的内容较多,本节重点介绍五项与本文密切相关的基本知识点。如果读者对这些已经了然于掌可以直接跳过本章节。
2.1 账户类型
TRON采用了Account balance模型。账户的唯一标识是地址(address
)。每个账户拥有TRX、Token、带宽、能量等各种资源。账户可以发送交易来增减TRX或者TRC10余额(需要消耗带宽),也可以发布智能合约,也可以调用他人发布的智能合约等等。TRON所有的活动都围绕账户进行。
目前TRON支持的账户类型有两种, 普通账户和智能合约账户,区别在于智能合约账户中存在虚拟机可执行的bytecode。
2.2 资产类型
TRON常见的资产类型有3种,分别是TRX,TRC10和TRC20。
- TRX是TRON网络中使用的货币的名称
- TRC10是TRON公链内置的TRC10通证,TRC10是系统级代币类型,TRC10没有使用TRON虚拟机(TVM)
- TRC20是基于TRON智能合约技术标准发行的token,智能合约运行在TRON虚拟机(TVM)中。 它与 ERC20完全兼容。
2.3 系统合约与智能合约
TRON区块链共有两种合约:系统合约和智能合约。TRON的智能合约由Solidity语言写成(兼容Ethereum),经过书写和测试之后,它们被编译成字节码然后部署到区块链上,TVM是智能合约的执行机,智能合约交易触发的时候,从链上加载合约到TVM进行执行。被部署之后,智能合约可以通过它们的地址被查询到。合约的ABI展示了合约可调用的功能。大量DAPP都是基于智能合约实现的。同时,TRON也支持系统合约,不同于智能合约,系统合约是TRON预置的合约,开发者可以直接调用,TRC10就是基于系统合约实现的一种Token。这里需要强调的的是系统合约的执行不经过TVM。这也是困惑很多开发者的原因所在,由于系统合约不经过TVM,导致使用系统合约与智能合约交互所带来的改变不能被智能合约正确的记录。很多从Ethereum过来的开发者由于不知道此特性,盲目的使用了系统合约给智能合约转账,引发了很多问题和困扰。(在Ethereum中,给合约转账必然触发合约中的fallback
函数)
2.4 fallback
函数
fallback
是智能合约中的一个特殊函数,一个合约最多只能拥有一个fallback
函数。这个函数没有参数,也不能有返回值,函数类型必须是external
类型的。该函数通过fallback () external [payable]
的形式声明(注意这里没有function
关键字)。当调用合约中一个不存在的函数或者调用空方法,亦或直接使用合约地址的内置函数 transfer()
或者 send()
时,都会执行目标合约的fallback
函数。这种特性被很多开发者用来记录用户转账。fallback
在执行的时候会接收所有传入的数据,如果fallback
函数是payable
的话,fallback
就可以接收TRX或者TRC10。
2.5 给不存在的地址转入TRX或者TRC10会自动激活目标账户
在java-tron Odyssey-v3.6.5之前的版本中,使用GRPC API CreateTransaction (TransferContract)
或者TransferAsset
给一个不存在的地址转入TRX/TRC10的时候,会自动激活目标地址,但是在智能合约中使用transfer
和transferToken
给不存在地址转账的时候,则会导致transaction失败。 这种不一致的行为给开发者们带来了很多的不便和困扰,因此在 Odyssey-v3.6.5版本中,引入了一种新的机制,当合约调用transfer
, send
, transfertoken
给不存在的地址转入TRX或者TRC10的时候,会自动激活目标地址。此过程会消耗10000energy, 相当于0.1TRX。 详细内容请参看 https://github.com/tronprotocol/tips/issues/54
三 触发智能合约中的payable
函数给合约转账
在实际的应用中,智能合约都应该提供资产转入和资产提取的函数,正确的资产转入方式是调用智能合约的资产转入函数完成转账,同理,用户也可以调用智能合约中的资产提取函数从合约中取回资产。所有调用智能合约函数完成转账的操作都会被记录在合约的存储中。由于各种原因,很多已经部署的智能合约没有提供资产转入和资产提取的函数 ,本文接下来将重点阐述对于这类合约如何正确的转入资产。
在阐述如何给智能合约转入TRX/TRC10之前,我们先讲一下TRC20,TRON的TRC20 Token技术标准规定了TRC20 Token的转账方式,即所有的TRC20 token转账都只能通过触发智能合约的方式,所有的TRC20 Token转账都会被记录到智能合约里的存储里。因此TRC20 Token的转账更简单且易操作。
对于给智能合约转入TRX和TRC10,除了触发智能合约的方式之外,还可以使用系统合约的方式,系统合约的执行不经过TVM,通常都只消耗Bandwidth,并且不会触发合约的任何函数,因此用户通过系统合约的方法给智能合约转入TRX或者TRC10均不会被记录到智能合约的存储里,从而引发很多问题和陷阱。 因此下面详细叙述如何正确的给合约转入TRX和TRC10。
如何判断一个转账方式是否是正确的呢? 其核心的原理是该转账能否被智能合约正确的记录,所有能被智能合约正确记录的转账方式都是正确的。换句话说,转账方式必须要经过合约里的函数,与此同时,此函数必须能够接收TRX/TRC10,也就是函数必须是payable
的,通过触发payable
函数引发的改变都会被记录到合约的存储里。理论上来说,调用合约中的任何payable
的函数都能实现给合约转账,用户可以根据自己的需求调用payable
的函数并指定TRX/TRC10数量完成转账。如果用户只是想完成TRX/TRC10的转账而不希望触发任何其他功能,那么触发payable
的fallback
函数是最好的选择。在使用中,常使用以下两种方式触发fallback
函数。
3.1 触发合约中不存在的函数或者空方法
前提:智能合约中必须定义了payable
的fallback
函数。
支持的资产类型: 此方法可以完成TRX或者TRC10的转账。
原理:根据TVM特性,调用合约中的空方法或者不存在的函数时,TVM会自动调用fallback
来执行,借助payable
的fallback
函数,用户就可以完成指定数量的TRX/TRC10转账。由于智能合约的执行都是在TVM里,所以最终这些转账都会被记录到合约的存储里。
具体过程:使用triggersmartcontract
触发合约中的空方法或者不存在的函数。TRX的数量通过call_value
指定,TRC10的数量通过call_token_value
指定。
局限性:如果fallback
函数不是payable
的,无法使用此功能
3.2 通过 address
内置函数完成转账。
前提:智能合约中必须定义了payable
的fallback
函数。
支持的资产类型: 此方法仅适用于TRX的转账
原理:address
有三个内置函数可以完成TRX的转账,分别是address.send()
, address.transfer()
, address.call.value()()
,其原理和第一种方法的原理类似,调用这三个函数的时候均会触发智能合约的fallback
函数。
具体过程:尽管这三个方法均可以成功的触发fallback
函数,但是这里会涉及到energy的问题。由于 transfer
和 send
函数只能附带2300的Energy,而对一个32 byte数据的读取(SSLOAD指令)、修改(SSTORE指令)和创建(SSTORE 指令)分别用到了400,5000和20000 Energy。所以,2300的 Energy 不足以创建和修改数据。也就是说,如果一个能量消耗超过2300的 fallback
函数,必然会引起 transfer
和 send
的失败。因此如果想通过地址的transfer
和 send
给合约转入TRX,请务必保证fallback
函数的energy消耗不超过2300。除Energy之外,还有其他需要注意的地方,详细内容如下:
address.send(amount)
使用address.send()
方法需要注意两点,第一点,如上所述,它只提供了2300 Energy。 第二点,对于执行失败的send
方法,send
函数仅仅返回false
,不会抛出任何异常。因此调用send
方法的时候需要配合require
使用,否则可能会出现交易上链,用户支付了fee,但是所有的状态改动没有生效。
address.transfer(amount)
address.transfer()
方法相当于 require(address.send())
, 使用transfer
方法也需要注意两点,第一点,跟send
方法一样,transfer
也只提供了2300 Energy。 第二点,不同于send
方法,transfer
方法提供了一种更安全的机制,失败的时候会抛出异常,所有已经完成的操作都会回滚。
address.call.value(amount)()
相对于前两种方法,address.call.value(amount)( )
使用起来更加灵活,适用的范围也更加广泛。因为这种方式提供了指定energy数量的接口,使用的时候不再受2300 Energy的限制,可以允许接收函数执行更复杂的操作。使用这种方法也需要注意两点问题。第一个,同send()
一样,执行失败的时候此函数不会抛出异常,只返回false
,需要用户手动处理返回结果,使用的时候建议配合require
一起使用。第二点,如果不显示指定Energy数量,默认的Energy数量是用户所有可用的Energy。Energy数量可以通过修饰器 .gas(energyLimit)
来设定。 参看示例: someAddress.call.value(trxAmount).gas(energyAmount)()
局限性:这三种方法均可以实现TRX的转账,但是他们三个在使用的时候略有不同且各有优缺点。在使用的时候需要根据自己的实际情况选择最佳的函数。
以上就是给智能合约转账的正确方式,细心的读者们一定发现这两种方式都有一个共同的前提:智能合约中必须定义了payable
的fallback
函数。但是现实中,由于各种原因,很多已经部署的智能合约没有定义payable
的fallback
函数,对于这种情况应该如何处理呢?本文稍后会讲到如何处理此类合约。
四 通过系统合约给智能合约转账
本节将介绍TRON特有的系统合约转账方式。在TRON的系统合约里,有两个合约可以完成TRX和TRC10的转账,TransferContract
用于TRX转账,TransferAssetContract
用于TRC10转账,这两个合约既可以完成对普通账户的转账,也可以完成对智能合约账户的转账。由于系统合约的方法只消耗Bandwidth
,不经过TVM,也不会触发合约中的任何函数,所以这种转账无法正确的被记录到到合约存储里。导致的结果是合约的资产余额变化了但是合约的存储和内容都没有变化。从用户的角度看,用户的资产成功转入到了智能合约里(由于没有正确的触发合约,导致合约内的存储将不会受这次转账的影响)但是智能合约却没有正确的记录此笔转账。对于有提取资产函数的智能合约来说,用户是没有权利去提取这笔资产的,因为合约里并没有正确记录这笔资产。 对于没有提取资产函数的合约来说,用户的资产将永久的锁在了合约里。使用这两个系统合约给普通账户转账是没有任何问题和风险的,但是用它给智能合约账户转账就存在风险,部分用户使用这种方式给智能合约转账导致资产丢失,因此使用这两种系统合约给智能合约转账的时候需要格外小心。这就是使用系统合约给智能合约转账的局限性,这两个系统合约的定义如下:
TransferContract :
message TransferContract { bytes owner_address = 1; bytes to_address = 2; int64 amount = 3; }
//owner_address:合约持有人地址。 to_address: 目标账户地址。 amount:转账金额,单位为 sun。
TransferAssetContract
message TransferAssetContract { bytes asset_name = 1; bytes owner_address = 2; bytes to_address = 3; int64 amount = 4; }
//asset_name:发布Token的id。 owner_address:合约持有人地址。 to_address: 目标账户地址。 amount:转账Token的数量。
五 TIP37带来的变化
由于系统合约的这种特殊性,以及频繁的收到用户关于资产丢失问题的咨询,TRON在TIP37中建议禁用TransferContract
和TransferAssetContract
给智能合约账户转账的功能,注意,这里只是禁用对合约账户转账的功能,对普通账户的转账依然有效。TIP37中给出的建议是在这两个系统合约中增加 validate()
方法,执行系统合约之前验证toAddress
的合法性, 如果toAddress
是一个合约地址,系统合约就会抛出一个contract validate exception
的异常。如果toAddress
不是合约地址,则正常执行。此TIP自提出以来就受到了广泛的关注,社区关于此TIP的讨论异常激烈,由于目前社区开发者还依赖这两种系统合约,因此TIP37目前还没有生效,但是TIP37一旦生效,所有使用这两种系统合约给智能合约转账的方式都将失效,以下是目前我们可预知的改变:
- 如果已部署合约中没有定义
payable
的fallback
函数,那么无法使用前文提到的正确方式给该合约充值TRX/TRC10。 - Tronlink以及类似的钱包需要通过新的方式完成对智能合约账户的TRX/TRC10转账(如果之前是通过系统合约完成对合约转账的)
- DAPP开发者们需要更新相关代码使用新的方式完成对合约的资产转入(如果之前是通过系统合约完成对合约转账的)
- TronWeb同样也需要做类似改动
对于这里的第一种情况,如果已部署的合约没有payable
的fallback
函数,且TIP37生效之后,那还有什么其他的方式可以完成对合约的转账呢? 接下来我们将介绍两种特殊的方法。
5.1 selfdestruct/suicide
selfdestruct
是solidity的一个预置方法,其作用是销毁当前合约,把余额发送到参数指定的地址。该功能从合约地址中删除所有的字节码,清除所有的storage状态变量并将余额清零。如果参数地址是一个合约地址,则不会调用该合约的任何功能。因此,使用selfdestruct
可以无视目标合约中的任何代码, 强制将TRX/TRC10发送给目标合约。selfdestruct
函数的声明方式:selfdestruct(address recipient):
注意,此方式不会触发fallback
函数。
借助selfdestruct
函数的特殊性,我们可以通过部署新的合约,然后调用新合约的selfdestruct
方式给其他合约充值,这种充值方式可以用于未定义payable
的fallback
函数的智能合约。新合约参考代码如下:
// solidity source code
pragma solidity 0.5.8;
contract ForceTransfer{
constructor(address payable toAddress) public payable{
selfdestruct(toAddress);
}
}
// 编译参数:--optimize --optimize-runs=200
// 编译器:TRON 0.5.8 编译器 https://github.com/tronprotocol/solidity/releases
// Binary: 6080604052604051602080603083398101806040526020811015602157600080fd5b50516001600160a01b038116fffe
Contract JSON ABI
[{"inputs":[{"name":"toAddress","type":"address"}],"payable":true,"stateMutability":"payable","type":"constructor"}]
开发者可通过如下方式,可对目标地址 TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy(示例地址,此地址并不一定真实存在)进行强制性的转账:
- 给目标合约地址 TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy, 转 x 数量的TRX、y 数量的z TRC10资产
deploycontract ForceTransfer [] 6080604052604051602080603083398101806040526020811015602157600080fd5b50516001600160a01b038116fffe ForceTransfer(address) "TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy" false 10000000 0 10000000 x y z
- 给目标合约地址 TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy 转 x 数量的TRX
deploycontract ForceTransfer [] 6080604052604051602080603083398101806040526020811015602157600080fd5b50516001600160a01b038116fffe ForceTransfer(address) "TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy" false 10000000 0 10000000 x 0 #
- 给目标合约地址TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy 转入 y 数量的z TRC10资产
deploycontract ForceTransfer [] 6080604052604051602080603083398101806040526020811015602157600080fd5b50516001600160a01b038116fffe ForceTransfer(address) "TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy" false 10000000 0 10000000 0 y z
5.2 create2
带来的特殊方式
目前,TRON的智能合约地址的生成方式有两种。 第一种,不可预知类型的地址,通常使用deployContract
部署合约产生的地址和在solidity代码中使用create
指令创建的合约地址都属于这种类型,这种地址的创建过程是基于当前交易的hash值根据固定公式计算出来的,因为交易中包含时间戳。因此这类地址可以说是随机且不可预测的。相对不可预测地址,create2
指令的引入带来的一种新的地址产生方式,这种方式产生的地址是可以提前预知的。TRON在编译器0.5.4 版本中引入了create2
指令, 同时在java-tron Odyssey-v3.6.0也支持了create2
指令,create2在生成地址的过程中,只要address
, salt
, init_code
都相同, 那么产生的地址肯定是相同的。这个属性对于链下扩容至关重要,比如链下计算的参与方可以事先商量好这个合约地址,从而大大减少了无意义的合约部署。当然这种可以预知的合约地址也存在一定的安全隐患。关于create2
更多的内容不是本文的重点,就不在这里展开,有兴趣的同学可以自己查阅资料或者关注我们的后续文章。
既然通过Create2
创建的合约地址是可以预先知晓的,在合约部署之前, 如果给此地址转入TRX或TRC10,那么该地址就会被激活成为一个普通账户地址,且拥有资产。create2
创建合约过程中,会将这个已经存在的普通账户地址转变成一个合约地址。转换过程不会清零资产,所以通过此方法可以成功的在合约部署之前给合约转入TRX或者TRC10。
六 总结
本文详细介绍了给已部署智能合约转账的正确方式和原理,同时也介绍了其他的已经存在的转账方式。后续文章我们将介绍智能合约DelegateCall以及如何开发可升级的智能合约,敬请期待。
七 参考资料
solidity官方文档:https://solidity.readthedocs.io/en/v0.5.12/index.html
TIP37: https://github.com/tronprotocol/tips/blob/master/tip-37.md
address.send 和address.transfer:https://ethereum.stackexchange.com/questions/19341/address-send-vs-address-transfer-best-practice-usage/19343#19343
八 关于我们
我们是波场公链核心开发者团队,致力于依托区块链技术打造下一代分布式计算平台,如果您想持续了解我们并获取技术支持,请关注我们的公众号:波场核心开发者团队
九 关于波场公链
波场公链代码仓库: https://github.com/tronprotocol/java-tron
波场开发者电报群: https://t.me/troncoredevscommunity
波场开发者微信群:请添加TRON Robot的微信,微信号:hello-zyuming
,成为好友之后TRON Robot会邀请您加入波场开发者微信群。