2024-02-18
Web3
00

目录

函数重载
库函数
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 许可协议。转载请注明出处!