2024-02-18
Web3
00
请注意,本文编写于 277 天前,最后修改于 277 天前,其中某些信息可能已经过时。

目录

函数重载
库函数
Import
接收ETH
receive函数
回退函数fallback
receive和fallback的区别
发送ETH
接收ETH合约
发送ETH合约
调用其他合约
call()函数
delegatecall
在合约中创建新合约
创建新合约方式1 new
创建新合约方式2
删除合约 selfdestruct
ABI编码解码
选择器
try catch

solidity入门学习课程2,推荐去官方学习

函数重载

  • solidity中允许函数进行重载(overloading),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,solidity不允许修饰器(modifier)重载
  • 最终重载函数在经过编译器编译后,由于不同的参数类型,都变成了不同的函数选择器(selector)
  • 如果出现多个匹配的重载函数,则会报错。

库函数

库函数是一种特殊的合约,为了提升solidity代码的复用性和减少gas而存在。库合约一般都是好用的函数合集(库函数),由大神或者项目方创作。

它和普通合约的区别

  1. 不能存在状态变量
  2. 不能够继承或者被继承
  3. 不能接受以太币
  4. 不可以被销毁

使用示例

solidity
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; using Strings for uint256; contract UseLib { function getString1(uint256 _number) public pure returns(string memory) { return _number.toHexString(); } // 直接通过库合约名调用 function getString2(uint256 _number) public pure returns(string memory){ return Strings.toHexString(_number); } } library Strings { function toHexString(uint256 value) public pure returns (string memory) { // todo something return "0x00"; } }

一些常见的库合约

  1. String: 将uint256转换为String
  2. Address: 判断某个地址是否为合约地址
  3. Create2: 更安全的使用Create2 EVM opcode
  4. Arrays:跟数组相关的库函数

Import

import用法

  1. 通过源文件相对位置导入import './Yeye.sol';
  2. 通过源文件网址导入智能合约 import 'https://github.com/xxx/../Address.sol';
  3. 通过npm的目录导入 import '@xx/../Ownable.sol';
  4. 通过全局符号导入特定的合约 import {Yeye} from './Yeye.sol';
  5. 引用(import)在代码中的位置为:在声明版本号之后,在其余代码之前。

import 可以用 as 可以用 *

solidity
import {Yeye as Wowo} from "./Yeye.sol"; import * as Wowo from "./Yeye.sol";

Q: 被导入文件中的全局符号想要被其他合约单独导入,应该怎么编写?
A: 与合约并列在文件结构中

Q: import导入文件中的全局符号可以单独指定其中的:
A: 合约、纯函数、结构体类型

接收ETH

Solidity支持两种特殊的回调函数,receive()fallback(),他们主要在两种情况下被使用:

  1. 接收ETH
  2. 处理合约中不存在的函数调用(代理合约proxy contract)

注意: 在solidity0.6.x版本之前只有fallback()

receive函数

receive()函数的规则

  • receive()只用于处理接收ETH。
  • 一个合约最多只有一个receive函数
  • 声明:不需要function关键字: receive() external payable{ ... }
  • receive() 函数不能有任何的参数,不返回任何值,
  • 必须包含externalpayable

当合约接收ETH时receive函数被触发。

receive()函数的注意事项:

  • 最好不要执行太多的逻辑,因为如果别人用send和transfer方法发送ETH的话,gas会限制在2300, receive()太负载的话可能会触发Out of Gas报错;如果用call就可以自定义gas执行更复杂的逻辑。

由于receive函数没有返回值,可以通过事件的形式通知

solidity
// 定义事件 event Received(address Sender, uint Value); // 接收ETH时释放Received事件 receive() external payable { emit Received(msg.sender, msg.value); }

有些恶意合约,会在receive() 函数(老版本的话,就是 fallback()函数)嵌入恶意消耗gas的内容或者使得执行故意失败的代码,导致一些包含退款和转账逻辑的合约不能正常工作,因此写包含退款等逻辑的合约时候,一定要注意这种情况。

回退函数fallback

  • fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可用于代理合约proxy contract
  • fallback()函数的声明规则和receive()函数一致

fallback()也是使用事件的形式通知

solidity
event fallbackCalled(address Sender, uint Value, bytes Data); fallback() external payable{ emit fallbackCalled(msg.sender, msg.value, msg.data); }

receive和fallback的区别

txt
触发fallback() 还是 receive()? 接收ETH | msg.data是空? / \ 是 否 / \ receive()存在? fallback() / \ 是 否 / \ receive() fallback() receive()和payable fallback()均不存在的时候,向合约直接发送ETH将会报错(你仍可以通过带有payable的函数向合约发送ETH)

发送ETH

Solidity 有三种方法向其它合约发送ETH,他们是:transfer()send()call(),其中call()是推荐用法。

接收ETH合约

solidity
contract ReceiveETH { // 收到eth事件,记录amount和gas event Log(uint amount, uint gas); // receive方法,接收eth时被触发 receive() external payable{ emit Log(msg.value, gasleft()); } // 返回合约ETH余额 function getBalance() view public returns(uint) { return address(this).balance; } }

发送ETH合约

有三种方式向ReceiveETH合约发送ETH。首先先在发送ETH合约SendETH中实现payable和构造函数receive(),让我们能够在部署时和部署后向合约转账

solidity
contract SendETH { constructor() payable {} receive() external payable {} }

transfer

  • 接收方地址.transfer(发送ETH数额)
  • transfer()的gas限额时2300,足够用于转账,但对方合约的fallbak()receive()函数不能实现太复杂的逻辑。
  • transfer() 如果转账失败,会自动revert(回滚交易)
solidity
function transferETH(address payable _to, uint256 amount) external payable { _to.transfer(amount); }

send

  • 用法接收方地址.send(发送ETH数额)
  • gas限制也是2300,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑
  • send() 如果转账失败,不会revert
  • send() 的返回值时bool ,代表转账成功或失败,需要额外代码处理一下

call

  • call 用法是接收方地址.call{value: 发送ETH数额}("")
  • call()没有gas限制,可以支持对方合约fallback() 或receive()`函数实现复杂逻辑。
  • call()如果转账失败,不会revert
  • call()的返回值是(bool, data),其中bool代表着转账成功或失败,需要额外代码处理一下

总结:

solidity三种发送ETH的方法:transfersendcall

  • call没有gas限制,最为灵活,是最提倡的方法;
  • transfer有2300 gas限制,但是发送失败会自动revert交易,是次优选择;
  • send有2300 gas限制,而且发送失败不会自动revert交易,几乎没有人用它。

调用其他合约

solidity
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; contract OtherContract { uint256 private _x = 0; // 状态变量x // 收到eth事件,记录amount和gas event Log(uint amount, uint gas); // 返回合约ETH余额 function getBalance() view public returns(uint) { return address(this).balance; } // 可以调整状态变量_x的函数,并且可以往合约转ETH (payable) function setX(uint256 x) external payable{ _x = x; // 如果转入ETH,则释放Log事件 if(msg.value > 0){ emit Log(msg.value, gasleft()); } } // 读取x function getX() external view returns(uint x){ x = _x; } } contract CallContract{ function callSetX(address _Address, uint256 x) external{ OtherContract(_Address).setX(x); } // 调用合约方法1 function callGetX(OtherContract _Address) external view returns(uint x){ x = _Address.getX(); } // 调用合约方法2 function callGetX2(address _Address) external view returns(uint x){ OtherContract oc = OtherContract(_Address); x = oc.getX(); } // 调用合约方法 同时转账 function setXTransferETH(address otherContract, uint256 x) payable external{ OtherContract(otherContract).setX{value: msg.value}(x); } }

call()函数

call函数除了前面说的可以发送ETH,可以可以调用合约的任意方法

目标合约地址.call(二进制编码);

二进制编码利用结构化编码函数 abi.encodeWithSignature获得: abi.encodeWithSignature('函数签名', "逗号分割的具体参数")

call还可以调用不存在的函数,会触发合约的fallback函数

总结:

call这一低级函数可以用来调用其他合约。call不是调用合约的推荐方法,因为不安全。但他能让我们在不知道源代码和ABI的情况下调用目标合约,很有用。

call函数是address类型的函数。

delegatecall

delegatecallcall类似,是solidity中地址类型的低级成员函数。delegate是委托/代表的意思。

delegatecall语法和call类似也是目标合约地址.delegatecall(二进制编码)。其中二进制编码利用结构化编码函数abi.encodeWithSignature获得

abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)

call不同的是,delegatecall在调用合约时可以指定交易发送的gas,但不能指定发送的ETH数额

什么情况下会用到delegatecall ?

  1. 代理合约 将智能合约的存储合约和逻辑合约分开:代理合约存储所有相关的变量,并且保存逻辑合约的地址;所有函数存在逻辑合约中,通过delegatecall执行。当升级时,只需要将代理合约指向新的逻辑合约即可。
  2. 砖石: 钻石是一个支持构建可在生产中扩展的模块化智能合约系统的标准。钻石是具有多个实施合同的代理合同。 更多信息请查看:钻石标准简介

delegatecall的安全隐患问题
当前合约和目标合约的状态变量存储结构相同,并且目标合约安全

原文的案例说明delegatecallcall的另一个区别 语境不同 前者修改的本合约的状态变量,而后者修改的是代理的目标合约的对象

使用delegatecall时,要求 变量类型可以不同,变量名、声明顺序必须相同

solidity
function delegatecallMint(address _addr, uint _num) external payable{ (bool success, bytes memory data) = _addr.delegatecall(abi.encodeWithSignature("mint(uint256)", _num)); }

在代理合约中,存储所有相关的变量是 代理合约 存储所有函数的是 逻辑合约,同时 代理合约delegatecall逻辑合约

在合约中创建新合约

在以太坊上,用户(外部账户,EOA)可以创建智能合约,智能合约同样也可以创建新的智能和鱼。 去中心化交易所 uniswap就是利用工厂合约创造了无数的币对合约(Pair)。

创建新合约方式1 new

有两种方式可以在合约中创建新合约,newcreate2

Q: Contract x = new Contract{value: _value}(params),表达式中value代表什么?
A: 当前合约发送给新创建合约的Token

Q: Pair合约创建时的msg.sender是?
A: 工厂合约PairFactory

创建新合约方式2

create2 操作码

新地址 = hash("0xFF",创建者地址, salt, bytecode)

使用create2方式创建合约,我们可以在部署合约前确定合约地址,这也是一些layer2项目的基础

create2的实际应用场景

  1. 交易所为新用户预留创建钱包合约地址。
  2. 由create2驱动的factory合约,可以得到一个确定的pair地址,不需要在执行一次跨合约调用

删除合约 selfdestruct

  • selfdestruct命令可以用来删除智能合约,并将该合约剩余ETH转向指定地址
  • selfdestruct 是为应对合约出错的极端情况而设计的。

注意事项

  1. 对外提供合约销毁接口时,最好设置为只有合约所有者可以调用,可以使用函数修饰符`onlyOwner进行函数声明。
  2. 当合约被销毁后与智能合约的交互也能成功,并且返回0。
  3. 当合约中有selfdestruct功能时,常常会带来安全问题和信任问题

ABI编码解码

  • ABI是与以太坊智能合约交互的标准。数据基于他们的类型编码;并且由于编码后不包含类型信息,解码时需要注明它们的类型。

solidity中,ABI编码有4个函数

  • abi.encode 每个数据都填充32个字节
  • abi.encodePacked 给定参数根据所需最低空间编码
  • abi.encodeWithSignatureabi.encode 功能类似,只是第一个参数为函数签名
  • abi.encodeWithSelectorabi.encodeWithSignature类似,只不过第一个参数为函数选择器

ABI解码有一个函数 abi.decode,用于解码abi.encode的数据

编码规则

ABI的使用场景

  1. ABI常配合call来实现对合约的底层调用
  2. ethers.js中常用ABI实现合约的导入和函数调用
  3. 对不开开源合约进行反编译后,某些函数无法查到函数签名,可以通过ABI进行调用

Q: 如果对于某个哈希函数,我们统计大量不同字符串对应的哈希值(二进制串),发现其前 n 位全部为 0 的频率恰好约为 1/2^n,则我们认为该哈希函数具有良好的

A 均一性: 每个哈希值被取到的概率应该基本相等。

选择器

当我们调用智能合约时,本质上是向目标合约发送了一段calldata,在remix中发送一次交易后,可以在详细信息中,看见input 即为此次交易的calldata。

  • 发送calldata中前4个字节是selector(函数选择器)
  • calldata的后面32个字节是msg.datamsg.datasolidity中的一个全局变量,值为完整的calldata(调用函数时传入的数据)

method idselector函数签名之间的关系

  • method id 定义为 函数签名Keccak哈希后的前四个字节。即bytes4(keccak256("mint(address)"))
  • selectormethod id相匹配时,即表示调用该函数。
  • 函数签名 函数名(逗号分隔的参数类型)。对于不同的函数及函数重载问题,通过调用不同的函数签名,确定具体调用哪一个函数。

注意

在函数签名中,uintint要写成uint256int256

try catch

solidity0.6版本添加了try catch捕捉智能合约的异常

try catch 只能被用于 external函数或 创建合约时 constructor的调用。基本语法如下

solidity
try externalContract.f() { // call成功的情况下,运行一些代码 } catch { // call失败的情况下,运行一些代码 }

如果调用的函数有返回值

solidity
try externalContract.f() returns( returnType val){ // 在try模块中可以使用返回的变量 // 如果是创建合约,那么返回值是新创建的合约变量 } catch { }

另外合约支持捕获特殊的异常原因

solidity
try externalContract.f() returns(returnType) { // call成功的情况下 运行一些代码 } catch Error(string memory reason) { // 捕获失败的revert()和require() } catch (bytes memory reason) { // 捕获失败的assert() }

总结

  • try catch 只能用于处理外部合约调用和创建
  • 如果try执行成功,返回变量必须声明,并且与返回的变量类型相同。

本文作者:郭郭同学

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!