Solidity 8.0 中阶-常见用法
修饰关键词
可视范围
修饰符可以用在修饰 函数 或 状态变量 上
修饰符 | 内部可见 | 继承合约可见 | 外部可见 |
---|---|---|---|
private | Yes |
|
|
| internal | Yes | Yes |
|
| public | Yes | Yes | Yes |
| external | | | Yes |
不可变量
**immutable**
** **创建合约的时候不知道,创建用户地址(想要设置位常量)只定义一次后面就会成为常量, 作用就是给常量赋值(低Gas费)
// 可以节省 gas 费用
address public immutable owner = msg.sender;
回退函数
:::infofallback
和 receive
同时存在的情况下, 没有发送 msg.value
数据时,优先调用 receive
:::
函数 | 接受 ETH 主币 | 接受数据 |
---|---|---|
fallback | yes | yes |
receive | yes | no |
触发回退函数:
- 调用合约中不存在的函数
- 向合约中发送主币
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
/*
1. 调用函数在合约中不存在
2. 向合约中发送主币的时候
*/
contract Fallback{
event Log(string func, address sender, uint value, bytes data);
// 写法1: 【外部可见】【可以接受主币发送】
fallback() external payable{
emit Log("fallback", msg.sender, msg.value, msg.data);
}
// 写法2:只接受主币的回退方法 【receive 不接受 msg.data】只接受主币
receive() external payable{
emit Log("receive", msg.sender, msg.value, "");
}
}
自毁合约
selfdestruct
自毁
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
/*
selfdestruct
- 删除合约
- 强制发送主币到指定地址
*/
contract Kill{
constructor() payable{}
function kill() external {
selfdestruct(payable(msg.sender));
}
}
支付 ETH
payable
关键词,加上之后可以支付主币
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
contract VisibiltyBase {
// 地址变量标记 payable 后可以发送主币
address payable public owner;
constructor() {
owner = payable(msg.sender); // 必须要给 msg.sender 也配上 payable 属性
}
// payable 标记后,函数可以接收 ETH 主币的传入
function desposit() external payable{}
// 测试函数,显示合约余额
function getBalance() external view returns(uint){
return address(this).balance; // 获取当前地址余额
}
}
发送 ETH
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
/*
1. transfer (2300gas) 失败会返回 reverts (如果 gas 消耗完也会异常)
2. send (2300gas) 返回 bool
3. call 发送所有 gas ,返回 bool 和 data (data可以是合约的返回值)
*/
contract SendETH{
constructor() payable {}
receive() external payable {} // 生成合约的时候创建 eth
function sendTransfer(address payable _to) external payable {
_to.transfer(123);
}
function sendSend(address payable _to) external payable {
bool res = _to.send(123);
require(res, "send failed!!");
}
function sendCall(address payable _to) external payable{
(bool res, bytes memory data) = _to.call{value: 123}("");
require(res, "Call failed!!");
}
}
// 测试合约:接收主币发送的目标地址
contract EthReceiver {
event Log(uint amount, uint gas);
receive() external payable {
emit Log(msg.value, gasleft());
}
}
存储位置
返回值或者参数是数组、字符串、结构体,就必须指定存储位置
storage
存储在链上(状态变量)memory
存在函数中局部变量calldata
类似memory,但只能是参数
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
/*
数据存储位置
存在 storage 是状态变量
存在 memory 内存是局部变量
存在 calldata 和内存相似,但只能是参数
*/
contract Test {
// 结构体
struct MyStruct {
uint foo;
string text;
}
mapping (address => MyStruct) public myStructs;
// storage
function examples() external {
myStructs[msg.sender] = MyStruct({foo: 123, text: "bar"});
MyStruct storage myStruct = myStructs[msg.sender]; // 可以进行读取写入操作
myStruct.text = "foo"; // 要设置状态变量就不能用存储在内存上
}
// memory
// 返回值或者参数是数组、字符串、结构体,就必须指定存储位置
function examples(uint[] memory y, string memory s) external returns(uint[] memory){
uint[] memory memArr = new uint [](3); // 定义数组并指定长度 3(局部内存变量必须是定长数组)
return memArr;
}
// calldata 可以节约 gas
function examples2(uint[] calldata y, string calldata s) external returns(uint[] memory){
uint[] memory memArr = new uint [](3); // 定义数组并指定长度 3(局部内存变量必须是定长数组)
testCalldata(y);
return memArr;
}
function testCalldata(uint[] calldata t) private {
// code
}
}
事件 Event
当前合约中有任何一个状态变量被改变,都应该汇报一个事件出来,这个事件就可以在区块链的浏览器上看到, 当然也可以被 web3 或其他程序监听到,用于记录信息
:::info
通常事件命名以大写字母开头
加上 indexed 可以顺带输出操作地址 msg.sender
:::
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
contract TinyAuction {
event Store(uint256 num); // 事件 (定义一个商店的事件)
event IndexedLog(address indexed sender, uint val); // 加入索引,记录是哪个地址在操作
event Message(address indexed _from, address indexed _to, string message);
function store(uint256 num) public { // 这里算上上面一个例子的装饰器
emit Store(num); // 向外部汇报这个变量
emit IndexedLog(msg.sender, 789); // 有索引的变量最多3个参数
}
function testMessage(address _to, string calldata mssage) external {
emit Message(msg.sender, _to, mssage);
}
}
函数修改器(父类)
使复用的代码简化的方法 Basic, inputs, sandwich
- 普通父类
- 带参数父类
- 三明治父类
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
/*
Basic 基本类型
inputs 带参数
sandwich 三明治(父类写好开头和结尾代码,中间留给子类)
*/
contract FunctionModifier{
bool public paused; // 是否暂停
uint public count;
function setPause(bool _paused) external {
paused = _paused;
}
// 父类 modifier
// 两个函数运行前都要检测是否暂停
modifier whenNotPaused(){
require(!paused, "paused"); // 避免重复代码
_; // 后面代码位置
}
function inc() external whenNotPaused{
count += 1;
}
function dec() external whenNotPaused{
count -= 1;
}
// 带参数的父类
modifier cap(uint _x){
require(_x < 100, "x >= 100");
_;
}
function incBy(uint _x) external whenNotPaused cap(_x){ // 父类参数传入,父类会依次执行
count += _x;
}
// 三明治父类
modifier sandwich(){
count += 10;
_;
count *= 2;
}
function foo() external sandwich{
count += 1;
}
}
构造函数
constructor
:::info
合约被创建之时触发一次, 用来定义一些初始化值,如下
:::
// 创建合约的时候定义x 值,并且将创建者设置位合约所有者
contract Constructor{
address public owner;
uint public x;
// 通过构造函数初始化两个变量
constructor(uint _x){
owner = msg.sender;
x = _x;
}
}
继承合约
- 可以被重写的函数要添加关键词
virtual
- 子合约继承函数需要关键词
override
contract A{
function foo() public pure virtual returns (string memory){
return "A";
}
}
contract B is A{
function foo() public pure virtual override returns (string memory){
return "B";
}
}
contract C is B{
function foo() public pure override returns(string memory){
return "C";
}
}
- 多线继承, C 继承了 A 和 B 两个,如果A合约更原始,那么A要写在前面
调用父合约函数用super.xx()
调用其他合约
源码调用
通过合约调用合约函数
- 调用有参
- 调用含返回值
- 调用参数为主币的
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
/*
目标通过测试合约调用其他合约中的函数
*/
contract CallTestContract{
// 方法1:直接参数为 合约类恶心
// function setX(TestContract _test, uint _x) external pure{
// _test.setX(_x);
// }
// 方法2: 通过地址实例化
function setX(address _test, uint _x) external{
TestContract(_test).setX(_x);
}
function getX(address _test) external view returns(uint){
return TestContract(_test).getX();
}
// 调用 payable 函数接受主币【需要传递主币】
function setXandReceiveEther(address _test, uint _x) external payable {
// 传递{}
TestContract(_test).setXandReceiveEther{value: msg.value}(_x);
}
function getXandValue(address _test) external view returns(uint, uint){
return TestContract(_test).getXandValue();
}
}
// 测试合约
contract TestContract{
uint public x;
uint public value = 123;
function setX(uint _x) external {
x = _x;
}
function getX() external view returns(uint){
return x;
}
// 传入 主币
function setXandReceiveEther(uint _x) external payable {
x = _x;
value = msg.value;
}
// 2个返回值
function getXandValue() external view returns(uint, uint){
return (x, value);
}
}
接口合约
:::info
在不知道其他合约源码的情况下无法直接调用, 就需要调用它的接口函数
:::
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
/*
假设不知道合约源码,或源码非常庞大
*/
contract Counter{
uint public count;
function inc() external{
count += 1;
}
function dec()external{
count -= 1;
}
}
// ------- 假如我们不知道上面合约的源码 ------
// 接口通常以I开头
interface ICounter {
function count() external view returns(uint);
function inc() external; // 写入方法,必须有
}
// 测试合约
contract TestContract{
uint public count;
function examples(address _couunter) external {
ICounter(_couunter).inc(); // 调用后 ICounter 的状态变量才会发生变化 类似 Init
count = ICounter(_couunter).count();
}
}
低级Call
abi.encodeWithSignature()
通过签名确认函数abi.encodeWithSelector()
通过选择器确认函数
:::info
参数是 uint 必须写为 uint256
:::
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;
contract TestCall {
string public message;
uint public x;
event Log(string message);
fallback() external payable{
emit Log("fallback was called");
}
function foo(string memory _message, uint _x) external payable returns(bool, uint){
message = _message;
x = _x;
return(true, 999);
}
}
contract Call {
bytes public data;
// 不带主币发送 (注意 uint 必须写成 uint256)
// abi.encodeWithSignature("函数名(参数1,参数2)",参数1,参数2)
// 返回值1: bool 返回是否成功
// 返回值2: data bytes 类型 是调用函数的所有返回值
function callFoo(address _test) external {
(bool success, bytes memory _data) = _test.call(abi.encodeWithSignature(
"foo(string, uint256)", "call foo", 123
));
require(success, "call callFoo error");
data = _data;
}
// 带主币函数
// call 后面加上 {value: 123, gas:5000} 123就是带上的 wei 数量
function callFoo2(address _test) external payable {
(bool success, bytes memory _data) = _test.call{value: 123}(abi.encodeWithSignature(
"foo(string, uint256)", "call foo", 123
));
require(success, "call callFoo2 error");
data = _data;
}
// 调用不存在的函数(触发 fallback)
function callFallback(address _test) external {
(bool success, ) = _test.call(abi.encodeWithSignature("NoExist()"));
require(success, "call Failed");
}
}
委托调用
作用:做可升级合约,升级代码的时候我们只需要生成新的 C 合约,继续用B合约委托调用即可
A: 我们调用地址
B: 委托调用合约
C: 含有功能的调用目标合约
:::info
委托合约不能改变目标合约的状态变量,我们只能改变委托合约的状态变量,相当于套壳(只能使用目标合约的逻辑)且变量顺序必须相同
:::
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
/*
委托调用
A 地址 委托 B合约 去调用 C合约
C 合约视角就是 A 在操作调用它:
C 合约下 msg.sender = A
【委托调用不能改变所有的状态变量】
如果 A 合约委托B 向C发送 100wei, C是不能接受 wei (改变状态变量)只能存在 合约B 中
*/
// 目标合约
contract C {
uint public num;
address public sender;
uint public value;
function setValue(uint _num) external payable{
num = _num;
sender = msg.sender;
value = msg.value;
}
}
// 委托的合约
contract B{
// 变量顺序必须与目标合约相同
uint public num;
address public sender;
uint public value;
function setValue(address _test, uint _num) external payable{
// 委托调用合约
// 方式1:使用签名进行编码(通过签名定位到合约)
//_test.delegatecall(abi.encodeWithSignature("setValue(uint256)", _num));
// 方式2:使用 select 编码(直接查找定位到合约)
(bool success, bytes memory data) = _test.delegatecall(abi.encodeWithSelector(C.setValue.selector, _num));
require(success, "delegatecall failed");
}
}
// 结论: 委托合约不能改变目标合约的状态变量,我们只能改变委托合约的状态变量,相当于套壳(只能使用目标合约的逻辑)
// 作用: 我们一直调用委托合约,目标合约可以进行代码升级替换代码等等操作
合约部署合约
代理合约(内联汇编)
合约 A 和 合约 B, 我们要用代理合约去部署他们
● 代理合约使用
● 视频教程
尽管想升级已经部署的智能合约中的代码是不可能的,但是可以通过设计一个代理合约结构,这个结构可以让你可以通过新部署一个合约的方式,来实现升级主要的处理逻辑的目的。
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
contract TestContract1 {
address public owner = msg.sender;
function setOwner(address _owner) external {
require(msg.sender == _owner, "error owner!");
owner = _owner;
}
}
contract TestContract2{
address public owner = msg.sender;
uint public value = msg.value;
uint public x;
uint public y;
constructor(uint _x, uint _y) payable { // payable 代表是可以可以发送主代币的方法
x = _x;
y = _y;
}
}
// 通过代理合约部署上面2个测试合约
contract Proxy{
event Deploy(address); // 向外部汇报部署的合约地址
fallback() external payable{} // 回退代币函数 (代理合约可能收到主币)
function deploy(bytes memory _code) external payable returns(address addr){
// 通过写内联汇编部署合约
assembly {
/*
create(v, p, n) 返回部署的合约地址
v = 部署合约发送的ETH数量
p = 内存中机器码起始位置
n = 内存中机器码大小
*/
addr := create(callvalue(), add(_code, 0x20), mload(_code))
}
require(addr != address(0), "deploy Error");
emit Deploy(addr); // 触发汇报
}
// 设置
function execute(address _target, bytes memory _data) external payable{
(bool success, ) = _target.call{value: msg.value}(_data);
require(success, "failed");
}
}
contract Helper{
// 获取合约机器码 bytecode, 代理合约就可以用机器码(无参合约)进行部署
function getBytecode1() external pure returns (bytes memory){
bytes memory bytecode = type(TestContract1).creationCode;
return bytecode;
}
// 带参数的(有构造函数)合约, 需要在机器码后面 链接打包的参数。
function getBytecode2(uint _x, uint _y) external pure returns (bytes memory){
bytes memory bytecode = type(TestContract2).creationCode;
return abi.encodePacked(bytecode, abi.encode(_x, _y));
}
// 调用方法(这里调用设置管理员方法)
function getCalldate(address _owner) external pure returns (bytes memory) {
return abi.encodeWithSignature("setOwner(address)", _owner);
}
}
合约工厂(New合约)
不用繁琐的内联汇编,直接 New
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
// 账户合约
contract Account{
address public bank;
address public owner;
constructor(address _owner) payable {
bank = msg.sender;
owner = _owner;
}
}
// 账户工厂合约(批量创建账户合约)
contract AccountFactory{
Account[] public accounts;
function createAccount(address _owner) external payable{
// 如果有 Account 合约源码直接 New, 没有的话需要 import
Account account = new Account{value: 1}(_owner); // 因为账户合约可 payable 所以需要{}传递主币
accounts.push(account);
}
}
合约库
library 库合约: 工具类, 可以将仓用的通用方法存储在库合约方便调用
- 命名大写字母开头
- 设定内部可视(不需要外部可见)
library Math{
function max(uint x, uint y) internal pure returns (uint) {
return x>=y ? x : y;
}
}
contract test{
function testMax(uint _x, uint _y) external pure returns(uint) {
return Math.max(_x, _y); // 调用库
}
}
using
可以让约中所有 指定类型变量都可以直接 .合约库的方法
library ArrayLib{
// 因为传入数组变量是状态变量,这里用storage
function find(uint[] storage arr, uint x) internal view returns(uint) {
for(uint i=0; i<arr.length; i++){
if(arr[i]== x){
return i;
}
}
revert("not found");
}
}
contract TestArray {
// 查找指定下标的元素
uint[] public arr = [1,2,3]; // 状态变量
function testFind() external view returns (uint i){
return ArrayLib.find(arr, 2);
}
}
// 高级引用方法【推荐】
contract TestArray2{
using ArrayLib for uint[]; // 让合约中所有 uint[] 类型变量都可以直接 .合约库的方法
uint[] public arr = [1,2,3];
function testFind() external view returns (uint i){
return arr.find(2); // 直接加载方法即可
}
}
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!