区块链安全

区块链安全第 2 课——有参数合约交互:从合约提币

@snowming

0x01 本课导言

前面提到,「智能合约」(Smart Contract)是在以太坊计算基础框架上执行的程序。第 1 课中,已经编写并编译、运行了一个合约,然后通过 Meta Mask 钱包成功向此合约发送了以太币。

在本课中,将会基于上节课的合约添加一个带参数的函数,并使用 Remix 去调用此函数。至此,此合约已经实现了两个功能:「向此合约转以太币,合约余额增加」和「从此合约提币,合约余额减少」,这就是一个最简单的「水龙头」(Faucet)功能。

0x02 开始实验

step1:编写合约

打开 Remix IDE,为 Faucet.sol 文件添加一个名为 withdraw 的函数:


合约代码

逐行解释一下此函数的代码:

  • function withdraw(uint withdraw_amount) public {}

名为 withdraw 的函数,一个形参名为 withdraw_amount,类型是 unsigned int(无符号整数),最后的 public 属性意味着此函数是公开函数,可以被其他合约调用。

注意,在以太坊系统内部,以太币总是采用无符号整型数据来表示。这意味着如果你的形参是币的数额,那么可以总是指定为 uint 类型。

  • require(withdraw_amount <= 100000000);

require 是 Solidity 内置的函数,用于测试一个前提条件,若满足条件则调用 withdraw 函数,否则 require 函数将导致合约执行停止并因异常而失败。有点类似于 python 中的 try 或者 assert。

100000000 的单位是 wei。以太币可以被拆分为更小的单位,以太币的最小单位是 wei。1 ether = 10^18 wei。在以太坊系统内部,以太币以 wei 为单位。所以这里面的 100000000 指 100000000 wei,而 100 000 000 wei == 0.0000000001 ether。

  • msg.sender.transfer(withdraw_amount);

msg 对象是一个所有合约都可以访问的输入,它代表触发这个合约执行的交易。sender 是一个属性,顾名思义,msg.sender 指交易的 From Address,即发送方。transfer 是一个内置函数,此函数只接受一个数额参数,此函数的功能是把以太币从合约转账到调用这个合约的交易地址。也就是说把以太币从「合约余额」转到「From Address」。

至此,整个函数的功能非常清楚了:在一定额度内,将 withdraw_amount 大小的 ether 转到触发合约的 From Address。

所以现在这个合约有两个函数(可以看出合约类似面向对象编程语言中类的概念):一个是默认函数,用于接收 From Address 转入合约余额的 ether;另一个是 withdraw 函数,用于向 From Address 转一定额度的 ether。

整个合约程序模拟了 Ropsten 测试网络中的水龙头程序(还记得吗,就是我们网络乞讨的那个):

地址:https://faucet.metamask.io/

所以什么是 Faucet(水龙头) 呢?

它向任何提出申请的地址发送测试以太币,并且会定期被充满。我们编写的这个仅有两个函数的合约已经能实现水龙头的基本功能。

step2:编译合约

如何编译、部署运行合约已经在上一课介绍过,但是为了避免有读者没读过第1课,再次介绍下。

编译的目的是将 sol 文件转换为 bytecodes(字节码),这样 EVM 才能运行此程序。

step3:部署、运行合约

注意 ENVIRONMENT (环境)要选「Injected Web3」,这意味着向浏览器注入一个 web3 实例(供页面上的 JavaScript 与客户端交互)。然后 ACCOUNT 选择连接到我们的浏览器钱包 Meta Mask,就会自动出现钱包地址。这样就可以通过浏览器钱包与此合约交互了。

step4:调用合约的带参数函数

在这个 DEPLOY&RUN TRANSACTIONS 模块中,看到 「Deployed Contracts」已部署合约的地方,可以看到我们刚刚部署的合约地址。

什么叫已部署合约,意思是相当于你的这个合约的程序正在运行中。想一下类似你 python 起的程序正在运行中。

这里自动出现了函数名 「withdraw」,在右侧的框中,我们可以填入一个 uint256 的参数。

这里有两个注意点:

  1. 参数的单位也必须使用 wei 的格式。记住以太坊内部所有的币值单位都应该使用 wei 来表示。所以如果你想转 0.1 个 eth,应该写 “100 000 000 000 000 000”。而千万不要写成 0.1!
  2. 注意到上面的双引号了吗,由于 JavaScript 的限制,大于 10^17 的数值无法在 Remix 中处理。因此,通过双引号把树枝引起来,让 Remix 把它当作字符串形态的 BigNumber 来处理,如果不加引号,Remix IDE 就无法处理,会显示「Error encoding arguments: Error: Assertion failed.」

这里因为我代码中 require 了 withdraw_amout <= 100000000,所以我填入了下面的参数:

然后点击 withdraw 按钮,Remix 就帮我们发出了此交易消息!

稍等片刻,就能在区块链浏览器上面看到了多出了此笔交易:

before
after

你可能会奇怪,为什么 Transactions 的这笔 in 交易显示的 value 是 0 ether?这个其实是一个「内部交易」的问题。Meta Mask 会存在一些在交易过程的显示中,把数字取整的情况。但是下图中的接收账户(但是对整个交易消息是 From Address,因为刚刚在 Remix 中我们使用此账户触发了合约)的 0 ether 在此不是「取整」的原因,而是因为「内部交易」(Internal Transactions),内部交易这一概念在下文中会讲到。

合约的区块链浏览器详情请看此链接:https://ropsten.etherscan.io/address/0x9236d8a3ED91e207e00A998286210DF79f74aA8b

内部交易

现在来解答一下刚刚的伏笔,明明是从合约账户往外转钱,为什么方向是 in,而且显示 0 ether (没有包含任何以太币)?但是账户余额是对的。

这个对外支付的交易在哪里?再次查看 ropsten 测试网的区块链浏览器页面,可以看到合约地址历史记录页面中出现了一个新的区域,名为 Internal Transactions

因为这一笔转账是由合约发起的,这类「把以太币从合约转出」的转账交易被称为内部交易(也称为消息)。在这笔内部交易中我们终于可以看到对外支付的对象和金额,金额为 0.0000000001 Ether。

内部交易详情页

再来捋一下整体的思路:

  1. Meta Mask 账户触发了合约,调用方法为 withdraw,参数为 “100000000” wei。所以交易历史方向为 in。
  2. 这个交易导致合约在 EVM 中被执行。
  3. 首先调用 require 检查参数大小。
  4. 调用 transfer 函数向合约触发方这个地址发送以太币。
  5. 运行 transfer 会生成一个「内部交易」,把 0.0000000001 Ether 保存到合约调用方的钱包中。

这些就是在 Etherscan 的「Internal Transactions」标签页面上所显示的消息。具体信息可以通过链接查看:https://ropsten.etherscan.io/tx/0x0d5b5352ff3c300fe3910e960728ed6694fff9d174e5d516b93c0318881f1fbb

0x03 总结

在本节课中,我们调用了一个带参数的合约函数。

还记得我说的吗,合约是用于控制以太币的程序,控制就是诸如转入、转出这种操作。通过两节课的学习已经完整的实践了一次转入、一次转出的流程。

以下知识点需要留下印象:

  • 以太坊内部所有的币值单位都应该使用 wei 来表示。
  • 把以太币从合约转出」的转账交易被称为内部交易,在 Etherscan 的「Internal Transactions」标签页面上可以查看。
  • require 是 Solidity 内置的函数,用于测试一个前提条件,若满足条件则调用 withdraw 函数,否则 require 函数将导致合约执行停止并因异常而失败。
  • 水龙头(Faucet)的基本概念

发送给合约的交易可以包括以太币或数据,也可以同时包括这两者。本节课中,就同时发送给合约以太币和数据,而不比起上节课仅仅是发送给合约以太币又前进了一小步,下次课我们将从使用 Solidity 输出 Hello World 开始,逐步深入到对链上数据的读写操作,最终系统地学习一遍 Solidity 的基本语法。