# Solidity学习-交易、转账、合约

By [rbtree](https://paragraph.com/@rbtree) · 2022-04-30

---

1 交易transaction
===============

交易是一个以太坊账户发给另一个以太坊账户的指令，例如我给你转账1ETH，我mint某个NFT，这些都是交易。交易是以太坊上非常重要的概念，实际上**以太坊就是一个交易驱动的状态机**，我们发出并被成功执行的每一个交易都是在改变以太坊的状态。

![](https://storage.googleapis.com/papyrus_images/cf94bc949f0632419fb2ec89bf03a5934768ddf0584c5bd271903ef1df1c60b4.png)

那么一个交易包含哪些内容呢？

*   recipient：交易的接收地址。可以是外部账户地址，例如我给你转账，这里就会写上你的地址；也可以是合约账户地址，例如我mint某个NFT，这里就需要填写NFT的合约地址。
    
*   signature：发送者的签名，确切地说是发送者的私钥对交易内容的签名。该内容保证以太坊的安全性，如果有人想要模仿你发起一笔交易，ta可以伪造交易中的其他任何内容，但唯独无法生成签名。从这里大家也可以看出，为什么在不可信的网站随意用钱包签名是一件危险的事情，虽然签名这一操作并不会上链，但攻击者可以利用你的签名内容来发起交易。
    
*   value：发送给接收者的ETH，单位是WEI，1 ETH = 10e18 WEI
    
*   data（payload）：二进制数据，本项是可选的，主要在跟合约账户交互的时候使用，后面会详述。
    
*   gasLimit：交易的最大gas限制，若交易消耗的gas超过该限制，整个交易失败回滚。
    
*   maxPriorityFeePerGas：给矿工的小费。
    
*   maxFeePerGas：baseFeePerGas + maxPriorityFeePerGas的最大限制。其中baseFeePerGas由以太坊网络的拥堵状况决定，越拥堵就会越贵。
    
*   nonce：交易序号，当你创建了一个以太坊账户，你发起的第一笔交易nonce是0，第二笔交易nonce是1，以此类推。这个字段是用来保障交易顺序的，如果nonce为0的交易没有被处理，nonce为1的交易是不会被处理的。这个序号也可以用来防范攻击者进行重放攻击。此外，矿工挖矿的时候，也有一个叫nonce的东西，矿工需要找到一个能让区块hash符合挖矿难度要求的数字，挖矿的nonce和这里的nonce并不是一个东西。
    

在小狐狸钱包里，我们可以对以上内容进行修改：

**recipient，value，data（Hex Data）**

![](https://storage.googleapis.com/papyrus_images/7206a9ecdfba55c90986481fc8739b06c76bbc9671b77a1bf9e9fe152fb40c07.png)

**maxFeePerGas、maxPriorityFeePerGas、gasLimit**

（我不明白为什么小狐狸把那一项标注成Max base fee，实测可以发现那一项并不是最大的base，而是最大的base+priority）

![](https://storage.googleapis.com/papyrus_images/c9ffff25dcd6eb83ebda31a29cd54a4b9594e8272d9240ca62d63d91ddebccf3.png)

**nonce**

一般来说，我们不必主动调节nonce，但有一种情况是我们想取消某个交易。如果你的交易请求已经广播出去了，这个时候即使它还没有上链，也是没有办法取消的，只能用一个相同nonce的交易去覆盖，新交易的maxPriorityFeePerGas要比旧交易高10%以上，一般我们会用一笔给自己转账的交易来覆盖。当然如果你恰好有新的一笔交易要执行，你也可以直接发起新的交易，然后手动把nonce减1，确保priority比之前高10%。

![](https://storage.googleapis.com/papyrus_images/ee609b16bbd9a30498a62f8aedd620d2472fee39642a5864f88b19155a5b5bb3.png)

交易的类型
-----

*   **常规交易**：一个外部账户到另一个外部账户的交易，此时recipient是一个外部账户地址，例如我给你转0.1ETH。
    
*   **合约执行**：一个外部账户到一个合约账户的交易，此时recipient是一个合约账户地址，例如我mint一个NFT。
    
*   **合约部署**：当交易中不包含recipient字段或者该字段为null时，说明该交易的目的是在区块链上部署一个新的合约。
    

关于外部账户和合约账户，其实单看地址数字本身是无法区分这两种账户的，二者也都可以拥有资产（eth、token等）。二者之间最大的区别是，合约账户包含可执行代码，它并不像合约账户那样由私钥控制，而是由代码控制。合约账户地址也不是由公私钥对生成的，而是根据它部署者的地址和部署时那笔交易的nonce生成的。

只有外部账户可以主动发起交易，合约账户不可以主动发起交易。合约账户的代码是可以随意编写的，它当然也可以与其他合约账户或外部账户交互，这叫内部交易（internal transaction），内部交易并不是真正的交易，因为它不可以独立存在，必须由某个外部账户先执行合约，才有可能触发内部交易。

2 转账transfer
============

ETH转账
-----

单纯的ETH转账是以太坊上最简单的交易，当一笔交易不携带data的时候，它就是一个简单的ETH转账，转账的数额就是交易携带的value。

需要注意的是，既可以给外部账户转账，也可以给合约账户转账。虽然二者的data字段都为空，但给合约账户转账的时候，会自动执行合约账户的receive函数，如果合约账户没有receive函数，会执行fallback函数。此时要求fallback必须是payable的，如果fallback不是payable的或者合约没有定义fallback，转账会失败。可见，给合约账户转账是可能失败的，但是给外部账户转账一定会成功，即使这个外部账户不存在（不存在指某个地址在区块链历史记录里从未出现过，其实可以认为不存在的地址被默认为外部账户）。

与其把转账看成一种单独的类型，倒不如把它看成一种交易的特例，即不携带data的交易。而携带data的交易本身也是具备转账能力的，因为任何一个交易都有value字段。当我们不想给接收方账户转账ETH的时候，只不过是把value字段的值设置成了0而已。

ERC20代币转账
---------

在小狐狸钱包里，我们在转账的时候可以选择ETH或者任何一种代币，看起来似乎是一样的，但实际上ETH的转账和ERC20代币的转账完完全全是两码事。

![](https://storage.googleapis.com/papyrus_images/1373937db7b38efffd52891fa1ac1f03fa3bfbc1e62a7156fd8de749e12071bc.png)

ETH转账时，recipient填写的是接收方地址，data是空，value是转账的数额；ERC20代币转账时，recipient则需要填写代币的合约地址，value是0（因为此时你并不需要向合约转ETH），接收方地址和转账数额则是通过代币合约的transfer函数参数传入的。

transfer函数是ERC20代币标准要求必须实现的函数。你可以创建自己的代币，不实现transfer功能，不过这样的话它就不能称作ERC20代币了。

[https://ethereum.org/en/developers/docs/standards/tokens/erc-20/](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/)

    function transfer(address _to, uint256 _value) public returns (bool success)
    

ETH转账是以太坊内置的功能，每个账户的ETH余额也不需要我们费心维护。而ERC20代币转账则是需要合约开发者自行实现的，一般来说，合约内部会维护一个mapping，记录所有代币持有者的持有量，在transfer的时候调整账户余额。这一点给了代币设计者很大的自由度，例如我想创建一个转账就会导致通缩的代币，我就可以在调整账户余额的时候，只给接收者余额增加发送者支付额度的90%。

**ETH转账的data**

![](https://storage.googleapis.com/papyrus_images/228ea46f4d75cd20c43b8b4438a6d3e106bc9829dd0117f5f89e08827ad71ed7.png)

**ERC20代币转账的data**

![](https://storage.googleapis.com/papyrus_images/8d154d1130ce0ffefc4096bde48b3f22e34e07ded6aee280421a92921635b6ba.png)

3 合约交互
======

外部账户去调用合约账户的函数。此时，data字段就派上用场了。（刚才说的ERC20代币转账就是合约交互的一个例子）

我们可以在etherscan上找一个合约交互的例子，然后点开它的data数据

[https://etherscan.io/address/0xf13b8d8b5a86f770b662cf5bf8690db9492bbe6b](https://etherscan.io/address/0xf13b8d8b5a86f770b662cf5bf8690db9492bbe6b)

![](https://storage.googleapis.com/papyrus_images/66cfd46aa9c54602b04c01f7258c76ef78b15082fb1dcd143dc42ab7380dcc54.png)

etherscan具有一定的解析能力，可以看出这段input data其实是在调用合约的mint函数。

![](https://storage.googleapis.com/papyrus_images/5a509f30a99a41969f1f5417fed6f8f49cd6fabf345611e3fb20ef834157de6d.png)

实际上，我们可以在小狐狸钱包里面点转账，receive填写合约地址，然后在hex data里面填写上面那串二进制代码。和从网页触发的合约交互是一样的效果（当然要保证value足够）

![](https://storage.googleapis.com/papyrus_images/52b30b53723985365b657123df4b7e4afee2edb43664919ecb8b98743f09e92c.png)

**data字段**
----------

data字段的前4个字节是函数名和函数参数的hash，合约执行的时候，正是据此判断调用者想要调用合约里的哪个函数。

钱包可能会存储一些比较常用的函数名对应的hash，所以有些交易中，钱包可以帮我们识别出正在调用合约的哪个函数。如果没有存，就只会像上面的图中那样，只提示一个contract interaction。

我们在小狐狸钱包里面，试着把上面例子里面data的前四个字节改成0xab834bab，可以发现此函数被识别出来了-ATOMIC MATCH\_。当然，识别出来和能正确运行是两码事，因为这个是我瞎改的，recipient合约里并没有这个函数，肯定会执行失败。

![](https://storage.googleapis.com/papyrus_images/83d8cb990446c9b7b61aa8090c470b2faa0c72282761ef62384fbb013b387398.png)

data字段的后续内容为函数参数，是按照特定标准编码的，这里不再详述，有兴趣自行查看[https://docs.soliditylang.org/en/latest/abi-spec.html#formal-specification-of-the-encoding](https://docs.soliditylang.org/en/latest/abi-spec.html#formal-specification-of-the-encoding)

etherscan里也可以解析，例如上面的例子，前两个参数分别为2和0，后面两个参数为空

![](https://storage.googleapis.com/papyrus_images/007506178981fb8c2930800942ab75cd000c3c900497491a1862b50846625489.png)

有些复杂的交易etherscan解析不出来，也可以去这个网站 [https://ethtx.info/](https://ethtx.info/)

合约执行
----

从上面的描述可以发现，data数据里面并没有写明要调用合约的哪个函数，只是有一个hash，那么代码要如何执行呢？如果有熟悉C语言的朋友，可以考虑一下C语言的函数调用和这里有什么不一样。C语言的函数调用，需要已知函数的内存地址，而在区块链的世界里，肯定不能这样做，因为每个矿工的本地内存地址都不会一样的。在区块链的世界里，实际上使用哈希指针取代了内存指针，在执行的时候，需要遍历合约的所有函数hash，如果找到和data的前4个字节相同的，则开始执行相应的函数代码。

我们依然使用上面的合约作为例子

[https://etherscan.io/address/0xf13b8d8b5a86f770b662cf5bf8690db9492bbe6b#code](https://etherscan.io/address/0xf13b8d8b5a86f770b662cf5bf8690db9492bbe6b#code)

我们可以找到合约的部署code，一般以60806040开头

![](https://storage.googleapis.com/papyrus_images/65654e171e9e34f06f78c9268678de7e06b5642c1749d706cebb58f13c6711ee.png)

我们找到第二个60806040，然后复制到末尾，这才是最后部署到链上的代码（具体原因下一节再说明）。

![](https://storage.googleapis.com/papyrus_images/1ef6b4313e58fccb07c1c254cb41f80ac56ba750413c0795f8294433943bc32e.png)

把内容粘贴到这个在线反汇编工具上 [https://ethervm.io/decompile](https://ethervm.io/decompile) 进行decompose

可以看到下面的内容：

![](https://storage.googleapis.com/papyrus_images/7cb95cd142fdf906d8fc335cec59457118ea06fd12c2f8a96a157647f6f0bdea.png)

居然出现了一个main函数！如果你去看上面合约的源代码，会发现源代码里并不存在main函数，这个函数是自动生成的。这个函数的一个重要功能就是根据input前4字节的hash去寻找函数。上面的代码中，var0就是前4个字节的内容。下面就是用if条件语句，去和合约里的所有函数的hash进行比较，命中之后就会执行相应的函数。

![](https://storage.googleapis.com/papyrus_images/7552af356a96c8c41c30b979a96512044817fdaf20af42fb01abf8589b30b0d3.png)

所以实际上，部署在区块链上的合约是会有一个main入口函数的，所有和合约的交互都是从这个main函数开始的。

4 合约部署
======

上面复制合约code的时候，并没有从开头开始，而是从第二个60806040开始的，这是为什么呢？

之前说过，合约部署也是一个交易，它是recipient为空的特殊交易，它有自己的data。我们刚才在etherscan上看到的creation code实际上就是部署的时候的data。

可以在这里查证，[https://etherscan.io/tx/0x1bcabc8bd59af966a55591b396e8f620deb9f2dcfdd366e873811b47dbe7edbc](https://etherscan.io/tx/0x1bcabc8bd59af966a55591b396e8f620deb9f2dcfdd366e873811b47dbe7edbc)

那么这段data会只包含我们想要部署的合约内容吗？当然不是，这段data是我们想要部署的code的生成器，它的返回值才是我们想要部署的内容。这段data会执行我们想要部署的合约的构造函数，然后返回我们真正想要部署的code，这些code会真正被写入区块链。

合约中并非所有内容都会被部署到链上，只在部署时需要被调用的构造函数并不会上链，只会被构造函数调用的内部函数也不会上链。

**参考资料**

\[1\][https://ethereum.org/en/developers/docs/transactions/](https://ethereum.org/en/developers/docs/transactions/)

\[2\][https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html](https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html?highlight=message%20call#index-8)

\[3\][https://docs.soliditylang.org/en/latest/contracts.html#creating-contracts](https://docs.soliditylang.org/en/latest/contracts.html#creating-contracts)

---

*Originally published on [rbtree](https://paragraph.com/@rbtree/solidity)*
