solidity8.0 笔记1-基础知识
配套代码
https://solidity-by-example.org
代码规范
https://solidity-cn.readthedocs.io/zh/develop/style-guide.html#id16
函数可见性说明符
public:内部、外部均可见(参考为存储/状态变量创建 getter 函数) private:仅在当前合约内可见 external:仅在外部可见(仅可修饰函数)——就是说,仅可用于消息调用(即使在合约内调用,也只能通过 this.func 的方式) internal:仅在内部可见(也就是在当前 Solidity 源代码文件内均可见,不仅限于当前合约内,译者注)
函数修改器
pure 修饰函数时:不允许修改或访问状态——但目前并不是强制的。 view 修饰函数时:不允许修改状态——但目前不是强制的。 payable 修饰函数时:允许从调用中接收 以太币Ether 。 constant 修饰状态变量时:不允许赋值(除初始化以外),不会占据 存储插槽storage slot 。 constant 修饰函数时:与 view 等价。 anonymous 修饰事件时:不把事件签名作为 topic 存储。 indexed 修饰事件时:将参数作为 topic 存储。
函数使用
// SPDX-License-Identifier: MIT pragma solidity ^0.8.7; contract FunctionIntro { // pure 是纯函数的意思 没有链上操作 不能读取全局变量或者状态变量 而view可以读取 function add(uint x, uint y) external pure returns (uint) { return x + y; } // view 读取全局变量 function globalVars() external view returns (address, uint, uint) { address sender = msg.sender; uint timestamp = block.timestamp; uint blockNum = block.number; return (sender, timestamp, blockNum); } }
pure和view区别 (只读操作方法)
//变量类型分类状态变量、全局变量、局部变量,状态变量就是把一个数据存储到区块链上 contract ViewAndPureFunctions { uint public num; //状态变量 public是内部外部都可以读取 // view 可以调取状态变量 external 指的是只有外部可以读取 内部当前合约不可以读取 function ViewFunc() external view returns (uint) { return num; } // pure纯函数 不可读取链上信息 只有局部变量 function pureFunc() external pure returns (uint) { return 1; } }
计数器,写入方法练习
contract Counter { uint public count; // external 只能外部调用,内部不可以调用 function inc() external { count += 1; } // 由于是写入方法不可以有view或者pure关键字 function dec() external { count -= 1; } }
变量默认值
contract DefaultValues { bool public b; // false uint public u; // 0 int public i; // 0 address public a; // 0x0000000000000000000000000000000000000000 bytes32 public b32; // 0x0000000000000000000000000000000000000000000000000000000000000000 }
常量
我们需要把不需要修改的值变为常量,常量节约gas费,定义constant关键字 常量名称为大写
address public constant MY_ADDR = 0x...; uint public constant MYINT = 20;
三元运算符
return _x < 10 ? 1 : 2;
报错控制
contract Errors { require(i <= 10,"错误提示信息"); revert("仅提示错误信息"); assert(num == 123); //当条件不成立时报错 // 自定义错误 error MyError(address caller, uint i); function testCustomError(uint i) public view { if (i > 10) { revert MyError(msg.sender, i); // 输出变量调试 } } }
函数修改器
//函数修改器 //三种写法 1、Basic 2、inputs 3、sandwich contract FunctionModifier { bool public paused; function setPause(bool _paused) external { paused = _paused; } // 1、basic modifier whenNotPaused() { require(!paused,"合约已经暂停"); _; } function inc() external whenNotPaused { count += 1; } //2、inputs modifier cap(uint _x) { require(_x < 100, "myerror"); _; } //会先执行 whenNotPaused 修改器 再将参数传入cap修改器 function incBy(uint _x) external whenNotPaused cap(_x) { count += _x; } //3、sandwich 主要代码写在修改器里 就像三明治一样 modifier sandwich() { count += 10; _; count *= 2; } function foo() external sandwich { count += 1; } }
构造函数
contract Constructor { address public owner; uint public x; constructor(uint _x) { owner = msg.sender; x = _x; } }
返回值命名
function named() public pure returns (uint x, bool b) { return (1,true); } function named() public pure returns (uint x, bool b) { x = 1; b = true; }
数组
//创建动态数组 存在于状态变量中 uint[] public nums = [1, 2, 3]; //固定长度数组 uint[3] public numsFixed = [4, 5, 6]; nums.push(4); // [1,2,3,4] nums[2] = 666; // [1,2,666,4] //注意数组删除后数组被置空为默认值,长度不变 delete nums[1]; // [1,0,666,4] //pop后length长度改变 nums.pop(); // [1,0,666] uint len = nums.length; //create array in memory 内存中创建数组就是局部变量 内存中无法创建动态数组必须定义长度 uint[] memory arrname = new uint[](5); // 无法使用push pop arrname[0] = 10; // 返回数组中所有元素 function returnArray() external view returns (uint[] memory) { return nums; } 方法1、删除数组中的元素通过位置移动【顺序不变,费gas】 function test() external { arr = [1, 2, 3]; remove(1); } //[1,2,3] remove(1) 变为[1,3,3] 在pop删除最后的3 [1,3] function remove(uint _index) public { require(_index < arr.length, "index out of bound"); for (uint i = _index; i < arr.length - 1; i++) { arr[i] = arr[i + 1]; } arr.pop(); } 方法2、通过替换删除数组中的元素【会打乱顺序,节省gas】 //[1,2,3,4] remove(1) 变为[1,4,3,2] 在pop掉变为[1,4,3] function remove(uint _index) public { arr[_index] = arr[arr.length - 1]; // 替换元素和最后一个元素换位置 arr.pop(); }
映射
mapping(address => uint) public balances; mapping(address => mapping(address => bool)) public isFriend; //嵌套 balances[msg.sender] += 456; delete balances[msg.sender]; // 删除恢复默认值为0 嵌套映射定义 isFriend[msg.sender][address(this)] = true;
映射练习
contract IterableMapping { mapping(address => uint) public balances; mapping(address => bool) public inserted; address[] public keys; function set(address _key, uint _val) external { balances[_key] = _val; if (!inserted[_key]) { inserted[_key] = true; keys.push(_key); } } function get(uint _i) external view returns (uint) { return balances[key[_i]]; } function getSize() external view returns (uint) { return keys.length; } }
结构体
contract Structs {
struct Car {
string model;
uint year;
address owner;
}
Car public car;
Car[] public cars;
mapping(address => Car[]) public carsByOwner;
function examples() external {
Car memory toyota = Car("Toyota", 1990, msg.sender);
Car memory lambo = Car({year: 1980, model: "lb", owner: msg.sender});
Car memory tesla;
tesla.model = "Tesla";
tesla.year = 2010;
tesla.owner = msg.sender;
cars.push(toyota);
cars.push(lambo);
cars.push(tesla);
cars.push(Car("Fe", 2020, msg.sender));
// 重要 如果是memory是内存是值传递 storage 是&地址引用传递 将会改变cars 这也是其用法
Car storage _car = cars[0];
_car.year = 1999;
//删除仅恢复默认值
delete _car.owner;
delete cars[1];
}
}
枚举
contract Enum { enum Status { Open, Close } // 枚举作为一种类型存在 Status public stat; struct Order { address buyer; Status state; } Order[] public orders; //都是以索引的形式存在 function set(Status _s) external { stat = _s; } function change() external { stat = Status.Open; } function reset() external { //恢复枚举默认值就是0 也就是第一个值 delete stat; } }
存储位置
storage 存储 引用传递 用于状态变量
memory 内存 值传递 用于局部变量,一般会拷贝一次数据到内存
(不定长的函数returns返回值都要加上memory)
calldata 只可以用于函数参数中,节约gas
function examples(uint[] calldata x) external returns (uint[] memory) { _internal(x); // calldata和memory类似,更节约gas } function _internal(uint[] calldata x) private { uint x = y[0]; }
calldate和memory的区别
memory
可以用于函数声明参数,也可以用于函数逻辑。
可在函数内部被覆盖和更改。
calldata
必须用于外部函数的动态参数。
只能用于函数声明参数,不能用于函数逻辑声明。
是不可变的,不能被覆盖和更改。
测试代码
pragma solidity ^0.6.12; contract Test { function memoryTest(string memory _test) public returns (string memory) { _test = "Tx Dev"; return _test; // You can return memory } function calldataTest(string calldata _test) external returns (string memory) { return string(abi.encodePacked("Tx Dev", _test)); } }
练习题-待办事项列表
contract TodoList { struct Todo { string text; bool completed; } Todo[] public todos; function create(string calldata _text) external { todos.push(Todo({ text: _text, completed: false })); } function updateText(uint _index, string calldata _text) external { todos[_index].text = _text; } function get(uint _index) external view returns (string memory, bool) { Todo memory todo = todos[_index]; // memory 会对数据进行一次拷贝 storage直接引用 return (todo.test, todo.completed); } //反转completed function toggleCompleted(uint _index) external { todos[_index].completed = !todos[_index].completed; } }
事件通知
solidity语言中定义事件【可以在事件参数上增加indexed属性,最多可以对三个参数增加这样的属性。加上这个属性,可以允许你在web3.js或ethes.js中通过对加了这个属性的参数进行值过滤】
indexed属性在solidity事件中非常重要【过滤当前事件名中,设置了为indexed索引参数值,作为条件判断筛选】
相关文章参考 https://blog.csdn.net/weixin_43343144/article/details/91502148
contract Event { event Log(string message, uint val); //一个event最多定义3个indexed event IndexedLog(address indexed sender, uint val); function example() external { emit Log("foo", 1234); emit IndexedLog(msg.sender, 789); } event Message(address indexed _from, address indexed _to, string message); function sendMessage(address _to, string calldata message) external { emit Message(msg.sender, _to, message); } }
合约的继承
virtual 关键词表示可以被重写 override是重写合约
contract X { function foo() public pure virtual returns (string memory) { return "X"; } function bar() public pure virtual returns (string memory) { return "X"; } function x() public pure returns (string memory) { return "X"; } } contract Y is X { //virtual override 既可以被重写 又进行了重写 function foo() public pure virtual override returns (string memory) { return "Y"; } function y() public pure returns (string memory) { return "Y"; } } //线性继承顺序 合约继承时要先写原始合约再写派生合约 X,Y顺序 contract Z is X, Y { //override(X, Y) 要覆盖两个合约都有的同名函数进行重写 function foo() public pure override(X, Y) returns (string memory) { return "Z"; } function bar() public pure override(X, Y) returns (string memory) { return "Z"; } }
继承-构造函数
contract S { string public name; constructor(string memory _name) { name = _name; } } contract T { string public text; constructor(string memory _text) { text = _text; } } 构造函数继承之传值方法 构造函数运行的顺序会根据is S, T顺序去执行 如果is T, S会先运行T再运行S (也就是会按照继承顺序去运行) 方法1 已知参数 contract U is S("s"), T("t") { } 方法2 通过变量传递 contract V is S, T { constructor(string memory _name, string memory _text) S(_name) T(_text) { } } 方法3 通过固定值、变量分别传递 contract VV is S("s"), T { constructor(string memory _text) T(_text) { } }
调用父级合约函数
E.foo() super.bar() //寻找父级合约的bar方法 使用super方法如果继承了两个合约会全部依次他所有的父级合约 contract H is F, G { function bar() public override(F, G) { super.bar(); } }
函数可视范围
private – only inside contract 在本合约内访问
internal – only inside contract and child contracts 在本合约和继承合约访问
public – inside and outside contract 公开访问
external – only from outside contract 仅可以从外部访问
其实用this.external() 也可以访问 但是不推荐原理是从外部调取 消耗gas
不可变变量-节约gas
在部署合约时需要给定值(通过构造函数或直接定义)当做常量使用,节约gas费,不可在非构造函数进行赋值
address public immutable owner; constructor() { owner = msg.sender; }
支付ETH功能
contract Payable { address payable public owner; constructor() { owner = payable(msg.sender); } // 存款方法 function deposit() external payable { } function getBalance() external view returns(uint) { return address(this).balance; } }
回退函数
当CALLDATA有数据(Remix calldata写0x11测试)时候带ETH发送给合约会调用fallback,没有数据会调用receive
如果没有receive会调用fallback
fallback是一个后备函数,而receive只负责接受主币
contract Fallback { event Log(string func, address sender, uint value, bytes data); fallback() external payable { emit Log("fallback", msg.sender, msg.value, msg.data); } receive() external payable { emit Log("receive", msg.sender, msg.value, ""); } }
发送ETH
contract SendEther { constructor() payable {} // 用于创建合约时传入主币 receive() external payable {} // 直接转账到合约地址 function sendViaTransfer(address payable _to) external payable { _to.transfer(123); // 消耗 2300gas 不返回值 直接报错 } function sendViaSend(address payable _to) external payable { bool sent = _to.send(123); // 消耗 2300gas 反回一个布尔值 require(sent, "send failed"); } function sendViaCall(address payable _to) external payable { (bool success, ) = _to.call{value: 123}(""); // call会把剩余的gas都发送过去 可以传入data "" 返回布尔值 data require(success, "call failed"); } } //接收合约测试 contract EthReceiver { event Log(uint amount, uint gas); receive() external payable { emit Log(msg.value,gasleft()); } }
钱包提款合约练习题
contract EtherWallet { address payable public owner; constructor() { owner = payable(msg.sender); } receive() external payable {} function withdraw(uint _amount) external { require(msg.sender == owner, "caller is not owner"); payable(msg.sender).transfer(_amount); //这样写节约一些gas 使用msg.sender } function getBalance() external view returns (uint) { return address(this).balance; } }
调用其他合约
先部署TestContract合约获得合约地址然后传参到CallTestContract 进行操作合约
contract CallTestContract { //setX的两种写法 合约名TestContract为实例 function setX1(address _test, uint _x) external { TestContract(_test).setX(_x); } //setX的简化写法 function setX2(TestContract _test, uint _x) external { _test.setX(_x); } function getX(address _test) external view returns (uint x) { x = TestContract(_test).getX(); } function setXandSendEther(address _test, uint _x) external payable { TestContract(_test).setXandReceiveEther{value: msg.value }(_x); } function getXandValue(address _test) external view returns (uint x, uint value) { (x, value) = 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; } function getXandValue() external view returns (uint, uint) { return (x, value); } }
接口合约
在不知道对方合约的源代码或者合约特别大的时候需要做接口合约对接调取其他合约地址
源代码合约 部署拿到地址
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 CallInterface { uint public count; // 传入合约地址 function examples(address _counter) external { ICounter(_counter).inc(); count = ICounter(_counter).count(); } }
低级call调用合约方法
调用合约方法将直接修改调用合约内的状态变量与委托调用不同
测试合约用于部署获取地址
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); } }
call调用合约的方法
contract Call { bytes public data; function callFoo(address _test) external payable { //传 111wei 在 value传值时候也要输入金额转账 // 第一个参数是否成功 第二个参数是返回全部返回数据 (bool success, bytes memory _data) = _test.call{value: 111}( abi.encodeWithSignature( "foo(string,uint256)", "call foo", 123 //注意:这里uint要定义准确的uint256 ) ); require(success, "call failed"); data = _data; } 调用不存在的函数 默认会进入回退函数调用成功 如果没有回退函数将报错 function callDoesNotExist(address _test) external { (bool success, ) = _test.call(abi.encodeWithSignature("doesNotExist()")); require(success, "call failed"); } }
委托调用合约
委托调用合约是可升级合约原理,委托调用合约案例 /4750.html
委托调用仅修改DelegateCall 合约内的状态变量 不修改TestDelegateCall调用合约内的状态变量,有点类似于局部的调用功能
先生成地址 contract TestDelegateCall { uint public num; address public sender; uint public value; function setVars(uint _num) external payable { num = _num; sender = msg.sender; value = msg.value; } }
contract DelegateCall { //注意:委托调用合约要和调取合约状态变量一致 否则出现问题 uint public num; address public sender; uint public value; function setVars(address _test, uint _num) external payable { // 上一章低级call的调用方法 // _test.DelegateCall( // abi.event("setVars(uint256)", _num) // ); // 委托调用 (bool success, bytes memory data) = _test.delegatecall( abi.encodeWithSelector(TestDelegateCall.setVars.selector, _num) ); require(success, "delegatecall failed"); } }
工厂合约
之前讲过关于bytecode创建合约,参考视频总结,而工厂合约创建合约比较简洁 // SPDX-License-Identifier: MIT pragma solidity ^0.8.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 { // new 合约名称 并且返回创建的合约地址 推入数组中 Account account = new Account{value: 111}(_owner); accounts.push(account); } }
合约部署流程 先部署AccountFactory生成0x86合约
然后调用create方法后 Internal Txns 是 由0x86d 创建新合约0xd8
库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); } }
库合约示例二
library ArrayLib { 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 { using是一种简洁的写法 将这种方法赋予给了uint[] 类型 using ArrayLib for uint[]; uint[] public arr = [3,2,1]; function testFind() external view returns (uint i) { //return ArrayLib.find(arr, 2); 库合约调取时候arr成为输入参数的第一个参数 return arr.find(2); } }
哈希运算
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract HashFunction {
function hash(
string memory _text,
uint _num,
address _addr
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_text, _num, _addr));
}
// Example of hash collision
// Hash collision can occur when you pass more than one dynamic data type
// to abi.encodePacked. In such case, you should use abi.encode instead.
function collision(string memory _text, string memory _anotherText)
public
pure
returns (bytes32)
{
uint num = 123;
// encodePacked(AAA, BBB) -> AAABBB
// encodePacked(AA, ABBB) -> AAABBB
// 注意 这两个结果会发生不同字符串获取相同的哈希结果,而abi.encode补0不会发生这种问题
// 还有一种解决方案就是中间加数字隔开 abi.encodePacked(_text, num, _anotherText)
return keccak256(abi.encodePacked(_text, _anotherText));
}
}
contract GuessTheMagicWord {
bytes32 public answer =
0x60298f78cc0b47170ba79c10aa3851d7648bd96f2f8e46a19dbc777c36fb0c00;
// Magic word is "Solidity"
function guess(string memory _word) public view returns (bool) {
return keccak256(abi.encodePacked(_word)) == answer;
}
}
验证签名
运行测试流程
1、先用metamask进入测试网比如Ropsten
2、部署签名合约
3、执行getMessageHash方法生成哈希 在用getEthSignedMessageHash 生成二次哈希
0x6a9af07094947a30360b4bc01f360c6a302c875e79e31a0d69f62da612a3c628
0x56a2136d4ac052a721abd16373dcef6515fed8192977d4b959dc16cfc92313b8
4、在chrome浏览器中的console执行metamask API
ethereum.enable(); account = "0x钱包地址" hash = "getMessageHash生成的第一次哈希" 使用以太坊生成的签名 ethereum.request({method: "personal_sign", params: [account, hash]}) 0x679916240b00168a891155b766f9530d466f9ca76add48b9d9f770ba4d1a5bb412355fe8e852bf6c5d113254d1551e7fb1ffcfa2ffedf5cc5b2e57ebc8c8857d1c
5、验证
recoverSigner 验证输入 第一个参数是getEthSignedMessageHash 哈希 和 以太坊生成签名 进行call 得出钱包地址
6、verify验证
输入 发送者 发送钱包地址 参数 100,”lee”,100,以太坊生成签名 验证是否为true
// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; /* Signature Verification How to Sign and Verify # Signing 1. Create message to sign 2. Hash the message 3. Sign the hash (off chain, keep your private key secret) # Verify 1. Recreate hash from the original message 2. Recover signer from signature and hash 3. Compare recovered signer to claimed signer */ contract VerifySignature { /* 1. Unlock MetaMask account ethereum.enable() */ /* 2. Get message hash to sign getMessageHash( 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C, 123, "coffee and donuts", 1 ) hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd" */ function getMessageHash( address _to, uint _amount, string memory _message, uint _nonce ) public pure returns (bytes32) { return keccak256(abi.encodePacked(_to, _amount, _message, _nonce)); } /* 3. Sign message hash # using browser account = "copy paste account of signer here" ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log) # using web3 web3.personal.sign(hash, web3.eth.defaultAccount, console.log) Signature will be different for different accounts 0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b */ function getEthSignedMessageHash(bytes32 _messageHash) public pure returns (bytes32) { /* Signature is produced by signing a keccak256 hash with the following format: "\x19Ethereum Signed Message\n" + len(msg) + msg */ return keccak256( abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash) ); } /* 4. Verify signature signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C amount = 123 message = "coffee and donuts" nonce = 1 signature = 0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b */ function verify( address _signer, address _to, uint _amount, string memory _message, uint _nonce, bytes memory signature ) public pure returns (bool) { bytes32 messageHash = getMessageHash(_to, _amount, _message, _nonce); bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash); return recoverSigner(ethSignedMessageHash, signature) == _signer; } function recoverSigner(bytes32 _ethSignedMessageHash, bytes memory _signature) public pure returns (address) { (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature); return ecrecover(_ethSignedMessageHash, v, r, s); } function splitSignature(bytes memory sig) public pure returns ( bytes32 r, bytes32 s, uint8 v ) { require(sig.length == 65, "invalid signature length"); assembly { /* First 32 bytes stores the length of the signature add(sig, 32) = pointer of sig + 32 effectively, skips first 32 bytes of signature mload(p) loads next 32 bytes starting at the memory address p into memory */ // first 32 bytes, after the length prefix r := mload(add(sig, 32)) // second 32 bytes s := mload(add(sig, 64)) // final byte (first byte of the next 32 bytes) v := byte(0, mload(add(sig, 96))) } // implicitly return (r, s, v) } }
自毁合约
自毁合约销毁后将返还全部以太币,并且销毁后内部方法不可用
部署KillContract时以太币输入1个再进行部署,合约内将带有一个以太币 contract KillContract { constructor() payable {} function kill() external { selfdestruct(payable(msg.sender)); } function testCall() external pure returns (uint) { return 123; } } //助手合约 contract Helper { function getBalance() external view returns (uint) { return address(this).balance; } 简化写法调取外部合约 function kill(KillContract _kill) external { _kill.kill(); } }