solidity入门学习课程2,推荐去官方学习
库函数是一种特殊的合约,为了提升solidity代码的复用性和减少gas而存在。库合约一般都是好用的函数合集(库函数),由大神或者项目方创作。
它和普通合约的区别
使用示例
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"; } }
一些常见的库合约
import用法
import './Yeye.sol';
import 'https://github.com/xxx/../Address.sol';
import '@xx/../Ownable.sol';
import {Yeye} from './Yeye.sol';
import
)在代码中的位置为:在声明版本号之后,在其余代码之前。import
可以用 as
可以用 *
solidityimport {Yeye as Wowo} from "./Yeye.sol"; import * as Wowo from "./Yeye.sol";
Q: 被导入文件中的全局符号想要被其他合约单独导入,应该怎么编写?
A: 与合约并列在文件结构中
Q: import
导入文件中的全局符号可以单独指定其中的:
A: 合约、纯函数、结构体类型
Solidity
支持两种特殊的回调函数,receive()
和fallback()
,他们主要在两种情况下被使用:
ETH
注意: 在solidity
0.6.x版本之前只有fallback()
receive()
函数的规则
function
关键字: receive() external payable{ ... }
receive()
函数不能有任何的参数,不返回任何值,external
和payable
。当合约接收ETH时receive函数被触发。
receive()
函数的注意事项:
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()
函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可用于代理合约proxy contract
。fallback()
函数的声明规则和receive()
函数一致fallback()
也是使用事件的形式通知
solidityevent fallbackCalled(address Sender, uint Value, bytes Data); fallback() external payable{ emit fallbackCalled(msg.sender, msg.value, msg.data); }
txt触发fallback() 还是 receive()? 接收ETH | msg.data是空? / \ 是 否 / \ receive()存在? fallback() / \ 是 否 / \ receive() fallback() receive()和payable fallback()均不存在的时候,向合约直接发送ETH将会报错(你仍可以通过带有payable的函数向合约发送ETH)
Solidity
有三种方法向其它合约发送ETH
,他们是:transfer()
,send()
和call()
,其中call()
是推荐用法。
soliditycontract 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; } }
有三种方式向ReceiveETH合约发送ETH。首先先在发送ETH合约SendETH中实现payable和构造函数receive(),让我们能够在部署时和部署后向合约转账
soliditycontract SendETH { constructor() payable {} receive() external payable {} }
transfer
接收方地址.transfer(发送ETH数额)
。transfer()
的gas限额时2300,足够用于转账,但对方合约的fallbak()
或receive()
函数不能实现太复杂的逻辑。transfer()
如果转账失败,会自动revert
(回滚交易)solidityfunction transferETH(address payable _to, uint256 amount) external payable { _to.transfer(amount); }
send
接收方地址.send(发送ETH数额)
send()
如果转账失败,不会revertsend()
的返回值时bool ,代表转账成功或失败,需要额外代码处理一下call
call
用法是接收方地址.call{value: 发送ETH数额}("")
。call()
没有gas
限制,可以支持对方合约fallback() 或
receive()`函数实现复杂逻辑。call()
如果转账失败,不会revert
。call()
的返回值是(bool, data)
,其中bool
代表着转账成功或失败,需要额外代码处理一下总结:
solidity
三种发送ETH的方法:transfer
,send
和call
。
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函数除了前面说的可以发送ETH,可以可以调用合约的任意方法
目标合约地址.call(二进制编码);
二进制编码
利用结构化编码函数 abi.encodeWithSignature
获得:
abi.encodeWithSignature('函数签名', "逗号分割的具体参数")
call还可以调用不存在的函数,会触发合约的fallback
函数
总结:
call
这一低级函数可以用来调用其他合约。call
不是调用合约的推荐方法,因为不安全。但他能让我们在不知道源代码和ABI
的情况下调用目标合约,很有用。
call函数是address类型的函数。
delegatecall
与call
类似,是solidity
中地址类型的低级成员函数。delegate
是委托/代表的意思。
delegatecall
语法和call
类似也是目标合约地址.delegatecall(二进制编码)
。其中二进制编码
利用结构化编码函数abi.encodeWithSignature
获得
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
。
和call
不同的是,delegatecall
在调用合约时可以指定交易发送的gas,但不能指定发送的ETH数额
什么情况下会用到delegatecall
?
delegatecall的安全隐患问题
当前合约和目标合约的状态变量存储结构相同,并且目标合约安全
原文的案例说明delegatecall
和call
的另一个区别 语境不同
前者修改的本合约的状态变量,而后者修改的是代理的目标合约的对象
使用delegatecall时,要求 变量类型可以不同,变量名、声明顺序必须相同
solidityfunction delegatecallMint(address _addr, uint _num) external payable{ (bool success, bytes memory data) = _addr.delegatecall(abi.encodeWithSignature("mint(uint256)", _num)); }
在代理合约中,存储所有相关的变量是 代理合约 存储所有函数的是 逻辑合约,同时 代理合约delegatecall逻辑合约
在以太坊上,用户(外部账户,EOA)可以创建智能合约,智能合约同样也可以创建新的智能和鱼。 去中心化交易所 uniswap
就是利用工厂合约创造了无数的币对合约(Pair
)。
有两种方式可以在合约中创建新合约,new
和create2
Q: Contract x = new Contract{value: _value}(params)
,表达式中value
代表什么?
A: 当前合约发送给新创建合约的Token
Q: Pair
合约创建时的msg.sender
是?
A: 工厂合约PairFactory
create2 操作码
新地址 = hash("0xFF",创建者地址, salt, bytecode)
使用create2方式创建合约,我们可以在部署合约前确定合约地址,这也是一些layer2项目的基础
create2的实际应用场景
selfdestruct
命令可以用来删除智能合约,并将该合约剩余ETH转向指定地址selfdestruct
是为应对合约出错的极端情况而设计的。注意事项
在solidity
中,ABI编码有4个函数
abi.encode
每个数据都填充32个字节abi.encodePacked
给定参数根据所需最低空间编码abi.encodeWithSignature
与abi.encode
功能类似,只是第一个参数为函数签名abi.encodeWithSelector
与abi.encodeWithSignature
类似,只不过第一个参数为函数选择器ABI解码有一个函数 abi.decode
,用于解码abi.encode
的数据
ABI的使用场景
Q: 如果对于某个哈希函数,我们统计大量不同字符串对应的哈希值(二进制串),发现其前 n 位全部为 0 的频率恰好约为 1/2^n,则我们认为该哈希函数具有良好的
A 均一性: 每个哈希值被取到的概率应该基本相等。
当我们调用智能合约时,本质上是向目标合约发送了一段calldata
,在remix中发送一次交易后,可以在详细信息中,看见input 即为此次交易的calldata。
calldata
中前4个字节是selector
(函数选择器)calldata
的后面32个字节是msg.data
,msg.data
是solidity
中的一个全局变量,值为完整的calldata(调用函数时传入的数据)method id
、selector
和 函数签名
之间的关系
method id
定义为 函数签名
的 Keccak
哈希后的前四个字节。即bytes4(keccak256("mint(address)"))
selector
与method id
相匹配时,即表示调用该函数。函数名(逗号分隔的参数类型)
。对于不同的函数及函数重载问题,通过调用不同的函数签名,确定具体调用哪一个函数。注意
在函数签名中,uint
和int
要写成uint256
和int256
solidity0.6版本添加了try catch
捕捉智能合约的异常
try catch 只能被用于 external函数或 创建合约时 constructor的调用。基本语法如下
soliditytry externalContract.f() { // call成功的情况下,运行一些代码 } catch { // call失败的情况下,运行一些代码 }
如果调用的函数有返回值
soliditytry externalContract.f() returns( returnType val){ // 在try模块中可以使用返回的变量 // 如果是创建合约,那么返回值是新创建的合约变量 } catch { }
另外合约支持捕获特殊的异常原因
soliditytry externalContract.f() returns(returnType) { // call成功的情况下 运行一些代码 } catch Error(string memory reason) { // 捕获失败的revert()和require() } catch (bytes memory reason) { // 捕获失败的assert() }
总结
try catch
只能用于处理外部合约调用和创建本文作者:郭郭同学
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!