
一.主要内容
-
函数
-
合约事件
-
合约继承
-
合约接口,抽象,lib 库
-
合约的异常处理
二.智能合约的函数
函数是用来执行特定任务或者操作的代码块,函数里面可以包含逻辑,访问状态变量,进行计算等,并且可以接受参数和返回值,通过 function 关键字来定义,而且 solidity 的函数和 Go 类似,可以支持多返回值。
函数的定义
funcion 函数名字(函数参数) 函数修饰符 (函数返回值) {
函数体
}
solidity
function add(uint256 a, uint256 b) public view returns(uint256) {
return a + b;
}
函数的特性
-
可见修饰符:可以使用 public, internal, external 和 private 来指定访问权限,默认修饰符是 public, 同时这些修饰也可以修饰状态。
-
状态变量的访问:函数可以读取和修改合约里面的状态变量
solidity
contract AAA {
uint256 public abc;
event UpdateAbc(uint256 oldAbc, uint256 newAbc);
function updateAbc() public {
oldAbc = abc;
abc = 100;
emit UpdateAbc(oldAbc, abc);
}
}
-
函数的修饰符:函数可以通过 pure, view,payable 和 virtual 来进行修饰,例如 view 和 payable;
-
view:只读数据,但是不修改状态变量
-
payble: 表示函数可以收发 native token
-
函数里面可以触发事件
函数的可见修饰符(面试题目)
函数可以使用 public、internal、external 和 private 来指定访问权限。默认为 public。
-
public 修饰符指定函数或状态变量可以被合约内部的其他合约和外部账户访问。对于函数而言,它们可以被合约内部和外部调用;对于状态变量而言,它们可以被合约内部和外部读取。
-
internal 修饰符指定函数或状态变量只能被当前合约及其派生合约内部访问。外部账户和合约无法直接访问或调用带有 internal 修饰符的函数或状态变量。
-
private 修饰符指定函数或状态变量只能被当前合约内部访问,即使是合约的派生合约也无法直接访问带有 private 修饰符的成员。
-
external 修饰符指定函数只能被外部账户和合约调用,即只能从合约外部进行访问。external 函数不能在合约内部或者合约的其他函数中直接调用。
【图示 1】原文此处为流程图/白板图示,因平台导出限制未能搬运。
源文档: https://w2z1amg2x5.sg.larksuite.com/docx/U5nldWAiAo9mQkxyaDQlq0Jwgdh
注意事项:
-
默认可见性:如果不显示指定可见性修饰符,则默认为 public。这意味着如果你只想让函数或状态变量在合约内部使用,应该显式使用 internal 或 private。
-
Gas 成本:不同的可见性修饰符可能会影响函数调用的 gas 成本,特别是 external 函数的 gas 成本较高,因为它们需要进行消息调用。
-
安全性:合理使用可见性修饰符可以提高合约的安全性和可读性,避免不必要的访问和操作。
函数修饰符
pure, view, payable 和 virtual 都是函数的修饰符,用于定义函数的行为和特性。不同的修饰在函数的调用方式,状态变更和 Gas 消耗等方面是不同的。
Pure 修饰符
pure: 修饰函数时候不读取合约状态,也不修改合约的状态,完全不访问状态变量
- 代码案例
solidity
contract ContractsFunction {
uint256 public aa;
function add(uint256 a, uint256 b) public pure returns (uint256) {
aa = 10;
return a + b + aa;
}
}
- 已经修改状态变量,这是不允许的,会报出一下错误
typescript
forge compile
Warning: This is a nightly build of Foundry. It is recommended to use the latest stable version. To mute this warning set `FOUNDRY_DISABLE_NIGHTLY_WARNING` in your environment.
[⠊] Compiling...
[⠒] Compiling 1 files with Solc 0.8.28
[⠢] Solc 0.8.28 finished in 22.34ms
Error: Compiler run failed:
Error (8961): Function cannot be declared as pure because this expression (potentially) modifies the state.
--> contracts/ContractsFunction.t.sol:8:9:
|
8 | aa = 10;
| ^^
Error (2527): Function declared as pure, but this expression (potentially) reads from the environment or state and thus requires "view".
--> contracts/ContractsFunction.t.sol:9:24:
|
9 | return a + b + aa;
| ^^
guoshijiang@guoshijiangdeMacBook-Pro s9-contracts-basic %
只有将状态变量的访问移除之后才能编译通过
solidity
contract ContractsFunction {
uint256 public aa;
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b + aa;
}
}
- 编译输出
bash
forge compile
[⠊] Compiling...
Pure 的特点
-
不允许访问 this 和 msg 对行
-
可以在编译的时候被内联优化
-
不能发送以太币
View 修饰符
View 修饰函数的时候,不允许修改合约的状态变量,但可以读取合约的状态变量
下面代码是将状态变量 aa 的值进行了修改,这是不允许的
solidity
contract ContractsFunction {
uint256 public aa;
function add(uint256 a, uint256 b) public view returns (uint256) {
aa = 100;
return a + b + aa;
}
}
编译报错
Error: Compiler run failed:
Error (8961): Function cannot be declared as view because this expression (potentially) modifies the state.
--> contracts/ContractsFunction.t.sol:8:9:
|
8 | aa = 100;
| ^^
guoshijiang@guoshijiangdeMacBook-Pro s9-contracts-basic %
移除修改的逻辑,只读状态变量,这是允许的
solidity
contract ContractsFunction {
uint256 public aa;
function add(uint256 a, uint256 b) public view returns (uint256) {
return a + b + aa;
}
}
编译结果如下
guoshijiang@guoshijiangdeMacBook-Pro s9-contracts-basic % forge compile
[⠊] Compiling...
No files changed, compilation skipped
guoshijiang@guoshijiangdeMacBook-Pro s9-contracts-basic %
View 函数里面调用 view 和 pure 函数
java
contract ContractsFunction {
编译结果如下:
guoshijiang@guoshijiangdeMacBook-Pro s9-contracts-basic % forge compile
[⠊] Compiling...
[⠒] Compiling 1 files with Solc 0.8.28
[⠢] Solc 0.8.28 finished in 44.00ms
Compiler run successful!
guoshijiang@guoshijiangdeMacBook-Pro s9-contracts-basic %
View 的特点
-
不允许修改状态变量
-
不能发送以太币
-
View 函数里面允许调用 view 和 pure 修饰的函数
Payable 修饰符
Payable 修饰的函数可以收发以太币
typescript
contract ContractsFunction {
address constant public NativeToken = address(0xeeeeeeeeeeeeee);
mapping(address =>uint256 ) public fundingBalance;
uint256 immutable public usdtDecimal = 18;
receive () external payable{}
function depositNative(uint256 amount) public payable {
require(msg.value >= amount, "ContractsFunction:depositNative deposit amount must be more than your address balance");
fundingBalance[NativeToken] += amount;
}
}
关于以太币收发,大家可以看看前面 address 章节的讲解
Payble 的特点
-
允许函数收发以太币
-
可以调用其他的 payable 函数
-
View 可以读取合约状态变量
-
不能修改状态变量
Virtual 修饰符
Virtual 的作用是用于声明函数可以被派生合约重写(覆盖)
特点
-
在父合约中声明函数为 virtual,表示该函数可以在派生合约中被重写。
-
派生合约中可以使用 override 关键字重写 virtual 函数。
-
virtual 函数可以有默认实现,但也可以是纯虚函数(即没有实现,只是声明)。
typescript
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Base {
uint256 public aa;
constructor() {
aa= 100;
}
总结
-
pure:不读取状态变量,不修改状态,不能发送以太币。
-
view:不修改状态变量,可以读取状态变量,不能发送以太币。
-
payable:可以接收以太币,可以读取状态变量(如果是 view 或 pure 函数),不能修改状态变量(如果是 view 函数)。
-
virtual:声明函数可以被派生合约重写,派生合约中使用 override 关键字进行重写。
函数的重写和重载
在 Solidity 中,函数重写(override)和函数重载(overload)是两个概念,它们在面向对象编程中有特定的含义和用法。
数重写(Override)
函数重写指的是在派生合约(子合约)中重新定义父合约中已存在的虚函数(virtual function)。在 Solidity 中,通过在派生合约中使用 override 关键字来标识要重写的函数。
typescript
contract Base {
uint256 public aa;
constructor() {
aa= 100;
}
function updateValeAA() public {
aa = 100;
}
function getVale() public virtual returns (uint256) {
return aa;
}
}
contract Derived is Base {
uint256 public bb;
constructor(uint _bb) {
bb = _bb;
}
function updateVale() public {
bb = 1000;
}
function getVale() public virtual override returns (uint256) {
return bb;
}
}
在上面的示例中:
-
Base 合约中定义了一个虚函数 getValue,返回固定的整数值 aa。
-
Derived 合约继承自 Base 合约,并重写了 getValue 函数,返回整数值 bb。使用了 override 关键字标识。
函数重写允许派生合约在不改变原有合约结构的情况下,根据具体的需求重定义函数的行为。
函数的重载
函数重载指的是在同一个合约中定义多个具有相同函数名但不同参数列表的函数。Solidity 支持函数重载,根据参数的类型和数量来区分不同的函数定义。
java
contract OverloadExample {
function foo(uint _value) public pure returns (uint) {
return _value;
在上面的示例中:
- OverloadExample 合约定义了三个名为 foo 的函数,它们分别接受不同类型和数量的参数,但函数名相同。
函数重载允许在同一个合约中根据参数的不同,提供不同的函数实现。Solidity 编译器会根据函数的参数列表生成唯一的函数签名,以便区分不同的函数定义。
区别总结
-
函数重写(Override):发生在派生合约中,重新定义父合约中已存在的虚函数,使用 override 关键字标识。
-
函数重载(Overload):发生在同一个合约中,定义具有相同函数名但不同参数列表的多个函数,根据参数的类型和数量来区分。
注意事项
-
Solidity 中的函数重写和重载可以帮助开发者根据具体的需求和逻辑,更灵活地设计和实现合约的功能。
-
在使用函数重载时,应当注意参数列表的唯一性,避免出现二义性,确保函数能够正确地被调用和使用。
-
函数重写通常用于实现继承中的多态特性,而函数重载则用于提供不同的功能选项或操作符号重载等场景。
函数的事件
在 Solidity 中,事件(Events)是一种合约特有的通信机制,用于向外部应用程序通知关键信息。事件允许合约与区块链上的外部系统进行交互,典型的应用包括记录状态变化、提供数据查询以及与前端应用程序进行实时通信等。下面详细说明 Solidity 中事件的定义、使用和特性。
合约事件的定义
在 solidity 里面,合约的事件使用 event 关键字来定义, 事件的定义通常放在合约的顶层,语法如下
event 合约事件名称(数据类型 indexed 变量名, 数据类型 变量名, 数据类型 变量名)
indexed: 合约索引,在对应的字段定义 indexed, 可以通过改字段快速索引合约, 一般放在地址上
-
例子
合约事件的定义:event DepositNative(address indexed msgSender, uint256 amount)
事件的抛出:emit DepositNative(msg.sender, amount);
-
完整案例
java
contract ContractsFunction {
address constant public NativeToken = address(0xeeeeeeeeeeeeee);
mapping(address =>uint256) public fundingBalance;
uint256 immutable public usdtDecimal = 18;
event DepositNative(address indexed msgSender, uint256 amount)
receive () external payable{}
合约事件的特性
-
合约里面的状态变更的时候,一般都是抛出合约事件,以便链下服务监听
-
Gas 成本:触发事件会消耗 gas,特别是如果事件有多个索引参数或者频繁触发。应当注意控制事件触发的频率和消耗,以避免不必要的 gas 费用。
-
事件日志:事件触发后,相关的数据将会被记录在区块链上的事件日志中。外部应用程序可以通过扫描事件日志来获取历史记录或者进行数据分析。
-
隐私性:事件的参数可以是公开的(例如用户地址),但也可以通过合适的权限控制保护隐私。
使用事件是 Solidity 中常见的编程模式,能够有效地增强合约与外部世界的交互性和实时通信能力,是 DApp 开发中不可或缺的一部分。
合约部署测试
使用的钱包地址
Successfully created new keypair.
Address: 0xd449306df27faB97ec0431F4558ab75966743983
Private key: 0xb857bcf6f3ee005a79eea257a4ab467a3488b5006fa4a9dbba7d54a9a41aafa5
领取测试币
- 自己去领取
部署合约
- 设置环境
bash
export PRIVATE_KEY=0xb857bcf6f3ee005a79eea257a4ab467a3488b5006fa4a9dbba7d54a9a41aafa5
export OWNER_ADDRESS=0xd449306df27faB97ec0431F4558ab75966743983
export MANAGER_ADDRESS=0xd449306df27faB97ec0431F4558ab75966743983
export PROXY_ADMIN_OWNER_ADDRESS=0xd449306df27faB97ec0431F4558ab75966743983
- 部署合约
bash
forge script script/RewardManager.s.sol:DeployRewardManagerScript --slow --multi --rpc-url https://bsc-testnet.bnbchain.org --broadcast --verify --etherscan-api-key I4C1AKJT8J9KJVCXHZKK317T3XV8IVASRX
- 部署的结果
yaml
== Logs ==
RewardManager deployment
deployer: 0xd449306df27faB97ec0431F4558ab75966743983
owner: 0xd449306df27faB97ec0431F4558ab75966743983
manager: 0xd449306df27faB97ec0431F4558ab75966743983
underlying token: 0x6aB7feE609617fbC3d47eA46E8EBABed57eB5064
proxy admin owner: 0xd449306df27faB97ec0431F4558ab75966743983
proxy: 0x72A6a0cbFD619D004bcde04B96572Ef8981e9A8f
Mint Token
- Mint Token
bash
cast send --rpc-url $RPCURL --private-key $PRIVATE_KEY 0x6aB7feE609617fbC3d47eA46E8EBABed57eB5064 "mint(address,uint256)" 0xd449306df27faB97ec0431F4558ab75966743983 10000000000000
- 查 Balance
bash
cast call --rpc-url $RPCURL 0x6aB7feE609617fbC3d47eA46E8EBABed57eB5064 "balanceOf(address)(uint256)" 0xd449306df27faB97ec0431F4558ab75966743983
给发奖励的合约设置调用权限
bash
cast send --rpc-url $RPCURL --private-key $PRIVATE_KEY 0x72A6a0cbFD619D004bcde04B96572Ef8981e9A8f "setAuthorizedCaller(address,bool)" 0xd449306df27faB97ec0431F4558ab75966743983 true
生成领取奖励的钱包
sql
guoshijiang@guoshijiangdeMacBook-Pro airdrop-reward % cast wallet n
Successfully created new keypair.
Address: 0xE5F445da5a72d0aaf3F844957C5E7BBE6c636125
Private key: 0x692379969c8c48e27258e47c7d95b61a9571a50e127a959258dfb6281bee876e
guoshijiang@guoshijiangdeMacBook-Pro airdrop-reward % cast wallet n
Successfully created new keypair.
Address: 0xFe2CE71F16594bb3f893f081C50b3B811c0BB22E
Private key: 0xcc8c6c940404415889b6a5936540bad0e71891715a3a743409c8d4808d78869f
guoshijiang@guoshijiangdeMacBook-Pro airdrop-reward % cast wallet n
Successfully created new keypair.
Address: 0xD58056FD35494F0CA982b4A7D11ad15b81fB5B21
Private key: 0x3ba98f76c97ab5baec7e6d4384c6d268e2fc9b5f0c46ac5151240f6f1a5a5fd0
guoshijiang@guoshijiangdeMacBook-Pro airdrop-reward %
发送奖励
bash
cast send --rpc-url $RPCURL --private-key $PRIVATE_KEY 0x72A6a0cbFD619D004bcde04B96572Ef8981e9A8f "distributeReward(address,address[],uint256[])" 0x6aB7feE609617fbC3d47eA46E8EBABed57eB5064 \
- 发完奖励之后会产生合约事件,我们要监听就是这个合约事件,交易链接:https://testnet.bscscan.com/tx/0x679b68c114cdc514ee6a14f8e255b16d34fe8633a424074fca95d7235285633e#eventlog
【配图 1】原文此处为图片示意,因平台导出限制未能搬运。
源文档: https://w2z1amg2x5.sg.larksuite.com/docx/U5nldWAiAo9mQkxyaDQlq0Jwgdh
转 gas Fee 给地址
bash
cast send --rpc-url $RPCURL --private-key $PRIVATE_KEY 0xE5F445da5a72d0aaf3F844957C5E7BBE6c636125 --value 0.001ether
cast send --rpc-url $RPCURL --private-key $PRIVATE_KEY 0xFe2CE71F16594bb3f893f081C50b3B811c0BB22E --value 0.001ether
cast send --rpc-url $RPCURL --private-key $PRIVATE_KEY 0xD58056FD35494F0CA982b4A7D11ad15b81fB5B21 --value 0.001ether
领取奖励
领取之后产生事件,交易链接:https://testnet.bscscan.com/tx/0x86d5fea196153e80ee6d2eee3330e5027f68094d87122aeb76ca4513dc02286b#eventlog
【配图 2】原文此处为图片示意,因平台导出限制未能搬运。
源文档: https://w2z1amg2x5.sg.larksuite.com/docx/U5nldWAiAo9mQkxyaDQlq0Jwgdh
ERC20 代币化标准
同质化代币和非同质化代币
非同质化代币或者半同质化代币,怎么理解非同质化代币或者半同质化代币?
同质化代币:资产可以进行分割,分割和同单位的每一个资产都是一样的,人民币:100 块分割 100 个 1 块钱,每一个 1 块钱都是一样,1 块钱又可以分割 100 个 1 分钱,每一个 1 分钱都是完全一样的...
非同质化代币:每个资产都是独一无二,相互独立,不可以分割。比方说 1 幅画,一个瓷器
半同质化代币:和非同质化代币,可以进行分割,通过交易包,交易包里面的资产是一个完整的资产,比如:用各种车的配件组织出来,这些配件可以单独交易,也可以一个交易包的形式进行交易。
NFT 表现形式:一幅图,一辆车,一个视频,一个文件,一个人,一个猴子
NFT 在区块链上:NFT 的创作者和持有者是可以被证明的,在链上可以清晰看到,可以通过区块链去朔源
ERC20 代币化标准
java
函数事件监听器
什么是合约的事件监听器
以空投奖励的程序说明合约事件监听的重要性,发空投给用户的时候会将奖励更新到链上,更新完奖励数据之后用户就可以去领取奖励,领取完成之后更新领取的记录。链上整个状态变更,一般都是通过抛出合约事件来完成的,合约事件和链下服务协同作用的结果。
合约事件案例代码:https://github.com/the-web3-contracts/airdrop-reward
合约事件监听的方式
从原生 RPC 获取合约链上事件进行解决
-
eth_getlogs 获取事件解析
-
eth_getTransactionRecept: 和单笔交易绑定的合约
-
eth_blockReceipt 和区块关联的合约事件
第三方工具(重点将 The Graph)
- The Graph(后面单独)
使用 go 代码通过 eth_getlogs 或者是 eth_getTransactionRecept 的方式去获取合约事件进行解析
- event.go
go
package event
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
)
type EthClient struct {
client *ethclient.Client
}
func NewEthClient(rpcUrl string) (*EthClient, error) {
ethClient, err := ethclient.DialContext(context.Background(), rpcUrl)
if err != nil {
log.Error("new eth client fail", "err", err)
return nil, err
}
return &EthClient{client: ethClient}, err
}
// GetTxReceiptByHash eth_getTransactionReceipt
func (eth *EthClient) GetTxReceiptByHash(txHash string) (*types.Receipt, error) {
- test.go
go
package event
上面的代码是对 DistributeReward 合约事件的解析,大家可以去把 ClaimReward 的事件自行尝试解析一下。
三.合约继承
合约继承的基本概念
合约的继承使用关键字 is 来表示,语法格式
solidity
pragma solidity ^0.8.20;
contract Base {
}
contract BaseTwo {
}
contract A is Base {
}
contract B is Base, BaseTwo {
}
把状态变量和合约的函数都会继承下来,除了 private 修饰的函数,所有父类的函数都会被继承,继承之后可以对父类的虚函数进行重写。
继承的代码案例
java
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
contract Base {
address public managerAddress;
address public fundingMangerAddress;
event SetManagerAddress(address indexed newAddress, address OldAddress);
event SetFundingAddress(address indexed newAddress, address OldAddress);
constructor(address _managerAddress, address _fundingMangerAddress) {
managerAddress = _managerAddress;
fundingMangerAddress = _fundingMangerAddress;
}
function getManagerAddress() public view returns (address) {
return managerAddress;
}
function getFundingAddress() public view virtual returns (address) {
return managerAddress;
}
function setAll (address _managerAddress, address _fundingMangerAddress) external {
ContractsInherit 继承了 Base,子类合约可以访问父类的 public 和 external 方法和状态变量,virtual 函数可以子类重写。internal 修饰的方法可以在子类里面被调用,但是 private 的不行。
四.抽象合约和接口
合约的接口
接口定义
接口使用 interface 关键字来声明,并且不允许包含任何状态变量或函数实现。
sql
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
interface ITheWebThreeToken {
struct theWeb3CoinPool {
address miningPool;
address directSalePool;
address investorSalePool;
address ecosystemPool;
address foundationPool;
}
event SetManager(address indexed newAddress, address oldAddress);
event SetPoolAddress(theWeb3CoinPool indexed pool);
function setManager(address _manager) external;
function setPoolAddress(theWeb3CoinPool memory _twPool) external;
function twtBalance(address _address) external view returns (uint256);
function poolAllocate() external;
}
在上面的示例中,InterfaceType 是一个接口,定义了两个函数 getValue 和 setValue,这些函数只有声明没有实现, 修饰符是 external。
实现接口
javascript
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
import "./TheWebThreeTokenStorage.sol";
import "./uitils/LibToken.sol";
contract TheWebThreeToken is Initializable, OwnableUpgradeable, TheWebThreeTokenStorage, LibToken {
modifier onlyManager() {
require(
msg.sender == manager,
"TheWebThreeToken onlyManager: Only manager can call this function"
);
_;
}
constructor() {
抽象合约
抽象合约是一种约束合约行为的机制,它本身无法被实例化,需要通过继承的方式来实现它的方法。抽象合约通常包含未实现的函数定义,而具体的实现逻辑则由继承它的子合约来完成。
定义抽象合约
在 Solidity 中,抽象合约通过使用 abstract 关键字来声明。抽象合约中可以包含函数的定义,但不能包含函数的实现。
- 案例 1:
java
pragma solidity ^0.8.0;
// 抽象合约
abstract contract MyAbstractContract {
function getValue() public view virtual returns (uint);
function setValue(uint _value) public virtual;
}
- 案例 2:
sql
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol";
import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20BurnableUpgradeable.sol";
abstract contract LibToken is ERC20Upgradeable, ERC20BurnableUpgradeable {
address public msgSender;
constructor() {
msgSender = msg.sender;
}
function tokenBalance(address _address) external view virtual returns (uint256) {
return balanceOf(_address);
}
function tokenDecimals() external view virtual returns (uint8);
}
在上面的示例中,MyAbstractContract 是一个抽象合约,它定义了两个函数 getValue 和 setValue,但没有实现它们的具体逻辑。
实现抽象函数
案例一实现:
-
virtual:虚函数修饰符
-
override:重写函数
java
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
案例二实现
javascript
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
import "./DappLinkTokenStorage.sol";
import "./uitils/LibToken.sol";
contract DappLinkToken is Initializable, OwnableUpgradeable, DappLinkTokenStorage, LibToken {
constructor() {
_disableInitializers();
}
function initialize(address _owner) public initializer {
require(
_owner != address(0),
"TheWebThreeToken initialize: _owner can't be zero address"
);
__ERC20_init(NAME, SYMBOL);
__ERC20Burnable_init();
__Ownable_init(_owner);
_transferOwnership(_owner);
总结
抽像合约:
-
构造函数
-
已实现的函数
-
未实现的函数(未实现的函数需要继承的子类合约来实现)
-
状态变量
接口:接口是一种特殊的抽象合约,只包含未实现的函数,而且函数必须使用 external
- 接口里面不允许以下这些东西存在
使用建议
-
使用抽象合约来定义具有共同行为和功能的合约族。
-
使用接口来定义外部行为和服务接口,以提高合约的互操作性和可扩展性。
-
合理设计和使用抽象合约和接口,可以帮助提高合约的模块化和可维护性,以及更好地符合面向对象编程的设计原则。
五.合约的 lib 库
在 Solidity 中,库(Library) 是一种特殊的智能合约,用于封装可复用的逻辑或功能。库与普通合约的区别在于,它不能保存状态变量,也不能接收 ETH。库可以被其他合约直接调用,从而减少代码冗余,提高开发效率。
库的特点
-
代码复用:封装通用逻辑,多个合约可以共享同一库
-
无状态:库不能定义或修改状态变量
-
直接调用:使用 delegatecall 将库的逻辑运行在调用合约的上下文中
-
不可以部署的独立逻辑:库的函数必须是内部函数或通过合约调用,不能独立执行
库的类型
内部库
-
函数直接被编译器内联到调用合约中
-
没有单独的部署地址
-
调用成本低,无需 delegatecall
go
library Math {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
return a + b;
}
}
contract Example {
using Math for uint256;
外部库
-
需要单独部署,库有自己的地址
-
调用时通过 delegatecall 运行在调用合约的上下文中
-
优点:可共享逻辑,减少重复部署
-
示范
typescript
mapping(address => uint256) public userTwoBalance;
uint256[100] private __gap;
}
contract ContractSlotTwo is ContractSlotTwoStorege {
function updateTwoBalance(uint256 _balance) public {
require(_balance > 0, "invalid balance");
userTwoBalance[msg.sender] = _balance;
}
}
contract ContractSlotThree is ContractSlotOne, ContractSlotTwo {
uint256 public bb;
}
- Slot 布局
【配图 3】原文此处为图片示意,因平台导出限制未能搬运。
源文档: https://w2z1amg2x5.sg.larksuite.com/docx/U5nldWAiAo9mQkxyaDQlq0Jwgdh
基础类型变量的存储布局
基础类型变量包括:uint256、address、bool 等。
solidity
contract SimpleStorage {
uint256 a; // slot 0
address b; // slot 1
bool c; // slot 2
}
-
32 字节的每个基础类型单独占用一个slot
-
变量不足 32 字节,所有变量加起来小于于 32 字节,单独占一个slot
-
大于 32 字节的往下移一个 slot
变量紧密打包(Packing)
当多个较小的变量(例如:uint8, uint16, bool 等)连续声明时,编译器会将多个变量紧密打包到同一个槽位中,以节约空间:
solidity
contract PackedStorage {
uint128 a; // slot 0 (前16字节)
uint64 b; // slot 0 (中间8字节)
uint64 c; // slot 0 (最后8字节)
uint256 d; // slot 1 (新槽位)
}
紧密打包规则:
-
Solidity 会尽量把连续声明的小变量塞进同一个32字节槽位中
-
如果超出32字节,则另开新槽位
动态类型变量的存储布局
动态类型变量主要包括:
-
动态长度数组(如:uint256\[\])
-
mapping 映射(如:mapping(address => uint256))
-
bytes 和 string 类型
动态数组存储布局
动态数组定义如下:
solidity
contract DynamicArray {
uint256[] arr; // slot 0
}
存储布局为:
-
槽位本身(slot 0):存储数组长度;
-
数组元素:元素从槽位 keccak256(slot) 开始存储,连续排列:元素 i 的槽位位置 = keccak256(slot) + i
例如:
-
arr.length 存储于 slot 0;
-
arr0 存储于 slot: keccak256(0);
-
arr1 存储于 slot: keccak256(0) + 1;
-
arr2 存储于 slot: keccak256(0) + 2;以此类推。
mapping 存储布局
mapping 是动态哈希表:
solidity
contract MappingStorage {
mapping(uint256 => uint256) map; // slot 0
}
-
槽位本身(slot 0): 不存储任何实际数据,仅用于哈希计算起点;
-
每个键值对存储位置计算方式:
例如:
-
map5 存储在 keccak256(abi.encode(5, 0));
-
map100 存储在 keccak256(abi.encode(100, 0))。
bytes 和 string 存储布局
bytes 和 string 类型本质上也是动态数组,其存储机制为:
-
如果长度小于等于31字节:
-
如果长度大于31字节:
结构体(struct)类型的存储布局
address addr; // slot 1
uint128 age; // slot 2 (前半)
uint128 score;// slot 2 (后半)
}
Person p; // 从 slot 0 开始排列(结构体内变量依次排列)
}
结构体:
-
每个成员依次占用连续的槽位;
-
小成员按紧密打包原则节省空间。
嵌套结构存储布局
复杂数据结构(如结构体数组、mapping 嵌套数组)存储布局,简单举例:
solidity
contract NestedStorage {
mapping(uint => uint[]) nested; // slot 0
}
计算位置步骤:mapping 元素位置计算方式:
nested[key] => keccak256(abi.encode(key, 0))
-
数组长度存储于上述位置;
-
数组元素依次存储于:
keccak256(keccak256(abi.encode(key, 0))) + index
插槽分配总结表
slot 存储解析的实际应用
在智能合约安全审计、调试、链上数据读取过程中,经常用到明确的 slot 定位和计算,比如:
-
通过链上存储位置,精确读取合约状态;
-
分析合约升级、代理合约、ERC-1967 存储槽位。
示例:手动计算 mapping 存储位置(实操)
例如定义
mapping(uint256 => uint256) public balances; // slot 3
计算 balances999 的具体位置
bytes32 location = keccak256(abi.encode(uint256(999), uint256(3)));
通过 EVM 读取此位置数据
assembly {
let value := sload(location)
}
案例代码
typescript
abstract contract ContractSlotOneStorege {
struct Person {
uint256 id;
address addr;
uint128 age;
uint128 score;
}
address public managerAddress;
address public operatorAddress;
-
状态变量
-
构造函数
-
已实现的函数
-
库代码
go
-
调用代码
-
存储在单个槽位内(紧密打包);
-
最低位的字节存储长度 length * 2,高位存储数据。
-
槽位本身存储:长度*2 + 1;
-
数据存储位置从 keccak256(slot) 开始,连续存储。