Solidity
是一款以由以太坊 (ETH ,Ethereum)开源社区推出的面向对象 的静态 程序设计语言,主要用于在
Web 3.0 世界创建智能合约,其语法特性受到了
C++、Python、JavaScript
等编程语言的影响。支持继承、库、复杂的用户自定义类型以及其它特性。官方推荐在生产环境撰写以太坊智能合约的时候,总是使用最新的
Solidity 版本,从而获得安全修复以及各种新特性,本篇文章撰写时 Solidity
最新的生产环境版本为 v0.8.24
。
除了 Solidity 的各种常用语言特性之外,还会介绍一系列 Web 3.0
开发过程当中,所经常使用的第三方开源项目。其中 Hardhat
是一个用于编译、部署、测试、调试以太坊应用的开发环境,而 Ganache
则是一款用于开发测试 dApps (Decentralized
Applications)的本地区块链应用。除此之外,OpenZeppelin
的 Contract
则是一款用于开发安全智能合约的库,提供有 ERC20 和
ERC721
的标准实现,以及灵活的的权限方案,乃至于各种常用的工具组件。
Web 3.0 简介
区块
区块 包含有大量捆绑的交易,以其作为最小单位在所有节点当中进行分发,如果两个交易相互矛盾,那么排在第二位的交易会被拒绝,不会成为区块的一部分。
区块链
区块链 就是指区块 按照时间 形成的线性序列,区块每间隔一段时间就会被添加到链上面,其本质上就类似于一个公共的事务型数据库。
以太坊虚拟机
以太坊虚拟机 (EVM,Ethereum
Virtual Machine)是以太坊智能合约的运行环境。
以太坊账户
以太坊账户主要分为两种:外部账户 (由公私钥对控制,地址由公钥确定)、合约账户 (由与账户一起存储的代码控制,地址在合约创建时被确定)。
以太坊账户余额
以太坊账户余额 的最小单位是
Wei
(\(1 ETH = 10^{18}
wei\) )),余额会因为发生以太币的交易而改变。
交易
交易 可以视为帐户之间相互发送的消息,每笔交易都会消耗一定数量的
Gas(由交易的发起人支付)。
构建 Hardhat 环境
npm 安装 Hardhat
Hardhat
是一款编译、部署、测试和调试以太坊应用的开发工具,可以用于实现智能合约与
dApps 开发过程当中的自动化任务,但是 Hardhat
最核心的地方依然是编译、运行、测试智能合约。
1 npm install --save-dev hardhat
注意 :Hardhat 需要运行在 NodeJS 基础之上,所以在安装 Hardhat
之前需要先行安装 NodeJS,并且将安装目录填写至 PATH
环境变量当中。
通过上面的语句,可以在一个 npm 工程当中快速的安装
Hardhat,然后在工程目录里执行
npx hardhat
,就可以快速查看当前可用的命令 与任务 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 λ npx hardhat Hardhat version 2.20.1 Usage: hardhat [GLOBAL OPTIONS] [SCOPE] <TASK> [TASK OPTIONS] GLOBAL OPTIONS: --config A Hardhat config file. --emoji Use emoji in messages. --flamegraph Generate a flamegraph of your Hardhat tasks --help Shows this message, or a task's help if its name is provided --max-memory The maximum amount of memory that Hardhat can use. --network The network to connect to. --show-stack-traces Show stack traces (always enabled on CI servers). --tsconfig A TypeScript config file. --typecheck Enable TypeScript type-checking of your scripts/tests --verbose Enables Hardhat verbose logging --version Shows hardhat' s version.AVAILABLE TASKS: check Check whatever you need clean Clears the cache and deletes all artifacts compile Compiles the entire project, building all artifacts console Opens a hardhat console coverage Generates a code coverage report for tests flatten Flattens and prints contracts and their dependencies. If no file is passed, all the contracts in the project will be flattened. gas-reporter:merge help Prints this message node Starts a JSON-RPC server on top of Hardhat Network run Runs a user-defined script after compiling the project test Runs mocha tests typechain Generate Typechain typings for compiled contracts verify Verifies a contract on Etherscan or Sourcify AVAILABLE TASK SCOPES: vars Manage your configuration variables To get help for a specific task run: npx hardhat help [SCOPE] <TASK>
初始化 Hardhat 工程
通过运行 npx hardhat init
可以初始化出一个基本的 Hardhat
工程目录结构:
contracts
目录:用于存放 .sol
智能合约
scripts
目录:用于存放任务脚本。
test
目录:用于存放测试文件。
hardhat.config.js
文件:Hardhat 配置文件。
编译智能合约
在工程目录运行 npx hardhat compile
命令,可以编译
contracts
目录下的智能合约(例如该工程当中的
contracts/Lock.sol
文件):
1 2 3 4 λ npx hardhat compile Downloading compiler 0.8.24 Compiled 1 Solidity file successfully (evm target: paris).
测试智能合约
然后再运行 npx hardhat test
命令,可以执行
contracts
目录下的测试脚本文件(例如本工程当中的
test/Lock.js
文件):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 λ npx hardhat test Lock Deployment √ Should set the right unlockTime (11217ms) √ Should set the right owner √ Should receive and store the funds to lock √ Should fail if the unlockTime is not in the future (141ms) Withdrawals Validations √ Should revert with the right error if called too soon √ Should revert with the right error if called from another account (39ms) √ Shouldn't fail if the unlockTime has arrived and the owner calls it (42ms) Events √ Should emit an event on withdrawals Transfers √ Should transfer the funds to the owner (88ms) 9 passing (12s)
部署智能合约
继续运行 npx hardhat run scripts/deploy.js
命令,就可以执行 scripts
目录下的 Hardhat
任务脚本(例如本工程当中的 scripts/deploy.js
文件),此时
Hardhat 会将智能合约部署到执行命令时,自动启动的 Hardhat Network
本地测试网络服务当中:
1 2 3 λ npx hardhat run scripts/deploy.js Lock with 0.001ETH and unlock timestamp 1708929986 deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3
启动 Hardhat Network
除此之外,也可以通过手动运行 npx hardhat node
命令,启动该本地测试网络服务的同时,还会生成一系列测试用账户:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 λ npx hardhat node Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/ Accounts ======== WARNING: These accounts, and their private keys, are publicly known. Any funds sent to them on Mainnet or any other live network WILL BE LOST. Account Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 Account Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d Account Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a Account Private Key: 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 Account Private Key: 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a Account Private Key: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... WARNING: These accounts, and their private keys, are publicly known. Any funds sent to them on Mainnet or any other live network WILL BE LOST.
注意 :Hardhat 内置的 Hardhat Network
是一个为开发而设计的本地以太坊网络 。
连接 Hardhat Network
通过 npx hardhat node
启动 Hardhat Network
本地测试网络服务之后,就会向外暴露一个 JSON-RPC 服务接口
http://127.0.0.1:8545/
,把区块链钱包等应用连接至该接口就可以使用。此时如果需要将上面的
Lock.sol
智能合约,部署到上述这个已经启动了的测试网络,则需要再添加上一个
--network localhost
参数:
1 2 3 λ npx hardhat run scripts/deploy.js --network localhost Lock with 0.001ETH and unlock timestamp 1708931638 deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3
日志打印 console.log()
通过向 Hardhat 项目当中的 .sol
智能合约里引入
console.sol
,就可以愉快的在项目当中使用
console.log();
日志打印方法,从而能够更加便捷的在 Hardhat
Network 控制台查看到智能合约打印的调试信息:
1 import "hardhat/console.sol" ;
引入以太坊 Web3.js
web3.js
是以太坊官方开源社区提供的一个 JavaScript 库,允许通过
HTTP、IPC、WebSocket 与本地或者远程的以太坊 EVM
区块链节点进行各种交互,可以通过下面的命令将其安装在 Hardhat
工程当中:
1 npm install --save-dev web3
除此之外,也可以通过在 Hardhat 项目当中安装插件的形式,将 Web3.js
无缝整合到到工程当中:
1 npm install --save-dev @nomicfoundation/hardhat-web3-v4
通过在 Hardhat 当中编写 script
脚本,就可以借助 Web3.js 与 Hardhat
本地的测试网络进行交互,具体步骤请参考下面的示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 const { Web3 } = require ("web3" );async function main ( ) { const web3 = new Web3 ("http://127.0.0.1:8545/" ); const TestAccounts = [ { ID : 0 , address : "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" , privateKey : "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" , }, { ID : 1 , address : "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" , privateKey : "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" , }, { ID : 2 , address : "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" , privateKey : "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" , }, ]; const BlockNumber = await web3.eth .getBlockNumber (); console .log ("当前区块号: " , BlockNumber ); const Balance = await web3.eth .getBalance (TestAccounts [0 ].address ); console .log ("指定账户地址的余额: " , Balance ); const ChainId = await web3.eth .getChainId (); console .log ("当前区块链 ID: " , ChainId ); const TransactionCount = await web3.eth .getTransactionCount (TestAccounts [0 ].address ); console .log ("指定账户的交易数量: " , TransactionCount ); const GasPrice = await web3.eth .getGasPrice (); console .log ("当前区块链网络的 Gas 价格: " , GasPrice ); const WalletAccounts = await web3.eth .accounts .wallet .create (3 ); console .log ("随机生成 3 个钱包账户: " , WalletAccounts ); const PrivateWalletAccount = await web3.eth .accounts .wallet .add (TestAccounts [0 ].privateKey ); console .log ("通过私钥生成的钱包账户地址: " , PrivateWalletAccount [0 ].address ); console .log ("通过私钥生成的钱包账户私钥: " , PrivateWalletAccount [0 ].privateKey ); const TX = { from : TestAccounts [0 ].address , to : TestAccounts [1 ].address , value : web3.utils .toWei ("0.000001" , "ether" ), }; const txReceipt = await web3.eth .sendTransaction (TX ); console .log ("转账交易哈希:" , txReceipt.transactionHash ); const AccessControlAddress = "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" ; const AccessControlABI = require ("../artifacts/contracts/TestAccessControl.sol/TestAccessControl.json" ).abi ; const AccessControlContract = new web3.eth .Contract (AccessControlABI , AccessControlAddress ); const ReturnValue = await AccessControlContract .methods .securedFunction ().call (); console .log ("智能合约调用返回值 :" , ReturnValue ); const ReceiptTX = await AccessControlContract .methods .addToRole (TestAccounts [1 ].address ).send ({ from : TestAccounts [0 ].address }); console .log ("智能合约调用交易哈希:" , ReceiptTX .transactionHash ); } main ().catch ((error ) => { console .error (error); process.exitCode = 1 ; });
智能合约基本结构
Solidity 将 Web 3.0
当中的智能合约 视为面向对象编程当中的类 ,每一份智能合约可以包含状态变量
、函数
、函数修饰器
、事件
、错误
、结构体类型
、枚举类型
的声明,并且智能合约之间也可以相互进行继承。
许可标识 & 编译指示
第 1 行的 // SPDX-License-Identifier: MIT
称为 SPDX
许可标识符 ,用于声明当前 Solidity 源代码基于 MIT
开源协议编写。
第 2 行的 pragma solidity >=0.8.24 <0.9.0;
称为版本编译指示 ,用于声明当前代码所要使用的 Solidity
编译器版本(大于或等于 0.8.24
但是低于 0.9.0
的版本)。
1 2 3 4 5 6 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { }
注意 :访问 Solidity
智能合约当中的状态变量(例如上面的 data
状态变量),通常不需要添加 this
关键字,通过变量名称就可以直接进行访问。
状态变量
状态变量 是指其值被永久地存储在合约存储中的变量。
1 2 3 4 5 6 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { uint data; }
函数
函数 function
用于接受参数并且返回变量,即可以在智能合约 contract
的内部定义,也可以在智能合约的外部定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { uint data; function set (uint value ) public { data = value; } function get ( ) public view returns (uint) { return data; } }
函数修饰器
函数修饰器 modifier
可以用于以声明的方式修改函数的语义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { address public user; modifier onlyUser ( ) { require ( msg.sender == user, "Only user can invoke this." ); _; } function remove ( ) public view onlyUser { } }
注意 :修饰器与函数一样也可以被重载 。
事件
事件 event
可以用于方便的调用以太坊虚拟机 EVM 的日志功能。
1 2 3 4 5 6 7 8 9 10 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { event myEvent (address user, uint money); function triggerEvent ( ) public payable { emit myEvent (msg.sender , msg.value ); } }
结构体类型
结构体类型 struct
是一种可以把多个具有关联关系的变量,组合在一起的自定义数据类型。当声明并且定义好一个结构体变量之后,就可以通过成员访问操作符
.
访问结构体的成员:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 pragma solidity >=0.8 .24 <0.9 .0 ; contract MyContract { struct Person { string name; uint age; bool isStudent; } Person public person; constructor ( ) { person = Person ("Hank" , 18 , true ); } function getPersonName ( ) public view returns (string memory) { return person.name ; } function getPersonAge ( ) public view returns (uint) { return person.age ; } function isPersonStudent ( ) public view returns (bool) { return person.isStudent ; } }
枚举类型
枚举可用来创建由一定数量的'常量值'构成的自定义类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 pragma solidity >=0.8 .24 <0.9 .0 ; contract MyEnumContract { enum Color { Red , Green , Blue } Color public favoriteColor; constructor ( ) { favoriteColor = Color .Blue ; } function getFavoriteColor ( ) public view returns (Color ) { return favoriteColor; } }
错误
错误 error
可以用于为系统异常定义描述性的名称和信息,其 Gas
开销要比使用字符串更加便宜。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pragma solidity >=0.8 .24 <0.9 .0 ; error NotEnough (uint requested, uint available); contract Token { mapping (address => uint) balances; function transfer (address to, uint amount ) public { uint balance = balances[msg.sender ]; if (balance < amount) revert NotEnough (amount, balance); } }
注释语句
Solidity 支持 C 语言风格的单行与多行注释:
除此之外,Solidity 还支持 NatSpec 风格的注释,也就是 ///
和 /** ... */
,主要用于函数声明和定义相关的语句上面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pragma solidity >=0.8 .24 <0.9 .0 ; contract SimpleStorage { uint storedData; function set (uint x ) public { storedData = x; } function get ( ) public view returns (uint) { return storedData; } }
变量作用域
Solidity
当中的变量按照作用域可以划分为状态变量 (State
Variable)、局部变量 (Local
Variable)、全局变量 (Global Variable)三种类型:
状态变量 是用于将数据保存在区块链上的变量,智能合约当中的函数都可以进行访问,所消耗的
Gas 比较高:
1 2 3 4 5 6 7 8 pragma solidity >=0.4 .16 <0.9 .0 ; contract Test { uint public x = 1 ; uint public y = 2 ; uint public z = x + y; }
局部变量 只在函数执行期间有效,存储在内存当中,不会上链,所消耗的
Gas 比较低:
1 2 3 4 5 6 7 8 9 10 11 pragma solidity >=0.4 .16 <0.9 .0 ; contract Test { function add ( ) external pure returns (uint256) { uint256 x = 1 ; uint256 y = 2 ; uint256 z = x + y; return (z); } }
全局变量 基本都是 Solidity
预留的关键字,可以在函数当中不声明直接进行使用,具体请叁考官方文档中的《单位和全局变量》 :
1 2 3 4 5 6 7 8 9 10 11 12 pragma solidity >=0.4 .16 <0.9 .0 ; contract Test { function global ( ) external view returns (address, uint, bytes memory ){ address mySender = msg.sender ; uint256 myNumber = block.number ; bytes memory myData = msg.data ; return (mySender, myNumber, myData); } }
常量 constant
常量 constant
必须在声明的同时进行初始化,后续不能再进行修改。
1 2 3 4 5 6 7 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { uint public constant year = 2024 ; uint public constant month = 2 ; }
不变量 immutable
不变量 immutable
可以在声明的时候,或者构造函数(非普通函数)当中进行初始化,使用起来将会更加便利。
1 2 3 4 5 6 7 8 9 10 11 12 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { uint public immutable year; uint public immutable month = 2 ; constructor ( ) { year = 2024 ; } }
条件判断 & 循环控制
Solidity 同样提供有 if else
条件判断语句和
for
、while
、do while
循环控制语句,以及 continue
、break
关键字和三元操作符。
if else 判断
1 2 3 4 5 6 7 8 9 10 11 12 13 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { function testIfElse (uint256 number ) public pure returns (bool) { if (number == 0 ) { return (true ); } else { return (false ); } } }
for 循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { function testFor ( ) public pure returns (uint256) { uint256 sum = 0 ; for (uint256 index = 0 ; index < 10 ; index++) { sum += index; } return (sum); } }
while 循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { function testWhile ( ) public pure returns (uint256) { uint256 sum = 0 ; uint256 index = 0 ; while (index < 10 ) { sum += index; index++; } return (sum); } }
do while 循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { function testDoWhile ( ) public pure returns (uint256) { uint256 sum = 0 ; uint256 index = 0 ; do { sum += index; index++; } while (index < 10 ); return (sum); } }
三元操作符
1 2 3 4 5 6 7 8 9 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { function testTernaryOperator (uint256 x, uint256 y ) public pure returns (uint256) { return x >= y ? x : y; } }
数值类型 Value Type
对数值类型的变量进行赋值时候,直接传递的是数值本身。
布尔类型 bool
Solidity 当中布尔类型 bool
可取的值只有
true
和 false
两个:
1 2 3 4 5 6 7 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { bool truly = true ; bool falsely = false ; }
整型 int/uint
关键字int
和 uint
分别表示有符号 和无符号 的整型变量(uint
和 int
本质上分别是 uint256
和
int256
的别名)。
关键字 int8
到 int256
以及
uint8
到 uint256
可以用于表示从 8 位到
256
位 ,以八位 作为步长递增的有符号 或者无符号 整型变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { int intValue = 2024 ; uint uintValue = 2024 ; int8 int8Value = 99 ; int32 int32Value = 2024 ; int256 int256Value = 2024 ; uint8 uint8Value = 99 ; uint32 uint32Value = 2024 ; uint256 uint256Value = 2024 ; }
地址类型 address
地址类型是 Solidity
提供的一种特殊数据类型,主要用于保存以太坊地址,并且拥有一系列的成员变量 :
address
: 用于保存 20 字节的太坊地址,;
address payable
: 保存太坊地址的同时,还拥有额外的
transfer()
和 send()
方法;
1 2 3 4 5 6 7 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { address myAddress = 0xd82b7E2f20C3FBAc76e74D1C8d8C6af8032bbEc0 ; address payable myPayableAddress = payable (myAddress); }
注意 :上述两种地址类型进行转换时,address payable
可以自动转换为 address
,而 address
则需要通过
payable(<address>)
,才能被强制转换为
address payable
。
枚举类型 enum
枚举类型 enum
用于为从 0
开始计数的
uint
类型数据分配名称(最大不能超过
256
),从而提高代码的可读性:
1 2 3 4 5 6 7 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { enum Cars {HAVAL , GEELY , CHERY } Cars public myFavoriteCar = Cars .GEELY ; }
引用类型 Reference Type
对引用类型变量进行赋值的时候,实际上传递的是地址指针。
数组 [ ]
Solidity
的数组可以在声明时指定长度(数组元素类型[长度]
),也可以动态调整长度(数组元素类型[]
):
1 2 3 4 5 6 7 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { int[5 ] intArray = [int (1 ), 2 , 3 , 4 , 5 ]; uint[] uintArray = [1 , 2 , 3 , 4 , 5 ]; }
1 2 3 4 uint[] memory number = new uint[](3 ); number[0 ] = 1985 ; number[1 ] = 2010 ; number[2 ] = 2024 ;
注意 :Solidity
判别数组元素类型的时候,总是会以第 1
个元素 的数据类型作为判定依据。
定长字节数组 bytesX
定长字节数组
bytes1
、bytes2
、bytes3
...
bytes32
用于表达从 1 至
32 长度的字节序列。
1 2 3 4 5 6 7 8 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { bytes1 one = "1" ; bytes3 three = "123" ; bytes5 five = "uinio" ; }
变长字节数组 bytes/string
变长字节数组 bytes
和变长 UTF-8
编码字符串 string
本质上是一种特殊的数组。
bytes
类似于
bytes1[]
,但是由于采用了紧打包,存储空间占用相对较少,更加节省
Gas 费用;
string
与 bytes
相同,不过不允许通过长度或者索引来进行访问;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { bytes1 one = "1" ; bytes1[] bytes1Array = [one, "2" , "3" ]; bytes bytesArray = "2024" ; string test = "Hello Hank!" ; }
注意 :bytes
和 string
类型都提供有一个 concat()
函数,用于连接两个字符串,该函数会分别返回 bytes
或
string
类型的 memory
存储位置数组。
结构体 struct
Solidity 可以通过结构体 struct
来自定义数据类型,其中的元素既可以是数值类型 ,也可以是引用类型 :
1 2 3 4 5 6 7 8 9 10 11 12 13 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { struct PCB { uint256 width; uint256 height; uint256 layer; } PCB board = PCB (100 , 100 , 4 ); }
映射类型 mapping
Solidity 的映射类型使用语法
mapping(键类型 键名称 => 值类型 值名称)
,其中键和值的名称都可以被省略,映射的值只能在函数内进行修改:
1 2 3 4 5 6 7 8 pragma solidity >=0.8 .24 <0.9 .0 ; contract MappingExampleWithNames { mapping (address => uint) public balances1; mapping (address user => uint balance) public balances2; }
Solidity 当中映射的存储位置必须为
storage
,向映射新增键值对的语法为
映射名称[键] = 值
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pragma solidity >=0.8 .24 <0.9 .0 ; contract MappingExampleWithNames { mapping (address => uint) public balances1; function updateBalances1 (uint value ) public { balances1[msg.sender ] = value; } mapping (address user => uint balance) public balances2; function updateBalances2 (uint value ) public { balances2[msg.sender ] = value; } }
存储位置
可以将引用类型的
数组 、结构体 、映射
存储位置,分别指定为
storage
、memory
、calldata
,不同存储类型所耗费的
Gas 成本不同:
storage
:智能合当中的状态变量 都默认为
storage
类型,存储在链上面,消耗 Gas 较多。
memory
:函数当中的参数和临时变量都属于
memory
类型,主要存储在内存当中,不会上链,消耗 Gas
较少。
calldata
:类似于 memory
存储在内存且不会上链,区别在于存储位置的变量不能被修改,消耗 Gas
较少。
数据类型默认值
不同于 JavaScript,在 Solidity 当中不存在 未定义
或者
空
值的概念,而且新声明的变量总是被指定为其所属数据类型的默认值 。
布尔类型
boolean
false
字符串类型
string
""
整型
int
0
无符号整型
uint
0
枚举类型
enum
首个元素
地址类型
address
0x0000000000000000000000000000000000000000
函数类型
function
空白函数
映射
mapping
所有元素都是其所属数据类型的默认值;
结构体
struct
所有成员都是其所属数据类型的默认值;
数组
array
动态数组 默认为
[]
,定长数组 为元素所属数据类型的默认值;
Solidity 提供了一个 delete
操作符,可以将指定的变换恢复为初始值:
1 2 3 4 5 6 7 8 9 10 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { bool public boolean = true ; function update ( ) external { delete boolean; } }
函数类型 function
函数的定义
Solidity
当中的函数可以接收参数,并且返回相应的处理结果,其基本的定义形式为:
1 2 3 4 5 function 函数名称(参数类型 _参数名称1 , 参数类型 _参数名称2 ) internal|external|public|private pure|view|payable returns (返回值类型 返回值名称1 , 返回值类型 返回值名称2 ){ 返回值名称1 = _参数名称1 + 1 ; 返回值名称2 = _参数名称2 + 2 ; }
1 2 3 4 5 6 7 8 9 10 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { uint public result = add (1 ); function add (uint parameter ) public pure returns (uint a) { a = parameter + 1 ; } }
当然,也可以显式的在 Solidity 当中使用 return
关键字返回值:
1 2 3 4 function 函数名称(参数类型 _参数名称1 , 参数类型 _参数名称2 ) internal|external|public|private pure|view|payable returns (返回值类型 返回值名称1 , 返回值类型 返回值名称2 ){ return (返回值1 , 返回值2 ); }
1 2 3 4 5 6 7 8 9 10 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { uint public result = add (1 ); function add (uint parameter ) public pure returns (uint a) { return parameter + 1 ; } }
读取返回值
除此之外,Solidity
函数返回值的读取,可以采用解构的方式,一次性读取全部或者部分的返回值:
1 2 3 4 5 6 变量类型 _变量名称1 ; 变量类型 _变量名称2 ; (_变量名称1 , _变量名称2 ) = 函数名称() (_变量名称1 , ) = 函数名称() (, _变量名称2 ) = 函数名称()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { function add (uint parameter ) public pure returns (uint a, uint b, uint c) { a = parameter + 1 ; b = parameter + 2 ; c = parameter + 3 ; } function invoke ( ) public pure { uint resultA; uint resultB; uint resultC; (resultA, resultB, resultC) = add (1 ); } }
状态可变性 view pure
声明为 view
的函数,可以读取状态,但是不能修改状态,这里的状态 是指:
修改状态变量;
产生事件;
创建其它智能合约;
使用了 selfdestruct
;
通过调用发送 ETH 以太币;
调用没有被标记为 view
或者 pure
的函数;
使用了低级调用;
使用了包含特定操作码的内联汇编;
1 2 3 4 5 6 7 8 pragma solidity >=0.8 .24 <0.9 .0 ; contract C { function sum (uint256 a, uint256 b ) public view returns (uint256) { return a + b + block.timestamp ; } }
声明为 pure
的函数,即不能读取状态,也不能修改状态,而这里的状态 则是指:
读取状态变量。
访问 address(this).balance
或者
<address>.balance
。
访问 block
、tx
、msg
当中的成员(除 msg.sig
和 msg.data
之外)。
调用没有被标记为 pure
的函数。
使用了包含某些操作码的内联汇编。
1 2 3 4 5 6 7 8 pragma solidity >=0.8 .24 <0.9 .0 ; contract C { function sum (uint256 a, uint256 b ) public pure returns (uint256) { return a + b; } }
注意 :由于被声明为 pure
和
view
的函数不能修改状态变量 ,因而调用时也就无需被收取 Gas
费用。
构造函数 constructor
每一份 Solidity 智能合约都可以定义一个 constructor
构造函数 ,该函数会在智能合约部署的时候自动被执行一次,因而可以用于初始化一些参数:
1 2 3 4 5 6 7 8 9 10 11 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { address owner; constructor ( ) { owner = msg.sender ; } }
函数修饰器 modifier
Solidity 提供的 modifier
修饰器语法,能够以声明 的方式来改变一些函数的行为,例如在执行函数之前自动进行一个检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { address owner; constructor ( ) { owner = msg.sender ; } modifier onlyOwner ( ) { require (msg.sender == owner); _; } function updateOwner (address _newOwner ) external onlyOwner { owner = _newOwner; } }
注意 :函数修饰器提供的占位符语句
_
用于表示添加了修饰符 的函数主体被插入的位置。
函数与状态变量的可见性
状态变量可见性
声明为 public
的状态变量 :编译器会自动为其生成 Getter
函数,从而允许其它智能合约读取其值。除此之外,同一个合约当中使用时,通过
this.x
外部访问时也会调用 Getter
函数,而通过
x
直接内部访问则会直接从存储获取变量值。 由于没有生成
Setter
函数,所以其它智能合约无法修改其值。
声明为 internal
的状态变量 :只能从其所定义的智能合约,或者派生出的智能合约当中进行访问,这也是状态变量的默认的可见性 。
声明为 private
的状态变量 :类似于内部变量,但是在派生出的智能合约当中不可以访问。
函数的可见性
声明为 external
的函数 :只能被其它智能合约或者交易调用,不能从智能合约内部被调用(无法通过
ext()
调用,但是可以通过 this.ext()
调用)。
声明为 public
的函数 :可以被任何智能合约或者交易调用。
声明为 internal
的函数 :只能在当前智能合约内部或者派生的智能合约当中访问,不能从智能合约的外部进行访问。
声明为 private
的函数 :只能在被定义的智能合约内部进行访问,无论是外部还是派生的智能合约都无法进行访问。
事件 event
Solidity 当中事件 event
的本质是以太坊虚拟机 EVM
日志功能的抽象,
1 event 事件名称(事件变量类型 事件变量名称);
下面的示例代码,每次调用 transfer()
函数进行转账的时候,都会触发 Transfer
事件,并且记录对应的变量:
1 2 3 4 5 6 7 8 9 10 11 12 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { event Transfer (address indexed from , address indexed to, uint256 value); function transfer (address from , address to, uint256 value ) external { emit Transfer (from , to, value); } }
注意 :上面代码当中出现的 indexed
关键字,可以将变量保存在以太坊虚拟机 EVM 日志的 topics
当中,从而可以方便的在后续进行检索。
以太坊虚拟机 EVM 会使用日志 Log
来存储 Solidity
事件,每一条 Log 日志都记录着 topics
主题和
data
数据两个部分:
主题 Topics
:用于描述事件,只能容纳 32
个字节,且只能保存最多三个 indexed
参数;
数据 Data
:用于保存没有被标注为
indexed
的参数,可以存储任意大小的数据;
异常处理 error
Solidity 可以使用 error()
方法定义一个不带参数的异常:
1 2 3 4 5 6 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { error TransferError (); }
除此之外,也可以使用 error()
方法定义一个携带有参数的异常:
1 2 3 4 5 6 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { error TransferError (address sender); }
通常情况下,error()
必须搭配回退命令
revert
进行使用。
1 2 3 4 5 6 7 8 9 10 pragma solidity >=0.8 .24 <0.9 .0 ; contract Test { error TransferError (address sender); function transferOwner ( ) public view { revert TransferError (msg.sender ); } }
除此之外,一些较早版本的 Solidity 还会使用已经废弃了的
require()
方法来处理异常,其缺点在于 Gas
费用会伴随异常描述字符串长度的增加而增加。
1 require (异常检查条件,"异常描述信息" );
而另外一个 assert()
方法则不能抛出自定义的异常信息,只能直接抛出默认的异常错误:
继承机制 is
Solidity 当中的智能合约可以通过 is
关键字来继承 其它合约,从而扩展其功能。子合约 可以继承父合约 当中
internal
和 public
的函数、状态变量以及事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 pragma solidity ^0.8 .0 ; contract ParentContract { uint public parentVariable; function parentFunction ( ) public virtual {} event ParentEvent (uint indexed value); } contract ChildContract is ParentContract { uint public childVariable; function parentFunction ( ) public override {} function childFunction ( ) public {} function triggerParentEvent (uint value ) public { emit ParentEvent (value); } }
注意 :Solidity
只能实现单继承 ,即一个子合约只能直接继承自一个父合约。
模块化导入 import
Solidity 支持 import
模块化导入,下面的一语句用于全局导入,可以将 filename
导入路径源文件中的全局符号 引入到当前源文件,但是会污染当前
Solidity 源文件的命名空间,并不建议使用:
下面的导入语句将 filename
当中的全局符号,导入到了一个新的命名空间 symbolName
当中,从而有效避免了命名空间的污染:
1 import * as symbolName from "filename" ;
上述的语句,可以简化的写为如下的形式:
1 import "filename" as symbolName;
如果导入源文件当中的命名符号存在冲突,则可以在导入的时候对其进行重命名:
1 import { symbol1 as alias, symbol2 } from "filename" ;
OpenZeppelin 基础
OpenZeppelin 是一家成立于
2015 年的区块链技术企业,其推出的 contracts
是一款是用于开发安全智能合约 的开源 Solidity
库,其主要提供了以下三方面的功能:
访问控制 :用于在智能合约当中,指定每个角色可以进行的操作。
Tokens :创建可以交易的资产或数字藏品,例如 ERC20
或者 ERC721。
工具 :一些通用工具函数,包括不会溢出的数学运算、签名验证等。
可以通过下面的 npm
命令快速安装
OpenZeppelin 的 contracts
库:
1 npm install @openzeppelin/contracts
OpenZeppelin 提供的大多数特性,都需要通过 Solidity
的 is
关键字,以继承 的方式来进行使用:
1 2 3 4 5 6 7 8 9 10 pragma solidity ^0.8 .20 ; import {ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol" ;contract MyNFT is ERC721 { constructor ( ) ERC721 ("MyNFT" , "MNFT" ) { } }
除此之外,还可以通过 Solidity 的 overrides
来重写 OpenZeppelin
当中提供的功能,例如希望改变 AccessControl
中的
revokeRole()
方法:
1 2 3 4 5 6 7 8 9 10 pragma solidity ^0.8 .20 ; import {AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol" ;contract ModifiedAccessControl is AccessControl { function revokeRole (bytes32, address ) public override { revert ("ModifiedAccessControl: cannot revoke roles" ); } }
有时候会想继承 某一部分 OpenZeppelin
当中的功能,并非完全的重写它们,此时就需要用使用到 solidity 的
super
关键字:
1 2 3 4 5 6 7 8 9 10 11 12 pragma solidity ^0.8 .20 ; import "@openzeppelin/contracts/access/AccessControl.sol" ;contract ModifiedAccessControl is AccessControl { function revokeRole (bytes32 role, address account ) public override { require ( role != DEFAULT_ADMIN_ROLE , "ModifiedAccessControl: cannot revoke default admin role" ); super .revokeRole (role, account); } }
访问控制 Ownerable.sol
OpenZeppelin 将发布智能合约的账户称为
owner
,其提供了 Ownerable.sol
来管理智能合约当中的所有权。
1 2 3 4 5 6 7 pragma solidity ^0.8 .20 ; import "@openzeppelin/contracts/access/Ownable.sol" ;contract MyContract is Ownable { function normalThing ( ) public {} function specialThing ( ) public onlyOwner {} }
注意 :示例代码当中的 onlyOwner
关键字是由 Openzeppelin 当中的 Ownerable.sol
所提供的。
Ownerable.sol
主要提供了如下两个功能函数:
transferOwnership()
将智能合约的所有权转移给另外一个账户;
renounceOwnership()
放弃智能合约的所有权关系;
访问控制 AccessControl.sol
除此之外,OpenZeppelin 还提供了
AccessControl.sol
来基于角色 进行访问控制(即定义多个角色,并且每个角色对应一组操作权限)。其使用非常简单,对于每个定义的角色都会创建一个角色标识符 ,用于授权、撤销、检查账户是否拥有该角色。
下面是一个基于 ERC20 Token 使用 AccessControl.sol
的例子,它定义了一个名为 minter
的角色,该角色允许账户创建新的 token
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pragma solidity ^0.8 .20 ; import "@openzeppelin/contracts/access/AccessControl.sol" ;import "@openzeppelin/contracts/token/ERC20/ERC20.sol" ;contract MyToken is ERC20 , AccessControl { bytes32 public constant MINTER_ROLE = keccak256 ("MINTER_ROLE" ); constructor (address minter ) ERC20 ("MyToken" , "TKN" ) { _setupRole (MINTER_ROLE , minter); } function mint (address to, uint256 amount ) public { require (hasRole (MINTER_ROLE , msg.sender ), "Caller is not a minter" ); _mint (to, amount); } }
OpenZeppelin 提供的 AccessControl.sol
亮点在于需要细粒度权限控制的场景,这可以通过定义多个角色来实现。通过这样的拆分,可以实现比
Ownerable.sol
提供的简单所有权控制,层级要更多的访问控制。请注意,如果需要的话,同一个账户可以拥有多个不同的角色 。
注意 :限制系统中每个组件能做的事情被称为最小权限原则 。
接下来定义一个 burner
角色来扩展上面的 ERC20 token
示例,并且通过使用 onlyRole
修饰符来允许账户销毁
token
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pragma solidity ^0.8 .20 ; import "@openzeppelin/contracts/access/AccessControl.sol" ;import "@openzeppelin/contracts/token/ERC20/ERC20.sol" ;contract MyToken is ERC20 , AccessControl { bytes32 public constant MINTER_ROLE = keccak256 ("MINTER_ROLE" ); bytes32 public constant BURNER_ROLE = keccak256 ("BURNER_ROLE" ); constructor (address minter, address burner ) ERC20 ("MyToken" , "TKN" ) { _setupRole (MINTER_ROLE , minter); _setupRole (BURNER_ROLE , burner); } function mint (address to, uint256 amount ) public onlyRole (MINTER_ROLE ) { _mint (to, amount); } function burn (address from , uint256 amount ) public onlyRole (BURNER_ROLE ) { _burn (from , amount); } }
上面的代码当中,使用了内部函数 _setupRole()
来分配角色,除此之外还可以使用以下的工具函数来管理角色:
hasRole()
:判断角色。
grantRole()
:授予角色。
revokeRole()
:回收角色。
除此之外,OpenZeppelin 提供的
AccessControl.sol
当中,还包含有一个称为
DEFAULT_ADMIN_ROLE
的特殊角色,它是所有角色的默认管理员 ,拥有该角色的账户可以去管理其它的角色,除非手工调用
_setRoleAdmin()
内部函数来指定一个新的管理员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pragma solidity ^0.8 .0 ; import "@openzeppelin/contracts/access/AccessControl.sol" ;import "@openzeppelin/contracts/token/ERC20/ERC20.sol" ;contract MyToken is ERC20 , AccessControl { bytes32 public constant MINTER_ROLE = keccak256 ("MINTER_ROLE" ); bytes32 public constant BURNER_ROLE = keccak256 ("BURNER_ROLE" ); constructor ( ) ERC20 ("MyToken" , "TKN" ) { _setupRole (DEFAULT_ADMIN_ROLE , msg.sender ); } function mint (address to, uint256 amount ) public onlyRole (MINTER_ROLE ) { _mint (to, amount); } function burn (address from , uint256 amount ) public onlyRole (BURNER_ROLE ) { _burn (from , amount); } }
除此之外,AccessControl.sol
还提供了如下两个工具函数:
getRoleMember()
:返回某个角色当中账户的地址;
getRoleMemberCount()
:返回某个角色当中账户的数量;
1 2 3 4 5 6 7 8 const minterCount = await myToken.getRoleMemberCount (MINTER_ROLE );const members = [];for (let i = 0 ; i < minterCount; ++i) { members.push (await myToken.getRoleMember (MINTER_ROLE , i)); }
代币 ERC20 Token
代币 (Token)是指区块链上,各种可以通过智能合约来调用、交易、创建、销毁的虚拟资产,其中
ERC721 是以太坊上用于非同质化代币(NFT,Non Fungible
Token)的标准,OpenZeppelin 针对 ERC721
标准提供了大量的接口方法,下面的代码可以用于构建一个 ERC721
代币智能合约:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pragma solidity ^0.8 .20 ; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol" ;import "@openzeppelin/contracts/utils/Counters.sol" ;contract GameItem is ERC721URIStorage { using Counters for Counters .Counter ; Counters .Counter private _tokenIds; constructor ( ) ERC721 ("GameItem" , "ITM" ) {} function awardItem (address player, string memory tokenURI ) public returns (uint256) { uint256 newItemId = _tokenIds.current (); _mint (player, newItemId); _setTokenURI (newItemId, tokenURI); _tokenIds.increment (); return newItemId; } }
在上面的示例代码当中,新的 NFT 可以通过执行如下代码来进行生成:
1 2 3 > gameItem.awardItem(playerAddress, "http://uinio.com/NFT.json" ) - Transfer(0x0000000000000000000000000000000000000000, playerAddress, 5)
并且每个物品的所有者和元数据都可以通过如下的方式进行查询:
1 2 3 4 5 > gameItem.ownerOf(5) playerAddress > gameItem.tokenURI(5) "http://uinio.com/NFT.json"
最终,获得的 tokenURI
就是一个如下所示的 JSON
格式数据:
1 2 3 4 5 6 { "name" : "雷神之锤" , "description" : "一个漫威电影当中的道具" , "image" : "http://localhost:1985/Web/Solidity/logo.png" , "strength" : 20 }
财务功能 Finance
OpenZeppelin 在 Finance 目录下提供了
PaymentSplitter (只存在于 Contracts
库的 4.x
版本)和 VestingWallet (存在于
Contracts 库的 4.x
和 5.x
版本)两个财务相关的智能合约:
PaymentSplitter
智能合约 :通常用于管理和分配资金,可以允许将资金分割并发送到多个地址,通常基于预定义的分配规则或比例。通常用于众筹、团队资金分配或任何需要按照特定比例分割资金的场景。
VestingWallet
智能合约 :则是一种特殊的钱包,用于管理资产的逐步解锁或归属。它通常用于确保代币或资金在一定时间段内逐步释放给接收者,而不是立即全部可用。通常用于激励计划、团队代币锁定或者任何需要时间限制的资金释放场景。
概括起来,PaymentSplitter
与 VestingWallet
两者的区别主要体现在如下四个方面:
目的不同 :PaymentSplitter
旨在分割和分配资金,而 VestingWallet
旨在逐步解锁和释放资金。
使用场景不同 :PaymentSplitter
更适用于一次性的资金分配场景,而 VestingWallet
更适用于需要长期管理和逐步释放资金的场景。
功能不同 :PaymentSplitter
主要关注资金的即时分配,而 VestingWallet
关注资金的时间锁定和逐步解锁。
透明度与可追踪性 :两者都可能提供事件来增强透明度和可追踪性,但事件的具体内容和触发条件会根据合约的具体实现而有所不同。
PaymentSplitter 分帐
OpenZeppelin 的 PaymentSplitter
智能合约库允许将一个以太坊地址收到的付款按照指定的份额 (Shares)进行分割,并将这些部分按指定的份额值发送给收款人。这个合约非常适合用于在多个团队成员、投资者或合作伙伴之间分配资金的情况。
constructor(payees, shares_)
构造函数,数组
payees
(收款人)当中的每个账户,都会获得
shares_
(份额)数组中匹配位置的份额值。
receive()
接收 ETH 以太币(会被记录至
PaymentReceived
事件)。
totalShares()
获取收款人 payees
持有的全部份额。
totalReleased()
获取已经释放的 ETH 以太币总额。
totalReleased(token)
获取已经释放的 token
代币总额。
shares(account)
获取指定地址账户持有的份额值。
released(account)
获取已释放给指定地址收款人的 ETH
以太币数量。
released(token, account)
获取已释放给指定地址收款人的
token
代币数量。
payee(index)
获取收款人数组 payees
指定
index
索引的收款人的账户地址。
releasable(account)
获取指定账户地址的收款人,当前可以释放的
ETH 以太币数量。
releasable(token, account)
获取指定账户地址的收款人,当前可以释放的
token
代币数量。
release(account)
根据持有的份额比例和之前的提款历史,向指定账户地址的收款人释放
ETH 以太币。
release(token, account)
根据持有的份额比例和之前的提款历史,向指定账户地址的收款人释放
token
代币。
PayeeAdded(account, shares)
收款人添加事件,需要指定其账户地址
account
以及所占份额 shares
。
PaymentReceived(from, amount)
智能合约收款事件,向 from
地址收取 amount
数额 ETH 以太币的事件。
PaymentReleased(to, amount)
受益人提款事件,即向 to
地址支付 amount
数额 ETH 以太币的事件。
ERC20PaymentReleased(token, to, amount)
受益人提款事件,即向 to
地址支付 amount
数额 token
代币的事件。
注意 :上述表格当中的 token
是一个
IERC20 代币智能合约的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 pragma solidity ^0.8 .0 ; import "../token/ERC20/utils/SafeERC20.sol" ;import "../utils/Address.sol" ;import "../utils/Context.sol" ;contract PaymentSplitter is Context { event PayeeAdded (address account, uint256 shares); event PaymentReleased (address to, uint256 amount); event ERC20PaymentReleased (IERC20 indexed token, address to, uint256 amount); event PaymentReceived (address from , uint256 amount); uint256 private _totalShares; uint256 private _totalReleased; mapping (address => uint256) private _shares; mapping (address => uint256) private _released; address[] private _payees; mapping (IERC20 => uint256) private _erc20TotalReleased; mapping (IERC20 => mapping (address => uint256)) private _erc20Released; constructor (address[] memory payees, uint256[] memory shares_ ) payable { require (payees.length == shares_.length , "PaymentSplitter: payees and shares length mismatch" ); require (payees.length > 0 , "PaymentSplitter: no payees" ); for (uint256 i = 0 ; i < payees.length ; i++) { _addPayee (payees[i], shares_[i]); } } receive () external payable virtual { emit PaymentReceived (_msgSender (), msg.value ); } function totalShares ( ) public view returns (uint256) { return _totalShares; } function totalReleased ( ) public view returns (uint256) { return _totalReleased; } function totalReleased (IERC20 token ) public view returns (uint256) { return _erc20TotalReleased[token]; } function shares (address account ) public view returns (uint256) { return _shares[account]; } function released (address account ) public view returns (uint256) { return _released[account]; } function released (IERC20 token, address account ) public view returns (uint256) { return _erc20Released[token][account]; } function payee (uint256 index ) public view returns (address) { return _payees[index]; } function releasable (address account ) public view returns (uint256) { uint256 totalReceived = address (this ).balance + totalReleased (); return _pendingPayment (account, totalReceived, released (account)); } function releasable (IERC20 token, address account ) public view returns (uint256) { uint256 totalReceived = token.balanceOf (address (this )) + totalReleased (token); return _pendingPayment (account, totalReceived, released (token, account)); } function release (address payable account ) public virtual { require (_shares[account] > 0 , "PaymentSplitter: account has no shares" ); uint256 payment = releasable (account); require (payment != 0 , "PaymentSplitter: account is not due payment" ); _totalReleased += payment; unchecked { _released[account] += payment; } Address .sendValue (account, payment); emit PaymentReleased (account, payment); } function release (IERC20 token, address account ) public virtual { require (_shares[account] > 0 , "PaymentSplitter: account has no shares" ); uint256 payment = releasable (token, account); require (payment != 0 , "PaymentSplitter: account is not due payment" ); _erc20TotalReleased[token] += payment; unchecked { _erc20Released[token][account] += payment; } SafeERC20 .safeTransfer (token, account, payment); emit ERC20PaymentReleased (token, account, payment); } function _pendingPayment ( address account, uint256 totalReceived, uint256 alreadyReleased ) private view returns (uint256) { return (totalReceived * _shares[account]) / _totalShares - alreadyReleased; } function _addPayee (address account, uint256 shares_ ) private { require (account != address (0 ), "PaymentSplitter: account is the zero address" ); require (shares_ > 0 , "PaymentSplitter: shares are 0" ); require (_shares[account] == 0 , "PaymentSplitter: account already has shares" ); _payees.push (account); _shares[account] = shares_; _totalShares = _totalShares + shares_; emit PayeeAdded (account, shares_); } }
VestingWallet 归属权钱包
OpenZeppelin 的 VestingWallet
智能合约库,主要用于实现代币的逐步发放 功能。这里的
Vesting
是一种归属权兑现 机制,用于在一定时间内将代币
token
发放给特定的受益人。换而言之,就是在一定时间期限内,代币逐渐可用或者可提取的过程。
constructor(beneficiary, startTimestamp, durationSeconds)
默认将智能合约的发送方设置为初始所有者 ,其中
beneficiary
为受益人,startTimestamp
为开始时间戳,durationSeconds
为归属权存续时间。
receive()
用于接收 ETH 以太币。
start()
获取开始时间戳。
end()
获取结束时间戳。
duration()
获取归属权存续时间。
released()
已被释放的 ETH 以太币数量。
released(address token)
已被释放的 token
代币数量。
releasable()
可以释放的 ETH 以太币数量。
releasable(address token)
可以释放的 token
代币数量。
release()
释放已归属的 ETH 以太币。
release(token)
释放已归属的 token
代币。
vestedAmount(timestamp)
已归属的 ETH
以太币数量,默认实现为一个线性的释放曲线。
vestedAmount(token, timestamp)
已归属的 token
代币数量,默认实现为一个线性的释放曲线。
_vestingSchedule(totalAllocation, timestamp)
归属权公式的虚拟实现,返回值为已经释放的金额。
EtherReleased(amount)
以太币 ETH 被释放事件。
ERC20Released(token, amount)
代币 token
被释放事件。
注意 :上述表格当中的 token
是一个
IERC20 代币智能合约的地址。
OpenZeppelin 的 Contracts 库在其
finance
目录下的 VestingWallet.sol
智能合约当中提供了如下源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 pragma solidity ^0.8 .20 ; import {IERC20 } from "../token/ERC20/IERC20.sol" ;import {SafeERC20 } from "../token/ERC20/utils/SafeERC20.sol" ;import {Address } from "../utils/Address.sol" ;import {Context } from "../utils/Context.sol" ;import {Ownable } from "../access/Ownable.sol" ;contract VestingWallet is Context , Ownable { event EtherReleased (uint256 amount); event ERC20Released (address indexed token, uint256 amount); uint256 private _released; mapping (address token => uint256) private _erc20Released; uint64 private immutable _start; uint64 private immutable _duration; constructor (address beneficiary, uint64 startTimestamp, uint64 durationSeconds ) payable Ownable (beneficiary) { _start = startTimestamp; _duration = durationSeconds; } receive () external payable virtual {} function start ( ) public view virtual returns (uint256) { return _start; } function duration ( ) public view virtual returns (uint256) { return _duration; } function end ( ) public view virtual returns (uint256) { return start () + duration (); } function released ( ) public view virtual returns (uint256) { return _released; } function released (address token ) public view virtual returns (uint256) { return _erc20Released[token]; } function releasable ( ) public view virtual returns (uint256) { return vestedAmount (uint64 (block.timestamp )) - released (); } function releasable (address token ) public view virtual returns (uint256) { return vestedAmount (token, uint64 (block.timestamp )) - released (token); } function release ( ) public virtual { uint256 amount = releasable (); _released += amount; emit EtherReleased (amount); Address .sendValue (payable (owner ()), amount); } function release (address token ) public virtual { uint256 amount = releasable (token); _erc20Released[token] += amount; emit ERC20Released (token, amount); SafeERC20 .safeTransfer (IERC20 (token), owner (), amount); } function vestedAmount (uint64 timestamp ) public view virtual returns (uint256) { return _vestingSchedule (address (this ).balance + released (), timestamp); } function vestedAmount (address token, uint64 timestamp ) public view virtual returns (uint256) { return _vestingSchedule (IERC20 (token).balanceOf (address (this )) + released (token), timestamp); } function _vestingSchedule (uint256 totalAllocation, uint64 timestamp ) internal view virtual returns (uint256) { if (timestamp < start ()) { return 0 ; } else if (timestamp >= end ()) { return totalAllocation; } else { return (totalAllocation * (timestamp - start ())) / duration (); } } }