Solidity语法(上)
小白入门:https://github.com/dukedaily/solidity-expert ,欢迎star转发,文末加V入群。
职场进阶: https://dukeweb3.com
讲师介绍
资深web3开发者,前bybit交易所defi团队Tech Lead,MoleDAO技术顾问,国内第一批区块链布道者,专注海外defi,dex,AA钱包等业务方向。
- 公众号:阿杜在新加坡
- github:以太坊教程
- B站:杜旭duke
- Youtube:duke du
- Twitter:dukedu2022
环境准备
- 添加测试网络:https://chainlist.org/chain/97
- faucet:https://testnet.bnbchain.org/faucet-smart
- 浏览器:https://testnet.bscscan.com/
- remix:https://remix.ethereum.org/#optimize=false&version=soljson-v0.8.18+commit.87f61d96.js
两种账户
- EOA:Externally Owned Account,与一个私钥一一对应,例如小狐狸里面的account1就是EOA
- CA:Contract Account,合约账户,没有私钥与之对应,我们部署的合约就是一个CA,它也可以持有资金。
第一个dapp
- 写状态变量(上链)是一笔交易(tx),需要矿工打包,所以需要花费资金;
- 读取状态变量,是从账本中获取数据,不是一笔交易,所以免费。(必须加上view)
// 指定编译器版本,版本标识符
pragma solidity ^0.8.13;
// 关键字 contract 跟java的class一样 智能合约是Inbox
contract Inbox {
// 状态变量,存在链上
string public message;
// 构造函数
constructor(string memory initMessage) {
// 本地变量
string memory tmp = initMessage;
message = tmp;
}
// 写操作,需要支付手续费
function setMessage(string memory _newMessage) public {
message = _newMessage;
}
// 读操作,不需要支付手续费
function getMessage() public view returns(string memory) {
return message;
}
}
基础数据类型
- int(有符号整型,有正有负)int默认为int256
- uint(无符号整型,无负数)uint默认为uint256
- 以8位为区间,支持int8,int16,int24 至 int256(uint同理)
- bool类型:true,false
- 定长字节:bytes1~bytes32
- 地址:address(20个字节,40个16进制字符,共160位),如:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Primitives {
bool public flag = 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;
uint public u256 = 456;
uint 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;
int public i256 = 456;
int public i = -123; // int is same as int256
// minimum and maximum of int
int public minInt = type(int).min;
int public maxInt = type(int).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
uint public defaultUint; // 0
int public defaultInt; // 0
address public defaultAddr; // 0x0000000000000000000000000000000000000000
}
变量variables
- 状态变量(state)
- 定义在合约内,函数外
- 存储在链上
- 本地变量(local)
- 定义在函数内
- 不会存储在链上
- 全局变量(global)
- 与当前合约无关,描述整个区块链的信息(时间、块高等)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Variables {
// State variables are stored on the blockchain.
string public msg = "Hello";
uint public age = 26;
function test() public {
// Local variables are not saved to the blockchain.
uint i = 456;
// Here are some global variables
uint height = block.blocks; // Current block height
address sender = msg.sender; // address of the caller
}
}
描述区块链信息的全局变量,常用如下:
函数 | 含义 | 备注 |
---|---|---|
blockhash(uint blockNumber) | (byte32)哈希值 | |
block.coinbase | (address) 当前块矿工的地址 | |
block.difficulty | (uint)当前块的难度 | |
block.gaslimit | (uint)当前块的gaslimit | |
block.number | (uint)当前区块的块号 | |
block.timestamp | (uint)当前块的时间戳 | 常用 |
gasleft() | (uint)当前还剩的gas | |
tx.origin | (address)交易的原始发送者的地址,只能是EOA | 常用 |
==msg.sender== | (address)当前调用发起人的地址(可能是合约CA,也可能是EOA) | 常用 |
msg.sig | (bytes4)调用数据的前四个字节(函数标识符) | 常用 |
==msg.value== | (uint)这个消息所附带的货币量,单位为wei | 常用 |
==msg.data== | (bytes)完整的调用数据(calldata) | 常用 |
tx.gasprice | (uint) 交易的gas价格 |
常量 constant
- 常量与变量相对,需要硬编码在合约中,合约部署之后,无法改变。
- 常量更加节约gas,一般用大写来代表常量。
- 高阶用法:clone合约时,如果合约内有初始值,必须使用constant,否则clone的新合约初始值为空值。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Constants {
// coding convention to uppercase constant variables
address public constant MY_ADDRESS = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
uint public constant MY_UINT = 123;
}
不可变量 immutable
- 与常量类似,但是不必硬编码,可以在构造函数时传值,部署后无法改变。
- immutable仅支持值类型(如:int,address,bytes8),不支持非值类型(如:string,bytes)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Immutable {
// coding convention to uppercase constant variables
address public immutable MY_ADDRESS;
uint public immutable MY_UINT;
bytes1 public immutable MY_BYTES1 = 0xff;
// string public immutable greetings = "hello"; // error
constructor(uint _myUint) {
MY_ADDRESS = msg.sender;
MY_UINT = _myUint;
}
}
ether和wei
- 常用单位为:wei,gwei,ether
- 不含任何后缀的默认单位是 wei
- 1 ether = 10^9 gwei
- 1 gwei = 10^9 wei
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract EtherUnits {
uint price = 0.1 ether;
uint public oneWei = 1 wei;
// 1 wei is equal to 1
bool public isOneWei = 1 wei == 1;
uint public oneEther = 1 ether;
// 1 ether is equal to 10^18 wei
bool public isOneEther = 1 ether == 1e18;
}
msg三人组
当用户发起一笔交易时,相当于向合约发送一个消息(msg),这笔交易可能会涉及到三个重要的全局变量,具体如下:
- msg.sender:表示这笔交易的调用者是谁(地址),同一个交易,不同的用户调用,msg.sender不同;
- msg.value:表示调用这笔交易时,携带的ether数量,这些以太坊由msg.sender支付,转入到当前合约(wei单位整数);
- 注意:一个函数(或地址)如果想接收ether,需要将其修饰为:payable。
- msg.data:表示调用这笔交易的信息,由函数签名和函数参数(16进制字符串),组成代理模式时常用msg.data(后续讲解)。
msg.sender
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract MsgSender {
address public owner;
uint256 public value;
address public caller;
constructor() {
//在部署合约的时候,设置一个全局唯一的合约所有者,后面可以使用权限控制
owner = msg.sender;
}
//1. 对与合约而言,msg.sender是一个可以改变的值,并不一定是合约的创造者
//2. 任何人调用了合约的方法,那么这笔交易中的from就是当前合约中的msg.sender
function setValue(uint256 input) public {
value = input;
caller = msg.sender;
}
}
msg.value
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract MsgValue {
// uint256 public money;
mapping(address=> uint256) public personToMoney;
// 函数里面使用了msg.value,那么函数要修饰为payable
function play() public payable {
// 如果转账不是100wei,那么参与失败
// 否则成功,并且添加到维护的mapping中
require(msg.value == 100, "should equal to 100!");
personToMoney[msg.sender] = msg.value;
}
// 查询当前合约的余额
function getBalance() public view returns(uint256) {
return address(this).balance;
}
}
msg.data
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract MsgData {
event Data(bytes data, bytes4 sig);
// input0: addr: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// input1: amt : 1
function transfer(address addr, uint256 amt) public {
bytes memory data = msg.data;
// msg.sig 表示当前方法函数签名(4字节)
// msg.sig 等价于 this.transfer.selector
emit Data(data, msg.sig);
}
//output:
// - data: 0xa9059cbb0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000000000000000000000001
// - sig: 0xa9059cbb
// 对data进行分析:
// 0xa9059cbb //前四字节
// 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 //第一个参数占位符(32字节)
// 0000000000000000000000000000000000000000000000000000000000000001 //第二个参数占位符(32字节)
}
payable
- 一个函数(或地址)如果想接收ether,需要将其修饰为:payable。
- address常用方法:
- balance(): 查询当前地址的ether余额
- transfer(uint): 合约向当前地址转指定数量的ether,如果失败会回滚
- send(uint): 合约向当前地址转指定数量的ether,如果失败会返回false,不回滚(不建议使用send)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Payable {
// 1. Payable address can receive Ether
address payable public owner;
// 2. Payable constructor can receive Ether
constructor() payable {
owner = payable(msg.sender);
}
// 3. Function to deposit Ether into this contract.
function deposit() public payable {}
// 4. Call this function along with some Ether.
// The function will throw an error since this function is not payable.
function notPayable() public {}
// 5. Function to withdraw all Ether from this contract.
function withdraw() public {
uint amount = address(this).balance;
owner.transfer(amount);
}
// 6. Function to transfer Ether from this contract to address from input
function transfer(address payable _to, uint _amount) public {
_to.transfer(_amount);
}
}
gas相关
gas描述执行一笔交易时需要花费多少ether!(1 ether = 10^18wei)
交易手续费 = gas_used * gas_price,其中:
- gas:是数量单位,uint
- gas_used:表示一笔交易实际消耗的gas数量
- gas_price:每个gas的价格,单位是wei或gwei
- gas limit:表示你允许这一笔交易消耗的gas上限,用户自己设置(防止因为bug导致的损失)
- 如果gas_used小于gas_limit,剩余gas会返回给用户,这个值不再合约层面设置,在交易层面设置(如metamask)
- 如果gas_used大于gas_limit,交易失败,资金不退回
- block gas limit:表示一个区块能够允许的最大gas数量,由区块链网络设置
view和pure
view和pure用于修饰Getter函数(只读取数据的函数),其中:
- view:表示函数中不会修改状态变量,只是读取;
- pure:表示函数中不会使用状态变量,既不修改也不读取。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract ViewAndPure {
uint public x = 1;
// Promise not to modify the state.
function addToX(uint y) public view returns (uint) {
return x + y;
}
// Promise not to modify or read from the state.
function add(uint i, uint j) public pure returns (uint) {
return i + j;
}
function setX(uint num) public {
x = num;
}
}
bytes和string
byteN、bytes、string直接的关系
bytes:
- bytes是动态数组,相当于byte数组(如:byte[10])
- 支持push方法添加
- 可以与string相互转换
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Bytes {
bytes public name;
//1. 获取字节长度
function getLen() public view returns(uint256) {
return name.length;
}
//2. 可以不分空间,直接进行字符串赋值,会自动分配空间
function setValue(bytes memory input) public {
name = input;
}
//3. 支持push操作,在bytes最后面追加元素
function pushData() public {
name.push("h");
}
}
string:
- string 动态尺寸的UTF-8编码字符串,是特殊的可变字节数组
- string 不支持下标索引、不支持length、push方法
- string 可以修改(需通过bytes转换)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract String {
string public name = "lily";
function setName() public {
bytes(name)[0] = "L";
}
function getLength() public view returns(uint256) {
return bytes(name).length;
}
}
struct
- 自定义结构类型,将不同的数据类型组合到一个结构中,目前支持参数传递结构体。
- 枚举和结构体都可以定义在另外一个文件中,进行import后使用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Todos {
struct Todo {
string text;
bool completed;
}
// An array of 'Todo' structs
Todo[] public todos;
// [["hello", true], ["world", false]]
function createByElement(Todo[] memory _todos) public {
for (uint i = 0; i < _todos.length; i++) {
todos.push(_todos[i]);
}
}
function create(string memory _text) public {
// 3 ways to initialize a struct
// - calling it like a function
todos.push(Todo(_text, false));
// key value mapping
todos.push(Todo({text: _text, completed: false}));
// initialize an empty struct and then update it
Todo memory todo;
todo.text = _text;
// todo.completed initialized to false
todos.push(todo);
}
// Solidity automatically created a getter for 'todos' so
// you don't actually need this function.
function get(uint _index) public view returns (string memory text, bool completed) {
Todo storage todo = todos[_index];
return (todo.text, todo.completed);
}
// update text
function update(uint _index, string memory _text) public {
Todo storage todo = todos[_index];
todo.text = _text;
}
// update completed
function toggleCompleted(uint _index) public {
Todo storage todo = todos[_index];
todo.completed = !todo.completed;
}
}
mapping
- 定义:mapping(keyType => valueType) myMapping
- key可以是任意类型,value可以是任意类型(value也可以是mapping或者数组)
- mapping不支持迭代器
- 不需要实例化等,定义后直接可以使用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Mapping {
// Mapping from address to uint
mapping(address => uint) public myMap;
function get(address _addr) public view returns (uint) {
// Mapping always returns a value.
// If the value was never set, it will return the default value.
return myMap[_addr];
}
function set(address _addr, uint _i) public {
// Update the value at this address
myMap[_addr] = _i;
}
function remove(address _addr) public {
// Reset the value to the default value.
delete myMap[_addr];
}
}
contract NestedMapping {
// Nested mapping (mapping from address to another mapping)
mapping(address => mapping(uint => bool)) public nested;
function get(address _addr1, uint _i) public view returns (bool) {
// You can get values from a nested mapping
// even when it is not initialized
return nested[_addr1][_i];
}
function set(
address _addr1,
uint _i,
bool _boo
) public {
nested[_addr1][_i] = _boo;
}
function remove(address _addr1, uint _i) public {
delete nested[_addr1][_i];
}
}
修饰器modifier
修饰器用于修饰函数,在函数执行前或执行后进行调用,经常用于:
- 权限控制
- 参数校验
- 防止重入攻击等
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract FunctionModifier {
// We will use these variables to demonstrate how to use modifiers.
address public owner;
uint public x = 10;
bool public locked;
constructor() {
// Set the transaction sender as the owner of the contract.
owner = msg.sender;
}
// 1. Modifier to check that the caller is the owner of the contract.
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
// Underscore is a special character only used inside
// a function modifier and it tells Solidity to
// execute the rest of the code.
_;
}
// 2. Modifiers can take inputs. This modifier checks that the
// address passed in is not the zero address.
modifier validAddress(address _addr) {
require(_addr != address(0), "Not valid address");
_;
}
function changeOwner(address _newOwner) public onlyOwner validAddress(_newOwner) {
owner = _newOwner;
}
// Modifiers can be called before and / or after a function.
// This modifier prevents a function from being called while
// it is still executing.
modifier noReentrancy() {
require(!locked, "No reentrancy");
locked = true;
_;
locked = false;
}
function decrement(uint i) public noReentrancy {
x -= i;
if (i > 1) {
decrement(i - 1);
}
}
}
事件Event
事件是区块链上的日志,每当用户发起操作的时候,可以发送相应的事件,常用于:
- 监听用户对合约的调用
- 便宜的存储(用合约存储更加昂贵)
通过链下程序(如:subgraph)对合约进行事件监听,可以对Event进行搜集整理,从而做好数据统计,常用方式:
- 合约触发后发送事件
- subgraph对合约事件进行监听,计算(如:统计用户数量)
- 前端程序直接访问subgraph的服务,获得统计数据(这避免了在合约层面统计数据的费用,并且获取速度更快)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Event {
// Event declaration
// Up to 3 parameters can be indexed.
// Indexed parameters helps you filter the logs by the indexed parameter
event Log(address indexed sender, string message); // 修饰为indexed
event AnotherLog(); // 无参数的事件
event TestAnonymous(address indexed sender, uint256 num) anonymous; // 匿名事件
function test() public {
emit Log(msg.sender, "Hello World!");
emit Log(msg.sender, "Hello EVM!");
emit AnotherLog();
}
}
可见性visibility
合约的方法和状态变量需要使用关键字进行修饰,从而决定其是否可以被其他合约调用,修饰符包括:
- public:所有的合约和外部账户(EOA)都可以调用;
- private:只允许合约内部调用;
- internal:仅允许合约内部以及子合约中调用;
- external:仅允许外部地址(EOA或CA)调用,合约内部及子合约都不能调用;(早期版本可以使用this调用external方法)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Base {
// Private function can only be called
// - inside this contract
// Contracts that inherit this contract cannot call this function.
function privateFunc() private pure returns (string memory) {
return "private function called";
}
function testPrivateFunc() public pure returns (string memory) {
return privateFunc();
}
// Internal function can be called
// - inside this contract
// - inside contracts that inherit this contract
function internalFunc() internal pure returns (string memory) {
return "internal function called";
}
function testInternalFunc() public pure virtual returns (string memory) {
return internalFunc();
}
// Public functions can be called
// - inside this contract
// - inside contracts that inherit this contract
// - by other contracts and accounts
function publicFunc() public pure returns (string memory) {
return "public function called";
}
// External functions can only be called
// - by other contracts and accounts
function externalFunc() external pure returns (string memory) {
return "external function called";
}
// This function will not compile since we're trying to call
// an external function here.
// function testExternalFunc() public pure returns (string memory) {
// return externalFunc();
// }
// State variables
string private privateVar = "my private variable";
string internal internalVar = "my internal variable";
string public publicVar = "my public variable";
// State variables cannot be external so this code won't compile.
// string external externalVar = "my external variable";
}
contract Child is Base {
// Inherited contracts do not have access to private functions
// and state variables.
// function testPrivateFunc() public pure returns (string memory) {
// return privateFunc();
// }
// Internal function call be called inside child contracts.
function testInternalFunc() public pure override returns (string memory) {
return internalFunc();
}
}
ERC20(标准Token)
- EIP: Ethereum Improvement Propose
- 任何遵从EIP-20协议(ERC20标准)的Contract都属于ERC20 Token
标准接口
// 6 REQUIRED FUNCTIONS
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
// 2 REQUIRED EVENTS
event Transfer(address indexed _from, address indexed _to, uint256 _value)
event Approval(address indexed _owner, address indexed _spender, uint256 _value)
// 3. OPTIONAL FUNCTIONS
function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
发行Token
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/token/ERC20/IERC20.sol
interface IERC20 {
// 6 REQUIRED FUNCTIONS
// 总发行量
function totalSupply() external view returns (uint); // -> 总发行量 100000000 * 10**decimals
// 标准decimal: 18
// USDT: 6
// WBTC: 8
function balanceOf(address account) external view returns (uint); // 指定账户的余额
// 币的持有人直接调用,进行转账
function transfer(address recipient, uint amount) external returns (bool);
// 最常用的!!
// 1. 我这个owner对合约进行approve,此时approve内部会修改allowance变量
// 2. 合约内部调用transferFrom来支配owner的token
function transferFrom( // spender就是这个合约
address sender, // owner
address recipient, // 转给谁
uint amount // 金额
) external returns (bool);
// owner: 币的持有人
// spender: 是指定帮助花费的代理人(被授权的人)
function allowance(address owner, address spender) external view returns (uint); // 授权的额度
// decimals view,这是一个public 的变量,自动提供了一个读取的方法 // 返回精度
// 持有人对spender进行授权,在approve内部,会调用msg.sender来知道owner是谁
function approve(address spender, uint amount) external returns (bool);
// 2 REQUIRED EVENTS
// 事件
event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);
// 3. OPTIONAL FUNCTIONS
function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
}
以下是ERC20的案例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "./IERC20.sol";
contract ERC20 is IERC20 {
uint public totalSupply;
mapping(address => uint) public balanceOf;
mapping(address => mapping(address => uint)) public allowance;
string public name = "Solidity by Example";
string public symbol = "SOLBYEX";
uint8 public decimals = 18;
function transfer(address recipient, uint amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
function approve(address spender, uint amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(
address sender,
address recipient,
uint amount
) external returns (bool) {
allowance[sender][msg.sender] -= amount;
balanceOf[sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(sender, recipient, amount);
return true;
}
function mint(uint amount) external {
balanceOf[msg.sender] += amount;
totalSupply += amount;
emit Transfer(address(0), msg.sender, amount);
}
function burn(uint amount) external {
balanceOf[msg.sender] -= amount;
totalSupply -= amount;
emit Transfer(msg.sender, address(0), amount);
}
}
可以使用openzeppelin库进行创建自己的token:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.0.0/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
// Mint 100 tokens to msg.sender
// Similar to how
// 1 dollar = 100 cents
// 1 token = 1 * (10 ** decimals)
_mint(msg.sender, 100 * 10**uint(decimals()));
}
// 默认是18,可以进行override
function decimals() public view override returns (uint8) {
return 6;
}
}