Solidity快速梳理进阶要点
前言
本文高效梳理Solidity编程语言进阶知识点
1.底层调用 call、delegatecall 以及 Multicall
- call:用于调用其他合约的函数,可以修改目标合约的状态。
- delegatecall:在调用者的上下文中执行目标合约的代码,可以修改调用者的状态。
- Multicall:允许在一笔交易中执行多个调用,适用于批量操作,提高效率。
2.跨合约调用方式
类型
- 通过合约地址直接调用:简单直接,但需要注意返回值和错误处理。
- 通过接口调用:安全、易维护,推荐使用。
- 低级调用(call、delegatecall) :适用于高级用例,需要小心使用以避免安全问题。
- Multicall:适用于批量操作,提高效率。
方法
-
通过合约地址直接调用
# 合约B
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract ContractB {
function add(uint a, uint b) external pure returns (uint) {
return a + b;
}
}
# 合约A 调用B合约
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import "./5_b.sol";//B合约
contract ContractA {
function callAdd(address _contractBAddress, uint a, uint b) external pure returns (uint) {
ContractB contractB = ContractB(_contractBAddress);
return contractB.add(a, b);
}
}
-
通过接口调用
# 合约B接口
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
interface IContractB {
function add(uint a, uint b) external pure returns (uint);
}
# 合约B
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract ContractB {
function add(uint a, uint b) external pure returns (uint) {
return a + b;
}
}
# 合约A 调用B合约接口
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import "./5_ib.sol";//B合约接口
contract ContractA {
function callAdd(address _contractBAddress, uint a, uint b) external pure returns (uint) {
IContractB contractB = IContractB(_contractBAddress);
return contractB.add(a, b);
}
}
-
低级调用(call、delegatecall)
call
# 合约B
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract ContractB {
function add(uint a, uint b) external pure returns (uint) {
return a + b;
}
}
# 合约A 调用B合约
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract ContractA {
function callAdd(address _contractBAddress, uint a, uint b) external returns (uint) {
(bool success, bytes memory data) = _contractBAddress.call(
abi.encodeWithSignature("add(uint256,uint256)", a, b)
);
require(success, "Call failed");
return abi.decode(data, (uint));
}
}
delegatecall
使用场景
-
代理合约
-
EIP-2535 Diamonds(钻石)
-
Multicall
3.常见的特殊变量
-
address(this)
:当前合约的地址 -
msg.sender
:当前调用合约的地址 -
msg.value
:表示当前交易发送的以太数量(单位是wei) -
msg.data
:表示当前调用的数据部分,通常用于低级调用 -
tx.origin
:表示当前交易的发起者,即最初发送交易的地址 -
block.number
:表示当前区块的编号 -
block.timestamp
:表示当前区块的时间戳 -
block.difficulty
:表示当前区块的难度 -
block.gaslimit
:表示当前区块的Gas限制 -
block.coinbase
:表示当前区块的矿工地址 -
blockhash(uint blockNumber)
:返回指定区块的哈希值,只能获取最近256个区块的哈希值 -
abi.encodePacked(...)
:将多个值打包成一个字节数组 -
abi.encode(...)
:将多个值编码成一个字节数组,使用更严格的编码规则 -
abi.encodeWithSignature(string memory signature, ...)
:将多个值编码成一个字节数组,并包含函数签名 -
abi.decode(bytes memory data, (...))
:将字节数组解码成多个值 -
keccak256(bytes memory data)
:计算给定数据的Keccak-256哈希值 -
require(bool condition, string memory message)
:如果条件不满足,抛出错误并回滚交易 -
assert(bool condition)
:如果条件不满足,抛出错误并回滚交易,通常用于内部错误检查 -
revert(string memory reason)
:显式回滚当前交易 -
gasleft()
:返回当前交易剩余的Gas数量 -
this
:表示当前合约的实例,可以用来调用合约的函数 -
super
:在继承中,表示父合约的实例 -
type(C).name
:返回合约的名称 -
type(C).creationCode
:返回合约的创建代码 -
type(C).runtimeCode
:返回合约的运行时代码
4.创建合约Create1
直接上案例
合约
contract Pair{
address public factory; // 工厂合约地址
address public token0; // 代币1
address public token1; // 代币2
constructor() payable {
factory = msg.sender;
}
// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
token0 = _token0;
token1 = _token1;
}
}
工厂合约
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
import "./Pair.sol";
contract PairFactory{
mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
address[] public allPairs; // 保存所有Pair地址
function createPair(address tokenA, address tokenB) external returns (address pairAddr) {
// 创建新合约
Pair pair = new Pair();
// 调用新合约的initialize方法
pair.initialize(tokenA, tokenB);
// 更新地址map
pairAddr = address(pair);
allPairs.push(pairAddr);
getPair[tokenA][tokenB] = pairAddr;
getPair[tokenB][tokenA] = pairAddr;
}
}
案例说明
- 使用工厂合约中的createPair生成合约
- 通过allPairs获取合约地址
- 通过 at address输入获取的合约地址可以查看对应的实例
5.创建合约Create2
直接上案例
合约
contract Pair{
address public factory; // 工厂合约地址
address public token0; // 代币1
address public token1; // 代币2
constructor() payable {
factory = msg.sender;
}
// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
token0 = _token0;
token1 = _token1;
}
}
工厂合约
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
import "./Pair.sol";
contract PairFactory2{
mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
address[] public allPairs; // 保存所有Pair地址
function createPair2(address tokenA, address tokenB) external returns (address pairAddr) {
require(tokenA != tokenB, 'IDENTICAL_ADDRESSES'); //避免tokenA和tokenB相同产生的冲突
// 用tokenA和tokenB地址计算salt
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
// 用create2部署新合约
Pair pair = new Pair{salt: salt}();
// 调用新合约的initialize方法
pair.initialize(tokenA, tokenB);
// 更新地址map
pairAddr = address(pair);
allPairs.push(pairAddr);
getPair[tokenA][tokenB] = pairAddr;
getPair[tokenB][tokenA] = pairAddr;
}
}
总结Create1 和Create2 差异
特性 | CREATE | CREATE2 |
---|---|---|
地址生成方式 | 基于创建者地址和创建者账户中的nonce值,通过哈希计算生成 | 基于创建者地址、salt值和初始化代码的keccak256哈希,通过哈希计算生成 |
地址可预测性 | 合约地址是可预测的,但需要等待上一个创建者账户中的nonce增加 | 合约地址在创建时就能够预测,不受nonce的影响 |
用途 | 适用于在合约之间直接通信,无需事先知道合约地址 | 适用于在创建合约时预测合约地址,并通过地址存储信息,以便其他合约能够可靠地找到它 |
重复部署 | 如果两个不同的创建者同时尝试使用相同的nonce创建合约,可能会发生nonce竞争,导致一个创建失败 | 使用不同的salt,两个创建者可以同时创建具有相同初始化代码的合约,而不会发生地址冲突 |
灵活性 | 地址生成方式较为固定,依赖于创建者的nonce | 提供了更多的地址生成灵活性,可以通过选择不同的salt值来创建不同的地址 |
6.销毁合约
实例
注释:说明:在部署合约的时候给合约转一定量的eth代币 测试验证步骤:
- 编译、部署合约:在部署
- 调用getBalance获取余额返回代币的余额:例如1eth
- 调用deleteContract后,在调用getBalance返回的余额为:0eth
# 合约
// SPDX-License-Identifier: MIT
contract DeleteContract {
uint public value = 10;
constructor() payable {}
receive() external payable {}
function deleteContract() external {
// 调用selfdestruct销毁合约,并把剩余的ETH转给msg.sender
selfdestruct(payable(msg.sender));
}
function getBalance() external view returns(uint balance){
balance = address(this).balance;
}
}
7.Solidity 内联汇编
Solidity 内联汇编是什么:Solidity 提供的一种底层语言,允许开发者直接在 Solidity 代码中编写 EVM 指令,使用 Yul 语言
基本语法
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract InlineAssemblyExample {
function add(uint256 a, uint256 b) public pure returns (uint256 result) {
assembly {
result := add(a, b) // 直接使用 EVM 的 add 指令
}
}
}
常用操作码
算术运算
-
add(value1, value2)
:加法 -
sub(value1, value2)
:减法 -
mul(value1, value2)
:乘法 -
div(value1, value2)
:除法 -
mod(value1, value2)
:取模 -
exp(value1, value2)
:幂运算
位运算
-
not(x)
:按位取反 -
and(x, y)
:按位与 -
or(x, y)
:按位或 -
xor(x, y)
:按位异或 -
shl(x, y)
:逻辑左移 -
shr(x, y)
:逻辑右移 -
sar(x, y)
:算术右移
比较运算符
-
lt(x, y)
:小于 -
gt(x, y)
:大于 -
eq(x, y)
:等于 -
iszero(x)
:检查是否为零
存储操作
-
sload(p)
:从存储位置p
读取值 -
sstore(p, v)
:将值v
存储到位置p
内存操作
-
mload(p)
:从内存位置p
读取值 -
mstore(p, v)
:将值v
存储到内存位置p
-
msize()
:返回当前合约的内存大小
其他操作
-
keccak256(p, n)
:计算内存位置p
开始的n
字节的 Keccak-256 哈希值 -
gas()
:返回当前合约中可用的燃料数量 -
address()
:返回当前合约的地址 -
caller()
:返回当前函数的调用者地址 -
balance(a)
:返回地址a
的余额 -
extcodesize(a)
:返回地址a
的代码大小 -
create(v, p, n)
:创建新合约 -
create2(v, p, n, s)
:使用盐值s
创建新合约 -
call(g, a, v, in, insize, out, outsize)
:调用地址a
上的合约
8.合约的升级方式
- 可升级合约框架Openzeppelin:透明代理和UUPS代理,可以查看我专栏中的《智能合约可升级方式之通用可升级代理合约》
9.Solidity 内存布局
概念 :智能合约中变量和数据在内存中的存储方式,分为三部分存储(Storage)、内存(Memory)和栈(Stack)
-
存储(Storage):存储是智能合约的永久数据存储区域,它在区块链上。每个智能合约都有自己的存储空间,其中包含所有状态变量。存储中的数据是持久的,即使在交易执行完成后仍然存在
-
内存(Memory):内存是智能合约在执行交易时使用的临时数据存储区域。内存中的数据在交易执行完成后会被丢弃,不会保存在区块链上
-
栈(Stack):栈是智能合约在执行交易时使用的临时数据存储区域,用于存储函数调用的参数和返回值
应用
pragma solidity ^0.8.0;
contract StorageExample {
// 存储变量
uint256 public storedData;
// 内存变量
function set(uint256 x) public {
uint256 temp = x; // 内存变量
storedData = temp;
}
// 栈变量
function get() public view returns (uint256) {
return storedData;
}
}
10. Library库合约
特点
- 可重用性:库合约中的函数可以被多个合约调用,从而避免代码重复。
- 状态变量:库合约不能包含状态变量,因为它们不能拥有自己的存储。
- 部署方式:库合约在部署时会生成一个独立的地址,其他合约通过这个地址调用库合约中的函数。
-
调用方式:库合约中的函数可以通过
libraryName.functionName()
的方式调用,也可以通过this.functionName()
的方式调用。
使用案例
# 库合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library MathLibrary {
function add(uint a, uint b) internal pure returns (uint) {
return a + b;
}
function sub(uint a, uint b) internal pure returns (uint) {
require(b <= a, "Subtraction overflow");
return a - b;
}
}
# 调用库合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./MathLibrary.sol";
contract MyContract {
using MathLibrary for uint;
function addNumbers(uint a, uint b) public pure returns (uint) {
return a.add(b); // 使用库合约中的add函数
}
function subNumbers(uint a, uint b) public pure returns (uint) {
return a.sub(b); // 使用库合约中的sub函数
}
}
11.Openzeppelin 代码库
-
提供安全的合约模板:OpenZeppelin 提供了一组经过审计和验证的安全合约库,涵盖了 ERC20、ERC721 等常用标准的实现。这些模板帮助开发者快速、安全地构建智能合约,减少开发工作量,避免常见的漏洞和错误。
-
增强合约安全性:
- 其合约经过多次安全审计,减少了智能合约开发中的常见安全漏洞。
- 提供多种安全相关的功能,如访问控制模块、所有权控制合约、防止重入攻击的工具、数学计算时的溢出保护等。
- 使用 OpenZeppelin 的合约库,可以显著提高智能合约的安全性。
-
促进合约的模块化和可扩展性:
- 采用模块化的设计,允许开发者根据需求组合使用不同的功能模块。
- 提供了灵活的基于角色的权限控制方案和可重用的 Solidity 组件。
-
推动合约的标准化:其提供的 ERC20、ERC721 等标准的实现,被广泛应用于 DeFi 和 NFT 项目中,促进了智能合约的标准化发展。
-
助力合约的可升级性:OpenZeppelin 提供了可升级合约模式,帮助开发者在不重新部署合约的情况下,对合约逻辑进行升级。
-
拥有庞大的社区支持:作为一个开源项目,OpenZeppelin 拥有一个庞大且活跃的开发社区,贡献着各种工具、合约和改进。
-
对行业的影响:OpenZeppelin 的 ERC-20/721 库被 90% 的 DeFi 和 NFT 项目采用,其在智能合约开发中的广泛使用,对整个区块链行业的发展产生了重要影响
12.ABI 编解码
编码、解码
编、解码方法
abi.encode
abi.encodePacked
-
abi.encodeWithSignature
abi.encodeWithSelector
abi.decode
完整例子
// SPDX-License-Identifier: MIT
pragma solidity 0.8.14;
contract ABIFn {
uint x ;
address addr;
string name ;
uint[2] array;
function encode(uint x, address addr,string memory str, uint[] memory array) public view returns(bytes memory result) {
result = abi.encode(x, addr, name, array);
}
function encodePacked(uint x, address addr,string memory str, uint[] memory array) public view returns(bytes memory result) {
result = abi.encodePacked(x, addr, array);
}
function encodeWithSignature(uint x, address addr,string memory str, uint[] memory array) public view returns(bytes memory result) {
result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
}
function encodeWithSelector(uint x, address addr,string memory str, uint[] memory array) public view returns(bytes memory result) {
result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
}
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]));
}
}
总结
-
abi.encode
:遵循ABI规范,对数据进行填充和对齐,适用于编码函数参数、事件日志等。 -
abi.encodePacked
:不遵循ABI规范,不进行填充和对齐,编码后的数据更紧凑,适用于哈希计算、存储等场景。 -
abi.encodeWithSignature
:包含函数的4字节选择器和参数,适用于构造函数调用数据。 -
abi.encodeWithSelector
:与abi.encodeWithSignature
类似,但直接接受4字节选择器。 -
abi.decode
:解码ABI编码的数据,需要指定解码的类型,适用于解码函数调用数据、事件日志等。
13.Hash应用
特性
- 生成数据唯一标识
- 加密签名
- 安全加密
应用
// SPDX-License-Identifier: MIT
pragma solidity 0.8.14;
contract HashFn {
function hash(
uint _num,
string memory _string,
address _addr
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_num, _string, _addr));
}
}
总结
以上内容对Solidity的基础要点进行了梳理。如需深入了解,可查阅Solidity官方文档。