區塊鏈語法筆記
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(<address>) 進行轉換
bytes1 定長字節數組
bytes和string,uint[] 變長字節數組
多維數組的下標和一般是相反的,
a[2][4]
表示4個子數列,每個子數列里2個元素
函數
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]
函數是代碼的可執行單元。函數通常在合約內部定義,但也可以在合約外定義。
函數可以作為參數傳入
可見性
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);
}