Solidity 學(xué)習(xí) -練氣期(十三)

ABI 的編碼和解碼

ABI(application binary interface,應(yīng)用二進(jìn)制接口) 是與以太坊智能合約交互的標(biāo)準(zhǔn)。
數(shù)據(jù)基于他們的類型編碼,并且由于編碼后不包含類型信息,解碼事需要注明他們的類型。
編碼函數(shù):
(1)abi.encode
(2)abi.encodePacked
(3)abi.encodeWithSignature
(4)abi.encodeWithSelector
abi.decode可以解碼abi.encode編碼的數(shù)據(jù)。

準(zhǔn)備4個(gè)類型變量,uint256,address,string,uint256[2]

  1. abi.encode
    ABI 被設(shè)計(jì)來和智能合約交互,它將每個(gè)參數(shù)填充32字節(jié)的數(shù)據(jù)并拼接在一起。與合約交互手段就是abi.encode
    function encode() public view returns(bytes memory result) {
        result = abi.encode(x, addr, name, array);
    }

編碼結(jié)果
0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
每個(gè)數(shù)據(jù)都用0填充補(bǔ)充道32字節(jié)

  1. abi.encodePacked
    function encodePacked() public view returns(bytes memory result) {
        result = abi.encodePacked(x, addr, name, array);
    }

0x000000000000000000000000000000000000000000000000000000000000000a5b38da6a701c568545dcfcb03fcb875f56beddc43078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006
將給定參數(shù)根據(jù)其所需要的最低空間編碼。類似abi.encode會(huì)把其中填充的0省略。
比如只用一個(gè)字節(jié)編碼uint8 類型。如果需要節(jié)省空間不需要與合約交互,可以使用它。比如計(jì)算一些數(shù)據(jù)的hash時(shí)。

  1. abi.encodeWithSignature
    function encodeWithSignature() public view returns(bytes memory result) {
        result = abi.encodeWithSignature(
            "foo(uint256,address,string,uint256[2])", 
            x, addr, name, array);
    }

0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
相當(dāng)于在abi.encode的編碼結(jié)果加了4個(gè)字節(jié)的選擇器

  1. abi.encodeWithSelector
    function encodeWithSelector() public view returns(bytes memory result) {
        result = abi.encodeWithSelector(
            bytes4(keccak256("foo(uint256,address,string,uint256[2])")), 
            x, addr, name, array);
    }

0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
編碼結(jié)果月abi.encodeWithSignature編碼結(jié)果一致

ABI 解碼

    function decode(bytes memory data) public pure 
        returns(uint dx,address daddr, string memory dname, uint[2] memory darray) {
            (dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2]));
    }

用abi.encode的結(jié)果作為參數(shù),進(jìn)行解碼。可以還原。

使用場景

(1)在合約開發(fā)中,ABI場配合call來實(shí)現(xiàn)對(duì)合約的底層調(diào)用。

        bytes4 selector = contract.getValue.selector;
        bytes memory data = abi.encodeWithSelector(selector, _x);
        (bool success, bytes memory returnedData) =
            address(contract).staticcall(data);
        require(success);
        return abi.decode(returnedData, (uint256))

(2) ethers.js 常用ABI實(shí)現(xiàn)合約的導(dǎo)入和函數(shù)調(diào)用

    const wavePortalContract = new ethers.Contract(
        contractAddress,contractABI, signer
    );
    const waves = await wavePortalContract.getAllWaves();

(3)對(duì)不開源合約進(jìn)行反編譯后,某些函數(shù)無法查詢函數(shù)簽名,可以通過ABI調(diào)用。
智能看到

    function 0x533ba33a() public payable {
        return stor_14;
    }

這種情況通過ABI函數(shù)選擇器來調(diào)用函數(shù)

        bytes memory data = abi.encodeWithSelector(bytes4(0x533ba33a));
        (bool success, bytes memory returnedData) =
            address(contract).staticcall(data);
        require(success);
        return abi.decode(returnedData, (uint256))

哈希函數(shù)

哈希函數(shù)是一個(gè)密碼學(xué)概念,它可以講一個(gè)任意長度的消息,轉(zhuǎn)換為一個(gè)固定長度的值,稱作hash值

哈希函數(shù)的性質(zhì)和應(yīng)用

(1)單項(xiàng)性:從輸入的消息道它的哈希值的正向運(yùn)算,簡答且唯一確定。而反過來非常難,只能暴力枚舉。
(2)靈敏性:對(duì)輸入消息改動(dòng)微小,得到的哈希值改變很大
(3)高效性:從輸入的消息到哈希的運(yùn)算高效。
(4)均一性:每個(gè)哈希值被取到的概率應(yīng)該基本相等
(5)抗碰撞性:
弱抗碰撞性:給定一個(gè)消息x,找到另一個(gè)消息x‘ 使得hash(x) = hash(x’)是困難的
強(qiáng)抗碰撞性:找到任意一對(duì)不同的消息x和x‘ ,是的hash(x) = hash(x’)是困難的

應(yīng)用于現(xiàn)代計(jì)算機(jī)應(yīng)用,包括生成唯一標(biāo)識(shí),數(shù)字簽名,華聯(lián)網(wǎng)安全加密(SSL/TLS)等

keccak256函數(shù)

Solidity 中最常用的哈希函數(shù)keccak256,基于keccak算法
hash = keccak256(data);
輸入?yún)?shù)data時(shí)bytes類型的數(shù)據(jù),返回hash 是bytes32類型的哈希值

  1. Keccak算法和SHA-3算法
    SHA-3算法基于Keccak算法標(biāo)準(zhǔn)化而來。很多場合Keccak算法和SHA-3算法同義
    Solidity0.5.0版本移除了SHA-3算法,現(xiàn)在直接使用keccak256

  2. 生成數(shù)據(jù)唯一的標(biāo)識(shí)

  3. 弱抗碰撞

  4. 強(qiáng)抗碰撞

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract UseHash {
    //生成數(shù)據(jù)唯一的標(biāo)識(shí)
    function hash(uint _num, string memory _string, address _addr)
        public pure returns(bytes32) {
            return keccak256(abi.encodePacked(_num, _string ,_addr));
        }
    //弱抗碰撞
    bytes32 _msg = keccak256(abi.encodePacked("0xAA"));
    function weak(string memory string1) public view returns (bool) {
        return keccak256(abi.encodePacked(string1)) == _msg;
    }
    //強(qiáng)抗碰撞
    function strong(string memory string1,string memory string2) public pure returns (bool) {
        return keccak256(abi.encodePacked(string1)) == keccak256(abi.encodePacked(string2));
    }
}
image.png

函數(shù)選擇器

我們調(diào)用智能合約時(shí),本質(zhì)上是想目標(biāo)合約發(fā)送了一段calldata類型的數(shù)據(jù)。

  1. msg.data

msg.data 是Solidity中的一個(gè)全局變量,它的值為calldata,也就是調(diào)用函數(shù)時(shí)傳入的數(shù)據(jù)。
借助代碼輸出調(diào)用函數(shù)的calldata:
地址參數(shù)輸入;0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
輸出的calldata:0x6a6278420000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4

    event Log(bytes data);

    function mint(address to) external {
        emit Log(msg.data);
    }

(1)開頭4個(gè)字節(jié)選擇器 0x6a627842
(2)后面32個(gè)字節(jié)為輸入的參數(shù),其中前面12個(gè)字節(jié)為用來填充0,后面20個(gè)字節(jié)為地址參數(shù)to:
0x 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
calldata就是在告訴智能合約,用戶想要調(diào)用那個(gè)函數(shù),參數(shù)是什么。

  1. method id、函數(shù)選擇器和函數(shù)簽名

method id定義為函數(shù)簽名經(jīng)過keccak256函數(shù)計(jì)算后的哈希值前面4個(gè)字節(jié)。當(dāng)函數(shù)選擇器和method id相匹配,即表示調(diào)用該函數(shù)。

函數(shù)簽名,前面說過,“函數(shù)名(逗號(hào)分隔的參數(shù)類型)” 例如:mint 函數(shù)簽名“mint(address)”.同一個(gè)智能合約不同的函數(shù)簽名不同,因此我們通過函數(shù)簽名來確定調(diào)用那個(gè)函數(shù)。
注意??:在函數(shù)簽名中uint和int 要寫成uint256和int256
驗(yàn)證函數(shù)簽名:

    function minSelector() external pure returns(bytes4 mSelector) {
        return bytes4(keccak256("mint(address)"));
    }

驗(yàn)證結(jié)果一致:0x6a627842


image.png

image.png
  1. 使用函數(shù)選擇器調(diào)用函數(shù)
    function callWithSignature() external returns (bool, bytes memory) {
        (bool success, bytes memory data) = 
            address(this).call(abi.encodeWithSelector(
                0x6a627842, 
                0x005b38da6a701c568545dcfcb03fcb875f56beddc4));
            return (success, data);
    }

mint調(diào)用成功輸出Log事件

捕獲異常

程序遇到異常則會(huì)中斷運(yùn)行并報(bào)錯(cuò),并非所有異常都需要中斷運(yùn)行,某些可以通過主動(dòng)捕獲并處理,不影響到整個(gè)程序運(yùn)行。
try-catch是現(xiàn)代編程語言都具備的標(biāo)準(zhǔn)方式。solidity 6.0版本也添加了關(guān)鍵字

solidity中,try-catch 智能被用在聲明為external 的函數(shù)或合約的構(gòu)造函數(shù)constructor(被自動(dòng)聲明為external的函數(shù)) 中。語法

try externalContract.f() {
} catch {
}

externalContract.f() 是某個(gè)外部合約的函數(shù)調(diào)用,try模塊在調(diào)用的情況下執(zhí)行,catch模塊調(diào)用失敗跑出異常時(shí)執(zhí)行。
在合約本身的內(nèi)部可以this.f() 來替代externalContract.f() ,this.f() 也被視為外部調(diào)用。這種用法不可在構(gòu)造函數(shù)中使用,因?yàn)榇藭r(shí)還沒有創(chuàng)建。

如果調(diào)用的函數(shù)有返回值,那么必須在try之后聲明返回值returns(returnType val).并且在try模塊可以使用返回的變量;如果是創(chuàng)建合約,那么返回值是新創(chuàng)建的合約變量。

try externalContract.f() returns (returnType val)  {
} catch {
}

catch 模塊支持捕獲異常原因

      try externalContract.f() returns (returnType)  {
        
        } catch Error(string memory){
        
        } catch Panic(uint) {
            
        } catch (bytes memeory) {
            
        }

try-catch 實(shí)戰(zhàn)

  1. OnlyEvent合約
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract OnlyEvent {
    constructor(uint a) {
        require(a != 0,"invalid number");
        assert(a != 1);
    }

    function onlyEvent(uint256 b) external pure returns (bool success) {
        require(b % 2 ==0,"Ups! Reverting");
        success = true;
    }
}

OnlyEvent 包含一個(gè)構(gòu)造函數(shù),和一個(gè)onlyEvent函數(shù)
(1)構(gòu)造函數(shù)有一個(gè)參數(shù)a,當(dāng)a相當(dāng)于0時(shí),require語句會(huì)拋出異常;當(dāng)a等與1時(shí),assert會(huì)拋出異常;其他情況均正常。
(2)onlyEvent 函數(shù)有一個(gè)參數(shù),允許b為偶數(shù)。當(dāng)b為奇數(shù)時(shí),require語句會(huì)拋出異常。

  1. 處理外部函數(shù)調(diào)用的異常、合約創(chuàng)建中的異常
    創(chuàng)建一個(gè)TryCatch合約
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "./OnlyEvent.sol";

contract TryCatch {
    event SuccessEvent();
    event CatchEvent(string message);
    event CatchByte(bytes data);

    OnlyEvent even;
    constructor() {
        even = new OnlyEvent(2);
    }

    // 在 external call 中使用 try-catch
    function execute(uint amount) external returns (bool success) {
        try even.onlyEvent(amount) returns(bool _success) {
            emit SuccessEvent();
            return _success;
        } catch Error(string memory reason) {
            emit CatchEvent(reason);
        }
    }

    //在創(chuàng)建新合約中使用try-catch (合約創(chuàng)建視為external call)
    // executeNew(0) 會(huì)失敗釋放  `CatchEvent`
    // executeNew(1) 會(huì)失敗釋放  `CatchByte`
    // executeNew(2) 會(huì)失敗釋放  `CatchEvent`
    function executeNew(uint a) external returns (bool success) {
        try new OnlyEvent(a) returns (OnlyEvent _even) {
            emit SuccessEvent();
            return _even.onlyEvent(a);
        } catch Error(string memory reason) {
            emit CatchEvent(reason);
        } catch (bytes memory reason) {
            emit CatchByte(reason);
        }
    }

}

SuccessEvent 是調(diào)用成功時(shí)釋放的事件,而CatchEvent和CatchByte是拋出異常時(shí)事件,分別對(duì)應(yīng)require/revert 和 assert 異常情況。event 是個(gè)OnlyEvent合約類型的狀態(tài)變量。構(gòu)造函數(shù)中,我們創(chuàng)建OnlyEvent合約時(shí)將參數(shù)設(shè)置為2,使得創(chuàng)建時(shí)不拋出異常。

在execute函數(shù)中使用try-catch處理調(diào)用外部函數(shù)onlyEvent中的異常

在remix上部署OnlyEvent和TryCatch合約,然后測試結(jié)果
execute(0) 的時(shí)候因?yàn)?是偶數(shù),滿足onlyEvent函數(shù)require的命令條件,多億調(diào)用成功,沒有異常拋出來,并釋放SuccessEvent事件

execute(1),奇數(shù)不滿足require命令,調(diào)用失敗跑出異常被catch捕獲,并釋放CatchEvent事件

在execute改進(jìn)處理合約常見時(shí)候異常。把try模塊改成OnlyEvent合約創(chuàng)建即可

測試
executeNew(0) 會(huì)失敗釋放 CatchEvent
executeNew(1) 會(huì)失敗釋放 CatchByte
executeNew(2) 會(huì)失敗釋放 CatchEvent

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容