solidity學習筆記(二)

solidity 合約文件結構

合約文件:

  • 版本申明
  • 合約主體:
    1. 狀態變量
    2. 函數
    3. 結構類型
    4. 事件
    5. 函數修改器
  • 代碼註釋

例子:

  pragma solidity ^0.4.0;

  import "";

  contract Test {
    //狀態變量
    uint a; 
    
    function setA(uint x) public {
      a = x;
      // 調用事件
      emit Set_A(x)
    }

    // 定義事件
    event Set_A(uint a);

    // 定義結構類型
    struct Pos {
      int lat;
      int lng;
    }

    //函數修改器
    modifier owner () {

    }

  }

整型 —int/uint

int 是有符號的整型,而uint是無符號的整型 uint8 表示無符號的8位整型。

整型的運算符:

  • 比較運算符: <= , < , == , != ,>= , >
  • 位運算:& , | ,^(異或) , ~(位取反)
  • 算數運算: + , - , 一元運算 ,…(其中 ** 表示取冪,>> 表示又移兩位)
pragma solidity ^0.4.0;

contract Test {
  uint a;
  uint256 b = 20;
  uint256 c = 30;

  function testadd() public returns (int) {
    if(b>c){
      return b + c
    } else if(b == c){
      return b * c
    } else {
      return b >> 2;
      //結果是5 b>> 2 表示 20 / 2^2
    }
    
  }
}

常量 contant

  • 有理數和整形常量
  • 字符串常量
  • 十六進制常量
  • 地址常量

uint是指只支持固定的位數的變量,而constant 是支持任意精度的,所以有時候會運用到常量
並且常量的運算是不會有溢出的

pragma solidity ^0.4.0;

contract Test {
 
  function testLiterals() public constant returns (uint){
      return 1; // 這個 1 就是一個常量
      // return 1 + 1;
      // return 1.0 + 1e10;  有理數的常量
  }
  function testStringLiterals() public constant returns(string){
      return  "abc";  //字符串常量會被轉換成一個字節數組或者是字符串數組
  }

  function testHexLiterals() public constant returns(bytes2,bytes1,bytes1){
    // 十六進制常量返回的是一個字節數組,十六進制常量是以hex開頭的
      bytes2 a = hex"abcd";
      return (a,a[0],a[1]);  // "0xabcd","0xab","0xcd"
  }
}

contant 表示不會修改狀態變量的常量,新版本也可以使用 view
是一種數據類型,比如數字 1 和 1.000 就是常量
字符串常量:就是字符串

地址類型

表示一個賬戶的地址(20字節)
地址類型成員包括: 獲取賬戶餘額:`balance` ,轉移賬戶餘額:` transfer(金額數目)`
pragma solidity ^0.4.16;

contract AddrTest {
    //當表示一個方法可以接受以太幣時需要在函數聲明的時候加上 payable
    function deposit() public payable {
        
    }
    
    function getBalance() public constant returns (uint) {
        return this.balance;
    }
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7Xr2b3Ie-1572335194921)(.\img\1.png)]
實現轉賬功能

  pragma solidity ^0.4.16;

  contract AddrTest {
    //當表示一個方法可以接受以太幣時需要在函數聲明的時候加上 payable
    function deposit() public payable {
        
    }
    function getBalance() public constant returns (uint) {
        return this.balance;
    }
    function transferEther(address towho) public {
      //10 表示轉10個單位的幣種
      towho.transfer(10)
    }
  }

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-3lbMlMlh-1572335194922)(.\img\2.png)]

再來看一段代碼:

pragma solidity ^0.4.4;
 
contract Test {
  address public _owner;
  uint public _number;

  function Test() {
    _owner = this;
    _number = 100;
  }
  
  function msgSenderAddress() constant returns (address) {
    return msg.sender;
  }
  
  function setNumberAdd1() {
    _number = _number + 5;
  }
  
  function setNumberAdd2() {
    if (_owner == msg.sender) {
        _number = _number + 10;
    }
  }
  function returnContractAddress() constant returns (address) {
    return this;
  }
} 

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-960C8IXc-1572335194926)(.\img\3.png)]

  • msg.sender就是當前調用方法的用戶地址
  • this指的是當前合約的地址
  • address支持各種算數運算符

○ send() 函數
send 與transfer對應,但更底層。如果執行失敗,transfer不會因異常停止,而send會返回false。

警告:send() 執行有一些風險:如果調用棧的深度超過1024或gas耗光,交易都會失敗。因此,爲了保證安全,必須檢查send的返回值,如果交易失敗,會回退以太幣。如果用transfer會更好。

○ call(), callcode() 和 delegatecall() 函數
call函數返回一個bool值,以表明執行成功與否。正常結束返回true,異常終止返回false。但無法獲取到結果數據,因爲需要提前知道返回的數據的編碼和數據大小(因不知道對方使用的協議格式,所以也不會知道返回的結果如何解析)。
還可以提供.gas()修飾器進行調用:

引用類型

數據位置:memory ,storage
一般的定義會有默認的存儲位置,但是我們可以通過memory和storage來改變默認的存儲地址

memory:
就是我們常說的內存,他不是永久存在的,比如函數的參數和函數的返回值都默認的存在內存中,memory是一個臨時的空間,在函數調用的時候出現,在函數調用之後被釋放;

storage:
是永久存在的,比如一些複雜變量,狀態變量就是存在區塊鏈中,storage對gas的消耗是遠遠大於memory的;

數組

  • T[k]:元素類型爲T,固定長度爲k的數組;
  • T[]:元素類型爲T,長度爲動態的數組;
  • bytes string: 是一種特殊的數組;
  • string 可以轉換爲bytes,bytes類似於byte[];
    數組的成員有:length和push()
    通過bytes()可以將字符串轉換成一個字節數組
pragma solidity ^0.4.4;

contract ArrayTest {
    
  uint[] public u = [1,2,3];
  string s = 'abcde';
  
  function h() public returns (uint) {
    u.push(6);
    return bytes(s).length;
  }
  function f() public view returns (byte) {
    return bytes(s)[1];
  }
  function newM(uint len) constant public returns (uint){
    uint[] memory a = new uint[] (len);
    return a.length;
  }
  function g(uint[3] _data) public constant{
  }
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-eXGVCyly-1572335194929)(.\img\4.png)]

結構體

pragma solidity ^0.4.4;

contract ArrayTest {
    
   constructor() payable {
       
   }
   
   function testApi () public payable returns (address) {
    //  return msg.sender;(address) 當前用戶地址
    //  return msg.value;(uint)
    //  return block.coinbase;(address) 當前礦工的地址
    //  return block.difficulty;(uint) 當前挖礦的難度
    //  return block.number;(uint)當前區塊的號碼
    //  return block.timestamp;(uint)
    //  return now;(uint)
    //  return tx.gasprice;(uint)
   }
}

solidity的錯誤處理
如何處理:
assert & require 在老版本中是使用throw來實現拋出錯誤

  if(msg.sender != owner) { throw; }

  // 現可以完全等價於下面的代碼
  if(msg.sender != owner) { revert(); }
  assert(msg.sender == owner);
  require(msg.sender == owner);

注意在 assert()require() 例子中的條件聲明,是 if 例子中條件塊取反,也就是用 == 代替了 != 。
assert()和require()之間的區別:

  • 可以將 assert() 想象爲一個過於自信的實現方式,即使有錯誤,也會執行並扣除gas。然而 require() 可以被想象爲一個更有禮貌些的實現方式,會發現錯誤,並且原諒所犯錯誤(譯註:不扣除 gas)。
  • 基於以上理解,以上兩個函數真正區別在哪裏呢?在拜占庭網絡更新前, require() 和 assert() 表現完全一樣,但是他們的二進制代碼卻有略微區別---->assert() 使用 0xfe 操作碼引起錯誤條件
    require() 使用 0xfd 操作碼引起錯誤條件
使用assert()的情況:

比如:下標越界,除以零這類情況是assert類型異常;
require是判斷外部條件錯誤的;
檢查 overflow/underflow,即:c = a+b; assert(c > b)
檢查非變量(invariants),即:assert(this.balance >= totalSupply);
驗證改變後的狀態,不應該發生的條件
一般地,儘量少使用 assert 調用,如果要使用assert 應該在函數結尾處使用
基本上,require() 應該被用於函數中檢查條件,assert() 用於預防不應該發生的情況,但不應該使條件錯誤。

使用require()的情況:

比如:gas不足,沒有匹配到正常的函數的情況下的異常;
assert是判斷程序內部邏輯的錯誤;
驗證用戶輸入,即: require(input<20);
驗證外部合約響應,即: require(external.send(amount));
執行合約前,驗證狀態條件,即: require(block.number > SOME_BLOCK_NUMBER) 或者 require(balance[msg.sender]>=amount)
一般地,儘量使用 require 函數
一般地,require 應該在函數最開始的地方使用
在我們的智能合約最佳實踐中有很多使用 require() 的例子供參考。

函數參數

  • 輸入參數
  • 輸出參數
  • 命名參數
  • 參數解構
pragma solidity ^0.4.20;

contract Test {
    
  function simpleInput(uint a,uint b) public {
    // uint a,uint b 就是輸入參數
  }
  function simpleInput2(uint c,uint d) public returns (uint sum) {
    // uint sum 就是輸出參數  
  }
  function testSimpleInput() public constant returns (uint sum) {
    // 命名參數是和順序沒有關係的,所以可以調換b和a的順序 
    sum = simpleInput2({d:1,c:2});
  }
  function test2(uint a,uint b) 
    public constant returns (uint sum,uint mul) {
      // 參數的解構,同時返回兩個值
      sum = a + b;
      mul = a * b;
    }
  function test33() public constant returns (uint sum,uint mul) {
     
    (sum,mul) = test2({b:3,a:2});
  }
  function f() public constant returns (uint,bool,uint) {
    return (7,true,2);
  }
  function g() public {

    var (x,y,z) = f();
    (x,z) = (z,x);
  }
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FyS58FtK-1572335194933)(.\img\5.png)]

控制語句

控制語句有:if, else, while, do, for, break, continue, return, 三元表達式

沒有:switch, goto

pragma solidity ^0.4.20;

contract Test {
  function testWhile() public constant returns(uint) {
    uint i = 0;
    uint sumofOdd = 0;
    
    while(true) {
      i++;
      if( i % 2 == 0) {
          continue;
      }
      if( i > 10 ){
          break;
      }
      sumofOdd += i;
    }
    return sumofOdd;
  }
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-SEjxd3hr-1572335194937)(.\img\6.png)]

可見性

public 和 private
public:
公開函數是合約接口的一部分,可以通過內部或者消息來進行調用。對於public類型的狀態變量,會自動創建一個訪問器。 函數默認的可見性是public
private:
私有函數和狀態變量僅在當前合約中可以訪問,在繼承的合約內,不可訪問,外部也是無法訪問的。
internal:
這種狀態變量可供外部和子合約調用,這種類型的函數和private類型的函數一樣,智能合約自己內部調用,不過和private不一樣的是,他可以在繼承的合約裏面調用,它和其他語言中的protected不完全一樣。
external:
外部函數是合約接口的一部分,只能使用消息調用。

pragma solidity ^0.4.20;

contract Test {
    
  uint public data;
  
  function f(uint a) private returns (uint b) {
    return a + 1;
  }
  function setData(uint a) internal {
    data = a;
  }
  function exSetData(uint a) external {
    data = a;
  }
  function testsetData(uint a) public {
    setData(1);
    this.exSetData(1);
  }
}
contract D {
  function readData() {
    Test test = new Test();
    // test.setData(1); 這樣寫會報錯
    test.exSetData(1);
    test.testsetData(1);
  }
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-LST2DWTL-1572335194939)(.\img\7.png)]

函數

  • 構造函數
  • 視圖函數
  • 純函數
  • 回退函數
pragma solidity ^0.4.20;
// 構造函數
contract Test {
  uint internal data;
  constructor (uint a) {
    data = a;
  }
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vyedhSeu-1572335194941)(.\img\8.png)]

無名函數 也叫回退函數,一個合約裏面只能有一個無名函數,這個函數無參數,也無返回值。如果調用合約時,沒有匹配上任何一個函數(或者沒有傳哪怕一點數據),就會調用默認的回退函數。
下述提供給回退函數可執行的操作會比常規的花費得多一點。

  • 寫入到存儲(storage)
  • 創建一個合約
  • 執行一個外部(external)函數調用,會花費非常多的gas
  • 發送ether
pragma solidity ^0.4.20;

contract Test {
  uint internal data;
  
  constructor (uint a) {
    data = a;
  }
  event EVENTA(uint a);
  
  function testview() public constant returns(uint) {
    // data = 1;
    // emit EVENTA(1);
    return data;
  }
  function f() public pure returns (uint) {
    // this.balance; 會報錯
    // msg.value; 會報錯
    // return 1 * 2 + data; 會報錯
    // 以上會報錯是因爲在聲明f函數的時候,定義的是pure純函數,所以不對storage進行讀寫
    return 1 * 2 + 3;
  }
  // 無名函數,必須有payable屬性
  function () public payable {
    //  調用無名函數的情況:
    //  1、當我們對合約進行轉賬的時候會被觸發
    //  2、當調用這個合約中沒有被定義的函數的時候會調用這個無名函數 
  }
}

實現簡單的代幣開發

pragma solidity ^0.4.20;

contract Mytoken {

  // 定義一個變量來保存每個地址中的餘額
  mapping(address => uint256) public balanceOf;
  // 合約中的構造函數
  constructor(uint256 initSupply) public {
    
    balanceOf[msg.sender] = initSupply;
  }
  function transfer(address _to,uint256 _value) public {

    //  要求賬戶餘額要大於轉出的數額 
    require(balanceOf[msg.sender] >=  _value);
    // 目標地址的餘額要遠遠大於轉入的值,因爲uint定義的是256,如果溢出會出現比原始金額還要小的狀態
    require(balanceOf[_to] + _value >= balanceOf[ _to] );
    
    // 轉賬地址減去要轉的值
    balanceOf[msg.sender] -= _value;
    // 目標地址加上轉入的金額
    balanceOf[ _to] += _value;
  }
}

實現標準代幣接口

但是現實開發中是要求遵循EIPs標準的,EIPs中需要包含以下元素

pragma solidity ^0.4.20;

contract ERC20Interface {
  string public name;
  string public symbol;
  uint8 public decimals;
  uint public totalSupply;

  function balanceOf(address _ower) view returns (uint256 balance);
  // 輸入地址就可以獲取該地址的代幣餘額
  function transfer(address _to,uint256 _value) returns (bool success);
  // 調用transfer函數就可以將自己的token地址轉給_to 地址,_value 是轉賬個數
  function transferFrom(address _form,address _to,uint256 _value) returns (bool success);
  // 與approve搭配使用,approve批准之後,調用transferFrom函數來轉移token。
  function approve(address _spender,uint _value) returns (bool success);
  // 批准_spender賬戶從自己的賬戶轉移_value個token。可以分多次轉移
  function allowance(address _owner,address _spender) view returns (uint256 remaining);
  // 批准_spender賬戶從自己的賬戶轉移_value個token。可以分多次轉移

  // 事件
  event Transfer(address indexed _from,address indexed _to,uint256 _value);
  // 當成功轉移token時,一定要觸發Transfer事件
  event Approval(address indexed _owner,address indexed _spender,uint256 _value);
  // 當調用approval函數成功時,一定要觸發Approval事件
}

實現標準代幣

pragma solidity ^0.4.20;

contract ERC20Interface {
  string public name;
  string public symbol;
  uint8 public decimals;
  uint public totalSupply;

  // function balanceOf(address _ower) view returns (uint256 balance);
  function transfer(address _to,uint256 _value) returns (bool success);
  function transferFrom(address _form,address _to,uint256 _value) returns (bool success);
  function approve(address _spender,uint _value) returns (bool success);
  function allowance(address _owner,address _spender) view returns (uint256 remaining);
  event Transfer(address indexed _from,address indexed _to,uint256 _value);
  event Approval(address indexed _owner,address indexed _spender,uint256 _value);
}

//  approve、transferFrom及allowance解釋:
// 賬戶A有1000個ETH,想允許B賬戶隨意調用100個ETH。A賬戶按照以下形式調用approve函數approve(B,100)。
// 當B賬戶想用這100個ETH中的10個ETH給C賬戶時,則調用transferFrom(A, C, 10)。這時調用allowance(A, B)可以查看B賬戶還能夠調用A賬戶多少個token。


// contract ERC20 is ERC20Interface 表示合約ERC20繼承了合約ERC20Interface
contract ERC20 is ERC20Interface {
  // 用mapping來儲存每個地址和他對應的餘額
  mapping (address => uint256) public balanceOf;
  mapping (address => mapping (address => uint256)) internal allowed;
  // 表示allowed這個賬號可以操控address裏面的金額
  constructor() public {
    name = "BTC"; //代幣名稱叫EmmaToken
    symbol = "IMOOC";   //域名
    decimals = 0;//小數位爲0
    totalSupply = 10000;//發行了10000個幣
    balanceOf[msg.sender] = totalSupply; //,默認創建者擁有所有的代幣
  }
  function balanceOf(address _owner) view returns (uint256 balance){
    // 通過上面mapping的定義,此處可以用這種方法直接返回的傳入地址的餘額
    return balanceOf[ _owner]
  }

  function transfer(address _to,uint256 _value) returns (bool success) {
    // 在此之前要先做一些餘額的檢查
    require(_to != address(0)); // _to 這個賬號不能爲零
    require(balanceOf[msg.sender] >= _value); // 發送者的餘額一定要大於轉賬值
    require(balanceOf[_to] + _value >= balanceOf[_to]);// 表示接受者的餘額加上轉入值之後也要大於原來的餘額,防止溢出的一個判斷

    balanceOf[msg.sender] -= _value;
    balanceOf[ _to] += _value;
    emit Transfer(msg.sender,_to,_value);
  }
  
  function transferFrom(address _form,address _to,uint256 _value) returns (bool success) {
    require(_to != address(0)); // _to 這個賬號不能爲零
    require(balanceOf[_form] >= _value); // 發送者的餘額一定要大於轉賬值
    require(allowed[_form][msg.sender] >= _value);
    require(balanceOf[_to] + _value >= balanceOf[_to]);// 表示接受者的餘額加上轉入值之後也要大於原來的餘額,防止溢出的一個判斷

    balanceOf[_form] -= _value;
    balanceOf[ _to] += _value;
    emit Transfer(_form,_to,_value);
    success = true;
  }

  function approve(address _spender,uint _value) returns (bool success) {
    // 表示允許 _spender 賬戶調用msg.sender的value 個幣
    allowed[msg.sender][_spender] = _value;
    // 觸發Approval事件
    emit Approval(msg.sender, _spender, _value);

    success = true;
  }

  function allowance(address _owner,address _spender) view returns (uint256 remaining) {
    return allowed[_owner][_spender];
  }
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZfIGmPnM-1572335194943)(.\img\9.png)]

搭建項目

  • 瞭解工具

    ----->truffle
// 安裝truffle
npm i -g truffle

// 創建文件夾
mkdir pet-shop
cd pet-shop

// 初始化並下載框架
truffle init
// 可以用ls或者tree命令查看文件結構

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-65DC33md-1572335194945)(.\img\10.png)]
第二種創建方式:

mkdir pet-shop2
cd pet-shop2

// 用unbox來創建
truffle unbox pet-shop

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-6usixyui-1572335194946)(.\img\11.png)]

可以看出每次的初始化都會爲我們生成幾個文件

  • contracts/:智能合約的目錄
  • migrations/:用來做部署的目錄
  • test/:測試的目錄
  • truffle.js: truffle的配置文件

-----> ganache
用來模擬一個本地內存的節點的工具,可以理解爲是本地的區塊鏈,這個鏈只有一個節點,裏面會有10個地址賬戶,每個賬戶有100的餘額,爲了方便測試轉賬

抽象合約

抽象合約和繼承相關,類似於上面的例子

pragma solidity ^0.4.0;
 
contract Feline {
    function utterance() public returns (bytes32);
}

這樣的合約不能被編譯(即使它們同時包含具體函數和抽象函數),但它們可以用作父合約:

contract Feline {
    function utterance() public returns (bytes32);
}
 
contract Cat is Feline {
    function utterance() public returns (bytes32) { return "miaow"; }
}

et-shop

[外鏈圖片轉存中...(img-6usixyui-1572335194946)]

可以看出每次的初始化都會爲我們生成幾個文件
+ `contracts/`:智能合約的目錄
+ `migrations/`:用來做部署的目錄
+ `test/`:測試的目錄
+ `truffle.js`: truffle的配置文件

-----> ganache
用來模擬一個本地內存的節點的工具,可以理解爲是本地的區塊鏈,這個鏈只有一個節點,裏面會有10個地址賬戶,每個賬戶有100的餘額,爲了方便測試轉賬


### 抽象合約
抽象合約和繼承相關,類似於上面的例子
```javascript
pragma solidity ^0.4.0;
 
contract Feline {
    function utterance() public returns (bytes32);
}

這樣的合約不能被編譯(即使它們同時包含具體函數和抽象函數),但它們可以用作父合約:

contract Feline {
    function utterance() public returns (bytes32);
}
 
contract Cat is Feline {
    function utterance() public returns (bytes32) { return "miaow"; }
}

如果一個合約是從抽象合約中繼承的,但沒實現所有的函數,則它也是抽象合約。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章