# Solidity学习-交易、转账、合约 **Published by:** [rbtree](https://paragraph.com/@rbtree/) **Published on:** 2022-04-30 **URL:** https://paragraph.com/@rbtree/solidity ## Content 1 交易transaction交易是一个以太坊账户发给另一个以太坊账户的指令,例如我给你转账1ETH,我mint某个NFT,这些都是交易。交易是以太坊上非常重要的概念,实际上以太坊就是一个交易驱动的状态机,我们发出并被成功执行的每一个交易都是在改变以太坊的状态。那么一个交易包含哪些内容呢?recipient:交易的接收地址。可以是外部账户地址,例如我给你转账,这里就会写上你的地址;也可以是合约账户地址,例如我mint某个NFT,这里就需要填写NFT的合约地址。signature:发送者的签名,确切地说是发送者的私钥对交易内容的签名。该内容保证以太坊的安全性,如果有人想要模仿你发起一笔交易,ta可以伪造交易中的其他任何内容,但唯独无法生成签名。从这里大家也可以看出,为什么在不可信的网站随意用钱包签名是一件危险的事情,虽然签名这一操作并不会上链,但攻击者可以利用你的签名内容来发起交易。value:发送给接收者的ETH,单位是WEI,1 ETH = 10e18 WEIdata(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)maxFeePerGas、maxPriorityFeePerGas、gasLimit (我不明白为什么小狐狸把那一项标注成Max base fee,实测可以发现那一项并不是最大的base,而是最大的base+priority)nonce 一般来说,我们不必主动调节nonce,但有一种情况是我们想取消某个交易。如果你的交易请求已经广播出去了,这个时候即使它还没有上链,也是没有办法取消的,只能用一个相同nonce的交易去覆盖,新交易的maxPriorityFeePerGas要比旧交易高10%以上,一般我们会用一笔给自己转账的交易来覆盖。当然如果你恰好有新的一笔交易要执行,你也可以直接发起新的交易,然后手动把nonce减1,确保priority比之前高10%。交易的类型常规交易:一个外部账户到另一个外部账户的交易,此时recipient是一个外部账户地址,例如我给你转0.1ETH。合约执行:一个外部账户到一个合约账户的交易,此时recipient是一个合约账户地址,例如我mint一个NFT。合约部署:当交易中不包含recipient字段或者该字段为null时,说明该交易的目的是在区块链上部署一个新的合约。关于外部账户和合约账户,其实单看地址数字本身是无法区分这两种账户的,二者也都可以拥有资产(eth、token等)。二者之间最大的区别是,合约账户包含可执行代码,它并不像合约账户那样由私钥控制,而是由代码控制。合约账户地址也不是由公私钥对生成的,而是根据它部署者的地址和部署时那笔交易的nonce生成的。 只有外部账户可以主动发起交易,合约账户不可以主动发起交易。合约账户的代码是可以随意编写的,它当然也可以与其他合约账户或外部账户交互,这叫内部交易(internal transaction),内部交易并不是真正的交易,因为它不可以独立存在,必须由某个外部账户先执行合约,才有可能触发内部交易。2 转账transferETH转账单纯的ETH转账是以太坊上最简单的交易,当一笔交易不携带data的时候,它就是一个简单的ETH转账,转账的数额就是交易携带的value。 需要注意的是,既可以给外部账户转账,也可以给合约账户转账。虽然二者的data字段都为空,但给合约账户转账的时候,会自动执行合约账户的receive函数,如果合约账户没有receive函数,会执行fallback函数。此时要求fallback必须是payable的,如果fallback不是payable的或者合约没有定义fallback,转账会失败。可见,给合约账户转账是可能失败的,但是给外部账户转账一定会成功,即使这个外部账户不存在(不存在指某个地址在区块链历史记录里从未出现过,其实可以认为不存在的地址被默认为外部账户)。 与其把转账看成一种单独的类型,倒不如把它看成一种交易的特例,即不携带data的交易。而携带data的交易本身也是具备转账能力的,因为任何一个交易都有value字段。当我们不想给接收方账户转账ETH的时候,只不过是把value字段的值设置成了0而已。ERC20代币转账在小狐狸钱包里,我们在转账的时候可以选择ETH或者任何一种代币,看起来似乎是一样的,但实际上ETH的转账和ERC20代币的转账完完全全是两码事。ETH转账时,recipient填写的是接收方地址,data是空,value是转账的数额;ERC20代币转账时,recipient则需要填写代币的合约地址,value是0(因为此时你并不需要向合约转ETH),接收方地址和转账数额则是通过代币合约的transfer函数参数传入的。 transfer函数是ERC20代币标准要求必须实现的函数。你可以创建自己的代币,不实现transfer功能,不过这样的话它就不能称作ERC20代币了。 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转账的dataERC20代币转账的data3 合约交互外部账户去调用合约账户的函数。此时,data字段就派上用场了。(刚才说的ERC20代币转账就是合约交互的一个例子) 我们可以在etherscan上找一个合约交互的例子,然后点开它的data数据 https://etherscan.io/address/0xf13b8d8b5a86f770b662cf5bf8690db9492bbe6betherscan具有一定的解析能力,可以看出这段input data其实是在调用合约的mint函数。实际上,我们可以在小狐狸钱包里面点转账,receive填写合约地址,然后在hex data里面填写上面那串二进制代码。和从网页触发的合约交互是一样的效果(当然要保证value足够)data字段data字段的前4个字节是函数名和函数参数的hash,合约执行的时候,正是据此判断调用者想要调用合约里的哪个函数。 钱包可能会存储一些比较常用的函数名对应的hash,所以有些交易中,钱包可以帮我们识别出正在调用合约的哪个函数。如果没有存,就只会像上面的图中那样,只提示一个contract interaction。 我们在小狐狸钱包里面,试着把上面例子里面data的前四个字节改成0xab834bab,可以发现此函数被识别出来了-ATOMIC MATCH_。当然,识别出来和能正确运行是两码事,因为这个是我瞎改的,recipient合约里并没有这个函数,肯定会执行失败。data字段的后续内容为函数参数,是按照特定标准编码的,这里不再详述,有兴趣自行查看https://docs.soliditylang.org/en/latest/abi-spec.html#formal-specification-of-the-encoding etherscan里也可以解析,例如上面的例子,前两个参数分别为2和0,后面两个参数为空有些复杂的交易etherscan解析不出来,也可以去这个网站 https://ethtx.info/合约执行从上面的描述可以发现,data数据里面并没有写明要调用合约的哪个函数,只是有一个hash,那么代码要如何执行呢?如果有熟悉C语言的朋友,可以考虑一下C语言的函数调用和这里有什么不一样。C语言的函数调用,需要已知函数的内存地址,而在区块链的世界里,肯定不能这样做,因为每个矿工的本地内存地址都不会一样的。在区块链的世界里,实际上使用哈希指针取代了内存指针,在执行的时候,需要遍历合约的所有函数hash,如果找到和data的前4个字节相同的,则开始执行相应的函数代码。 我们依然使用上面的合约作为例子 https://etherscan.io/address/0xf13b8d8b5a86f770b662cf5bf8690db9492bbe6b#code 我们可以找到合约的部署code,一般以60806040开头我们找到第二个60806040,然后复制到末尾,这才是最后部署到链上的代码(具体原因下一节再说明)。把内容粘贴到这个在线反汇编工具上 https://ethervm.io/decompile 进行decompose 可以看到下面的内容:居然出现了一个main函数!如果你去看上面合约的源代码,会发现源代码里并不存在main函数,这个函数是自动生成的。这个函数的一个重要功能就是根据input前4字节的hash去寻找函数。上面的代码中,var0就是前4个字节的内容。下面就是用if条件语句,去和合约里的所有函数的hash进行比较,命中之后就会执行相应的函数。所以实际上,部署在区块链上的合约是会有一个main入口函数的,所有和合约的交互都是从这个main函数开始的。4 合约部署上面复制合约code的时候,并没有从开头开始,而是从第二个60806040开始的,这是为什么呢? 之前说过,合约部署也是一个交易,它是recipient为空的特殊交易,它有自己的data。我们刚才在etherscan上看到的creation code实际上就是部署的时候的data。 可以在这里查证,https://etherscan.io/tx/0x1bcabc8bd59af966a55591b396e8f620deb9f2dcfdd366e873811b47dbe7edbc 那么这段data会只包含我们想要部署的合约内容吗?当然不是,这段data是我们想要部署的code的生成器,它的返回值才是我们想要部署的内容。这段data会执行我们想要部署的合约的构造函数,然后返回我们真正想要部署的code,这些code会真正被写入区块链。 合约中并非所有内容都会被部署到链上,只在部署时需要被调用的构造函数并不会上链,只会被构造函数调用的内部函数也不会上链。 参考资料 [1]https://ethereum.org/en/developers/docs/transactions/ [2]https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html [3]https://docs.soliditylang.org/en/latest/contracts.html#creating-contracts ## Publication Information - [rbtree](https://paragraph.com/@rbtree/): Publication homepage - [All Posts](https://paragraph.com/@rbtree/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@rbtree): Subscribe to updates