+5

WTF Solidity 102

image.png

15. Overloading

Overloading là tính năng cho phép nhiều hàm có tên giống nhau nhưng khác về tham số.

function saySomething() public pure returns(string memory) {
    return("Nothing");
}

function saySomething(string memory something) public pure returns(string memory) {
    return(something);
}

image.png

Lưu ý: Solidity không cho phép tính các modifier có cùng tên

Hai hàm có thể giống nhau hoàn toàn về tên và tham số đầu vào, nhưng khác nhau ở kiểu trả về.

    function f(uint8 _in) public pure returns (uint8 out) {
        out = _in;
    }

    function f(uint256 _in) public pure returns (uint256 out) {
        out = _in;
    }

16. Library (thư viện)

Thư viện là một dạng hợp đồng đặc biệt, giúp các nhà phát triển có thể tái sử dụng các đoạn mã đã được viết trước đó để tích hợp.

Thư viện có một số điểm khác biệt so với các hợp đồng thông thường: 1.Các biến trạng thái phải là hằng số 2. Không có tính năng kế thừa 3. Không thể nhận ETH 4. Không thể hủy (Self Destruct như contract thường)

String library contract

Chúng ta cùng xem qua 1 ví dụ về thư viện String, giúp chuyển đổi từ kiểu uint256 sang string.

library Strings {
    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";

    /**
     * @dev Chuyển số dạng unit256 sang dạng chuỗi string
     */
    function toString(uint256 value) public pure returns (string memory) {
        // Inspired by OraclizeAPI's implementation - MIT licence
        // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
            value /= 10;
        }
        return string(buffer);
    }

    /**
     * @dev Chuyển số dạng unit256 sang dạng chuỗi string (hệ cơ số 16)
     */
    function toHexString(uint256 value) public pure returns (string memory) {
        if (value == 0) {
            return "0x00";
        }
        uint256 temp = value;
        uint256 length = 0;
        while (temp != 0) {
            length++;
            temp >>= 8;
        }
        return toHexString(value, length);
    }

    /**
     * @dev Chuyển số dạng unit256 sang dạng chuỗi string (hệ cơ số 16) (với độ dài cố định)
     */
    function toHexString(uint256 value, uint256 length) public pure returns (string memory) {
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = "0";
        buffer[1] = "x";
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = _HEX_SYMBOLS[value & 0xf];
            value >>= 4;
        }
        require(value == 0, "Strings: hex length insufficient");
        return string(buffer);
    }
}

Sử dụng thư viện với từ khóa using

// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.19;

import "./Strings.sol";

contract Test {
    using Strings for uint256;
    function getString1(uint256 _number) public pure returns(string memory) {
        return _number.toHexString();
    }
}

Gọi trực tiếp bằng tên contract

    function getString2(uint256 _number) public pure returns(string memory){
        return Strings.toHexString(_number);
    }

Thử deploy và kiểm tra, kết quả là 2 hàm cho đầu ra hoàn toàn như nhau.

image.png

17. Import

Import là một tính năng cơ bản trong Solidity, hỗ trợ việc thêm các file từ bên ngoài bằng nhiều kiểu khác nhau.

  • Import trực tiếp bằng đường dẫn thư mục (đối với các file nằm trong mã nguồn)
Cây thư mục
├── Import.sol
└── Yeye.sol

// Import
import './Yeye.sol';
  • Import bằng URL
// Import by URL
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
  • Import qua package npm
import '@openzeppelin/contracts/access/Ownable.sol';
  • Import tên contract từ file (file có thể có nhiều contract)
import {Yeye} from './Yeye.sol';

18. Fallback

Solidity có 2 hàm đặc biệt là receive()fallback(), chủ yếu được sử dụng trong 2 TH:

  1. Nhận ETH
  2. Thực thi khi lời gọi đến hợp đồng không chỉ định gọi đến 1 hàm cụ thể nào

Nhận ETH: receive()

Khi gửi ETH từ 1 tài khoản vào hợp đồng, hàm receive() sẽ được gọi.

    event Received(address Sender,  uint Value); 
    
    /*
    - Không định nghĩa với từ khóa function, không thể có tham số
    - Không có kiểu dữ liệu trả về và 1 hợp đồng chỉ có tối đa 1 hàm receive
    - Phải là external và có từ khóa payable
    */ 
    receive() external payable {
        emit Received(msg.sender,  msg.value); 
    }

Fallback function: fallback()

Hàm fallback() được gọi khi lời gọi đến hợp đồng ko gọi bất cứ hàm nào, hoặc nhận ETH nếu không có hàm receive() ở đó.

    event fallbackCalled(address Sender,  uint Value,  bytes Data); 

     /*
    - Không định nghĩa với từ khóa function
    - Phải là external
    - Nếu muốn nhận ETH thì cần thêm từ khóa payable
    */ 
    fallback() external payable {
        emit fallbackCalled(msg.sender,  msg.value,  msg.data); 
    }

Sự khác nhau giữa receive()fallback()

Execute fallback() or receive()?
         Nhận ETH
              |
      msg.data rỗng hay không?
            /  \
          Có   Không
          /      \
Có receive()?   fallback()
        / \
      Có  Không
      /     \
receive()   fallback()

19. Gửi ETH

Có 3 cách để gửi ETH trên Solidity đó là: transfer(), send()call(). Trong đó call() được khuyến nghị sử dụng nhất.

Trước tiên, ta viết 1 contract nhận ETH

contract ReceiveETH {
    // Nhận ETH, log amount và gas
    event Log(uint amount,  uint gas);
    
    receive() external payable {
        emit Log(msg.value,  gasleft());
    }
    
    // Kiểm tra số dư ETH của contract
     function getBalance() view public returns (uint) {
        return address(this).balance;
    }
}

transfer

  • gas limit là 2300, vừa đủ để gửi. Nhưng sẽ không đủ nếu hàm receive() hay fallback() của contract nhận có thêm logic tiêu thụ gas.
  • Nếu chuyển không thành công thì giao dịch sẽ bị revert
// sending ETH with transfer()
// Đơn vị của amount là wei
function transferETH(address payable _to,  uint256 amount) external payable{
	_to.transfer(amount);
}

send

  • gas limit 2300 như transfer()
  • Giao dịch cũng sẽ bị revert nếu chuyển không thành công
  • Giá trị trả về kiểu boolean
// sending ETH with send()
// Đơn vị của amount là wei
function sendETH(address payable _to,  uint256 amount) external payable{
    bool success = _to.send(amount);
    if(!success){
    	revert SendFailed();
    }
}

call

  • gas limit không bị giới hạn. Có thể gửi đến các contract mà hàm fallback có nhiều logic.
  • call() lỗi thì sẽ không revert
  • Giá trị trả về kiểu (bool, data)
// sending ETH with call()
function callETH(address payable _to,  uint256 amount) external payable{
    // check result of call(),revert with error when failed
    (bool success, ) = _to.call{value: amount}("");
    if(!success){
    	revert CallFailed();
    }
}

20. Gọi contract

Chúng ta sẽ tìm hiểu cách tương tác với 1 contract đã deployed như thế nào trong Soldity. Trước tiên ta có OtherContract.

contract OtherContract {
    uint256 private _x = 0
    
    event Log(uint amount, uint gas);
    
    function getBalance() view public returns(uint) {
        return address(this).balance;
    }

    // Thay đổi giá trị của x, có thể nhận ETH gửi vào
    function setX(uint256 x) external payable {
        _x = x;
        // emit Log event when receiving ETH
        if(msg.value > 0){
            emit Log(msg.value, gasleft());
        }
    }

    function getX() external view returns(uint x){
        x = _x;
    }
}

Các cách mà chúng ta có thể tương tác với OTherContract từ 1 contract khác như sau

1. Truyền địa chỉ contract

    function callSetX(address _Address, uint256 x) external{
        OtherContract(_Address).setX(x);
    }

image.png

2. Truyền biến contract

    function callGetX(OtherContract _Address) external view returns (uint x) {
        x = _Address.getX();
    }

image.png

3. Tạo biến contract

    function callGetX(OtherContract _Address) external view returns (uint x) {
        x = _Address.getX();
    }

image.png

4. Gọi hàm đồng thời chuyển ETH

    function setXTransferETH(address otherContract, uint256 x) payable external {
        OtherContract(otherContract).setX{value: msg.value}(x);
    }

image.png

21. Call

Ở mục 19. Send ETH, chúng ta đã có đề cập đến lệnh call. Và giờ chúng ta sẽ tìm hiểu sâu hơn 1 chút về call trong Solidity.

call là một chức năng ở mức thấp (low-level), được sử dụng để gọi đến các hợp đồng khác. Giá trị trả về của call có dạng (bool, data) lần lượt là trạng thái và dữ liệu trả về từ lời gọi.

  • call được khuyến nghị sử dụng để gửi ETH khi contract được gọi đến có các hàm receive hay fallback
  • call không được khuyến khích dùng để tương tác với các contract khác do tiềm ẩn rủi ro bảo mật.

Cú pháp

targetContractAddress.call(binary code);

/* binary code có dạng: abi.encodeWithSignature("function signature", giá trị các tham số)
VD: abi.encodeWithSignature("f(uint256,address)", _x, _addr)
*/

// Khi gửi kèm cả ETH thì ta có cú pháp như sau:
contractAdress.call{value:ETH value, gas:gas limit}(binary code);

Ví dụ

Chúng ta có OtherContract là contract mà chúng ta sẽ gọi đến

contract OtherContract {
    uint256 private _x = 0; // state variable x
    // Receiving ETH event, log the amount and gas
    event Log(uint amount, uint gas);

    fallback() external payable{}

    // get the balance of the contract
    function getBalance() view public returns(uint) {
        return address(this).balance;
    }

    // set the value of _x, as well as receiving ETH (payable)
    function setX(uint256 x) external payable{
        _x = x;
        // emit Log event when receiving ETH
        if(msg.value > 0){
            emit Log(msg.value, gasleft());
        }
    }

    // read the value of x
    function getX() external view returns(uint x){
        x = _x;
    }
}

Gọi hàm setX

function callSetX(address payable _addr, uint256 x) public payable {
	// gọi setX và gửi thêm ETH
	(bool success, bytes memory data) = _addr.call{value: msg.value}(
		abi.encodeWithSignature("setX(uint256)", x)
	);

	emit Response(success, data); //emit event
}

image.png

Kết quả trả về : (true, 0x), trạng thái giao dịch thành công, hàm setX không có kết quả trả về nên data sẽ là 0x.

Gọi hàm getX

function callGetX(address _addr) external returns(uint256){
	// call getX()
	(bool success, bytes memory data) = _addr.call(
		abi.encodeWithSignature("getX()")
	);

	emit Response(success, data); //emit event 
	return abi.decode(data, (uint256));
}

image.png

Do đã gán x = 5 ở phần trên, data trả về có dạng 0x0000000000000000000000000000000000000000000000000000000000000005, sau khi abi.decode thì nó chính bằng 5, giá trị của x.

Gọi 1 hàm không tồn tại

function callNonExist(address _addr) external{
	// Gọi 1 hàm tên là foo
	(bool success, bytes memory data) = _addr.call(
		abi.encodeWithSignature("foo(uint256)")
	);

	emit Response(success, data); //emit event
}

image.png

Mặc dù hàm foo không tồn tại nhưng kết quả trả về vẫn là true do lời gọi đã được chuyển tiếp đến hàm fallback của OtherContract.

22. DelegateCall

Khi 1 contract A gọi hàm của contract C thông qua contract B, hàm được thực thi trên contract C sẽ thay đổi biến, trạng thái tại contract C, msg.sender sẽ là contract B. Rất cơ bản và dễ hiểu 😄

execution context of call

Với delegateCall, msg.sender sẽ là contract A chứ không phải contract B. Hàm được gọi trên contrac C nhưng sẽ thay đổi trạng thái ở contract B, nghe hơi khó hiểu. Chúng ta cùng xem ngay ví dụ dưới đây

execution context of delegatecall

Ví dụ

Ta có contract C:

// Target contract C
contract C {
    uint public num;
    address public sender;
		
		// Hàm gán giá trị của num và sender
    function setVars(uint _num) public payable {
        num = _num;
        sender = msg.sender;
    }
}

Tiếp đó là contract B:

contract B {
    uint public num;
    address public sender;

    // gọi hàm setVars() của contract C with `call()`, sẽ thay đổi biến trạng thái trên C
    function callSetVars(address _addr, uint _num) external payable{
        // call setVars()
        (bool success, bytes memory data) = _addr.call(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    }
    // gọi setVars() với delegatecall, sẽ thay đổi biến trạng thái trên B
    function delegatecallSetVars(address _addr, uint _num) external payable{
        // delegatecall setVars()
        (bool success, bytes memory data) = _addr.delegatecall(
            abi.encodeWithSignature("setVars(uint256)", _num)
        );
    }
}
  1. Deploy B và C deploy.png
  2. Kiểm tra giá trị các biến ban đầu trên 2 contract initialstate.png
  3. Gọi callsetVars với tham số là địa chỉ của C và 10 call.png4. Sau khi thực thi callsetVars giá trị num trên C bằng 10, sender là địa chỉ của B resultcall.png5. Gọi delegateCallSetVarsvới tham số là địa chỉ của C và 100 delegatecall.png6. Giá trị biến num và sender trên B đã thay đổi. num bằng 100 và sender là địa chỉ ví trên Remix resultdelegatecall.png

Ứng dụng của delegateCall

  1. Proxy Contract: Proxy contract có trách nhiệm xử lý các biến trạng thái và lưu địa chỉ của hợp đồng chứa logic chính. Mỗi khi cần nâng cấp hay thay đổi về logic ứng dụng, chỉ cần deploy contract mới và thay đổi giá trị địa chỉ trên proxy.
  2. EIP-2535 Diamonds: Là một đề xuất xây dựng các hợp động theo kiểu mô-đun, có khả năng mở rộng cao. Các bạn có thể tìm hiểu kỹ hơn về EIP-2535 Diamonds tại đây

23. Create

Trong Ethereum, không chỉ tài khoản ví (Externally-owned account) có thể tạo mới hợp đồng mà chính hợp đồng thông minh cũng có thể tạo ra các hợp đồng mới. Ví dụ điển hình là contract Factory trong mô hình Uniswap có thể tạo ra nhiều contract Pair là các cặp token có thể trao đổi.

create và create2

Có 2 cách để tạo mới contract là createcreate2. Ở mục này chúng ta sẽ giải thích create trước khi nói về create2 ở mục tiếp theo.

/* Sử dụng create rất đơn giản với từ khóa `new`
`Contract` là tên contract sẽ tạo mới
- Nếu constructor có từ khóa payable thì ta có thể truyền _value khi tạo mới contract để chuyển 1 lượng ETH tùy ý vào đó 
- param là các đối số có trong constructor của Contract
*/

Contract x = new Contract{value: _value}(params)

Ví dụ Uniswap

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

// Contract Pair giúp quản lý cặp token, thanh khoản, swap
contract Pair{ 
    address public factory; // factory contract address
    address public token0; // token1
    address public token1; // token2

    constructor() payable {
        factory = msg.sender;
    }
    
    // Chỉ được gọi 1 lần sau khi deploy
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'UniswapV2: FORBIDDEN');
        token0 = _token0;
        token1 = _token1;
    }
}

// Contract PairFactory có chức năng tạo mới cũng như quản lý các Pair contract 
contract PairFactory{
		// mapping lưu trữ địa chỉ các Pair đã tạo và các token của Pair đó 
    mapping(address => mapping(address => address)) public getPair;
		
		// Lưu trữ địa chỉ của tất cả các Pair contract đã tạo
    address[] public allPairs;

    function createPair(address tokenA, address tokenB) external returns (address pairAddr) {
        // Tạo mới 1 contract Pair
        Pair pair = new Pair(); 
        // Gọi hàm init 
        pair.initialize(tokenA, tokenB);
        
        // Cập nhật thông tin về Pair mới
        pairAddr = address(pair);
        allPairs.push(pairAddr);
        getPair[tokenA][tokenB] = pairAddr;
        getPair[tokenB][tokenA] = pairAddr;
    }
}

24. Create2

create2 giúp chúng ta có thể dự đoán trước địa chỉ của smart contract trước khi được triển khai.

Cách create tính toán địa chỉ

/*
- creator's address là địa chỉ deploy contract
- nonce là số nonce của creator
*/
new address = hash(creator's address, nonce)

địa chỉ của người tạo sẽ không thay đổi, nhưng nonce có thể thay đổi theo thời gian, vì vậy rất khó để dự đoán địa chỉ của hợp đồng được tạo bằng CREATE.

Cách create2 tính toán địa chỉ

// salt là giá trị được truyền vào bởi creator
// bytecode là mã bytecode của contract
new address = hash("0xFF", creator's address, salt, bytecode)

CREATE2 không phụ thuộc vào nonce mà là các tham số có thể biết trước được giá trị như salt hay bytecode, do đó dễ dự đoán địa chỉ của contract mới hơn.

Cú pháp của create2

Contract x = new Contract{salt: _salt, value: _value}(params)

Ví dụ

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

contract Pair {
    address public factory; // factory contract address
    address public token0; // token1
    address public token1; // token2

    constructor() payable {
        factory = msg.sender;
    }

    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'UniswapV2: FORBIDDEN');
        token0 = _token0;
        token1 = _token1;
    }
}

contract PairFactory2 {
        mapping(address => mapping(address => address)) public getPair;
        address[] public allPairs;

        function createPair2(address tokenA, address tokenB) external returns (address pairAddr) {
            require(tokenA != tokenB, 'IDENTICAL_ADDRESSES');
            // Sắp xếp thứ tự cặp token (Đảm bảo cặp A-B và B-A là một) 
            (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
            
            // Tính toán salt từ địa chỉ 2 token
            bytes32 salt = keccak256(abi.encodePacked(token0, token1));
     
            // Deploy contract mới với create2
            Pair pair = new Pair{salt: salt}(); 
            
            pair.initialize(tokenA, tokenB);
            // Update address map
            pairAddr = address(pair);
            allPairs.push(pairAddr);
            getPair[tokenA][tokenB] = pairAddr;
            getPair[tokenB][tokenA] = pairAddr;
        }

        // Tính toán địa chỉ `Pair` contract trước
        function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){
            require(tokenA != tokenB, 'IDENTICAL_ADDRESSES');
            
            // Sắp xếp
            (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);		
            // Tính salt
            bytes32 salt = keccak256(abi.encodePacked(token0, token1));
            
            // Tính toán contract address
            predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
                bytes1(0xff),
                address(this),
                salt,
                keccak256(type(Pair).creationCode)
            )))));
        }
}

25. DeleteContract

selfdestruct là câu lệnh xóa contract và chuyển lượng ETH còn lại (nếu có) cho 1 địa chỉ khác.

Ví dụ

contract DeleteContract {

    uint public value = 10;

    constructor() payable {}

    receive() external payable {}

    function deleteContract() external {
        // xóa contract và gửi tiền đến địa chỉ msg.sender
        selfdestruct(payable(msg.sender));
    }

    function getBalance() external view returns(uint balance){
        balance = address(this).balance;
    }
}
  • Do tính chất khá "nguy hiểm" của selfdestruct, khi viết hàm xóa contract nên sử dụng thêm modifier như onlyOwner để giới hạn địa chỉ có thể thực thi hàm
  • Khi contract bị xóa, mọi tương tác đến contract đều trả về 0.

26. ABI encode và decode

ABI(Application Binary Interface) là một tiêu chuẩn giúp tương tác với các hợp đồng thông minh.

Trong Solidity, cách mã hóa ABI có 4 hàm:

  1. abi.encode
  2. abi.encodePacked
  3. abi.encodeWithSignature
  4. abi.encodeWithSelector

Trong khi đó, chỉ có 1 hàm là abi.decode để giải mã từ abi.encode

ABI encode

    uint x = 10;
    address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
    string name = "0xAA";
    uint[2] array = [5, 6]; 

abi.encode

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

Với abi.encode, mỗi tham số sẽ có không gian 32 bytes để biểu diễn. Như đoạn code trên, kết quả cho ra sẽ là: 0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000

  • x convert thành 0x000000000000000000000000000000000000000000000000000000000000000a
  • addr thành 0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c71 ...

abi.encodePacked

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

Kết quả encode sẽ là: 0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b7fe7c5bb630736c713078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006

abi.encodePacked cho kết quả ngắn gọn hơn abi.encode do sử dụng ít không gian cho việc mã hóa hơn. Ví dụ như kiểu uint chỉ cấp cho không gian 1 bytes.

abi.encodePacked sẽ được sử dụng trong các trường hợp muốn tiết kiệm không gian tính toán và không phải tương tác với contract. Như tính mã băm của 1 số dữ liệu là TH phổ biến hay sử dụng.

abi.encodeWithSignature

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

Kết quả encode sẽ là: 0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000

Sẽ có thêm phần đầu là function signature (0xe87082f1000000000000000000000000000000000000000000000000000000000000000), các tham số khác được mã hóa giống như abi.encode.

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);
    }

Giống như abi.encodeWithSignature nhưng phần đầu function selector sẽ là 4 bytes đầu của function signature

ABI decode

abi.decode là hàm ngược của abi.encode

    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]));
    }

27. Hash

Hàm băm là một khái niệm mật mã. Có thể chuyển đổi một thông điệp với độ dài tùy ý thành giá trị có độ dài nhất định.

Một thuật toán băm tốt sẽ cần thỏa mãn các tính chất sau:

  • Một chiều (one-way): Việc đảo ngược lại giá trị băm là bất khả thi.
  • Tính nhạy cảm (Sensitivity): Một thay đổi nhỏ của đầu vào sẽ cho ra giá trị băm khác.
  • Khả năng chống va chạm: Với x # y, rất khó để hash(x) = hash(y)

Hàm băm keccak256 trong Solidity đảm bảo được các yếu tố đó

hash = keccak256(data);

28. Selector

Khi tra cứu các giao dịch trên etherscan chẳng hạn, ta sẽ bắt gặp 1 trường thông tin là input, 4 bytes đầu tiên của input gọi là function selector

Lưu ý: Với các giao dịch không gọi hàm như chuyển ETH (trường input sẽ có giá trị 0x)

image.png 0x095ea7b3 chính là function selector

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

contract Selector {

    // output selector
    // "mint(address)": 0x6a627842
    // 4 bytes đầu tiên khi băm function signature với keccak256 gọi là `method id`
    function mintSelector() external pure returns (bytes4 mSelector) {
        return bytes4(keccak256("mint(address)"));
    }

    // use selector để gọi hàm
    function callWithSignature() external returns (bool, bytes memory) {
        //  use `abi.encodeWithSelector` to pack and encode the `mint` function's `selector` and parameters
        (bool success, bytes memory data) = address(this).call(
            abi.encodeWithSelector(
                0x6a627842, // method id
                "0x2c44b726ADF1963cA47Af88B284C06f30380fC78"// tham số truyền vào
            )
        );
        return (success, data);
    }
}

Chúng ta có thể dùng function selector kết hợp với lệnh call để gọi hàm trong Solidity.

29. Try Catch

Tính năng try-catch bắt đầu có từ phiên bản Solidity 0.6. Try-catch chỉ có thể sử dụng trong các hàm external hoặc hàm khởi tạo (constructor).

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

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

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

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

    // tạo object của OnlyEven contract
    OnlyEven even;

    constructor() {
        even = new OnlyEven(2);
    }
    
    // sử dụng try-catch khi gọi đến hàm ở contract khác
    // gọi hàm onlyEven, nếu thành công thì emit SuccessEvent
    // Thất bại thì emit CatchEvent
    function execute(uint amount) external returns (bool success) {
        try even.onlyEven(amount) returns(bool _success){
            // if call succeeds
            emit SuccessEvent();
            return _success;
        } catch Error(string memory reason){
            // if call fails
            emit CatchEvent(reason);
        }
    }

    // sử dụng try-catch khi tạo mới instance contract OnlyEven
    // executeNew(0) sẽ emit `CatchEvent` (vì dính require có message)
    // executeNew(1) sẽ emit `CatchByte` (vì dính assert)
    // executeNew(2) thành công emit `SuccessEvent`
    function executeNew(uint a) external returns (bool success) {
        try new OnlyEven(a) returns(OnlyEven _even){
            // if call succeeds
            emit SuccessEvent();
            success = _even.onlyEven(a);
        } catch Error(string memory reason) {
            // sẽ bắt các lỗi dạng như revert("reasonString") and require(false, "reasonString")
            emit CatchEvent(reason);
        } catch (bytes memory reason) {
            // sẽ bắt các lỗi dạng như assert()
            emit CatchByte(reason);
        }
    }
}

Nguồn

https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.