文章目录
-
- 参考网址
- 修饰符
- mapping
- 数组
- struct 结构体
- modifier
- Events 事件 indexed
- Constructor 构造方法
- 继承 Inheritance
- 调用父类合约
- payable
- 发送以太坊 Sending Ether (transfer, send, call)
- fallback 回调函数,receive,function()
- call
- delegatecall
- function-selector
- 调用其他合约 Calling Other Contract
- 合约中创建合约
- try catch
- Keccak256
- 验证签名 Verifying Signature
- solidity版本变化
参考网址
此处主要是针对链接中,增加中文理解说明,有需要可查看原文
(文中参考代码主要摘自该链接)https://solidity-by-example.org/
修饰符
初始变量修饰符 constants-immutable
- constants 常量,硬编码,不可修改
- immutable 不可变, 可在构造函数中赋值,之后不能修改
- public 外部可访问,public变量会默认有个getter
- private/default 私有,外部访问不了,子类可访问
数据位置 storage-memory-calldata
- storage 全局变量都属于storage,存储在区块中,方法中使用该声明,会对区块信息修改
- memory 存储在内存中,仅调用函数体内有效,可修改值
- calldata 主要针对external类方法的参数,和memory的区别是不可以修改该变量
方法修饰符 view-pure
- view 不会更改任何状态(会读取区块内变量)
- pure 不会更改任何状态且不读取区块内变量
错误 require-revert-assert
- require 执行的前置条件,如果这个不满足,无法往下执行, 且有提示语的话,会出现在浏览器和estimateGas提示
- revert 在某个if判断,可调用revrt,功能和require类似
- assert 断言某个条件是否为真
常用是require/revert, 可以将错误信息返出去,便于知道错误在哪
assert存在意义在哪不确定,可以使用require替代
另外针对assert的区别。 0.8.x测试没发现和require有什么区别,但是在低版本区别很大
例:0.4.x eth-usdt,可以去查看他的源码
如果一个交易gasLimit设置的100w,实际消耗大概是5w,代码最后面增加(require,revert)/ assert 导致出错
如果使用require/revert, 实际gasUsed = gasPrice * 5w,然后回退了
如果使用assert, 实际gasUsed = gasPrice * 100w, 会把gas消耗完…!
方法修饰符 public-private-internal-external
- public 公开的,本合约也可以调用,外部也可以调用
- private 私有的, 仅本合约可以调用
- internal 内部的, 当前合约和子合约可以调用
- external 公开的,当前合约自身不可调用
mapping
key=>value 格式
mapping(address => uint) public myMap;
mapping(address => mapping(uint => bool)) public nested;
myMap[_addr] = _i;
myMap[_addr];
delete myMap[_addr];
数组
pragma solidity ^0.8.7;
contract Array {
// 初始化数组的几种方式
uint[] public arr;//可变长度数组,初始长度0
uint[] public arr2 = [1, 2, 3];//可变长度数组,初始长度3,有对应值
// 固定长度数组,所有元素是默认值,当前例子为 0, 这个数组不可以push/pop改变长度
uint[10] public myFixedSizeArr;
//通过下标获取数组元素
function get(uint i) public view returns (uint) {
return arr[i];
}
//可返回整个数组,这种方法需要避免长度很长可增长的数组
//查询方法也受gasLimit限制,查询过多内容时会超限制
function getArr() public view returns (uint[] memory) {
return arr;
}
//数量很长的分页/区间查询
function getArr1(uint256 pageNo, uint256 pageSize)public view returns(uint256[]memory list) {
uint len = arr.length;
uint start = pageNo * pageSize;
if(len == 0 || start >= len){
return new uint[](0);
}
uint end = start + pageSize;
if(end > len){
end = len;
}
uint arrLen = end - start;
list = new uint[](arrLen);
uint index;
for(;start < end ; start ++){
list[index++] = start;
}
}
function push(uint i) public {
//追加到数组,数组长度加1
arr.push(i);
}
function pop() public {
//移除数组的最后一个元素
arr.pop();
}
//返回数组长度
function getLength() public view returns (uint) {
return arr.length;
}
function remove(uint index) public {
//delete 操作不对修改数组长度,只是把索引位置的值重置为默认值,当前例子为0
delete arr[index];
}
//如果想移除一个值,且改变数组长度,
//可以先替换值, 在pop
//注: 该方式会导致数组值不是原来的插入顺序
function remove2(uint index)public{
arr[index] = arr[arr.length-1];
arr.pop();
}
function examples() external {
// 在内存中创建数组,只能创建固定大小
uint[] memory a = new uint[](5);
}
}
struct 结构体
通过struct将相关数据放一起
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract Todos {
struct Todo {
string text;
bool completed;
}
// An array of 'Todo' structs
Todo[] public todos;//public声明, 外部可以通过 todos[index],输入index会返回整个结构体
function create(string memory _text) public {
// 3种初始化结构的方式
// 像方法一样顺序传参
todos.push(Todo(_text, false));
// key value mapping
todos.push(Todo({text: _text, completed: false}));
// 初始化一个空的结构体,并针对每个字段赋值, 如果结构体中存在数组/其他结构体时,这种方式合适
Todo memory todo;
todo.text = _text;
//未显视赋值的变量为类型初始值
// todo.completed initialized to false
todos.push(todo);
}
// Solidity automatically created a getter for 'todos' so
// you don't actually need this function.
function get(uint _index) public view returns (string memory text, bool completed) {
Todo storage todo = todos[_index];
return (todo.text, todo.completed);
}
//如果是低版本的,会要求在文件头部声明 pragma experimental ABIEncoderV2;
function getObject(uint _index)public view returns(Todo memory){
return todos[_index];
}
// update text
function update(uint _index, string memory _text) public {
Todo storage todo = todos[_index];
todo.text = _text;
}
// update completed
function toggleCompleted(uint _index) public {
Todo storage todo = todos[_index];
todo.completed = !todo.completed;
}
}
modifier
可在函数调用前做一定处理,以及执行之后,再做一定处理
主要功能
![](/qrcode.jpg)
- 限制访问
- 验证输入
- 防止重入
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
//下划线是一个特殊字符,仅在modifier中, 标志执行方法的其他代码
_;
}
//判断输入的地址不是0地址
modifier validAddress(address _addr) {
require(_addr != address(0), "Not valid address");
_;
}
//防止重入,调用函数前先把状态改了, 函数执行完后, 再把状态改回来
modifier noReentrancy() {
require(!locked, "No reentrancy");
locked = true;
_;
locked = false;
}
Events 事件 indexed
简单理解就是日志,便于知道区块内部执行了什么
event Transfer(address indexed from, address indexed to, uint value);
日志定义里面有个indexed修饰符, 最多允许3个参数使用该修饰符
event的数据,在transaction中logs中存在两个不同位置,
indexed 修饰的 在topic中 而其他的在data中
"logs": [
{
"address": "0x55d398326f99059ff775485246999027b3197955",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000eb2d2f1b8c558a40207669291fda468e50c12345",
"0x000000000000000000000000bcdc55ce32a3d875d440c6fec0354919ab812345"
],
"data": "0x00000000000000000000000000000000000000000000001b1ae4d6e2ef500000"
}
]
logs中说明
address 表示该事件是哪个合约地址的(注意是实际发出的地址, 比如A合约调用B代币, address是B代币的地址,而不是A合约的地址)
topics 是个数组,topics[0] 表示事件名,后面的就是顺序取事件中的indexed修饰的参数
以下针对Transfer事件的说明
topics[0]对应的内容是 keccak256(bytes('Transfer(address,address,uint256)'))
topics[1]对应的内容是事件中的 from
topics[2]对应的内容是事件中的 to
data 是非indexed修饰的参数,顺序取
去掉前面的0x后, 每64位表示一个参数,解析的时候对应参数类型解析即可
Constructor 构造方法
继承多父类是从左到右的顺序
参考下面代码中注释, 链接中描述顺序是Y-X-child 是错的
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
// Base contract X
contract X {
string public name;
constructor(string memory _name) {
name = _name;
}
}
// Base contract Y
contract Y {
string public text;
constructor(string memory _text) {
text = _text;
}
}
// 有两种方式用参数初始化父合约。
//方法一、 在合约继承的时候填参数
contract B is X("Input to X"), Y("Input to Y") {
}
//方法二、在构造方法中传参
contract C is X, Y {
constructor(string memory _name, string memory _text) X(_name) Y(_text) {}
}
// 父构造函数总是按照继承的顺序调用,而不管子合约的构造函数中列出的父合约的顺序。
// 继承是从左到右的
//下面两个的顺序都是 X,Y,child
contract D is X, Y {
constructor() X("X was called") Y("Y was called") {}
}
contract E is X, Y {
constructor() Y("Y was called") X("X was called") {}
}
继承 Inheritance
以下是例子
继承的顺序是从左到右;
例:E is C,B (和链接中有override(B, C)
有改动)
先继承C,再继承B; C/B有同名方法,后继承B,所以B会覆盖掉C的方法实现
虽然方法中有 override(B, C)
但是无效,以合约中is的继承顺序, B在后面返回的结果是B
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
/* Graph of inheritance
A
/ \
B C
/ \ /
F D,E
*/
contract A {
function foo() public pure virtual returns (string memory) {
return "A";
}
}
// Contracts inherit other contracts by using the keyword 'is'.
contract B is A {
// Override A.foo()
function foo() public pure virtual override returns (string memory) {
return "B";
}
}
contract C is A {
// Override A.foo()
function foo() public pure virtual override returns (string memory) {
return "C";
}
}
// Contracts can inherit from multiple parent contracts.
// When a function is called that is defined multiple times in
// different contracts, parent contracts are searched from
// right to left, and in depth-first manner.
contract D is B, C {
// D.foo() returns "C"
// since C is the right most parent contract with function foo()
function foo() public pure override(B, C) returns (string memory) {
return super.foo();
}
}
contract E is C, B {
// E.foo() returns "B"
// since B is the right most parent contract with function foo()
function foo() public pure override(B, C) returns (string memory) {
return super.foo();
}
}
// Inheritance must be ordered from “most base-like” to “most derived”.
// Swapping the order of A and B will throw a compilation error.
contract F is A, B {
function foo() public pure override(A, B) returns (string memory) {
return super.foo();
}
}
调用父类合约
根据下面的描述以及测试结果
B,C中foo/bar的区别是 bar都是使用super.bar(),而foo都是使用A.bar()
调用D.foo, 调用了C,再调用了A
调用D.bar,调用了C,然后调用B,最后调用了A,(A只调用了一次)
两个的区别可以看出,同名方法是后继承覆盖先继承。
C中bar调用的super,最终到了B, B再调用superA
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
/* Inheritance tree
A
/ \
B C
\ /
D
*/
contract A {
// This is called an event. You can emit events from your function
// and they are logged into the transaction log.
// In our case, this will be useful for tracing function calls.
event Log(string message);
function foo() public virtual {
emit Log("A.foo called");
}
function bar() public virtual {
emit Log("A.bar called");
}
}
contract B is A {
function foo() public virtual override {
emit Log("B.foo called");
A.foo();
}
function bar() public virtual override {
emit Log("B.bar called");
super.bar();
}
}
contract C is A {
function foo() public virtual override {
emit Log("C.foo called");
A.foo();
}
function bar() public virtual override {
emit Log("C.bar called");
super.bar();
}
}
contract D is B, C {
// Try:
// - Call D.foo and check the transaction logs.
// Although D inherits A, B and C, it only called C and then A.
// - Call D.bar and check the transaction logs
// D called C, then B, and finally A.
// Although super was called twice (by B and C) it only called A once.
function foo() public override(B, C) {
super.foo();
}
function bar() public override(B, C) {
super.bar();
}
}
payable
声明的函数和地址加了payable才可以接收ether
如果对一个普通地址转ether,需要先用payable(address)转换成 address payable,再进行转ether
pragma solidity ^0.8.7;
contract Payable {
// Payable address can receive Ether
address payable public owner;
// Payable constructor can receive Ether
constructor() payable {
owner = payable(msg.sender);
}
// Function to deposit Ether into this contract.
// Call this function along with some Ether.
// The balance of this contract will be automatically updated.
function deposit() public payable {}
}
发送以太坊 Sending Ether (transfer, send, call)
有三种方式给地址转ether
- transfer (2300 gas, 超出会抛异常回退)
- send (2300 gas, 返回bool,超出会调用失败)
- call (使用所有gas,或者设置gas,返回bool)
合约接收以太坊
接收 Ether 的合约必须至少具有以下功能之一
- receive() external payable
- fallback() external payable
如果msg.data为空,会调用 receive()
否则
会调用fallback()
应该使用哪个方法
如果接收地址是普通的地址,可以使用transfer最简单
如果接收地址是合约,且合约里面有功能的话,需要使用call,否则可能gas不足导致失败
文中介绍2019.12月后建议使用call和re-entrancy结合使用
OpenZeppelin-ReentrancyGuard
call 除了防止重入,也可以手动设置gas使用量,限制接收地址消耗太多gas
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract ReceiveEther {
/*
Which function is called, fallback() or receive()?
send Ether
|
msg.data is empty?
/ \
yes no
/ \
receive() exists? fallback()
/ \
yes no
/ \
receive() fallback()
*/
event RecInfo(address indexed _from, uint256 _type, uint256 _value, bytes _data);
// Function to receive Ether. msg.data must be empty
receive() external payable {
emit RecInfo(msg.sender, 1, msg.value, bytes(""));
}
// Fallback function is called when msg.data is not empty
fallback() external payable {
emit RecInfo(msg.sender, 2, msg.value,msg.data);
}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
contract SendEther {
function sendViaTransfer(address payable _to) public payable {
// This function is no longer recommended for sending Ether.
_to.transfer(msg.value);
}
function sendViaSend(address payable _to) public payable {
// Send returns a boolean value indicating success or failure.
// This function is not recommended for sending Ether.
bool sent = _to.send(msg.value);
require(sent, "Failed to send Ether");
}
function sendViaCall(address payable _to) public payable {
// Call returns a boolean value indicating success or failure.
// This is the current recommended method to use.
// (bool sent, bytes memory data) = _to.call{value: msg.value,gas:66666}("");
(bool sent, bytes memory data) = _to.call{value: msg.value, gas: 2300}("");
//0.5.x
//(bool sent, bytes memory data) = _to.call.value(msg.value).gas(2300)("");
require(sent, "Failed to send Ether");
}
}
fallback 回调函数,receive,function()
这是一个不带任何参数且不返回任何内容的函数
主要功能:
- 需要声明回调函数,合约才可以直接接收ether
- 调用的data匹配不到合约内方法时触发(参考delegatecall)
需要注意的是,如果使用transfer/send给该合约发送ether,受gas(2300)限制
低版本的实现(0.6.0前)
实现该方法即可接收
function() external payable {
}
0.6.0后的版本
分了两个动作;
- receive() 仅msg.data为空才会执行
- fallback() msg.data不为空,或者没有实现receive()方法时执行
如果只实现fallback()方法,就和之前的function()功能一样了
/*
Which function is called, fallback() or receive()?
send Ether
|
msg.data is empty?
/ \
yes no
/ \
receive() exists? fallback()
/ \
yes no
/ \
receive() fallback()
*/
call
call是与其他合约交互的低级功能
call会返回bool
call合约的时候,如果data没有匹配到合约方法,会调用fillback函数
以下举例几种使用call调用其他合约的方式
(比如有些合约没有开源,但是区块浏览器可以看到交互记录/data,这个时候可以直接使用call data的方式调用合约)
contract Caller {
event Response(bool success, bytes data);
// Let's imagine that contract B does not have the source code for
// contract A, but we do know the address of A and the function to call.
function testCallFoo(address payable _addr) public payable {
// You can send ether and specify a custom gas amount
(bool success, bytes memory data) = _addr.call{value: msg.value, gas: 5000}(
abi.encodeWithSignature("foo(string,uint256)", "call foo", 123)
);
emit Response(success, data);
}
// Calling a function that does not exist triggers the fallback function.
function testCallDoesNotExist(address _addr) public {
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("doesNotExist()")
);
emit Response(success, data);
}
function getCalldata(uint256 params1)public pure returns (bytes memory){
bytes4 method = 0xbe221111;
return abi.encodeWithSelector(method, params1);
}
}
delegatecall
delegatecall是一个类似于call的低级函数。
当合约A执行delegatecall到合约时B,B代码被执行
执行后修改的信息是合约A里面的,且调用其他合约时,msg.sender是A,msg.value也是从A扣
function-selector
调用合约时的data,前4个字节(8位)是指定调用哪个方法名
transfer(address,uint256) 方法hash的结果是
0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
取前面8位就是 a9059cbb, 后面跟随的就是参数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract FunctionSelector {
/*
"transfer(address,uint256)"
0xa9059cbb
"transferFrom(address,address,uint256)"
0x23b872dd
*/
function getSelector(string calldata _func) external pure returns (bytes4) {
return bytes4(keccak256(bytes(_func)));
}
}
调用其他合约 Calling Other Contract
调用合约有两种方式
- 知道接口名,通过 Callee(contractAddress).method("");调用合约
- 不知道接口名,知道方法id, 通过区块浏览器记录分析方法名和方法参数,构建交易,使用call调用
另外下面附上参数是结构体的形式调用方式, key/value的方式对应
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract Callee {
uint public x;
uint public value;
function setX(uint _x) public returns (uint) {
x = _x;
return x;
}
function setXandSendEther(uint _x) public payable returns (uint, uint) {
x = _x;
value = msg.value;
return (x, value);
}
}
contract Caller {
function setX(Callee _callee, uint _x) public {
uint x = _callee.setX(_x);
}
function setXFromAddress(address _addr, uint _x) public {
Callee callee = Callee(_addr);
callee.setX(_x);
}
function setXandSendEther(Callee _callee, uint _x) public payable {
(uint x, uint value) = _callee.setXandSendEther{value: msg.value}(_x);
}
}
//结构体调用例子
ISwapRouter.ExactInputSingleParams memory params =
ISwapRouter.ExactInputSingleParams({
tokenIn: DAI,
tokenOut: WBNB,
fee: poolFee,
recipient: msg.sender,
deadline: block.timestamp,
amountIn: amountIn,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
// The call to `exactInputSingle` executes the swap.
amountOut = swapRouter.exactInputSingle(params);
合约中创建合约
其他合约可以使用new,或者create2的方式创建
0.8.0开始,create2 也支持使用new关键字加上指定salt来创建
后面附上new和create2提前计算合约地址的方式
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract Car {
address public owner;
string public model;
address public carAddr;
constructor(address _owner, string memory _model) payable {
owner = _owner;
model = _model;
carAddr = address(this);
}
}
contract CarFactory {
Car[] public cars;
function create(address _owner, string memory _model) public {
Car car = new Car(_owner, _model);
cars.push(car);
}
function createAndSendEther(address _owner, string memory _model) public payable {
Car car = (new Car){value: msg.value}(_owner, _model);
cars.push(car);
}
function create2(
address _owner,
string memory _model,
bytes32 _salt
) public {
Car car = (new Car){salt: _salt}(_owner, _model);
cars.push(car);
}
function create2AndSendEther(
address _owner,
string memory _model,
bytes32 _salt
) public payable {
Car car = (new Car){value: msg.value, salt: _salt}(_owner, _model);
cars.push(car);
}
function getCar(uint _index)
public
view
returns (
address owner,
string memory model,
address carAddr,
uint balance
)
{
Car car = cars[_index];
return (car.owner(), car.model(), car.carAddr(), address(car).balance);
}
}
合约内提前计算地址
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.17;
contract Car {
address public owner;
string public model;
address public carAddr;
constructor() public payable {
}
}
contract CarFactory {
bytes32 public initCodeHash;
//使用create2的方式提前计算地址
function pairFor(uint256 _n) public view returns (address pair) {
pair = address(uint(keccak256(abi.encodePacked(
hex'ff',
address(this),
keccak256(abi.encodePacked(_n)),
initCodeHash
))));
}
uint256 public curNum = 0;
//该方式不知道如何传参数
function createPair() public returns (address pair) {
bytes memory bytecode = type(Car).creationCode;
bytes32 salt = keccak256(abi.encodePacked(curNum++));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
}
//使用create的方式提前计算地址
function addressFrom(address _origin, uint _nonce) public pure returns (address _address) {
bytes memory data;
if (_nonce == 0x00) data = abi.encodePacked(byte(0xd6), byte(0x94), _origin, byte(0x80));
else if (_nonce <= 0x7f) data = abi.encodePacked(byte(0xd6), byte(0x94), _origin, uint8(_nonce));
else if (_nonce <= 0xff) data = abi.encodePacked(byte(0xd7), byte(0x94), _origin, byte(0x81), uint8(_nonce));
else if (_nonce <= 0xffff) data = abi.encodePacked(byte(0xd8), byte(0x94), _origin, byte(0x82), uint16(_nonce));
else if (_nonce <= 0xffffff) data = abi.encodePacked(byte(0xd9), byte(0x94), _origin, byte(0x83), uint24(_nonce));
else data = abi.encodePacked(byte(0xda), byte(0x94), _origin, byte(0x84), uint32(_nonce));
bytes32 hash = keccak256(data);
assembly {
mstore(0, hash)
_address := mload(0)
}
}
}
try catch
try/catch 只允许外部调用以及合约创建时才可以捕获
下面例子还可以catch revert/require, 和assert的区别
或者统一处理
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
// External contract used for try / catch examples
contract Foo {
address public owner;
constructor(address _owner) {
require(_owner != address(0), "invalid address");
assert(_owner != 0x0000000000000000000000000000000000000001);
owner = _owner;
}
function myFunc(uint x) public pure returns (string memory) {
require(x != 0, "require failed");
return "my func was called";
}
}
contract Bar {
event Log(string message);
event LogBytes(bytes data);
Foo public foo;
constructor() {
// This Foo contract is used for example of try catch with external call
foo = new Foo(msg.sender);
}
// Example of try / catch with external call
// tryCatchExternalCall(0) => Log("external call failed")
// tryCatchExternalCall(1) => Log("my func was called")
function tryCatchExternalCall(uint _i) public {
try foo.myFunc(_i) returns (string memory result) {
emit Log(result);
} catch {
emit Log("external call failed");
}
}
// Example of try / catch with contract creation
// tryCatchNewContract(0x0000000000000000000000000000000000000000) => Log("invalid address")
// tryCatchNewContract(0x0000000000000000000000000000000000000001) => LogBytes("")
// tryCatchNewContract(0x0000000000000000000000000000000000000002) => Log("Foo created")
function tryCatchNewContract(address _owner) public {
try new Foo(_owner) returns (Foo foo) {
// you can use variable foo here
emit Log("Foo created");//创建成功
} catch Error(string memory reason) {
// catch failing revert() and require()
emit Log(reason);//revert/require中的提示错误
} catch (bytes memory reason) {
// catch failing assert()
emit LogBytes(reason);//assert的错误
}
}
}
Keccak256
keccak256 计算输入内容的Keccak-256哈希值
主要场景
- hash后确认唯一性
下面例子中有说明一个hash冲突的问题
// 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)
{
// encodePacked(AAA, BBB) -> AAABBB
// encodePacked(AA, ABBB) -> AAABBB
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;
}
}
encodePacked,encode后的区别
(AAA, BBB) 和 (AA, ABBB) 为例
encodePacked 的结果都是
0x414141424242
所以keccak256的结果都是一样的
encode
AAA BBB
0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003414141000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034242420000000000000000000000000000000000000000000000000000000000
展开
0x
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000003
4141410000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000003
4242420000000000000000000000000000000000000000000000000000000000
AA ABBB
0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002414100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044142424200000000000000000000000000000000000000000000000000000000
展开
0x
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000002
4141000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000004
4142424200000000000000000000000000000000000000000000000000000000
验证签名 Verifying Signature
可以在链下签名,链上延签,再执行一定的操作
签名步骤
1、所有参数进行hash
msgHash = hash(allparams...)
2、加上前缀再hash
ethPrefix = "\x19Ethereum Signed Message:\n32";
ethHash = hash(ethPrefix,msgHash);
3、使用私钥签名得到签名结果
signResult = sign(ethHash,prikey)
通过signResult得到r,s,v 传入到合约中
注意使用的时候,一般都会在合约里面维护一个nonces,参考UniswapV2ERC20中的permit
使用场景:
- UniswapV2ERC20中的permit,验证签名通过后,进行授权,避免发送两次交易
- 小狐狸签名后,把交易信息给到中心化后端,后端用于确认用户生成token鉴权
- 项目方地址签名,合约内验签,验证通过后给调用者发送收益(参考链游提收益)
上面所说的链游提收益,是针对游戏中心化产生的积分,需要实际提到链上时,可参考meli提PCE, 代码没开源,应该也是这种方式
// 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)
}
}
solidity版本变化
参考链接
https://learnblockchain.cn/docs/solidity/060-breaking-changes.html
0.5.0
- sha3改用keccak256, keccak256只允许接收一个参数,使用abi.encodePacked等组合params
- 构造函数由同名空参方法变成constructor
0.6.0
- 仅标记virtual的接口才可以被覆盖,覆盖时需要使用新关键字override,如果多个基类同方法名时,需要像这样列出 override(Base1, Base2)
- 不能通过修改length来修改数组长度,需要通过push(),push(value),pop的方式,或者赋值一个完整的数组
- 使用abstract标识抽象合约,抽象合约不能使用new创建
- 回调函数由function()拆分为fallback()和receive()
- 新增try/catch,可对调用失败做一定处理
- 数组切片,例如: abi.decode(msg.data[4:], (uint, uint)) 是一个对函数调用payload进行解码底层方法
- payable(x) 把 address 转换为 address payable
0.7.0
- call方式调用方法由x.f.gas(1000).value(1 ether)(arg1,arg2)改成 x.f{gas:1000,value:1 ether}(arg1,arg2)
- now 不推荐使用,改用block.timestamp
- gwei增加为关键字
- 字符串支持ASCII字符,Unicode字符串
- 构造函数不在需要 public修饰符,如需防止创建,可定义成abstract
- 不允许在同一继承层次结构中具有同名同参数类型的多个事件
- using A for B,只在当前合约有效, 以前是会继承的,现在需要使用的地方,都得声明一次
0.8.0
- 弃用safeMath,默认加了溢出检查,如需不要检查使用
unchecked { ... }
, 可以节省丢丢手续费 - 默认支持ABIEncoderV2,不再需要声明
- 求幂是右结合的,即表达式
a**b**c
被解析为a**(b**c)
。在 0.8.0 之前,它被解析为(a**b)**c
- assert 不在消耗完 完整的gas,功能和require基本一致,但是try/catch错误里面体现不一样,还有一定作用…
- 不再允许使用uint(-1),改用type(uint).max