Solidity Development Book
Welcome to the world of decentralized blockchain: The document begins with an introduction to blockchain technology and Ethereum, providing essential context for understanding smart contracts. It then delves into the syntax and features of Solidity, covering key concepts such as data types, functions, modifiers, and inheritance. Practical examples are included to illustrate how to write and deploy smart contracts, along with best practices for security and optimization. Additionally, the document addresses common pitfalls and debugging strategies to help learners navigate challenges they may encounter. Finally, the document provides resources for further learning, including links to online courses, documentation, and community forums. This structured approach aims to equip readers with the knowledge and skills needed to confidently create and manage their own smart contracts in Solidity
This book will guide you through the development of a decentralized application, including:
- smart-contract development (in Solidity);
This book is not for complete beginners.
I expect you to be an experienced developer, who has ever programmed in any programming language. It’ll also be helpful if you know the syntax of Solidity, the main programming language of this book. If not, it’s not a big problem: we’ll learn a lot about Solidity and Ethereum Virtual Machine during our journey.
However, this book is for blockchain beginners.
If you only heard about blockchains and were interested but haven’t had a chance to dive deeper, this book is for you! Yes, for you! You’ll learn how to develop for blockchains (specifically, Ethereum), how blockchains work, how to program and deploy smart contracts, and how to run and test them on your computer.
Alright, let’s get started!
Useful Links
- This book is hosted on GitHub: https://github.com/yuhuajing/solidity-book
Data Types
数据类型
Solidity EVM
在宽256bit
深2^256
的栈空间存储合约数据- 合约内部数据分为定长数值类型和非定长的引用类型
数值类型
数值类型赋值时直接传递值,包含 boolean
,整数型(uint8~uint256,int8~int256
),address,定长bytes(bytes1~bytes32)
boolean
类型是二值变量, 取值true|false,default:false
- 运算符包括:
- !(非)
- && (与,短路规则,如果前者false,就不会执行后者)
- || (或,短路规则,如果前者true,就不会执行后者)
- == (判等)
- != (不等)
uint/int
整型,default: 0
- 运算符包括:
- 比较运算符,返回
bool (> < >= <= == !=)
- 算数运算符
(+ - * / % ** << >>)
address
类型address
类型,可以使用payable()
修饰,用于接收NativeToken
(触发receiver()
函数或缺省函数fallback()
) 数值类型合约示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Static_variables {
bool public boo = true;
/*
uint stands for unsigned integer, meaning non negative integers
different sizes are available
uint8 ranges from 0 to 2 ** 8 - 1
uint16 ranges from 0 to 2 ** 16 - 1
...
uint256 ranges from 0 to 2 ** 256 - 1
*/
uint8 public u8 = 1;
uint256 public u256 = 456;
uint256 public u = 123; // uint is an alias for uint256
/*
Negative numbers are allowed for int types.
Like uint, different ranges are available from int8 to int256
int256 ranges from -2 ** 255 to 2 ** 255 - 1
int128 ranges from -2 ** 127 to 2 ** 127 - 1
*/
int8 public i8 = -1;
int256 public i256 = 456;
int256 public i = -123; // int is same as int256
// minimum and maximum of uint
uint256 public minUInt = type(uint256).min;
uint256 public maxUInt = type(uint256).max;
// minimum and maximum of int
int256 public minInt = type(int256).min;
int256 public maxInt = type(int256).max;
address public addr = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c;
/*
In Solidity, the data type byte represent a sequence of bytes.
Solidity presents two type of bytes types :
- fixed-sized byte arrays
- dynamically-sized byte arrays.
The term bytes in Solidity represents a dynamic array of bytes.
It’s a shorthand for byte[] .
*/
bytes1 a = 0xb5; // [10110101]
bytes1 b = 0x56; // [01010110]
// Default values
// Unassigned variables have a default value
bool public defaultBoo; // false
uint256 public defaultUint; // 0
int256 public defaultInt; // 0
address public defaultAddr; // 0x0000000000000000000000000000000000000000
bytes1 public c; //0x00
}
引用类型
引用类型:array[]数组,bytes,定长数组,struct结构体,mapping映射
- 数组:动态数组拥有
push/pop
内置函数,分别在数组最后增加或删除一个元素
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Dynamic_variables_array {
// Several ways to initialize an array
uint256[] public indeterminate_arr;
uint256[] public indeterminate_init_arr = [1, 2, 3];
// Fixed sized array, all elements initialize to 0
uint256[10] public determinate_arr;
function get(uint256 i) public view returns (uint256) {
return indeterminate_arr[i];
}
// Solidity can return the entire array.
// But this function should be avoided for
// arrays that can grow indefinitely in length.
function getArr() public view returns (uint256[] memory) {
return indeterminate_arr;
}
function indeterminate_push(uint256 i) public {
// Append to array
// This will increase the array length by 1.
indeterminate_arr.push(i);
indeterminate_init_arr.push(i);
}
function determinate_push(uint256 index, uint256 i) public {
// Append to array
// This will increase the array length by 1.
determinate_arr[index] = i;
}
function indeterminate_pop() public {
// Remove last element from array
// This will decrease the array length by 1
indeterminate_arr.pop();
indeterminate_init_arr.pop();
}
function getLength() public view returns (uint256) {
return indeterminate_arr.length;
}
function remove_not_change_length(uint256 index) public {
// Delete does not change the array length.
// It resets the value at index to it's default value,
// in this case 0
delete indeterminate_arr[index];
delete determinate_arr[index];
}
// Deleting an element creates a gap in the array.
// One trick to keep the array compact is to
// move the last element into the place to delete.
function remove_change_length(uint256 index) public {
// Move the last element into the place to delete
indeterminate_arr[index] = indeterminate_arr[
indeterminate_arr.length - 1
];
// Remove the last element
indeterminate_arr.pop();
}
function examples_new_determinate_arr() external pure {
// create array in memory, only fixed size can be created
uint256[] memory a = new uint256[](5);
a[0] = 5;
}
}
Data Types
变量作用域
- 状态变量
- 定义在合约中,但是在函数外的需要存储在合约
slot
的变量
- 定义在合约中,但是在函数外的需要存储在合约
- 局部变量
- 定义在合约函数内部,仅在函数执行过程中有效的数据,变量生命周期和函数执行周期一致
- 全局变量。
- 链上数据,全局变量编码到
EVM
字节码中
- 链上数据,全局变量编码到
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Variables {
// State variables are stored on the blockchain.
string public hello = "Hello";
uint256 public num = 123;
function interVariables() public view returns (uint256 ts) {
// Local variables are not saved to the blockchain.
uint256 i = 456;
// Here are some global variables
ts = block.timestamp; // 147 gas cost
// assembly {
// ts := timestamp() // 213 gas cost
// }
{
// 可以使用合约状态变量/本函数内部的局部变量/区块链上全局变量
uint interTs = 356;
interTs += block.timestamp;
interTs += i;
interTs += num;
}
// interTs +=9; 外部无法访问作用域内部的参数
}
}
Data Types
变量修饰符
参数修饰符包括:public,private,immutable,constant
public
,自动生成Getter
函数,表明函数在合约中可以通过abi
查询private
,参数无法通过abi
直接查询,只能通过自定义的合约函数或sload(xx)
通过slot
获得数据immutable
,参数必须在构造函数中初始化,并且编码在字节码中,后续无法修改constant
,参数在定义时,直接初始化,并且编码在字节码中,后续无法修改payable
,用于修饰地址,表明允许该地址接收NativeToken
变量存储方式
参数在合约中的存储方式
- 合约数据存储
- `storage(可修改),Bytecodes(constant/immutable,不可修改)
- 合约函数运行时的数据存储
- (memory,stack,calldata)`,传参或返回数据
合约数据存储
Bytecodes
immutable|constant
变量在合约编译时将值存储在合约代码中,因此后续数据变量无法更改- 在合约使用期间,无需在内部存储中维护该常量的状态
- 由于参数不存储在内部
slot
,数据都是通过字节码读取,因此在合约外部调用中,immutable|constant
的值只会从被调用的合约字节码中获取
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract Constants {
// coding convention to uppercase constant variables
// 存储在字节码
address public constant MY_ADDRESS =
0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc;
// 存储在字节码
uint256 public constant MY_UINT = 123;
// 存储在字节码
uint256 private immutable a;
// slot 0
uint256 public ty = 9;
constructor() {
a = 99;
}
function getslot(uint256 slot) public view returns (bytes32 value) {
assembly {
value := sload(slot)
}
}
}
Storage
- 存储合约状态变量,通过写交易修改
运行时数据存储
- memory. 函数执行过程中用于存储动态分配的数据,如临时变量、函数参数和函数返回值等
- stack. 函数执行过程中存储数据,如基本数据类型和值类型的局部变量
- calldata. 和
memory
类似,数据存储在内存中,但是 calldata 数据只读,一般用于函数的输入参数
function fCalldata(uint[] calldata _x) public pure returns(uint[] calldata){
//参数为calldata数组,不能被修改
// _x[0] = 0 //这样修改会报错
return(_x);
}
变量引用作用域
- 普通状态变量 -> 普通状态变量(拷贝)
- 普通状态变量 -> storage变量(引用)
- 普通状态变量 -> memory变量(拷贝)
- storage变量 -> storage变量(引用)
- storage变量 -> memory变量(拷贝)
- storage变量 -> 普通状态变量(引用)
- memory变量 -> memory变量(引用)
- memory变量 -> 普通状态变量(拷贝)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract StateToStateContract {
uint8[3] public static_array = [1, 2, 3]; //State 状态变量
uint8[3] public static_array_two;
uint256[] public dynamic_array;
event LogUint8(uint8);
event staticArrays(uint8[3], uint8[3]);
event dynamicArrays(uint256[], uint256[]);
function stateToany() public {
//状态变量 -> 状态变量(拷贝),双方互不影响
static_array_two = static_array;
static_array_two[0] = 8;
emit staticArrays(static_array, static_array_two); //[1,2,3],[8,2,3]
//状态变量 -> storage变量(引用),引用拷贝,修改任意变量的值会影响另一个状态变量的值,更新合约状态参数
uint256[] storage tem_dynamic_array = dynamic_array;
tem_dynamic_array.push(10086);
emit dynamicArrays(dynamic_array, tem_dynamic_array); //[10086],[10086]
//状态变量 -> memory变量(拷贝)
uint256[] memory tem_dynamic_array_two = dynamic_array;
tem_dynamic_array_two[tem_dynamic_array_two.length - 1] = 999;
emit dynamicArrays(dynamic_array, tem_dynamic_array_two); //[10086],[999]
}
function storageToany() public {
//storage变量 -> storage变量(引用),引用拷贝,修改任意变量的值会影响另一个状态变量的值,更新合约状态参数
uint256[] storage tem_dynamic_array = dynamic_array;
tem_dynamic_array.push(12);
emit dynamicArrays(dynamic_array, tem_dynamic_array); //[12],[12]
uint256[] storage tem_dynamic_array_two = tem_dynamic_array;
tem_dynamic_array_two.push(13);
emit dynamicArrays(dynamic_array, tem_dynamic_array); //[12,13],[12,13]
emit dynamicArrays(tem_dynamic_array, tem_dynamic_array_two); //[12,13],[12,13]
//storage变量 -> memory变量(拷贝)
uint256[] memory tem_dynamic_array_memory = tem_dynamic_array;
tem_dynamic_array_memory[0] = 14;
emit dynamicArrays(tem_dynamic_array, tem_dynamic_array_memory); //[12,13],[14,13]
// storage变量 -> 状态变量(引用),引用拷贝,修改任意变量的值会影响另一个状态变量的值,更新合约状态参数
dynamic_array = tem_dynamic_array;
dynamic_array[0] = 15;
emit dynamicArrays(dynamic_array, tem_dynamic_array); //[15,13],[15,13]
tem_dynamic_array.push(16);
emit dynamicArrays(dynamic_array, tem_dynamic_array); //[15,13,16],[15,13,16]
}
function memoryToany() public {
// memory变量 -> 状态变量(拷贝)
uint8[3] memory tem_static_array = static_array;
tem_static_array[0] = 4;
emit staticArrays(static_array, tem_static_array); //[1,2,3],[4,2,3]
// memory变量 -> memory变量(引用),引用拷贝,修改任意变量的值会影响另一个状态变量的值,更新合约状态参数
uint8[3] memory tem_static_array_two = tem_static_array;
tem_static_array_two[2] = 5;
emit staticArrays(static_array, tem_static_array); //[1,2,3],[4,2,5]
emit staticArrays(tem_static_array, tem_static_array_two); //[4,2,5],[4,2,5]
}
}
ENUM
- 枚举 enum 作为用户自定义的变量集合,用于定义有限的多种状态
- 由于
enum
有限,后续除非重新部署合约,否则无法新增状态 enum
适用于确定的有限状态,否则使用动态数组
更适合后续扩展
- 由于
- enum内部的变量从
0 index
开始,default: 0
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract enumExamples {
enum ActionChoices {
GoLeft,
GoRight,
GoStraight,
SitStill
}
ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;
function setGoStraight() public {
choice = ActionChoices.GoStraight;
}
function setChoice(ActionChoices _choice) public {
choice = _choice;
}
// Since enum types are not part of the ABI, the signature of "getChoice"
// will automatically be changed to "getChoice() returns (uint8)"
// for all matters external to Solidity.
function getChoice() public view returns (ActionChoices) {
return choice;
}
function getDefaultChoice() public pure returns (uint256) {
return uint256(defaultChoice);
}
function getLargestValue() public pure returns (ActionChoices) {
return type(ActionChoices).max;
}
function getSmallestValue() public pure returns (ActionChoices) {
return type(ActionChoices).min;
}
}
bytes/string
bytes
bytes
是不用加[]
声明的动态数组hex
修饰bytes
数据bytes.concat()
直接拼接bytes
数据- 字节数组
bytes
分为定长数值类型和不定长引用类型- 定长数值数组(
bytes1~bytes32
)能通过index
获取数据 - 不定长数组
bytes ...
是动态类型,传参时需要memory|calldata
修饰,作为参数返回时需要memory
修饰
- 定长数值数组(
string
string
是UTF-8
编码的bytes
类型string
和bytes
数据可以互相转换string(), bytes()
string
传参ASCII
字符,此时每个字符占据1位,可以通过index
直接读取值string
传参Unicode
字符,此时每个字符占据多位,不能通过index
直接读取值
- 在
solidity 0.8.12+
,string.concat()
直接拼接多个string
字符串
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract BytesStringData {
string private constant NATIE = unicode"👋酱香拿铁";
string private constant HELL = unicode"Hello 😃";
string private constant UTF8CODE = "Hello World";
bytes private hexData = hex"68656C6C6F776F726C64";
function characterOfString(string memory input, uint256 index)
external
pure
returns (string memory)
{
bytes memory char = new bytes(1);
char[0] = bytes(input)[index];
return string(char);
}
function characterOfStringLength(string memory input)
external
pure
returns (uint256)
{
return bytes(input).length;
}
function concatMulString() external view returns (string memory) {
return string.concat(string(hexData), NATIE, UTF8CODE);
}
function concatMulBytes() external view returns (bytes memory) {
return bytes.concat(hexData, bytes(NATIE));
}
}
Preference
https://www.rareskills.io/learn-solidity/strings
mapping
Mapping
只能定义成状态变量,不能在函数内部定义成局部变量Mapping
无法遍历,只能通过key
值获取对应的value
Mapping
类型不能作为函数的返回值mapping(key ==> value) [public|private] xxx
- 键值
key
必须是默认的类型,不能使用自定义的类型 value
可以是任意类型,包含自定义的结构体数据结构key
值对应的value
不存在的时候,会返回value
的默认值,不会revert
- 键值
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract Mapping {
// Mapping from address to uint
mapping(address => uint256) public myMap;
// Mapping from nft address and user address to his nft id
mapping(address => mapping(address => uint256)) public addressNFTIds;
struct myStruct {
address NFT;
address Operator;
}
// Mapping from nft address and user address to his approvor
mapping(address => myStruct) public addressApprovedInfo;
function updateMapping(
address nft,
address operator,
uint256 id
) public {
myMap[msg.sender] = id;
addressNFTIds[nft][msg.sender] = id;
addressApprovedInfo[msg.sender] = myStruct(nft, operator);
}
function updateOperatorNFT(
address nft,
address operator,
uint256 id
) public {
mapping(address => uint256) storage _tokensByNft = addressNFTIds[nft];
_tokensByNft[operator] = id;
}
}
合约逻辑
控制流
if(statement){}else{}
for(statement){}
for
循环长度不定的元素时,一定要控制范围,防止out-of-gas
while(statement){}
while
循环执行直到不满足条件,同样在statement
中要控制循环范围
do{}while(statement)
- 先执行逻辑,在执行判断
- 执行直到不满足条件,同样在
statement
中要控制循环范围
- 循环过程关键字
continue
:跳出当前循环,立即进入下一个循环break
:终止当前循环
try()catch{}
- 三元操作符
- a>b?a:b
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract Logic {
bool ifFlag = true;
uint32[] raffleIds;
uint256 executeNumber;
function IfLogic() external view returns (bool) {
if (ifFlag) {
return (true);
} else {
return (false);
}
}
function quicksort(uint256[] memory a)
public
pure
returns (uint256[] memory)
{
uint256 len = a.length;
for (uint256 i = 1; i < len; i++) {
uint256 key = a[i];
uint256 j = i;
while (j >= 1 && key < a[j - 1]) {
a[j] = a[j - 1];
j--;
}
a[j] = key;
}
return a;
}
// Don't write loops that are unbounded as this can hit the gas limit, causing your transaction to fail.
function forExecuteRaffle(uint256 count) external {
uint256 length = raffleIds.length;
uint256 ncount = executeNumber + count >= length
? length
: executeNumber + count;
uint256 temp = executeNumber;
executeNumber = ncount;
for (uint256 i = temp; i < ncount; i++) {
// do something
}
}
// Don't write loops that are unbounded as this can hit the gas limit, causing your transaction to fail.
function whileExecuteRaffle(uint256 count) external {
uint256 length = raffleIds.length;
uint256 ncount = executeNumber + count >= length
? length
: executeNumber + count;
uint256 temp = executeNumber;
executeNumber = ncount;
while (temp < ncount) {
// do something
}
}
// do something first, then do statement check.
function dowhileCheck(uint256 _number) public pure returns (uint256) {
do {
_number += 1;
} while (_number < 15);
return _number; //_number+1
}
}
ABI 编码
abi.encode
Encode
按照传参顺序编码合约函数传参,传参被编码成256bit(32bytes)
, 不满256bit
的参数通过前向或后向补0- 静态数据直接编码
- 动态数据由于动态长度的变化,编码数据分为三部分
静态传参编码规则:
- 静态参数(
int,uint,address,bool,bytes-n,定长数组,只包含静态参数的结构体
)高位补0,编码成256bits
. - 结构体作为
tuples
, 按照数据定义顺序直接依次编码 - 枚举类型占位
uint8
,会高位补0直到满足 32 byets - 自定义的类型按照原类型编码
动态传参编码规则:
- 动态参数编码(
string,bytes,不定长数组,包含动态参数的结构体
)分三部分:- 第一部分的
256bits(32bytes)
表明offset,calldata
的起始位置 - 第二部分的
256bits(32bytes)
表明length,calldata
中动态参数的长度 - 第三部分就是真实传参数数据,不满
256bit
的在补0- 静态类型高位补0
bytes,string
类型的参数,低位补0
- 第一部分的
Examples-String:
play(string) // play(“Eze”)
传参编码的规则:
- 函数需要动态类型
string
传参 - 按照传参顺序,第一行直接编码动态数组
offset
, 之后紧接动态数据大小,(string数据低位补0)
Examples-动态数组:
transfer(uint[], address)// [5769, 14894, 7854], 0x1b7e1b7ea98232c77f9efc75c4a7c7ea2c4d79f1
传参编码的规则:
- 函数需要两个传参,第一个为动态数组,第二个为静态地址
- 按照传参顺序,第一行先编码动态数组
offset
, 动态数据大小为3,内部为静态类型(高位补0) - 第二行编码静态数据
Examples-静态参数结构体:
- 带结构体传参的函数选择器和结构体内部参数类型相关
- 函数需要两个传参,第一个为全是静态参数的结构体,第二个为静态地址
- 按照传参顺序,从第一行开始,直接按照结构体内部参数的定义顺序编码内部参数,每个参数补位
256 bit
- 静态地址编码到全部结构体参数后面
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract C {
struct RareToken {
uint256 n;
}
// `foo((uint256),address)`
function foo(RareToken memory point, address addr) external pure {
//...
}
function getSelector(string calldata str)
external
pure
returns (bytes4, bytes4)
{
return (
bytes4(this.foo.selector),
bytes4(keccak256(abi.encodePacked(str))) //foo((uint256),address)
);
}
}
Examples-动态参数结构体:
struct RareToken {
string str;
uint128 m;
uint128 n;
}
function foo(RareToken memory point, address addr) external pure {
//...
}
- 带结构体传参的函数选择器和结构体内部参数类型相关:
foo((string,uint128,uint128),address)
- 函数需要两个传参,第一个为是包含动态参数的结构体,第二个为静态地址
- 按照传参顺序,从第一行开始,编码结构体参数的
offset
- 第二行编码静态地址
addr
- 结构体数据从
offset
开始,再次按照数据编码模式进行编码- 结构体按照顺序存储
string, uint128, uint128
- 第一行编码
string
参数的存储offset
- 第二三行编码下面的静态参数
- 之后开始编码
srting
传参(数据低位补0)
- 结构体按照顺序存储
0x5c325b3d [“Eze”,12,23] 5b38da6a701c568545dcfcb03fcb875f56beddc4
0000000000000000000000000000000000000000000000000000000000000040 //动态结构体类型数据的起始位置
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 // 静态数据,存储address
0000000000000000000000000000000000000000000000000000000000000060 //结构体数据动态类型的起始位置
000000000000000000000000000000000000000000000000000000000000000c //结构体静态数据1
0000000000000000000000000000000000000000000000000000000000000017 //结构体静态数据2
0000000000000000000000000000000000000000000000000000000000000003 //结构体数据动态类型的数据长度
4578650000000000000000000000000000000000000000000000000000000000 //结构体数据动态数据
Examples-静态参数的固定大小数组,传参编码的规则:
- 静态参数的固定大小数组作为静态参数编码variables.md
- 按照传参顺序,直接依次编码静态数据传参
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Pair {
uint256 x;
address addr;
string name;
uint256[2] array;
constructor() payable {
x = 10;
addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
name = "0xAA";
array = [5, 6];
}
function abiCode() public view returns (bytes memory data) {
data = abi.encode(x, addr, name, array);
}
}
0x 000000000000000000000000000000000000000000000000000000000000000a //x = 10
0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c71 // address
00000000000000000000000000000000000000000000000000000000000000a0 // offset = 160, 表示数据从当前 data 的 第160位 开始读取
0000000000000000000000000000000000000000000000000000000000000005 // array[0]
0000000000000000000000000000000000000000000000000000000000000006 // array[1]
0000000000000000000000000000000000000000000000000000000000000004 // 表明数据 length = 4
3078414100000000000000000000000000000000000000000000000000000000
Examples-动态参数的固定大小数组,传参编码的规则:
- 动态参数的固定大小数组作为动态参数编码
- 按照传参顺序,依次编码各自参数的
offset
,之后从offset
开始存储真实数据的大小和数据
plays(string[2])//play(["Eze","Sunday"])
非固定大小的数组传参
string[]
,需要将数组大小一起编码:
Examples-嵌套数组的编码,传参编码的规则:
- 直接按照参数顺序,按照动态数组的编码规则依次进行编码
abi.encodePacked
- 在数据顺序上进行最低空间编码,省略编码中的填充0
- 动态类型数据只保留数据,不填充长度字段
keccak256(abi.encodePacked(“a”, “b”, “c”)) == keccak256(abi.encodePacked(“a”, “bc”)) == keccak256(abi.encodePacked(“ab”, “c”))
- 当数据不需要和合约交互,只是用来查看数据的情况下就可以使用
encodepacked
节省数据空间 encodePacked
不能编码struct
、nextedArray
(以及包含多维数组的map
)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Pair {
uint40 x;
address addr;
string name;
uint8[2] array;
Tree tree;
uint8[][]loc;
mapping(uint256=>uint8[][])nextArrayMap;
struct Tree {
uint256 leaves;
uint256 age;
}
constructor() payable {
x = 10;
addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
name = "0xAA";
array = [5, 6];
tree = Tree({leaves: 100, age: 1000});
loc[0].push(6);
loc[0].push(7);
nextArrayMap[0]= loc;
}
function abiEncodePacked()
public
view
returns (bytes memory encodeData, bytes memory encodePackedData)
{
encodeData = abi.encode(x, addr, name, array);
encodePackedData = abi.encodePacked(x, addr, name, array,nextArrayMap,tree,loc);
}
}
0x000000000a //uint40 40bit
7a58c0be72be218b41c608b7fe7c5bb630736c71
30784141
0000000000000000000000000000000000000000000000000000000000000005
0000000000000000000000000000000000000000000000000000000000000006
abi.decode
解码 encode
的合约参数,将编码数据解码回原本数据,解码函数中需要提供待解析的编码和解析后的参数类型
function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) {
(dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2]));
}
Preference
https://www.rareskills.io/post/abi-encoding
Solidity参数
数值数据类型占位存储
Type | Bit |
---|---|
boolean | 8 |
uint8/int8/bytes1 | 8 |
uint32/int32/bytes4 | 32 |
uint128/int128/bytes16 | 128 |
uint256/int256/bytes32 | 256 |
address | 160 |
enum | 8 |
slot
存储位置和合约状态变量声明顺序相关,不满栈宽256bit
高位补到同一个slot
contract AddressVariable {
address owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
// new
bool Boolean = true;
uint32 thirdvar;
}
- 其中,
owner
占位160 bit
,Boolean
占位8 bit
,thirdvar
占位32 bit
- 三个变量按照证明顺序,高位编码到同一个slot存储
- 此时,
slot
剩余56 bit
,下个声明变量的类型如果超出56 bit
,会顺延到 下一个slot
存储
contract AddressVariable {
address owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
bool Boolean = true;
uint32 thirdVar;
// new
address admin = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
}
Solidity参数
动态数据类型存储
Mapping数据存储
Mapping value
数据的存储位置和声明顺序baseSlot
以及key
值相关- 直接聚合
bytes32(Key)
和bytes32(baseSlot)
- 哈希聚合结果,找出
mapping value
的真实数据存位置
- 直接聚合
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;
contract MyMapping {
uint256 a; // storage slot 0
uint256 b; // storage slot 1
uint256 c; // storage slot 2
uint256 d; // storage slot 3
uint256 e; // storage slot 4
uint256 f; // storage slot 5
mapping(address => uint256) private balance; // storage slot 6
mapping(string => uint256) private strbalance; // storage slot 7
constructor() {
strbalance["aaa"] = 9; // RED
balance[address(0x1)] = 7;
}
//*** NEWLY ADDED FUNCTION ***//
function getStorageSlot(address _key)
public
pure
returns (uint256 balanceMappingSlot, bytes32 slot)
{
assembly {
// `.slot` returns the state variable (balance) location within the storage slots.
// In our case, balance.slot = 6
balanceMappingSlot := balance.slot
}
slot = keccak256(
abi.encode(
bytes32(abi.encode(_key)),
bytes32(abi.encode(balanceMappingSlot))
)
);
}
function getValue(address _key)
public
view
returns (bytes32 slot, uint256 value)
{
// CALL HELPER FUNCTION TO GET SLOT
(, slot) = getStorageSlot(_key);
assembly {
// Loads the value stored in the slot
value := sload(slot)
}
}
//*** NEWLY ADDED FUNCTION ***//
function getStringStorageSlot(string memory key)
public
pure
returns (uint256 balanceMappingSlot, bytes32 slot)
{
assembly {
// `.slot` returns the state variable (strbalance) location within the storage slots.
balanceMappingSlot := strbalance.slot
}
slot = keccak256(
abi.encodePacked(
abi.encodePacked(key),
bytes32(abi.encode(balanceMappingSlot))
)
);
}
function getStringValue(string memory _key)
public
view
returns (bytes32 slot, uint256 value)
{
// CALL HELPER FUNCTION TO GET SLOT
(, slot) = getStringStorageSlot(_key);
assembly {
// Loads the value stored in the slot
value := sload(slot)
}
}
}
嵌套Mapping数据存储
- Mapping(key=>Mapping(…))
- 多重嵌套
mapping
的slot
存储规则- 每层的
key
作为下层的baseSlot
值参与计算 - 最外层的
key
和参数声明顺序相关
- 每层的
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;
contract MyNestedMapping {
uint256 a; // storage slot 0
uint256 b; // storage slot 1
uint256 c; // storage slot 2
uint256 d; // storage slot 3
uint256 e; // storage slot 4
uint256 f; // storage slot 5
mapping(address => mapping(uint256 => uint256)) public balance; // storage slot 6
constructor() {
balance[0x5B38Da6a701c568545dCfcB03FcB875f56beddC4][123] = 9; // RED
}
//*** NEWLY ADDED FUNCTION ***//
function getStorageSlot(address key1, uint256 key2)
public
pure
returns (uint256 balanceMappingSlot, bytes32 slot)
{
assembly {
// `.slot` returns the state variable (balance) location within the storage slots.
// In our case, balance.slot = 6
balanceMappingSlot := balance.slot
}
bytes32 slot1 = keccak256(
abi.encode(
bytes32(abi.encode(key1)),
bytes32(abi.encode(balanceMappingSlot))
)
);
slot = keccak256(abi.encode(bytes32(abi.encode(key2)), slot1));
}
function getValue(address key1, uint256 key2)
public
view
returns (bytes32 slot, uint256 value)
{
// CALL HELPER FUNCTION TO GET SLOT
(, slot) = getStorageSlot(key1, key2);
assembly {
// Loads the value stored in the slot
value := sload(slot)
}
}
function convert(string memory key) internal pure returns (bytes32 ret) {
require(bytes(key).length <= 32);
assembly {
ret := mload(add(key, 32))
}
}
}
Solidity参数
动态数据类型存储
Arrays数据存储
定长数组
- 定长数组作为固定大小的参数
- 数据存储按照静态类型依次
slot
存储 - 定长数组不和其他参数共享
slot
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;
contract MyFixedUint256Array {
uint256 num; // storage slot 0
uint64[3] myArr = [
4, // storage slot 1
9, // storage slot 1
2 // storage slot 1
];
uint64 next = 5; //storage slot 2
uint128[3] bigArr = [
4, // storage slot 3
9, // storage slot 3
2 // storage slot 4
];
uint64 bnext = 5; //storage slot 5
function getValue(uint256 index) public view returns (uint256 value) {
// CALL HELPER FUNCTION TO GET SLOT
assembly {
// Loads the value stored in the slot
value := sload(index)
}
}
}
不定长数组–arrays[]
- 不定长数据在合约编译时无法确认数据
size
,所以在baseSlot
存储当前参数的size
- 不定长数组具体数据的起始位置和
baseSlot
相关:keccak256(baseSlot)
- 不定长数组的数据依次从起始位置开始入栈存储
- 不定长数组的数据按照数值类型补位存储,
slot
进行高位补足存储数组参数 - 数值长度超出
256bit
后,顺延至下一个slot
存储(index += 1)
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;
contract MyDynArray {
uint256 private someNumber; // storage slot 0
address private someAddress; // storage slot 1
uint32[] private myDynamicArr = [3, 4, 5, 9, 7]; // storage slot 2
function getSlot(uint256 _index) public pure returns (uint256 slot) {
uint256 baseSlot;
assembly {
// `.slot` returns the state variable (balance) location within the storage slots.
// In our case, balance.slot = 6
baseSlot := myDynamicArr.slot
}
slot = uint256(keccak256(abi.encode(baseSlot))) + _index;
}
function getSlotValue(uint256 slot) public view returns (bytes32 value) {
assembly {
value := sload(slot)
}
}
}
- 数据类型变成
uint64[] private myDynamicArr = [3, 4, 5, 9, 7]; // storage slot 2
:- 一个
slot
最多存储4
个数组参数 - 因此,起始位置
index=0
的slot
能够存储3,4,5,9
四个参数 - 剩余参数顺眼至下一个
slot
- 因此,起始位置
index=1
的slot
存储7
- 一个
多维不定长数组
- array[][]…
uint64[][] private nestedDynamicArray = [[2, 9, 6, 3, 2], [7, 4, 8, 10, 2]];
baseSlot
存储当前数组大小,示例大小为2
- 进一步确认内部嵌套的数组大小,存储内部数组大小的
slot_array_size
和 数组index
相关:slot_array_size = keccak256(baseSlot)+ index
- 嵌套数组内部的数据
slot_array_data_loc
和slot_array_size
以及内部internal_index
相关:slot_array_data_loc = keccak256(slot_array_size)+ internal_index
- 嵌套数组内部的数据存储按照单维存储规则
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;
contract MyNestedArray {
uint256 private someNumber; // storage slot 0
// Initialize nested array
uint64[][] private nestedDynamicArray = [[2, 9, 6, 3, 2], [7, 4, 8, 10, 2]]; // storage slot 1
function getArrSizeLoc_BaseSlot_Index(uint256 index)
public
pure
returns (uint256 slot)
{
uint256 baseSlot;
for (uint256 i; i <= index; i++) {
if (i == 0) {
assembly {
// `.slot` returns the state variable (balance) location within the storage slots.
// In our case, balance.slot = 6
baseSlot := nestedDynamicArray.slot
}
}
slot = uint256(keccak256(abi.encode(baseSlot))) + index;
}
}
function getArrDataLoc_SlotLoc_InternalIndex(uint256 locslot, uint256 index)
public
pure
returns (uint256 slot)
{
slot = uint256(keccak256(abi.encode(locslot))) + index;
}
function getSlot(
uint256 baseSlot,
uint256 _index1,
uint256 _index2
) public pure returns (bytes32 _finalSlot) {
// keccak256(baseSlot) + _index1
uint256 _initialSlot = uint256(keccak256(abi.encode(baseSlot))) +
_index1;
// keccak256(_initialSlot) + _index2
_finalSlot = bytes32(
uint256(keccak256(abi.encode(_initialSlot))) + _index2
);
}
function getSlotValue(uint256 _slot) public view returns (uint256 value) {
assembly {
value := sload(_slot)
}
}
function addArray() external {
nestedDynamicArray.push([22, 6, 99, 14]);
}
}
数据:[[2, 9, 6, 3, 2], [7, 4, 8, 10, 2]],Array[array1,array2]
动态数据类型存储
说明 | slot | Value |
---|---|---|
baseSlot | 1 | 2 |
array1 存储size的slot,s1 | keccack(1)+0= 80084422859880547211683076133703299733277748156566366325829078699459944778998 | 5 |
array2 存储size的slot,s2 | keccack(1)+1= 80084422859880547211683076133703299733277748156566366325829078699459944778999 | 5 |
array1 存储index0的数据slot,d1 | keccack(s1)+0= 82253526175936117417672031222849803842933200219522072251142807856800200228130 | 0x0000000000000003000000000000000600000000000000090000000000000002 |
array1 存储index1的数据slot,d2 | keccack(s1)+1= 82253526175936117417672031222849803842933200219522072251142807856800200228131 | 0x0000000000000000000000000000000000000000000000000000000000000002 |
array2 存储index0的数据slot,d3 | keccack(s2)+0= 106053296617608346790393806727882046642653284128270527600775845709961105489201 | 0x000000000000000a000000000000000800000000000000040000000000000007 |
array2 存储index1的数据slot,d4 | keccack(s2)+1= 106053296617608346790393806727882046642653284128270527600775845709961105489202 | 0x0000000000000000000000000000000000000000000000000000000000000002 |
Solidity参数
动态数据类型存储
String
- string 作为动态类型:
- 短
string(<31bytes),baseSlot
存储string以及size(len(string) * 2)
,string
中每个字符占据两位 - 短
string
,string data
编码在高位,size
编码在低位
- 短
- 长
string(>31bytes)
,baseSlot
仅存储size(len(string) * 2 + 1)
- 长
string data
存储的起始slot = keccak256(baseSlot)
,依次顺延存储
size
- 长/短
string
的size
计算方式不同的原因在于让字节码更好的区分string
的长短 - 短
string
的size = len(string)*2
表示偶数,因此最后一位一定是0
- 长
string
的size = len(string)*2+1
表示奇数,最后一位一定是1
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;
contract ModMethod {
// Gas cost: 326
function isEvenMod(uint256 num) public pure returns (bool x) {
x = (num % 2) == 0;
}
// Gas cost: 272
function isEvenAnd(uint256 num) public pure returns (bool x) {
x = (num & 1) == 0;
}
}
Bytes
bytes
动态数组slot
存储规则和string
一致bytes0x
固定大小数组 和array
固定数组存储规则一致
Solidity参数
动态数据类型存储
Struct
- 结构体内部数据直接从
baseSlot
依次按照类型存储
Slot 存储读取
sload
读取当前slot
位置的value
sstore
更新当前slot
位置的value
,sload
和sstore
将参数全部作为bytes32
处理,更节省gasFee
.slot
返回当前参数的baseSlot
读取slot数据
// SPDX-License-Identifier: MIT
pragma solidity =0.8.26;
contract slotLoc {
uint256 private someNumber = 5; // storage slot 0
struct Payment {
address payee;
uint128 payId;
uint256 payPrice;
}
Payment private payment =
Payment(
address(0x1), // storage slot 1
12345, // storage slot 2
22 // storage slot 3
);
address private someAddress = address(0x2); // storage slot 4
uint32[] private myDynamicArr = [3, 4, 5, 9, 7]; // storage slot 5
function getSlot()
public
pure
returns (
uint256 numslot,
uint256 paymentslot,
uint256 addressslot
)
{
assembly {
// `.slot` returns the state variable (balance) location within the storage slots.
// In our case, balance.slot = 6
numslot := someNumber.slot
paymentslot := payment.slot
addressslot := someAddress.slot
}
}
function getSlotValue(uint256 slot) public view returns (bytes32 value) {
assembly {
value := sload(slot)
}
}
function sstore_x(uint256 newval) public {
assembly {
sstore(someNumber.slot, newval)
}
}
}
Preference
https://www.rareskills.io/post/solidity-dynamic
User Defined Types
- Type xx is xxx
- 用户定义类型别名,通过
wrap/unwrap
包装和解包装
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// Code copied from optimism
// https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/dispute/lib/LibUDT.sol
type Duration is uint64;
type Timestamp is uint64;
type Clock is uint128;
library LibClock {
function wrap(Duration _duration, Timestamp _timestamp)
internal
pure
returns (Clock clock_)
{
assembly {
// data | Duration | Timestamp
// bit | 0 ... 63 | 64 ... 127
clock_ := or(shl(0x40, _duration), _timestamp)
}
}
function duration(Clock _clock) internal pure returns (Duration duration_) {
assembly {
duration_ := shr(0x40, _clock)
}
}
function timestamp(Clock _clock)
internal
pure
returns (Timestamp timestamp_)
{
assembly {
timestamp_ := shr(0xC0, shl(0xC0, _clock))
}
}
function unwrap(Clock clock_)
internal
pure
returns (Duration _duration, Timestamp _timestamp)
{
_duration = duration(clock_);
_timestamp = timestamp(clock_);
}
}
contract userDefinedValue {
function wrap_uvdt() external view returns (Clock clock) {
// Turn value type into user defined value type
Duration d = Duration.wrap(1);
Timestamp t = Timestamp.wrap(uint64(block.timestamp));
// Turn user defined value type back into primitive value type
// uint64 d_u64 = Duration.unwrap(d);
// uint64 t_u54 = Timestamp.unwrap(t);
clock = LibClock.wrap(d, t);
}
function unwrap_uvdt(Clock clock)
external
pure
returns (Duration d, Timestamp t)
{
(d, t) = LibClock.unwrap(clock);
}
}
函数
函数修饰符
function <function name> (<parameter types>) {internal|external|public|private} [pure|view|payable|virtual|override|Modifier] [returns (<return types>)]
- internal:未公开的函数,只能在合约内部使用,不能通过
abi
直接/外部调用,允许在继承的子合约中使用 - external:公开的函数,可以通过
abi
直接调用,也可以在本合约内通过 this.func 调用,允许在继承的子合约中使用 - public:公开的函数,可以通过
abi
直接调用,允许在继承的子合约中使用 - private: 未公开的函数,只能在合约内部使用,不允许通过
abi
直接调用,不允许在继承的子合约中使用 - pure: 只读,函数只能读取局部变量
- view: 只读,函数可以读取状态变量、局部变量、全局变量
- payable:
payable
修饰符表明函数支持接收NativeToken(msg.value!=0)
- virtual: 虚函数,表明函数允许重载修改内部逻辑, 多用于接口合约
- override: 重载函数,重新定义函数内部逻辑,但是函数
selector
必须保持一致(函数名称、函数参数类型和数量) - Modifier: 修饰器,内部定义条件判断, 允许在继承的子合约中使用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract parentOne {
uint256 public oneValue;
function OneInternal() internal virtual {
oneValue += 1;
}
function OneExternal() external virtual {
oneValue += 2;
}
function OnePublic() public virtual {
oneValue += 3;
}
function OnePrivate() private {
oneValue += 4;
}
}
contract functionsChecker is parentOne {
function addOne() external {
OneInternal();
}
// function addTwo() external {
// this.OneExternal();
// }
function addThree() external {
super.OnePublic(); // super调用最近继承者合约的内部函数, +3
}
// function addFive() external {
// OnePublic(); //调用本合约的函数,+ 5
// }
function OnePublic() public virtual override {
//重写函数,修改函数逻辑
oneValue += 5;
}
//receive() external payable {}
}
函数
构造函数
- 构造函数用于初始化合约参数
- 允许传参,初始化状态变量
- 支持
payable
修饰,允许合约部署时msg.value!=0
constructor(parameters)[payable] {
// to-do
}
- 构造函数编码在 initCode,并不会存储上链,仅用来初始化合约参数
- 构造函数的代码不会存储上链,但是构造函数的传参会编码到合约代码
- 如果构造函数内部逻辑为空,那么链上可执行的合约代码和该函数无关
- 继承合约时,需要传参初始化继承的父合约的构造函数
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
contract tree {
uint256[] Ids;
constructor(uint256 id) {
Ids.push(id);
}
}
contract leaf is tree {
constructor(uint256 id) tree(id) {}
}
函数修饰器
modifier
修饰符用于判断合约方法的执行前置条件
- 将函数内部的语句放置在
modifier
函数的-
中执行判断 - 允许继承使用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract modofier {
address owner; //slot 0
modifier onlyOwner() {
require(msg.sender == owner, "Not_Owner"); // 判断 slot0存储的值和当前发送者的地址是否一致
_;
}
}
contract modofierContract is
modofier // 继承父合约的状态变量,private变量继承后无法更新状态值
{
constructor() {
address _addr = msg.sender;
assembly {
sstore(owner.slot, _addr)
}
}
function getValue(uint256 slot) external view returns (address addr) {
assembly {
addr := sload(slot)
}
}
function changeOwner(address _newowner) public onlyOwner {
owner = _newowner;
}
}
函数选择器
selector
- 选择器和
函数名、函数参数
相关,和函数&参数的修饰符无关,bytes4(keccak256(abi.encodePacked(functionName)))
- 计算选择器时,函数参数之间无空格
- 使用4个字节的函数选择器降低
msg.data
的size
(函数名称可以无限长,选择器就是4bytes
) - 函数选择器不会和
fallback()
进行匹配,只有在全部的功能函数没匹配上的时候,才会调用fallback()
函数 - 因此,
solidity
支持同名不同参数的函数, 因为函数选择器的hash
值不一样
selector外部调用
- abi.encodePacked
- abi.encodeWithSelector
- abi.encodeWithSignature
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract SelectorTest {
uint256 public x;
event Sig(bytes4 sig);
function foo(uint256 num) public {
emit Sig(msg.sig); //this.foo.selector
x += num;
}
function func(uint256 num) external {
x += num;
}
function func(uint256 num, bool flag) external {
if (flag) {
x += num;
}
}
function getSelectorOfFoo() external pure returns (bytes4) {
return this.foo.selector; // 0xc2985578
}
}
contract CallFoo {
event Sig(bytes4 sig);
function callFooLowLevel(SelectorTest _contract, uint256 num) external {
// _contract.foo(num);
// bytes4 fooSelector = 0xc2985578;
emit Sig(msg.sig); // this.callFooLowLevel.selector
bytes4 fooSelector = SelectorTest.foo.selector; //bytes4(keccak256("foo(uint256)"))
(bool ok, ) = address(_contract).call(
abi.encodePacked(fooSelector, num)
);
// | (bool ok, ) = address(_contract).call(abi.encodeWithSelector(fooSelector, num));
// | (bool ok, ) = address(_contract).call(abi.encodeWithSignature("foo(uint256)", num));
require(ok, "call failed");
}
}
Reference
https://www.rareskills.io/post/function-selector
转账
receive() external payable {}
,函数用于接收NativeToken
(EOA
直接转账,msg.value !=0
)fallback()external payable{}
,函数缺省情况(包括转账后没有receive()
方法)- send 执行转账
- 传递
2300
的gas
- 转账返回
boolean
- 转账失败不会
revert
交易,因此,需要判断转账结果
- 传递
- transfer
- 传递
2300
的gas
- 转账失败的话,整笔交易回滚
- 传递
- call的转账
- 默认会发送 63/64 gas
- 返回
bool
和data
bool
表示转账是否成功data
返回执行调用的结果
- 交易失败的话不会回滚。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract receiver {
constructor() {}
event ReceiveReceived(address Sender, uint256 Value);
event FallbackReceived(address Sender, uint256 Value);
receive() external payable {
emit ReceiveReceived(msg.sender, msg.value);
}
fallback() external payable {
emit FallbackReceived(msg.sender, msg.value);
}
}
contract payer {
constructor() payable {}
function sendValue(address payable recipient, uint256 amount) external {
bool success = recipient.send(amount); //ReceiveReceived
if (!success) {
revert("Send Trans Failure");
}
}
function transferValue(address payable recipient, uint256 amount) external {
recipient.transfer(amount); //ReceiveReceived
}
function callSendValue(
address payable recipient,
uint256 amount,
bytes memory _data
) external {
(bool success, ) = recipient.call{value: amount}(_data); //ReceiveReceived
if (!success) {
revert("call_send_value_failure");
}
}
// 11053 gas cost
function safeTransferNativeToken(
address to,
uint256 amount,
uint256 txGas
) external {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Transfer the ETH and store if it succeeded or not.
//success := call(gas(), to, amount, 0, 0, 0, 0)
success := call(txGas, to, amount, 0, 0, 0, 0)
}
require(success, "ETH_TRANSFER_FAILED");
}
}
异常捕获
Solidity
将异常情况分为三类:前置(require
)、后置(assert
)、抛出异常(revert
):
三种异常通过 try-catch
捕获处理
require 前置判断
require(condition,"ErrorMsg“)
, which throws if the condition is false- 条件不满足时就会抛出异常,输出异常字符串(
ErrorMsg
) gas
花销和描述异常的字符串的长度正相关
- 条件不满足时就会抛出异常,输出异常字符串(
require(
address(this).balance >= value,
"Address: insufficient balance for call"
);
assert 后置判断
assert(condition)
, which throws if the condition is false- 一般作为后置条件判断,但是无法抛出具体的异常信息
assert (balanceAfter < balanceBefore);
revert
revert() | revert(ErrorMsg) | revert Error()
- 可以通过
revert()
直接抛出异常 - 异常内支持添加异常信息
revert(ErrorMsg)
- 允许抛出自定义的
Error()
, 通过revert Error()
抛出
- 可以通过
error TransferNotOwner(); // 自定义error
function transferOwner1(uint256 tokenId, address newOwner) public {
if(_owners[tokenId] != msg.sender){
revert TransferNotOwner();
}
_owners[tokenId] = newOwner;
}
try catch
Lower-level
lower-level的调用在EVM层面,直接返回 boolean,bytes
,让用户自行捕获异常:
- 外部调用的合约函数执行出现了
revert
- 外部调用的合约函数执行了非法逻辑(
/0,out-of-bound
) - 外部调用的合约函数
out-of-gas
异常捕获
try external function calls and contract creation calls{}catch
try
用户捕获外部函数调用的异常- 不可用于
selector
调用的异常捕获 catch
匹配异常- Panic(errorCode) via assert
- 0x00,编译器错误
- 0x01,assert 报错
- 0x11,数字越界错误
- 0x12,/0
- 0x21,转化枚举类型时:传参负数或越界
- 0x22,访问错误编码的bytes数组
- 0x31,空数组pop
- 0x32,数组越界
- 0x41,申请内存超额或数组太大
- 0x51,访问局部变量
- 返回数据:
bytes4(keccak256(”Panic(uint256)”))) + encode_uint256
- Error via require|revert
require(bool) = if(bool){revert()}
,直接 revert(),返回数据为空require(bool, string) = if(bool){revert(string)}
,返回数据是Error(string)
的编码require(bool, UserCustomError()) = if(bool){revert UserCustomError()}
,报用户自定义错误,返回数据是bytes4(keccak256("UserCustomError()")))
- Panic(errorCode) via assert
function callContractB() external view {
try functionFromAnotherContract() {
//<-- Handle the success case if needed
} catch Panic(uint256 errorCode) {
//<-- handle Panic errors
} catch Error(string memory reason) { //revert(string), require(false, “reason”)
//<-- handle revert with a reason
} catch (bytes memory lowLevelData) { //revert UserCustomError(),require(bool, UserCustomError())
//<-- handle every other errors apart from Panic and Error with a reason
// revert without a message
if (lowLevelData.length == 0) {
console.log("revert without a message occured");
}
// Decode the error data to check if it's the custom error
if (bytes4(abi.encodeWithSignature("CustomError(uint256)")) ==bytes4(lowLevelData)
) {
// handle custom error
console.log("CustomError occured here");
}
}
}
Preference
https://www.rareskills.io/post/try-catch-solidity
import
import
导包在声明版本号后,在合约代码前import {contractName} from '...'
- 导入合约后,相当于引入了完整的合约文件
import 导包
- 导入本地文件 >import {Address} from ‘./Address.sol’;
- 从网页导入
import {Address} from “https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/refs/heads/master/contracts/utils/Address.sol”;
- 通过npm 本地包导入
import “@openzeppelin/contracts/utils/Address.sol”;
包内函数
- 直接使用:通过合约名直接调用引入的合约函数
- 继承使用:除library外的合约,继承后可以直接使用包内函数、状态变量
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {IERC20} from "https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/refs/heads/master/contracts/token/ERC20/IERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
contract sendValue {
function tokenAllowance(
address token,
address owner,
address spender
) public view returns (uint256) {
return IERC20(token).allowance(owner, spender); //Interface contracts
}
function transferValue(address beneficiary, uint256 value) public payable {
Address.sendValue(payable(beneficiary), value); //Library contracts
}
function allowance(address owner, address spender)
external
view
returns (uint256)
{}
}
创建合约
CREATE
- 新合约地址
address = keccak256(rlp([sender_address,sender_nonce]))[12:]
- 同一条链上的账户地址
nonce
递增,因此账户在相同链上无法部署同样的合约账户 - 同样地址在不同链上,能过够通过相同
nonce
值部署相同地址的合约
- 同一条链上的账户地址
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract FindSC {
function contractAddressFrom(address _origin, uint256 _nonce)
public
pure
returns (address)
{
bytes memory data;
if (_nonce == 0x00)
data = abi.encodePacked(
bytes1(0xd6),
bytes1(0x94),
_origin,
bytes1(0x80)
);
else if (_nonce <= 0x7f)
data = abi.encodePacked(
bytes1(0xd6),
bytes1(0x94),
_origin,
uint8(_nonce)
);
else if (_nonce <= 0xff)
data = abi.encodePacked(
bytes1(0xd7),
bytes1(0x94),
_origin,
bytes1(0x81),
uint8(_nonce)
);
else if (_nonce <= 0xffff)
data = abi.encodePacked(
bytes1(0xd8),
bytes1(0x94),
_origin,
bytes1(0x82),
uint16(_nonce)
);
else if (_nonce <= 0xffffff)
data = abi.encodePacked(
bytes1(0xd9),
bytes1(0x94),
_origin,
bytes1(0x83),
uint24(_nonce)
);
else
data = abi.encodePacked(
bytes1(0xda),
bytes1(0x94),
_origin,
bytes1(0x84),
uint32(_nonce)
);
return address(uint160(uint256(keccak256(data))));
}
}
CREATE2
creation_code = memory[offset:offset+size]
address = keccak256(0xff + sender_address + salt + keccak256(creation_code))[12:]
- 通过自定义的
salt
和合约代码
替换递增的nonce
值合约代码
一般选择合约的 creationCodecreationCode
包含完整的合约代码initCode + runtimeCode
,在构造函数发生任何变化后,也会造成合约地址的变化
- 在相同链上通过相同的
salt
和合约代码
,就可以实现同地址合约的提前使用
New
- 新合约地址:
Contract x = new Contract{value: _value,salt: salt}(params)
new
关键字创建新的合约地址value
表明创建合约是是否转账,构造函数需要使用payable
修饰salt
表明当前新建地址是否采用CREATE2
关键字constructor parameters
表明新建合约时传递的初始化参数
Solidity Contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// This is the older way of doing it using assembly
contract CreateNewContract {
event Create2Created(address addr, bytes32 salt);
event CreateCreated(address addr);
// 1. Deploy the contract by CREATE
function Create(address _owner, uint256 _num) external payable {
bytes memory bytecode = getBytecode(_owner, _num);
_create(bytecode, "");
}
// 2. Deploy the contract by CREATE2
function Create2(
address _owner,
uint256 _num,
bytes32 salt
) external payable {
bytes memory bytecode = getBytecode(_owner, _num);
_create(bytecode, salt);
}
function _create(bytes memory bytecode, bytes32 salt) internal {
address addr;
if (keccak256(abi.encode(salt)) == keccak256("")) {
/*
NOTE: How to call create
create(v, p, n)
create new contract with code at memory p to p + n
and send v wei
and return the new address
*/
assembly {
addr := create(
callvalue(), // wei sent with current call
// Actual code starts after skipping the first 32 bytes
add(bytecode, 0x20),
mload(bytecode) // Load the size of code contained in the first 32 bytes
)
if iszero(extcodesize(addr)) {
revert(0, 0)
}
}
emit CreateCreated(addr);
} else {
/*
NOTE: How to call create2
create2(v, p, n, s)
create new contract with code at memory p to p + n
and send v wei
and return the new address
where new address = first 20 bytes of keccak256(0xff + address(this) + s + keccak256(mem[p…(p+n)))
s = big-endian 256-bit value
*/
assembly {
addr := create2(
callvalue(), // wei sent with current call
// Actual code starts after skipping the first 32 bytes
add(bytecode, 0x20),
mload(bytecode), // Load the size of code contained in the first 32 bytes
salt // Salt from function arguments
)
if iszero(extcodesize(addr)) {
revert(0, 0)
}
}
emit Create2Created(addr, salt);
}
}
// 3. Deploy the create contract by new
function NewCreate(address _owner, uint256 _num)
public
payable
returns (address)
{
return address(new targetContract{value: msg.value}(_owner, _num));
}
// 4. Deploy the create2 contract by new
function NewCreate2(
address _owner,
uint256 _num,
bytes32 salt
) public payable returns (address) {
// This syntax is a newer way to invoke create2 without assembly, you just need to pass salt
// https://docs.soliditylang.org/en/latest/control-structures.html#salted-contract-creations-create2
return
address(
new targetContract{salt: salt, value: msg.value}(_owner, _num)
);
}
// Get bytecode of contract to be deployed
// NOTE: _owner and _num are arguments of the targetContract's constructor
function getBytecode(address _owner, uint256 _num)
internal
pure
returns (bytes memory)
{
bytes memory bytecode = type(targetContract).creationCode;
return abi.encodePacked(bytecode, abi.encode(_owner, _num));
}
// Compute the address of the contract to be deployed
// NOTE: _salt is a random number used to create an address
function getAddress(
address _owner,
uint256 _num,
bytes32 salt
) external view returns (address) {
bytes memory _bytecode = type(targetContract).creationCode;
bytes memory bytecode = abi.encodePacked(
_bytecode,
abi.encode(_owner, _num)
);
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(bytecode)
)
);
// NOTE: cast last 20 bytes of hash to address
return address(uint160(uint256(hash)));
}
}
contract targetContract {
address public owner;
uint256 public num;
constructor(address _owner, uint256 _num) payable {
owner = _owner;
num = _num;
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
contract-creation
读取内存数据
codecopy 从内存中取出数据
destOffset
: 内存中读取数据的起始位置offset
: 待拷贝数据的起始位置size
: 拷贝数据的长度
Introduction-creation code
- 合约编译成两部分:
<initCode> + <Runtimecode>(part1_runtimeCode + part2_Metadata)
Miniature Contract Examples
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17; // optimizer: 200 runs
contract Minimal {
// 空 constructor()函数不影响合约部署的字节码,存在或不存在 constructor()函数,字节码都一样
constructor() payable {}
}
编译后的字节码:
0x6080604052603f8060116000396000f3fe
+ 6080604052600080fdfea2646970667358221220a4c95008952415576a18240f5049a47507e1658565a8ec11a634c25a9aa17cf164736f6c63430008110033
initCode
initcode
执行codecopy
,栈内数据自栈顶向下依次存在值:00,11,3f,3f
- 从内存
0index
开始,间隔0x11=17
开始,跳过initCode
部分,读取runtimeCode
的0x3f=63bytes
数据
- 从内存
return
返回内存数据,除initCode
部分的值,作为合约数据存储上链
payable|Non-payable constructor()
payable
修饰的构造函数下的initCode
:0x6080604052603f8060116000396000f3fe
Non-payable
修饰的构造函数下的initCode
:0x6080604052348015600f57600080fd5b50603f80601d6000396000f3fe
Non-payable
的initCode
更大- 构造函数需要执行
msg.value
的判断:348015600f57600080fd5b50 (12 bytes)
<initCode> + <extra 12 byte sequence (payable case)>
Non-payable evm codes
// check the amount of wei that was sent
CALLVALUE
DUP1
ISZERO
// Jump to 0x0f (contract deployment step)
PUSH1 0x0f
JUMPI
// revert if wei sent is greater than 0
PUSH1 0x00
DUP1
REVERT
RuntimeCode
RuntimeCode
由两部分组成:<Runtimecode>(part1_runtimeCode + part2_Metadata)
part1_runtimeCode
存储真正的合约逻辑代码,进行函数选择器的匹配和处理part2_Metadata
由两部分组成:当前合约编译环境等源数据 + 构造函数传参
- 在空合约中,
part1_runtimeCode
为空,但是part2_Metadata
源数据不为空
Constructor without parameters
pragma solidity 0.8.17;
contract Runtime {
address lastSender;
constructor () payable {}
receive() external payable {
lastSender = msg.sender;
}
}
编译后的字节码:<initCode> + <Runtimecode>(part1_runtimeCode + part2_Metadata)
>part1_runtimeCode := 0x608060405236601c57600080546001600160a01b03191633179055005b600080fdfe
>
>part2_Metadata := 0xa2646970667358221220e9b731ab28726d97cbf5219f1e5eaec508f23254c60b15ed1d3456572547c5bf64736f6c63430008070033a2646970667358221220e9b731ab28726d97cbf5219f1e5eaec508f23254c60b15ed1d3456572547c5bf64736f6c63430008070033
- part1_runtimeCode
msg.data != ""
, 由于不存在fallback()
函数,直接revert
msg.value != ""
, 由于存在receive()
函数,直接跳转到receive
函数执行逻辑
Constructor with parameters
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17; // optimizer: 200 runs
contract Minimal {
// optimizer: 200contract MinimalLogic {
uint256 private x;
constructor(uint256 _x) payable {
x = _x;
}
}
编译后的字节码:<initCode> + <Runtimecode>(part1_runtimeCode + part2_Metadata)
>“Init code”: 0x608060405260405160893803806089833981016040819052601e916025565b600055603d565b600060208284031215603657600080fd5b5051919050565b603f80604a6000396000f3fe“Runtime code (metadata only)“: 0x6080604052600080fdfea26469706673582212204a131c1478e0e7bb29267fd8f6d38a660b40a25888982bd6618b720d4498b6b464736f6c63430008070033“Constructor arguments”:0000000000000000000000000000000000000000000000000000000000000001
- initCode
- 构造函数需要一个
address
类型的传参,传参无效的话,直接revert
- 初始化传参变量
- 构造函数需要一个
- part1_runtimeCode
msg.data != ""
, 由于不存在fallback()
函数,直接revert
msg.value != ""
, 由于不存在receive()
函数,直接revert
- part2_Metadata
- 当前合约编译环境等源数据
- 构造函数的传参编码到源数据最后
selfdestruct
- selfdestruct 关键字强制将当前合约余额转给传参地址
- 自毁关键字下的转账行为是不可控,任何合约内部执行 selfdestruct 后都会把余额强制转给传参地址
Dencun upgrade
Dencun
升级后针对 selfdestruct
的问题进行了修正升级:
selfdestruct
交易仅仅将合约余额发送到传参地址, 并且不会清除合约状态和代码- 销毁后,不用再次通过 CREATE2 部署相同地址合约,因为原地址的状态和代码还保留在链上
- 销毁后,任何的外部调用 call|delegateCall 也不会失败
- 也不会存在代币锁仓的隐患,因为之前合约的逻辑和状态并未清空
Solidity Examples
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Sale {
uint256 public num;
function buy() external payable {
num += 1;
// _end();
}
function _end() public {
// 仍然保留 num 状态变量的参数
selfdestruct(payable(msg.sender));
}
function bal() external view returns (uint256 baln) {
baln = address(this).balance;
}
}
contract caller {
constructor() payable {}
function buy(Sale sale) external {
(bool success, ) = address(sale).call{value: 100 wei}(
abi.encodeWithSelector(sale.buy.selector)
);
if (!success) {
revert();
}
}
}
event事件
Event
事件是EVM上日志的抽象,具有两大特点:
emit
触发事件,可以直接过滤、订阅事件- 经济友好,通过
Event
存储数据,每次存储大概花销2000gas
event Transfer(address indexed from, address indexed to, uint256 value);
事件Transfer内部参数表示需要记录在链上的变量类型和参数名称
- 通过
indexed
关键字,单独将事件参数作为一个topic
进行存储和索引 - 一个
Event
最多拥有三个indexed
数据 - 没有
indexed
关键字修饰的参数作为data
变量存储在data
域中,作为参数解析
Golang 解析 event事件
func ParseLogFromReceipet(contract, txhash string) {
receipet, err := client.TransactionReceipt(context.Background(), common.HexToHash(txhash))
if err != nil {
log.Fatalf("Error in get TransactionReceipt: %s", err)
}
if receipet.Status == 0 {
fmt.Printf("Tx has failed\n")
return
}
logs := receipet.Logs
ParseLogs(logs, contract)
}
func ParseLogs(logs []*types.Log, contract string) bool {
for _, logData := range logs {
if logData.Address != common.HexToAddress(contract) {
continue
}
Sender := common.HexToAddress(logData.Topics[1].Hex())
types, _ := strconv.ParseInt(hexutil.Encode(logData.Topics[2].Bytes())[2:], 16, 64)
uuid, _ := strconv.ParseInt(hexutil.Encode(logData.Topics[3].Bytes())[2:], 16, 64)
fmt.Printf("Contract: %s, Sender: %s, types: %d, uuid: %d\n", contract, Sender, types, uuid)
return true
}
return false
}
event topic过滤
同时过滤topic1 + topic2 的 logs
for startBlockHeight < latestblockNum {
toblock := startBlockHeight + 10000
if toblock > latestblockNum {
toblock = latestblockNum
}
query := ethereum.FilterQuery{
FromBlock: big.NewInt(startBlockHeight),
ToBlock: big.NewInt(toblock),
Addresses: []common.Address{common.HexToAddress(contract)},
Topics: [][]common.Hash{{common.HexToHash("topic1"), common.HexToHash("topic2")}},
}
logs, err := mainnetLPClient.FilterLogs(context.Background(), query)
if err != nil {
fmt.Printf("Error in filter logs: error:%v", err)
return
}
}
contract-codes
判断是否是合约地址的三种方式
msg.sender==tx.origin
EXTCODESIZE
读取code.length
EXTCODEHASH
读取code.hash
msg.sender==tx.origin
tx.origin
是当前交易的签名地址msg.sender
是当前EVM
执行环境中的交易发送地址- 对于
EOA
直接发起的的合约交易,合约内部:msg.sender==tx.origin
- 合约之间外部调用重启
EVM
执行环境的外部调用:msg.sender!=tx.origin
EXTCODESIZE
codeSize = 0
, 不表示该地址一定是EOA
地址- 有可能是预定义的
create2
地址,未来会部署成为合约 - 对方在
constructor()
中调用call
交易,此时合约未存储上链,codeSize = 0
- 有可能是预定义的
- selfdestruct在
dencun
升级后并不会清除合约状态和代码,因此 codeSize != 0
Solidity Examples
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract TestAddressSize {
event codeSize(uint256);
function constructorCodeSize() external {
uint256 codes;
address sender = msg.sender;
assembly {
codes := extcodesize(sender)
}
emit codeSize(codes);
}
// 765 gas
function codesize(address target) public view returns (bool isContract) {
if (target.code.length == 0) {
isContract = false;
} else {
isContract = true;
}
}
// 779 gas
function codesizeAssm(address target) public view returns (bool) {
uint256 size;
assembly {
size := extcodesize(target)
}
return size != 0;
}
}
contract onlyConstructor {
constructor() {
address addr = 0x10E2fC1dE57DDC788489122151a6c45254D3ba59;
(bool success, ) = addr.call(
abi.encodeWithSignature("constructorCodeSize()")
);
if (!success) {
revert();
}
}
// [
// {
// "from": "0x10E2fC1dE57DDC788489122151a6c45254D3ba59",
// "topic": "0x35bbf8dac6652434e49dd256e75001562f8cabc9ab024b4ed3f7826b3ab5a81f",
// "event": "codeSize",
// "args": {
// "0": "0"
// }
// }
// ]
}
contract afterSelfDestruct {
function deposit() external payable {}
// 销毁后并不会影响合约的使用
// 销毁保留合约的状态的代码
// 销毁仅仅是强制将合约余额转出
function kill() external {
selfdestruct(payable(msg.sender));
}
}
EXTCODEHASH
- 返回
keccak256(codes)
- 如果地址
balance == 0 && codeSize == 0
, 返回bytes32(0)
- 如果地址
balance != 0 && codeSize == 0
, 返回keccak256(""")=0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
- 如果地址
codeSize != 0
, 返回keccak256(codes)
- 全部 预编译合约预存
1wei
,因此会返回空值的hash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract TestAddressSize {
event codeHash(bytes32);
function constructorCodeHash() external {
bytes32 codehashs;
address sender = msg.sender;
assembly {
codehashs := extcodehash(sender)
}
emit codeHash(codehashs);
}
// gas
function codehash(address target) public view returns (bytes32 hash) {
hash = target.codehash;
}
// gas
function codesizeAssm(address target) public view returns (bytes32 hash) {
assembly {
hash := extcodehash(target)
}
}
}
contract onlyConstructor {
//0x0000000000000000000000000000000000000000000000000000000000000000
constructor() {
address addr = 0x16a90f9ec7A46514b47487bDc2F00d11740c3BA0;
(bool success, ) = addr.call(
abi.encodeWithSignature("constructorCodeSize()")
);
if (!success) {
revert();
}
}
}
contract afterSelfDestruct {
function deposit() external payable {}
// before kill: 0xd0516dff3313077772b6176aa83c5ad4da898b2f66e2dd058b612dbace072fbc
// after kill: 0xd0516dff3313077772b6176aa83c5ad4da898b2f66e2dd058b612dbace072fbc
// 销毁后并不会影响合约的使用
// 销毁保留合约的状态的代码
// 销毁仅仅是强制将合约余额转出
function kill() external {
selfdestruct(payable(msg.sender));
}
}
判断地址是否是合约地址
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library ContractAddress {
function isContract(address contractAddress) internal view returns (bool) {
bytes32 existingCodeHash = contractAddress.codehash;
// https://eips.ethereum.org/EIPS/eip-1052
// keccak256('') == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
return
existingCodeHash != bytes32(0) &&
existingCodeHash != 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;
}
}
接口
- 接口合约定义函数骨架
- 接口合约通过interface关键字修饰
- 接口合约不能继承除接口外的其他合约
- 接口合约不能定义状态变量
- 接口合约不能包含构造函数
- 接口函数必须使用external修饰
- 接口函数不需要virtual修饰,因为继承接口需要实现接口内的全部函数
- 继承接口合约,必须实现接口内的全部函数
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
interface IERC721 {
event Getbal(address indexed owner);
function balanceOf(address owner) external returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner);
}
contract myToken is IERC721 {
mapping(address => uint256) balances;
function deposit() public payable {
balances[msg.sender] += 1000;
}
function balanceOf(address owner) external returns (uint256) {
emit Getbal(owner);
return balances[owner];
}
function ownerOf(uint256 tokenId) external view returns (address owner) {}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
}
contract interactBAYC {
// 利用BAYC地址创建接口合约变量(ETH主网)
IERC721 BAYC = IERC721(0xAc40c9C8dADE7B9CF37aEBb49Ab49485eBD3510d);
// 通过接口调用BAYC的balanceOf()查询持仓量
function balanceOfBAYC(address owner) external returns (uint256 balance) {
return BAYC.balanceOf(owner);
}
}
库合约
- 库合约使用library关键字修饰
- 库合约作为完整函数的封包使用
- 库合约不能接收token
- 库合约不能被继承或继承别的合约
- 库合约不能存在状态变量
- 库合约不能定义构造函数
- 库合约的使用分为两种:
- 通过
Using library_name for type
,此时type
类型的变量就可以调用库合约内部函数 - 直接通过
library_name
调用函数
- 通过
库合约修饰符
- 仅带有
Internal
修饰符的库合约函数代码直接被编码到执行合约中,通过jump
跳转执行 - 带有
external
修饰符的库合约,必须先被部署在链上,在将链上的库合约地址 编码到执行合约,执行合约通过delegateCall
库合约 DelegateCall
比jump
跳转执行花费更多的gas
- 并且带有
external
函数的库合约必须先部署在链上,也需要额外的合约部署成本
Solidity Library Internal Function Example
- 仅使用
internal
修饰的library
合约不需要被部署到区块链上 - 库合约的内部函数被编码到当前执行合约中,直接
jump
到当前代码执行
Example: ContractsA
使用 库合约 LibInternal
中的 FuncA
编译合约 ContractA
编译库合约 LibInternal
库合约中的 funcA bytecode 被复制编码到 ContractA bytecode
ContractA bytecode 部署到链上
ContractA 调用 FuncA 的方式和自己内部函数一样,执行 selector 匹配 以及jump 跳转
Solidity Library External Function Example
- 调用任一带有
external
修饰的library
合约函数,必须先把库合约部署到区块链上 - 执行合约通过
delegateCall
调用库合约函数中的external
函数
Example: ContractsB
调用库合约 LibInternal
中的 External
修饰的 FuncA
编译合约 ContractB
编译库合约 LibExternal
LibExternal 首先被部署到链上
ContractB bytecode 中嵌入库合约地址,方便后续执行 delegateCall 调用
ContractB 被部署到链上
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
library Address {
struct st {
string name;
uint256 value;
}
function isContract(address account) internal view returns (bool) {
uint256 size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
}
contract StateToStateContract {
using Address for address;
function isSC(address _addr) public view returns (bool) {
return Address.isContract(_addr); //_addr.isContract();
}
}
preference
Difference of internal and external library
抽象合约
- 抽象合约介于接口合约和完整合约之间,内部存在未定的函数,不能被直接部署
- 抽象通过abstract关键字修饰,内部包含至少一个未实现具体功能的函数方法(没有
{}
) - 抽象合约是用来被继承后补全使用的base contract
逻辑未定函数
- 未实现的函数必须使用virtual修饰,支持后续继承合约的override重写
- 继承函数时,必须继承函数的全部函数、变量、数据结构、修饰器
- 必须override重写函数,补全抽象合约中未定的函数
- 不能修改函数[名称、修饰符、返回类型]、传参的[参数类型、参数数量]
- 允许继承
immutable|constant
变量 immutable
直接在继承时初始化constant
直接继承使用
Solidity Contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation.
/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol)
/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)
/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it.
abstract contract ERC20 {
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(
address indexed owner,
address indexed spender,
uint256 amount
);
/*//////////////////////////////////////////////////////////////
METADATA STORAGE
//////////////////////////////////////////////////////////////*/
string public name;
string public symbol;
uint8 public immutable decimals;
/*//////////////////////////////////////////////////////////////
ERC20 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/*//////////////////////////////////////////////////////////////
EIP-2612 STORAGE
//////////////////////////////////////////////////////////////*/
uint256 internal immutable INITIAL_CHAIN_ID;
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR;
mapping(address => uint256) public nonces;
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
INITIAL_CHAIN_ID = block.chainid;
INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
}
/*//////////////////////////////////////////////////////////////
ERC20 LOGIC
//////////////////////////////////////////////////////////////*/
function approve(address spender, uint256 amount)
public
virtual
returns (bool)
{
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transfer(address to, uint256 amount)
public
virtual
returns (bool)
{
balanceOf[msg.sender] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) public virtual returns (bool) {
uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals.
if (allowed != type(uint256).max)
allowance[from][msg.sender] = allowed - amount;
balanceOf[from] -= amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(from, to, amount);
return true;
}
/*//////////////////////////////////////////////////////////////
EIP-2612 LOGIC
//////////////////////////////////////////////////////////////*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual;
function DOMAIN_SEPARATOR() public view virtual returns (bytes32) {
return
block.chainid == INITIAL_CHAIN_ID
? INITIAL_DOMAIN_SEPARATOR
: computeDomainSeparator();
}
function computeDomainSeparator() internal view virtual returns (bytes32) {
return
keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256(bytes(name)),
keccak256("1"),
block.chainid,
address(this)
)
);
}
/*//////////////////////////////////////////////////////////////
INTERNAL MINT/BURN LOGIC
//////////////////////////////////////////////////////////////*/
function _mint(address to, uint256 amount) internal virtual {
totalSupply += amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal virtual {
balanceOf[from] -= amount;
// Cannot underflow because a user's balance
// will never be larger than the total supply.
unchecked {
totalSupply -= amount;
}
emit Transfer(from, address(0), amount);
}
}
contract myToken is ERC20 {
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
) ERC20(_name, _symbol, _decimals) {}
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) public virtual override {
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED");
// Unchecked because the only math done is incrementing
// the owner's nonce which cannot realistically overflow.
unchecked {
address recoveredAddress = ecrecover(
keccak256(
abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(
keccak256(
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
),
owner,
spender,
value,
nonces[owner]++,
deadline
)
)
)
),
v,
r,
s
);
require(
recoveredAddress != address(0) && recoveredAddress == owner,
"INVALID_SIGNER"
);
allowance[recoveredAddress][spender] = value;
}
emit Approval(owner, spender, value);
}
}
继承合约
继承合约时,也需要初始化合约的构造函数
合约函数的继承
- 按照继承顺序,继承父合约的全部状态变量
immutable
变量需要在 构造函数中声明constant
变量直接继承使用- 按照继承顺序,继承父合约的全部修饰器
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
contract parent {
uint256 public immutable base;
uint256 public constant DAY = 1 days;
uint256 public num;
constructor(uint256 _base) {
base = _base;
}
modifier isOdd(uint256 number) {
require(number % 2 == 0);
_;
}
function checkNumber(uint256 number) external isOdd(number) {
//do someThing
}
}
contract a is parent {
constructor(uint256 _base) parent(_base) {
base = _base;
}
function doAnotherCheck(uint256 number) external isOdd(number) {
// num in slot0
// immutable|constant 直接编码到合约codes,不占slot
num += number;
}
}
- 按照继承顺序,继承父合约的全部函数
override
可以重写函数super
调用父合约的函数,补充增加函数的限制条件
- 后继承的函数会重写之前继承的函数,因此不能菱形继承(函数互相重写依赖)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
/* Graph of inheritance
A
/ \
B C
\
D,
*/
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();// C ->foo() ==>return C
}
}
- 父合约中
private
修饰的状态变量无法直接读取或更新,但可以使用sload|sstore
直接操作slot
- 父合约中
private
修饰的函数无法调用
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
contract parent {
uint256 public immutable base;
uint256 public num; //slot0
uint256 private prinum; //slot1
uint256 public num2; // slot2
constructor(uint256 _base) {
base = _base;
}
modifier isOdd(uint256 number) {
require(number % 2 == 0);
_;
}
function checkNumber(uint256 number) external isOdd(number) {
//do someThing
}
}
contract a is parent {
constructor(uint256 _base) parent(_base) {
base = _base;
}
function getSlotValue(uint256 slot) public view returns (bytes32 value) {
assembly {
value := sload(slot)
}
}
function sstore_x(uint256 slot, uint256 newval) public {
assembly {
sstore(slot, newval)
}
}
}
代理合约
- 代理合约底层采用 delegateCall,分离逻辑和数据。
- 由于逻辑和数据分离,后续可以灵活替换逻辑业务
- 代理合约按照逻辑合约代码去更新/读取
EVM
环境中的slot
的状态变量- 因此替换逻辑合约时,应该保持
slot
的状态变量的顺序 - 新增参数只能在末端添加
- 因此替换逻辑合约时,应该保持
代理合约逻辑
- 用户直接对接代理合约,代理合约存储合约数据
- 代理合约
delegateCall
调用逻辑合约,只能更改代理合约内部的状态变量
- 代理合约
- 逻辑合约仅仅负责代理合约数据处理的逻辑
- 逻辑合约的数据不受代理合约的影响
- 因此,需要独立初始化代理合约和逻辑合约的内部参数
代理合约插槽存储
This is the keccak-256 hash of “eip1967.proxy.implementation” subtracted by 1.
bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
This is the keccak-256 hash of “eip1967.proxy.admin” subtracted by 1.
bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
最简代理合约(不可升级clone)
initcode
,拷贝calldata
数据- 指定逻辑合约地址
- 执行
delegateCall
,返回执行结果或者失败revert
examples
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/utils/Create2.sol";
contract Factory {
error AccountCreationFailed();
error InvalidSignerInput();
error InvalidOwnerInput();
error DuplicatedAddress();
event AccountCreated(address indexed _account);
address[] public userwallet;
address public implementation;
address public manager;
address public owner;
mapping(string => address) identiWallet;
function getWalletLength() public view returns (uint256) {
return userwallet.length;
}
constructor(address _manager, address _implement) payable {
owner = msg.sender;
manager = _manager;
implementation = _implement;
}
modifier onlyOwner() {
require(msg.sender == owner, "NOT_OWNER");
_;
}
modifier onlyManager() {
require(msg.sender == manager, "NOT_OWNER");
_;
}
function updateOwner(address newOwner) external onlyOwner {
require(
newOwner != address(0) && newOwner != owner,
"DUP/INVALID_NEWOWNER"
);
owner = newOwner;
}
function updateManager(address newManager) external onlyOwner {
require(
newManager != address(0) && newManager != manager,
"DUP/INVALID_NEWMANAGER"
);
manager = newManager;
}
function updateImpl(address newImpl) external onlyOwner {
require(
newImpl != address(0) && newImpl != implementation,
"DUP/INVALID_NEWIMPL"
);
implementation = newImpl;
}
function getCreationCode(bytes32 _identifier)
internal
view
returns (bytes memory)
{
return
abi.encodePacked(
hex"3d60ad80600a3d3981f3363d3d373d3d3d363d73",
implementation,
hex"5af43d82803e903d91602b57fd5bf3",
abi.encode(_identifier)
);
}
function createAccount(
address _owner,
address _signer,
string memory _identifier
) external onlyManager returns (address _account) {
(bytes memory code, bytes memory salt) = checkAccount(
_owner,
_signer,
_identifier
);
assembly {
_account := create2(0, add(code, 0x20), mload(code), salt)
}
if (_account == address(0)) revert AccountCreationFailed();
emit AccountCreated(_account);
(bool success, bytes memory result) = _account.call(
abi.encodeWithSignature(
"initData(address,address,address)",
_owner,
manager,
_signer
)
);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
identiWallet[_identifier] = _account;
userwallet.push(_account);
}
function checkAccount(
address _owner,
address _signer,
string memory _identifier
) internal view returns (bytes memory code, bytes memory salt) {
if (_owner == address(0)) revert InvalidOwnerInput();
if (_signer == address(0)) revert InvalidSignerInput();
require(
identiWallet[_identifier] == address(0),
"ONLY_ALLOW_ONE_WALLET"
);
code = getCreationCode(convertStringToByte32(_identifier));
salt = abi.encode(_identifier, _owner, _signer);
address _account = Create2.computeAddress(
bytes32(salt),
keccak256(code)
);
if (_account.code.length != 0) revert DuplicatedAddress();
}
function account(
address _owner,
address _signer,
string memory _identifier
) external view returns (address _account) {
bytes memory code = getCreationCode(convertStringToByte32(_identifier));
bytes memory salt = abi.encode(_identifier, _owner, _signer);
_account = Create2.computeAddress(bytes32(salt), keccak256(code));
return _account;
}
function convertStringToByte32(string memory _texte)
internal
pure
returns (bytes32 result)
{
assembly {
result := mload(add(_texte, 32))
}
}
}
简单可升级代理合约实现
逻辑合约地址
- eip1967 允许函数更新
IMPLEMENTATION_SLOT
数据 (严格的权限控制),
initialize
- 一般情况下,逻辑合约定义
initialize()
函数,按照插槽顺序初始化代理合约数据- 但是需要注意:
initialize()
应该只能调用一次,或者拥有严格的权限控制 - 代理合约的初始化不会影响逻辑合约的状态变量
- 逻辑合约必须单独执行数据的初始化,并确保逻辑合约中的初始化只能调用一次,或者拥有严格的权限控制
- 但是需要注意:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "erc721a/contracts/extensions/ERC721ABurnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract LogisNFT is Ownable, ERC721ABurnable {
bool public mintable;
uint256 public maxSupply;
uint256 private initilaized;
address private launchcaller;
// Current base URI.
string public currentBaseURI;
// The suffix for the token URL, e.g. ".json".
string private tokenURISuffix;
string private nftname;
string private nftsymbol;
error NotMintable();
constructor() payable ERC721A(nftname, nftsymbol) Ownable(_msgSender()) {
initilaized = 1;
}
function initailize(
string memory _name,
string memory _symbol,
string memory _tokenURISuffix,
string memory _currentBaseURI,
uint256 _maxSupply,
address _caller
) public virtual {
require((initilaized == 0), "ALREADY_INITIALIZED");
nftname = _name;
nftsymbol = _symbol;
tokenURISuffix = _tokenURISuffix;
currentBaseURI = _currentBaseURI;
maxSupply = _maxSupply;
launchcaller = _caller;
initilaized = 1;
}
/**
* @dev Returns the token collection name.
*/
function name()
public
view
virtual
override(ERC721A, IERC721A)
returns (string memory)
{
return nftname;
}
/**
* @dev Returns the token collection symbol.
*/
function symbol()
public
view
virtual
override(ERC721A, IERC721A)
returns (string memory)
{
return nftsymbol;
}
/**
* @dev Returns the address of the current owner.
*/
function caller() public view virtual returns (address) {
return launchcaller;
}
/**
* @dev Sets mintable.
*/
function setMintable(bool _mintable) external onlyOwner {
mintable = _mintable;
}
}
contract ProxyBase is ERC1967Proxy, Ownable {
constructor(
address implementation,
bytes memory _data,
address _owner
) ERC1967Proxy(implementation, _data) Ownable(_owner) {}
function implement() external view returns (address) {
return _implementation();
}
function upgradeImpl(address implementation, bytes memory _data)
public
virtual
onlyOwner
{
ERC1967Utils.upgradeToAndCall(implementation, _data);
}
function renounceOwnership() public view override onlyOwner {
revert("CLOSED_INTERFACE");
}
receive() external payable {}
}
contract Factory is Ownable {
address[] public proxys;
event ProxyCreated(address indexed _account);
constructor() Ownable(_msgSender()) {}
function newproxy(
address implementation,
string memory _name,
string memory _symbol,
string memory _tokenURISuffix,
string memory _currentBaseURI,
uint256 _maxSupply,
address _caller,
address _owner
) external onlyOwner {
bytes memory _data = abi.encodeWithSignature(
"initailize(string,string,string,string,uint256,address)",
_name,
_symbol,
_tokenURISuffix,
_currentBaseURI,
_maxSupply,
_caller
);
ProxyBase proxy = new ProxyBase(implementation, _data, _owner);
proxys.push(address(proxy));
emit ProxyCreated(address(proxy));
}
function renounceOwnership() public view override onlyOwner {
revert("CLOSED_INTERFACE");
}
}
合约升级
- 代理合约直接
delegateCall
逻辑合约 - 按照逻辑合约的代码更新 代理合约
EVM slot
数据
因此,合于升级后不能影响旧的状态变量相对顺序
- 合约升级只能添加新的函数、事件、
error
、结构体、immutable|constant
变量 - 合约升级允许更新函数逻辑
- 合约升级可以在最后添加新的状态变量,确保旧状态变量的顺序
- 合约升级不能更改状态变量顺序、不能继承新的合约(会改变状态变量顺序)
beacon代理合约
- 多个代理合约使用同一个逻辑合约,并且通过单笔交易可以升级多个代理合约的逻辑合约地址
- 适用于一个逻辑产生多个衍生合约的场景(班级学生采用同一个逻辑管理,但是每人拥有各自的状态)
beacon合约
beacon
合约作为灯塔,提供当前implementation
地址- 全部代理合约去
beacon
合约读取逻辑合约地址,发送delegateCall
交易
- 更换新的逻辑合约后,只需要更新
beacon
合约的IMPLEMENTATION_SLOT
数据,就可以实现all proxy
的逻辑地址更新
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "erc721a/contracts/extensions/ERC721ABurnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract LogisNFT is Ownable, ERC721ABurnable {
bool public mintable;
uint256 public maxSupply;
uint256 private initilaized;
address private launchcaller;
// Current base URI.
string public currentBaseURI;
// The suffix for the token URL, e.g. ".json".
string private tokenURISuffix;
string private nftname;
string private nftsymbol;
error NotMintable();
constructor() payable ERC721A(nftname, nftsymbol) Ownable(_msgSender()) {
initilaized = 1;
}
function initailize(
string memory _name,
string memory _symbol,
string memory _tokenURISuffix,
string memory _currentBaseURI,
uint256 _maxSupply,
address _caller
) public virtual {
require((initilaized == 0), "ALREADY_INITIALIZED");
nftname = _name;
nftsymbol = _symbol;
tokenURISuffix = _tokenURISuffix;
currentBaseURI = _currentBaseURI;
maxSupply = _maxSupply;
launchcaller = _caller;
initilaized = 1;
}
/**
* @dev Returns the token collection name.
*/
function name()
public
view
virtual
override(ERC721A, IERC721A)
returns (string memory)
{
return nftname;
}
/**
* @dev Returns the token collection symbol.
*/
function symbol()
public
view
virtual
override(ERC721A, IERC721A)
returns (string memory)
{
return nftsymbol;
}
/**
* @dev Returns the address of the current owner.
*/
function caller() public view virtual returns (address) {
return launchcaller;
}
/**
* @dev Sets mintable.
*/
function setMintable(bool _mintable) external onlyOwner {
mintable = _mintable;
}
}
// 管理Implementation
contract Beacon is UpgradeableBeacon {
constructor(address implementation_)
UpgradeableBeacon(implementation_, _msgSender())
{}
}
// 管理beacon,用户直接交互的合约
// address private immutable _beacon;
// beacon地址不允许更新,值直接写到合约codes,读取的一直是合约codes中初始化的那个值
// Factory产生的子合约
contract ProxyBase is BeaconProxy, Ownable {
constructor(
address beacon,
bytes memory data, // delegateCall implementation()'s address
address _owner
) BeaconProxy(beacon, data) Ownable(_owner) {}
function implement() external view returns (address) {
return _implementation();
}
function renounceOwnership() public view override onlyOwner {
revert("CLOSED_INTERFACE");
}
function getBeacon() external view returns (address) {
return _getBeacon();
}
receive() external payable {}
}
contract Factory is Ownable {
address[] public proxys;
event ProxyCreated(address indexed _account);
constructor() Ownable(_msgSender()) {}
function newproxy(
address beacon,
string memory _name,
string memory _symbol,
string memory _tokenURISuffix,
string memory _currentBaseURI,
uint256 _maxSupply,
address _caller,
address _owner
) external onlyOwner {
bytes memory _data = abi.encodeWithSignature(
"initailize(string,string,string,string,uint256,address)",
_name,
_symbol,
_tokenURISuffix,
_currentBaseURI,
_maxSupply,
_caller
);
ProxyBase proxy = new ProxyBase(beacon, _data, _owner);
proxys.push(address(proxy));
emit ProxyCreated(address(proxy));
}
function renounceOwnership() public view override onlyOwner {
revert("CLOSED_INTERFACE");
}
}
合约调用
Call
call
的外部调用在low-level
层面新启一个EVM
作为外部合约被call
后的交易的执行环境- 在新启的
EVM
执行环境中,整体复制外部合约的代码- 按照外部合约的逻辑执行当前交易,更新外部合约的状态变量
- 对于外部合约来讲,当前被
call
的交易处在新的EVM
执行环境,交易的发起方就是发起调用的合约地址
High-level
contract interface
- 通过函数接口调用外部合约函数,_Name(_Address).func()
call
返回(bool success, bytes memory data)
- 通过函数直接调用的话,
Solidity
在语言层面直接判断返回值 - 返回值如果为
false
,抛出revert()
异常- 通过 try/catch捕获异常
- 通过函数直接调用的话,
Low-level
function selector
- 外部函数支持通过合约函数选择器传参调用,
abi.encodeWithSignature | encodeWithSelector | encodePacked
public|external
修饰的函数允许外部调用 以及 继承的合约使用internal|private
修饰的函数不支持外部调用internal
修饰的函数允许继承使用private
修饰的函数不允许继承使用
- 外部调用返回
(bool success, bytes memory data)
boolean
表明当前调用是否成功data
是执行函数返回的数据call
执行失败的话,不会revert
回滚交易,因此需要执行异常判断
call
外部合约不存在的函数- 外部合约存在缺省
fallback()
,就执行fallback()
逻辑 - 不存在缺省函数的话,直接返回
false
- 外部合约存在缺省
Call types validation
call
- 交易执行在新的
EVM
执行环境 address(this)
= 被调用的外部合约的地址
- 交易执行在新的
Solidity Examples
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Caller {
// first call to ops()
// just return false, but not revert
function callByCall(address _address) public returns (bool) {
(bool success, bytes memory returndata) = _address.call(
abi.encodeWithSignature("ops()")
);
if (!success) _revert(returndata, "Call_Faliure");
return success;
}
// second call to ops()
// it will revert()
function callByInterface(address _address) public {
Called called = Called(_address);
called.ops();
}
function _revert(bytes memory returndata, string memory errorMessage)
private
pure
{
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
contract Called {
// ops() always reverts
function ops() public {
revert();
}
}
Calling address(0)
High-level
- high-level 在
solidity
层面直接发起外部合约调用- 在发起对地址的调用前先校验
caller
是否合法- 只能对合约代码不为空的地址发起外部调用
- 在发起对地址的调用前先校验
Lower-level
lower-level
在EVM
层面直接发起外部合约调用- 直接发起对地址的调用,不会在语言层面校验
caller
是否合法
- 直接发起对地址的调用,不会在语言层面校验
外部调用 Error:
- 执行中遇到
REVERT
关键字 out-of-gas
- 异常(
/0,out-of-bound
)
Solidity Contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface ERC20 {
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
}
contract receiver {
constructor() {}
event ReceiveReceived(address Sender, uint256 Value);
event FallbackReceived(address Sender, uint256 Value);
receive() external payable {
emit ReceiveReceived(msg.sender, msg.value);
}
fallback() external payable {
emit FallbackReceived(msg.sender, msg.value);
}
}
contract payer {
constructor() payable {}
function sendValue(address payable recipient, uint256 amount) external {
bool success = recipient.send(amount); //ReceiveReceived
if (!success) {
revert("Send Trans Failure");
}
}
function transferValue(address payable recipient, uint256 amount) external {
recipient.transfer(amount); //ReceiveReceived
}
function callSendValue(
address payable recipient,
uint256 amount,
bytes memory _data
) external {
(bool success, ) = recipient.call{value: amount}(_data); //ReceiveReceived
if (!success) {
revert("call_send_value_failure");
}
}
// 11053 gas cost
function safeTransferNativeToken(
address to,
uint256 amount,
uint256 txGas
) external {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Transfer the ETH and store if it succeeded or not.
//success := call(gas(), to, amount, 0, 0, 0, 0)
success := call(txGas, to, amount, 0, 0, 0, 0)
}
require(success, "ETH_TRANSFER_FAILED");
}
function safeTransfer(
ERC20 token,
address to,
uint256 amount
) internal {
bool success;
/// @solidity memory-safe-assembly
assembly {
// Get a pointer to some free memory.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
mstore(
freeMemoryPointer,
0xa9059cbb00000000000000000000000000000000000000000000000000000000
)
mstore(
add(freeMemoryPointer, 4),
and(to, 0xffffffffffffffffffffffffffffffffffffffff)
) // Append and mask the "to" argument.
mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
or(
and(eq(mload(0), 1), gt(returndatasize(), 31)),
iszero(returndatasize())
),
// We use 68 because the length of our calldata totals up like so: 4 + 32 * 2.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(gas(), token, 0, freeMemoryPointer, 68, 0, 32)
)
}
require(success, "TRANSFER_FAILED");
}
}
合约调用
delegateCall
delegateCall
在low-level
层将外部合约的代码整个复制到当前EVM
环境,与origin
交易复用同一个EVM
执行环境delegateCall
执行在本合约的EVM
环境delegateCall
拥有完整的外部调用合约的合约代码delegateCall
按照外部合约代码去更新/读取EVM
环境中的slot
的状态变量
返回状态
delegateCall
返回(bool success, bytes memory data)
boolean
表明当前调用是否成功data
是执行函数返回的数据delegateCall
执行失败的话,不会revert
回滚交易,因此需要执行异常判断
delegateCall
外部合约不存在的函数- 外部合约存在缺省
fallback()
,就执行fallback()
逻辑 - 不存在缺省函数的话,直接返回
false
- 外部合约存在缺省
Solidity Examples
delegateCall
按照外部合约代码去更新/读取EVM
环境中的slot
的状态变量- 示例合约在
EVM
中的逻辑为:slot1 += slot0
- 本合约按照上述逻辑更新自己合约内部的
slot
参数
- 本合约按照上述逻辑更新自己合约内部的
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
contract Called {
uint256 base = 3;
uint256 public number; //slot 1
function increment() public returns (uint256) {
number += base; // slot1 += slot0
return number;
}
}
contract Caller {
uint256 base = 99; // 按照业务逻辑会读取slot0数据,参与计算
uint256 public myNumber; // 每次调用:slot1 += slot0(myNumber+=99)
// there is a new storage variable here
address public calledAddress = 0xd9145CCE52D386f254917e481eB44e9943F39138;
function execute(address to, uint256 txGas)
external
returns (bool success)
{
bytes memory data = abi.encodeWithSignature("increment()");
return _execute(to, data, txGas);
}
function _execute(
address to,
bytes memory data,
uint256 txGas
) internal returns (bool success) {
assembly {
success := delegatecall(
txGas,
to,
add(data, 0x20),
mload(data),
0,
0
)
}
}
function delegateCallIncrement(
address delegatedCalled //28187 gas cost
) public returns (uint256) {
(bool success, bytes memory resdata) = delegatedCalled.delegatecall(
abi.encodeWithSignature("increment()") //0xd09de08a
);
if (!success) {
assembly {
revert(add(resdata, 32), mload(resdata))
}
} else {
// 解码call|delegateCall的返回值
return abi.decode(resdata, (uint256));
}
}
}
Immutable|Constant in delegateCall
delegateCall
是将外部合约的代码整个拷贝到当前EVM
执行immutable|constant
是编码到合约代码存储,并不占据slot
存储- 也就是
immutable|constant
类型的数据,是从外部合约代码直接读取
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Caller {
uint256 private immutable a = 3;
function getValueDelegate(address called) public returns (uint256) {
(bool success, bytes memory data) = address(called).delegatecall(
abi.encodeWithSignature("getValue()")
);
if (!success) {
_revert(data, "Calles_Failure");
}
return abi.decode(data, (uint256)); // returns 2
}
function _revert(bytes memory returndata, string memory errorMessage)
private
pure
{
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
contract Called {
uint256 private immutable a = 2;
function getValue() public pure returns (uint256) {
return a;
}
}
Call types validation
delegateCall
- 交易执行在当前的
EVM
执行环境 address(this)
= 当前合约的地址__self
是immutable
类型数据,因此该值不从slot
读取,而是从外部合约代码读取:读取到的是外部合约的合约地址- 因此,采用
delegateCall
的话,两者的地址应该不同
- 交易执行在当前的
call
- 交易执行在新的
EVM
执行环境 address(this)
= 被调用的外部合约的地址__self
是immutable
类型数据,因此该值不从slot
读取,而是从外部合约代码读取:读取到的是外部合约的合约地址- 因此,采用
call
的话,两者的地址应该相同
- 交易执行在新的
总之:
delegateCall –> require(address(this) != __self)
call –> require(address(this) == __self)
address private immutable __self = address(this);
/**
* @dev Check that the execution is being performed through a delegatecall call
* 要求使用delegateCall调用该修饰器修饰的函数
* 使用delegateCall调用时:
** 1. address(this) = 当前合约的地址
** 2. __self是immutable类型数据,从外部合约代码读取,读取到的应该是 外部合约的合约地址
* 因此,采用 delegateCall的话,两者的地址应该不同
*/
modifier onlyProxy() {
require(
address(this) != __self,
"Function must be called through delegatecall"
);
require(
_getImplementation() == __self,
"Function must be called through active proxy"
);
_;
}
/**
* @dev Check that the execution is not being performed through a delegate call.
* 要求使用call调用该修饰器修饰的函数
* 使用call调用时, 交易被打包进新的EVM 执行环境
** 1. address(this) = 外部调用的合约地址
** 2. __self是immutable类型数据,从外部合约代码读取,读取到的应该是 外部合约的合约地址
* 因此,采用 call的话,两者的地址应该相同
*/
modifier notDelegated() {
require(
address(this) == __self,
"UUPSUpgradeable: must not be called through delegatecall"
);
_;
}
staticCall
staticcall{gas: gasAmount}(abiEncodedArguments);
staticcall
外部合约函数只读数据- 函数必须被
view|pure
修饰,表明只读
- 函数必须被
- 天然适用于 预编译合约
staticcall
无法更新状态变量:- 更新合约内部的状态变量
emit event
触发链上事件- 创建其他合约
self destruct
销毁合约(合约余额强制转移到传参地址)- 转账,更新账户余额
- 调用其他未标记
pure/view
的函数 - 使用内联编码更改状态数据库
Solidity Examples
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
interface IERC20 {
function balanceOf(address) external view returns (uint256);
}
contract Token is IERC20 {
mapping(address => uint256) balance;
function mint(address addr, uint256 qty) external {
balance[addr] += qty;
}
function balanceOf(address addr) external view returns (uint256) {
return balance[addr];
}
}
contract ERC20User {
// 5837 gas cost
function myBalance(IERC20 token, address addr)
public
view
returns (uint256 balance)
{
balance = token.balanceOf(addr);
}
// 6294 gas cost
function myBalanceLowLevelEquivalent(IERC20 token, address addr)
public
view
returns (uint256 balance)
{
(bool ok, bytes memory result) = address(token).staticcall(
abi.encodeWithSignature("balanceOf(address)", addr)
);
require(ok);
balance = abi.decode(result, (uint256));
}
}
Security Issues
Denial of Service
staticCall
支持指定 gas
63/64 执行只读的调用,但是对方函数逻辑不明,存在恶意消耗 gas
的安全隐患
Read only Re-entrancy
只读函数会受到其他函数的影响
Preference
https://www.rareskills.io/post/solidity-staticcall
预编译合约(0x01 to 0x09)
- Elliptic curve digital signature recovery
- 0x01: ecRecover
- 签名验证失败会返回
address(0)
,不会revert
整笔交易 - 需要进行签名地址校验 ECDSA校验签名
- 签名验证失败会返回
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/ECDSA.sol)
pragma solidity ^0.8.20;
/**
* @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.
*
* These functions can be used to verify that a message was signed by the holder
* of the private keys of a given address.
*/
library ECDSA {
enum RecoverError {
NoError,
InvalidSignature,
InvalidSignatureLength,
InvalidSignatureS
}
/**
* @dev The signature derives the `address(0)`.
*/
error ECDSAInvalidSignature();
/**
* @dev The signature has an invalid length.
*/
error ECDSAInvalidSignatureLength(uint256 length);
/**
* @dev The signature has an S value that is in the upper half order.
*/
error ECDSAInvalidSignatureS(bytes32 s);
/**
* @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not
* return address(0) without also returning an error description. Errors are documented using an enum (error type)
* and a bytes32 providing additional information about the error.
*
* If no error is returned, then the address can be used for verification purposes.
*
* The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
*
* Documentation for signature generation:
* - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]
* - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]
*/
function tryRecover(
bytes32 hash,
bytes memory signature
) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
assembly ("memory-safe") {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return tryRecover(hash, v, r, s);
} else {
return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length));
}
}
/**
* @dev Returns the address that signed a hashed message (`hash`) with
* `signature`. This address can then be used for verification purposes.
*
* The `ecrecover` EVM precompile allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it.
*/
function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
(address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature);
_throwError(error, errorArg);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.
*
* See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures]
*/
function tryRecover(
bytes32 hash,
bytes32 r,
bytes32 vs
) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
unchecked {
bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
// We do not check for an overflow here since the shift operation results in 0 or 1.
uint8 v = uint8((uint256(vs) >> 255) + 27);
return tryRecover(hash, v, r, s);
}
}
/**
* @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.
*/
function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) {
(address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs);
_throwError(error, errorArg);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `v`,
* `r` and `s` signature fields separately.
*/
function tryRecover(
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s
) internal pure returns (address recovered, RecoverError err, bytes32 errArg) {
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return (address(0), RecoverError.InvalidSignatureS, s);
}
// If the signature is valid (and not malleable), return the signer address
address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) {
return (address(0), RecoverError.InvalidSignature, bytes32(0));
}
return (signer, RecoverError.NoError, bytes32(0));
}
/**
* @dev Overload of {ECDSA-recover} that receives the `v`,
* `r` and `s` signature fields separately.
*/
function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
(address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s);
_throwError(error, errorArg);
return recovered;
}
/**
* @dev Optionally reverts with the corresponding custom error according to the `error` argument provided.
*/
function _throwError(RecoverError error, bytes32 errorArg) private pure {
if (error == RecoverError.NoError) {
return; // no error: do nothing
} else if (error == RecoverError.InvalidSignature) {
revert ECDSAInvalidSignature();
} else if (error == RecoverError.InvalidSignatureLength) {
revert ECDSAInvalidSignatureLength(uint256(errorArg));
} else if (error == RecoverError.InvalidSignatureS) {
revert ECDSAInvalidSignatureS(errorArg);
}
}
}
- Hash methods to interact with bitcoin and zcash
- 0x02 and 0x03: SHA-256 and RIPEMD-160
Ethereum
使用keccak256
作为地址的哈希算法Bitcoin
使用SHA-256 and RIPEMD-160
作为基础的哈希计算
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Called {
function hashSha256(uint256 numberToHash) public view returns (bytes32 h) {
(bool ok, bytes memory out) = address(2).staticcall(
abi.encode(numberToHash)
);
require(ok);
h = abi.decode(out, (bytes32));
}
function hashSha256Yul(uint256 numberToHash) public view returns (bytes32) {
assembly {
mstore(0, numberToHash) // store number in the zeroth memory word
let ok := staticcall(gas(), 2, 0, 32, 0, 32)
if iszero(ok) {
revert(0, 0)
}
return(0, 32)
}
}
function hashRIPEMD160(bytes calldata data)
public
view
returns (bytes20 h)
{
(bool ok, bytes memory out) = address(3).staticcall(data);
require(ok);
h = bytes20(abi.decode(out, (bytes32)) << 96);
}
}
Goland Sha256:
s := hexutil.EncodeBig(big.NewInt(12))
prefix := ""
num := 64 - len(s[2:])
for index := 0; index < num; index++ {
prefix += "0"
}
s = s[:2] + prefix + s[2:]
//fmt.Println(s)
byteD, err := hexutil.Decode(s)
if err != nil {
fmt.Println(err)
}
h := sha256.New()
h.Write(byteD)
bs := h.Sum(nil)
fmt.Println(hexutil.Encode(bs))
- Memory copying
- Address 0x04: Identity
- 拷贝内存数据
- Methods to enable elliptic curve math for zero knowledge proofs
- Address 0x05: Modexp
- 幂模运算
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract MoxExp {
function modExp(
uint256 base,
uint256 exp,
uint256 mod
) public view returns (uint256) {
bytes memory precompileData = abi.encode(32, 32, 32, base, exp, mod);
(bool ok, bytes memory data) = address(5).staticcall(precompileData);
require(ok, "expMod failed");
return abi.decode(data, (uint256));
}
function Exp(uint256 base, uint256 exp) public view returns (uint256) {
uint256 max = type(uint256).max;
bytes memory precompileData = abi.encode(32, 32, 32, base, exp, max);
(bool ok, bytes memory data) = address(5).staticcall(precompileData);
require(ok, "expMod failed");
return abi.decode(data, (uint256));
}
}
- Address 0x06 and 0x07 and 0x08: ecAdd, ecMul, and ecPairing (EIP-196 and EIP-197)
- ECC 运算用于零知识证明和 TornadoCash
Preference
https://www.evm.codes/precompiled?fork=grayGlacier
MerkleProof
构建树
h(a) = h(h(ℓ₁) + h(ℓ₂))
h(e) = h(h(a) + h(b))
h(g) = h(h(e) + h(f))
return root == h(g)
Questions
The attack requires 64 byte leaves
MerkleTree
只运算叶子节点数据的 hash
,上述构造树的过程中:
- 数据
ℓ₁
的 叶子节点数据h(ℓ₁) = 32 bytes
a = h(ℓ₁) + h(ℓ₂) = 64 bytes
MerkleTree
校验仅支持32bytes
的叶子节点数据
校验过程中必须将提供的原值 hash 后参与计算
因此,攻击者无法提供 a = 64 bytes 的原值,proof = [h(b), h(f)] 跳过校验
如果 攻击者提供 h(a) 作为原址,原值再次 hash 后将不满足树的校验
因此,32bytes 的叶子节点可以有效限制 攻击者跳过真实叶子节点的校验攻击问题
MerkleTree
校验如果支持64bytes
的叶子节点数据
校验过程中必须将 提供的原值 hash 后参与计算
因此,攻击者可以提供 a = 64 bytes 的原值,proof = [h(b), h(f)] 跳过校验
因此,64bytes 的叶子节点不能限制 攻击者跳过真实叶子节点的校验攻击问题,存在安全问题
叶子节点和父节点采用不同的hash运算
- 再不限制叶子节点数据的前提下,可以通过采用不同的节点运算方式避免攻击问题
- 为简化使用,叶子节点执行双
hash h’(x) = h(h(x))
- 父节点只进行单次
hash
参与构建- 此时,攻击者提供
a
值 - 根据
hash
运算规则,a
执行 两次hash
参与校验,此时不满足树的校验
- 此时,攻击者提供
preference
merkle-tree-second-preimage-attack
openzeppelin-merkle-proof-golang
校验签名:
ECDSA(Elliptic Curve Digital Signature Algorithm)
用来对特定的数据进行密码学上的计算,运行数据的签名者(signer
)签名封装数据,签名公钥(verifier
)可以校验解封数据
- 签名数据被篡改的话,公钥无法解封出正确的数据
- 签名公钥和私钥不配对的话,数据无法解封
Signing
- Create message to sign
- Hash the message
- Sign the hash (off chain, keep your private key secret)
Verify
- Recreate hash from the original message
- Recover signer from signature and hash
- Compare recovered signer to claimed signer
preference
Common mistakes
Div before Mul
Solidity只有整数,不支持小数运算,默认的整数乘除向下取整。
**// Wrong way:**
If principal = 3000,
interest = principal / 3333 * 10000
interest = 3000 / 3333 * 10000
interest = 0 * 10000 (rounding down in division)
interest = 0
// **Correct Calculation:**
If principal = 3000,
interest = principal * 10000 / 3333
interest = 3000 * 10000 / 3333
interest = 30000000 / 3333 interest approx 9000
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/math/Math.sol";
import "https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMath64x64.sol";
contract MathPaper {
// k, N 都是整数
function func1(uint256 k, uint256 N) public pure returns (uint64) {
uint256 numerator = k + N + 1;
uint256 denominator = N + 1;
return log2(numerator) - log2(denominator);
}
function func3(uint256 dt, uint256 e) public pure returns (uint256) {
if (dt < e) {
return 100;
}
uint256 exp = Math.ceilDiv((dt - e) * 100, e); //0.2==>20
uint256 dominator = power(100, exp);
return 1000000 / dominator;
}
function power(uint256 base, uint256 exp) public pure returns (uint256) {
// Represent base as a fixed-point number.
int128 baseFixed = ABDKMath64x64.fromUInt(base);
// Calculate ln(base)
int128 lnBase = ABDKMath64x64.ln(baseFixed);
// Represent exp as a fixed-point number.
int128 expFixed = ABDKMath64x64.divu(exp, 100);
// Calculate ln(base) * exp
int128 product = ABDKMath64x64.mul(lnBase, expFixed);
// Calculate e^(ln(base) * exp)
int128 result = ABDKMath64x64.exp(product);
// Multiply by 10^5 to keep 5 decimal places
result = ABDKMath64x64.mul(result, ABDKMath64x64.fromUInt(10**2));
// Convert the fixed-point result to a uint and return it.
return ABDKMath64x64.toUInt(result);
}
function log2(uint256 base) public pure returns (uint64) {
// Represent base as a fixed-point number.
int128 baseFixed = ABDKMath64x64.fromUInt(base);
// Calculate ln(base)
int128 lnBase = ABDKMath64x64.log_2(baseFixed) * 1e4;
uint64 t = ABDKMath64x64.toUInt(lnBase);
return t;
}
}
Not following check-effects-interaction
check-effects-interaction
模式将合约交互放在条件校验、数据更新之后,防止重入攻击。- 合约交互包含 外部合约的
call|delegateCall|staticCall
调用、Native Token
转账 - 合约交互操作应该放在整个函数的最后,确保无法正常执行重入攻击(即使重入,数据也是已经更新后的数值)
重入攻击的示例:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
// DO NOT USE
contract BadBank {
mapping(address => uint256) public balances;
constructor() payable {
require(msg.value == 10 ether, "deposit 10 eth");
}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
// should be after the check and effects
(bool ok, ) = msg.sender.call{value: balances[msg.sender]}("");
require(ok, "transfer failed");
// shoule be the first
balances[msg.sender] = 0;
}
}
contract attack {
function deposit(BadBank bank) external payable {
bank.deposit{value: msg.value}();
bank.withdraw();
}
receive() external payable {
if (msg.sender.balance >= 1 ether) {
BadBank(msg.sender).withdraw();
}
}
}
Transfer Native Token by ‘transfer’ or ‘send’
transfer or send
转账模式- 为了避免重入攻击,只传递有限的 gas,让调用的合约不能执行过多的合约逻辑
- 默认调用时传递
2300 gas
,其中transfer
执行失败会revert
,send
不会revert
但是会返回待处理的boolean
sload
字节码在升级后的gas 花销分为:non-warm-2100, warm-100
- 升级后如果仍然传递默认的
2300gas
,也无法容忍 对方合约中fallback|receiver
存在读取地址的行为
- 升级后如果仍然传递默认的
// in the bank receiver() function, recors the deposit behavior
receive() external payable {
balances[msg.sender] += msg.value;
}
// In the call contract, the deposit transfer function will fail, because only passing 2300 gas could not support the record behavior
contract attack {
function transfer(BadBank bank) external payable {
payable(address(bank)).transfer(msg.value);
}
}
- 在
check-effects-interation
模式中使用 call 进行安全转账,处理 call 调用返回的 (bool success, bytes data)
function sendValue(address payable recipient, uint256 amount) internal {
if (address(this).balance < amount) {
revert Errors.InsufficientBalance(address(this).balance, amount);
}
(bool success, ) = recipient.call{value: amount}("");
if (!success) {
revert Errors.FailedCall();
}
}
misusing of tx.origin
and msg.sender
tx.origin
指的是当前交易的签名者,初始构造者,在整个交易的生命周期中不会发生变化msg.sender
指的是当前EVM
环境中的交易发起方,会随着 EVM 环境的话变化而返回不同的地址
Not using safeTransfer in ERC20
- ERC20代币存在两套不同的转账函数
- 有些代币的转账函数不存在返回值,比如 USDT的转账
- 有些代币的转账函数存在返回值,比如标准的ERC20合约
- 针对不同的转账标准, safeTransfer 可以处理全部情况
- 在外部调用存在 revert 的情况下,直接 revert
- 外部调用不存在返回值的情况下,如果被调用者不是一个合约地址的话,直接 revert报错
- 外部调用不存在返回值的情况下,如果被调用者是一个合法合约地址的话,交易成功
- 外部调用存在返回值的情况下,如果返回值不是 1(true),直接 revert报错
function _callOptionalReturn(IERC20 token, bytes memory data) private {
uint256 returnSize;
uint256 returnValue;
assembly ("memory-safe") {
let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
// bubble errors
if iszero(success) {
let ptr := mload(0x40)
returndatacopy(ptr, 0, returndatasize())
revert(ptr, returndatasize())
}
returnSize := returndatasize()
returnValue := mload(0)
}
if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
revert SafeERC20FailedOperation(address(token));
}
}
No need safeMath in solidity ^0.8.0+
Invalid access control or Uninitialized functions in the logic contract
使用确定的循环问题
使用确定的solidity 版本
Preference
https://www.rareskills.io/post/solidity-beginner-mistakes