区块链语法笔记
demo 铸币代码
pragma solidity ^0.4;
contract Coin{
//set the "address" type variable minter
address public minter;
/*convert "address"(for storing address or key )
to the type of "uint" which is as subscrip of object balances*/
mapping (address =>uint) public balances;
// set an event so as to be seen publicly
event Sent(address from,address to,uint amount);
//constructor only run once when creating contract,unable to invoke
//"msg" is the address of creator."msg.sender" is
constructor()public{
minter=msg.sender;
}
//铸币
//can only be called by creator
function mint(address receiver,uint amount)public{
require(msg.sender ==minter);
balances[receiver]+=amount;
}
//转账
function send(address receiver,uint amount)public{
require(balances[msg.sender]>= amount);
balances[msg.sender]-=amount;
balances[receiver]+=amount;
emit Sent(msg.sender,receiver,amount);
}
}
源文件结构
pragma 版本标识
只对本文件有效,如果导入其他文件,版本标识不会被导入,启动编译器检查
^0.5.2; #从0.5.2到0.6(不含)的版本
import 导入文件
import * as symbolName from "filename"; #等价
import "filename" as symbolName;
状态变量
状态变量是永久地存储在合约存储中的,有基本类型.函数外的都是store
状态变量。
- bool,
- int/uint(有符号和无符号)
- fixed / ufixed (有符号和无符号的定长浮点型。)
在关键字 ufixedMxN 和 fixedMxN 中,M 表示该类型占用的位数,N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。 ufixed 和 fixed 分别是 ufixed128x19 和 fixed128x19 的别名
- address 地址类型
address:保存一个20字节的值(以太坊地址的大小)。
address payable :可支付地址,与 address 相同,不过有成员函数 transfer 和 send
address payable 可以完成到 address 的隐式转换,但是从 address 到 address payable 必须显示的转换, 通过 payable() 进行转换
bytes1 定长字节数组
bytes和string,uint[] 变长字节数组
多维数组的下标和一般是相反的,
a[2][4]
表示4个子数列,每个子数列里2个元素
函数
function () {internal|external} [pure|constant|view|payable] [returns ()]
函数是代码的可执行单元。函数通常在合约内部定义,但也可以在合约外定义。
函数可以作为参数传入
可见性
https://solidity-by-example.org/visibility/
内部(internal) 函数类型,只能在当前合约内被调用
外部(external) 函数类型,由一个地址和函数签名组成,在调用时会被视作function
类型
external
: 外部函数作为合约接口的一部分,可以被交易或者其他合约调用。 外部函数 f
不能以内部调用的方式调用(即 f
不起作用,但 this.f()
可以)。
public
: public 函数是合约接口的一部分,可以在内部或通过消息调用。对于 public 状态变量, 会自动生成一个 getter 函数。
internal
: 只能在当前合约内部或它子合约中访问,不使用 this
调用。
private
: private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用(如继承)
有且仅有以下三种转化:
-
pure
函数可以转换为view
和non-payable
函数 -
view
函数可以转换为non-payable
函数 -
payable
函数可以转换为non-payable
函数
参数和返回值
修饰符
https://solidity-by-example.org/function-modifier/
函数修饰符用来修饰函数,比如添加函数执行前必须的先决条件.函数修饰器通过继承在派生合约中起作用。
modifier onlyOwner { 函数体会插入在修饰函数的下划线_的位置
require(msg.sender == owner);
_;
}
如果一个函数中有许多修饰器,写法上以空格隔开,执行时依次执行:首先进入第一个函数修饰器,然后一直执行到_;接着跳转回函数体,进入第二个修饰器,以此类推。到达最后一层时,一次返回到上一层修饰器的_;后。
自由函数
定义在合约外的函数叫做自由函数,一定是internal
类型,就像一个内部函数库一样,会包含在所有调用他们的合约内,就像写在对应位置一样。但是自由函数不能直接访问全局变量和其他不在作用域下的函数(比如,需要通过地址引入合约,再使用合约内的函数)
view
view
函数不能产生任何修改。由于操作码的原因,view
库函数不会在运行时阻止状态改变,不过编译时静态检查器会发现这个问题。
以下行为都视为修改状态:
- 修改状态变量。
- 触发事件。
- 创建其它合约。
- 使用
selfdestruct
。 - 通过调用发送以太币。
- 调用任何没有标记为
view
或者pure
的函数。 - 使用低级调用。
- 使用包含特定操作码的内联汇编。
receive 函数
一个合约至多有一个receive函数,形如receive() external payable { ... }
,注意没有function
的标识,没有参数,只能是external
和payable
标识,可以有函数修饰器,支持重载。
receive
函数在没有任何调用数据时执行(如用.send()
或者.transfer()
给合约转账),如果没有设置receive
函数,那么就会执行fallback
函数,如果这两个函数都不存在,合约就不能通过交易的形式获取以太币
回退函数
一个合约至多一个回退函数,格式如:fallback () external [payable]
或者 fallback (bytes calldata _input) external [payable] returns (bytes memory _output)
,后者的函数参数会接收完整的调用信息(msg.data
),返回的时未经修改的数据(如为经过ABI编码)。
回退函数可以时virtual
的,可以重载,也可以被修饰器修饰。在函数调用时,如果没有与之匹配的函数签名或者消息调用为空且无receive
函数,就会调用fallbakc函数。
如果回退函数代替了receive
函数,那么仍然只有2300gas可用。
构造函数
contract X {
string public name;
constructor(string memory _name) {
name = _name;
}
}
// Base contract Y
contract Y {
string public text;
constructor(string memory _text) {
text = _text;
}
}
两种方式
contract B is X("Input to X"), Y("Input to Y") {
}
// Order of constructors called:
// 1. X
// 2. Y
// 3. C
contract C is X, Y {
// Pass the parameters here in the constructor,
// similar to function modifiers.
constructor(string memory _name, string memory _text) X(_name) Y(_text) {}
}
事件
事件是能方便地调用以太坊虚拟机日志功能的接口,分为设置事件和触发事件
pragma solidity >=0.4.21 <0.9.0;
contract TinyAuction {
event HighestBidIncreased(address bidder, uint amount); // 事件
function bid() public payable {
// ...
emit HighestBidIncreased(msg.sender, msg.value); // 触发事件
}
}
事件是对EVM日志的简短总结,可以通过RPC接口监听。触发事件时,设置好的参数就会记录在区块链的交易日志中,永久的保存,但是合约本身是不可以访问这些日志的。可以通过带有日志的Merkle证明的合约,来检查日志是否存在于区块链上。由于合约中仅能访问最近的 256 个区块哈希,所以还需要提供区块头信息。
继承
https://solidity-by-example.org/inheritance/
当合约继承其他的合约时,只会在区块链上生成一个合约,所有相关的合约都会编译进这个合约,调用机制和写在一个合约上一致。
所以如果继承了多个合约,希望把所有的同名函数都执行一遍,就需要super
关键词。
函数重写
父合约中被标记为virtual
的非private函数可以在子合约中用override
重写。
重写可以改变函数的标识符,规则如下:
- 可见性只能单向从
external
更改为public。
-
nonpayable
可以被view
和pure
覆盖。 -
view
可以被pure
覆盖。 -
payable
不可被覆盖。 - 函数修饰器也支持重写,且和函数重写规则一致
// Contracts inherit other contracts by using the keyword 'is'.
contract B is A {
// Override A.foo()
function foo() public pure virtual override returns (string memory) {
return "B";
}
}
contract C is A {
// Override A.foo()
function foo() public pure virtual override returns (string memory) {
return "C";
}
}
contract D is B, C {
// D.foo() returns "C" 执行最远的那个父类函数C的调用
// since C is the right most parent contract with function foo()
function foo() public pure override(B, C) returns (string memory) {
return super.foo();
}
}
接口
接口和抽象合约的作用很类似,但是它的每一个函数都没有实现,而且不可以作为其他合约的子合约,只能作为父合约被继承。
接口中所有的函数必须是external
,且不包含构造函数和全局变量。接口的所有函数都会隐式标记为external
,可以重写。但是多次重写的规则和多继承的规则和一般函数重写规则一致。
- 不能实现任何功能
- 可以从其他接口继承
- 所有声明的函数必须是外部的
- 不能声明构造函数
- 不能声明状态变量
interface ICounter {
function count() external view returns (uint);
function increment() external;
}
contract MyContract {
function incrementCounter(address _counter) external {
ICounter(_counter).increment();
}
function getCount(address _counter) external view returns (uint) {
return ICounter(_counter).count();
}
}
引用类型
引用类型可以通过不同变量名来修改指向的同一个值。目前的引用类型包括:结构体、数组和映射
- memory:存储在内存里,只在函数内部使用,函数内不做特殊说明为
memory
类型。 - storage:相当于全局变量。函数外合约内的都是
storage
类型。 - calldata:保存有函数的参数,不可修改,大多数时候和
memory
相似。它常作为外部函数的参数,也可以当作其他的变量使用 - 尽可能使用
calldata
,因为它既不会复制,也不能修改,而且还可以作为函数的返回值 -
storage
和memory
之间的赋值或者用calldata
对它们赋值,都是产生独立的拷贝,不修改原来的值,其他向storage
赋值是拷贝,结构体里面的赋值是一个拷贝。 - 于数组和结构体在函数中要有存储位置声明
基本类型转换
隐式转换:隐式转换发生在编译时期,如果不出现信息丢失,其实都可以进行隐式转换,比如uint8
可以转成uint16
。隐式转换常发生在不同的操作数一起用操作符操作时发生。
显式转换:如果编译器不允许隐式转换,而你足够自信没问题,那么就去尝试显示转换,但是这很容易造成安全问题
如果是uint
或者int
同类型强制转换,就是从最低位截断
单位、内置函数和变量
- 币的单位默认是
wei
,也可以添加后缀。
1 wei == 1;
1 gwei == 1e9;
1 ether == 1e18;
区块和交易属性
blockhash(uint blockNumber) returns (bytes32)
:指定区块的区块哈希,但是仅可用于最新的 256 个区块且不包括当前区块,否则返回0.block.chainid
(uint
): 当前链 idblock.coinbase
(address
): 挖出当前区块的矿工地址block.difficulty
(uint
): 当前区块难度block.gaslimit
(uint
): 当前区块 gas 限额block.number
(uint
): 当前区块号block.timestamp
(uint
): 自 unix epoch 起始当前区块以秒计的时间戳gasleft() returns (uint256)
:剩余的 gasmsg.data
(bytes
): 完整的 calldatamsg.sender
(address
): 消息发送者(当前调用)msg.sig
(bytes4
): calldata 的前 4 字节(也就是函数标识符)msg.value
(uint
): 随消息发送的 wei 的数量tx.gasprice
(uint
): 交易的 gas 价格-
tx.origin
(address payable
): 交易发起者(完全的调用链)错误处理
assert(bool condition)
,require(bool condition)
,require(bool condition, string memory message)
均是条件为假然后回滚。revert()
,revert(string memory reason)
立即回滚
内部调用
内部调用再EVM中只是简单的跳转,传递当前的内存的引用,效率很高。但是仍然要避免过多的递归,因为每次进入内部函数都会占用一个堆栈槽,而最多只有1024个堆栈槽。
外部调用
- 只有
external
或者public
的函数才可以通过消息调用而不是单纯的跳转调用,外部函数的参数会暂时复制在内存中。 - 注意
this
不可以出现在构造函数里,因为此时合约还没有完成。 - 调用时可以指定 value 和 gas 。这里导入合约使用的时初始化合约实例然后赋予地址。
元组的赋值行为
函数的返回值可以是元组,因此就可以用元组的形式接收,但是必须按照顺序排列。在0.5.0之后,两个元组的大小必须相同,用逗号表示间隔,可以空着省略元素。注意,不允许赋值和声明都出现在元组里,比如(x, uint y) = (1, 2);
不合法。
元祖就是多个不同类型数据组成的数组
错误
assert
函数,用于检查内部错误,返回Panic(uint256)
,错误代码分别表示:
- 0x00: 由编译器本身导致的Panic.
- 0x01:
assert
的参数(表达式)结果为 false 。 - 0x11: 在
unchecked { … }
外,算术运算结果向上或向下溢出。 - 0x12: 除以0或者模0.
- 0x21: 不合适的枚举类型转换。
- 0x22: 访问一个没有正确编码的
storage
byte数组. - 0x31: 对空数组
.pop()
。 - 0x32: 数组的索引越界或为负数。
- 0x41: 分配了太多的内存或创建的数组过大。
- 0x51: 如果你调用了零初始化内部函数类型变量
Error(string)
的异常由编译器产生,有以下情况:
-
require
的参数为false
。 - 触发
revert
或者revert("discription")
- 执行外部函数调用合约没有代码。
-
payable
修饰的函数(包括构造函数和 fallback 函数),接收以太币。 - 合约通过 getter 函数接收以太币 。
try/catch
try后面只能接外部函数调用或者是创建合约new ContractName的表达式,并且表达式内部的错误并不会被记录,只有调用的外部函数内出现错误才会返回回滚的信息。如果有returns的话,后面接外部函数的返回值类型,return后面是当前合约函数的返回值。
地址
以太坊地址是给定公钥哈希的最后 20 个字节。使用的散列算法是Keccak-256。所以对于一个唯一的私钥 => 唯一的哈希.
所有钱包都应该接受以大写或小写字符表示的以太坊地址
您可以将 Ether 发送到定义为的变量
address payable
您不能将 Ether 发送到定义为的变量
address
msg.sender() -> Returns : address payable
tx.origin() -> Returns : address payable
.transfer()` , `.send()` , `.call()` , `.delegatecall()` and `.staticcall() 是address payable 定义变量的用法
address 具有balance方法,表示eth余额
address payable` to `address 隐式转换可以,返过来不可以 contract NotPayable { } contract Payable { function() payable {} } contract HelloWorld { address x = address(NotPayable); //address类型 address y = address(Payable); //address payable类型,因为该合同有payable类型的回调函数 function hello_world() public pure returns (string memory) { return "hello world"; } }
EVM 提供了4种 特殊的操作码来与其他智能合约交互,其中 3 种可用作以下address
类型的方法:**call**
、**delegatecode**
和**staticcall**
call:
https://solidity-by-example.org/call/
address.call(bytes memory) returns (bool, bytes memory)
我是合约 A,我想为合约 B 存储执行合约 B 功能。调用 B.function() 只能更新 B 的存储
callcode
address.callcode(__payload__)
*.callcode()*
现在已弃用,取而代之的是*.delegatecall()*
. 但是,仍然可以在内联汇编中使用它。
合约A本质上是复制B的功能
我是合约 A,我想为我的存储执行合约 B 的功能。调用 B.function() 将更新 A 的存储
delegatecall
https://solidity-by-example.org/delegatecall/
_address.**delegatecall(**bytes memory**)** returns (bool, bytes memory)
我是合约A,我想执行合约B的功能,但是合约B可以伪装成我。
B 的函数可以覆盖 A 的存储,并在任何其他合约中伪装成A。
msg.sender 将是 A 的地址,而不是 B
在这种情况下,合约 A 本质上是将函数调用委托给 B。与前
callcode
一种方法的不同之处在于,使用delegatecall
not only enable 覆盖合约 A 的存储。如果合约 B 调用另一个合约 C,合约 C 将看到它
msg.sender
是合约 A把B的代码在A的执行环境中执行,数据使用A的
staticcall
address.**staticcall(**bytes memory**)** returns (bool, bytes memory)
规格
- 低级
STATICCALL
,请参阅操作码 OxF4)(具有当前合约看到的完整 msg 上下文)给定作为参数传递的memory
(数据有效负载)。 - 返回一个元组:
当交易在接收者字段中指定一个称为零地址的特定地址时,它打算创建一个新合约.向该地址发送资金实际上不会转移任何以太币。在以太坊网络中,矿工将包含此接收者的交易解释为创建新智能合约的指令。
应付
用来接收和提取eth
contract Payable {
// Payable address can receive Ether
address payable public owner;
// Payable constructor can receive Ether
constructor() payable {
owner = payable(msg.sender);
}
//把合约里的amount提取到owener地址上啊,call在那个地址上调用,就更新那个地址的数据
// Function to withdraw all Ether from this contract.
function withdraw() public {
// get the amount of Ether stored in this contract
uint amount = address(this).balance;
// send all Ether to owner
// Owner can receive Ether since the address of owner is payable
(bool success, ) = owner.call{value: amount}("");
require(success, "Failed to send Ether");
}
// Function to transfer Ether from this contract to address from input
function transfer(address payable _to, uint _amount) public {
// Note that "to" is declared as payable
(bool success, ) = _to.call{value: _amount}("");
require(success, "Failed to send Ether");
}
}
contract ReceiveEther { 接受者接收的逻辑
/*
Which function is called, fallback() or receive()?
send Ether
|
msg.data is empty?
/ \
yes no
/ \
receive() exists? fallback()
/ \
yes no
/ \
receive() fallback()
*/
// Function to receive Ether. msg.data must be empty
receive() external payable {}
// Fallback function is called when msg.data is not empty
fallback() external payable {}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
contract SendEther {
消息调用者把比发给to这个地址
function sendViaTransfer(address payable _to) public payable {
// This function is no longer recommended for sending Ether.
_to.transfer(msg.value);
}
消息调用者把比发给to这个地址,需要发送成功,
function sendViaSend(address payable _to) public payable {
// Send returns a boolean value indicating success or failure.
// This function is not recommended for sending Ether.
bool sent = _to.send(msg.value);
require(sent, "Failed to send Ether");
}
消息调用者把比发给to这个地址,需要发送成功,
function sendViaCall(address payable _to) public payable {
// Call returns a boolean value indicating success or failure.
// This is the current recommended method to use.
(bool sent, bytes memory data) = _to.call{value: msg.value}("");
require(sent, "Failed to send Ether");
}
}
回调函数fallback
fallback
是一个不接受任何参数且不返回任何内容的函数。
它在何时执行
- 调用不存在的函数或
- 以太币直接发送到合约但
receive()
不存在或msg.data
不为空 - fallback被
transfer
orsend
.调用时有 2300 气体限制
功能选择器
调用函数时,前 4 个字节calldata
指定调用哪个函数。
下面的这段代码。它用于在地址上call执行transfer合约addr。
addr.call(abi.encodeWithSignature("transfer(address,uint256)", 0xSomeAddress, 123))
调用其他合约
https://solidity-by-example.org/calling-contract/
最简单的方法就是直接调用它,比如A.foo(x, y, z)
.
调用其他合约的另一种方法是使用低级call
.
try/Catch
contract Foo {
address public owner;
constructor(address _owner) {
require(_owner != address(0), "invalid address");
assert(_owner != 0x0000000000000000000000000000000000000001);
owner = _owner;
}
function myFunc(uint x) public pure returns (string memory) {
require(x != 0, "require failed");
return "my func was called";
}
}
contract Bar {
event Log(string message);
event LogBytes(bytes data);
function tryCatchNewContract(address _owner) public {
try new Foo(_owner) returns (Foo foo) {
// you can use variable foo here
emit Log("Foo created");
} catch Error(string memory reason) {
// catch failing revert() and require()
emit Log(reason);
} catch (bytes memory reason) {
// catch failing assert()
emit LogBytes(reason);
}
}
}
ERC20
任何遵循ERC20 标准的合约都是 ERC20 代币。该标准提供了代币的基本功能:如转移代币,授权代币给其他人(如链上第三方应用)使用。标准接口允许以太坊上的任何代币被其他应用程序重用,如钱包、去中心化交易所等
这是一个发币的合约,平日里所接触的许许多多代币如usdt(erc20)、usdc、dai、unsiwap、chainlink、wbtc、sushi等等绝大都数都是erc20代币,
ERC20 代币提供以下功能
- 转移代币
- 允许其他人代表代币持有者转移代币
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/token/ERC20/IERC20.sol
interface IERC20 {
function totalSupply() external view returns (uint);
function balanceOf(address account) external view returns (uint);
function transfer(address recipient, uint amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint);
function approve(address spender, uint amount) external returns (bool);
function transferFrom(
address sender,
address recipient,
uint amount
) external returns (bool);
event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);
}