+3

WTF Solidity 104

image.png

40. WETH

image.png

WETH (viết tắt của Wrapped ETH) là phiên bản ERC-20 của ETH, tỷ lệ quy đổi là 1:1. Với các tính năng của ERC-20, WETH giúp cho việc trao đổi ETH được linh hoạt, tiện lợi hơn thông qua các giao dịch như chuyển tiền giữa các blockchain khác nhau, swap ....

WETH Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract WETH is ERC20 {
    // Events
    event Deposit(address indexed dst, uint wad);
    event Withdrawal(address indexed src, uint wad);

    // Constructor: Khởi tạo tên, symbol token
    constructor() ERC20("WETH", "WETH") {}

    // Callback function
    fallback() external payable {
        deposit();
    }

    // Callback function
    receive() external payable {
        deposit();
    }

    // Deposit function, người dùng gửi ETH vào sẽ được mint ra lượng WETH tương ứng
    function deposit() public payable {
        _mint(msg.sender, msg.value);
        emit Deposit(msg.sender, msg.value);
    }

    // Withdrawal function, người dùng rút ETH về và WETH của người dùng với số lượng tương ứng bị đốt
    function withdraw(uint amount) public {
        require(balanceOf(msg.sender) >= amount);
        _burn(msg.sender, amount);
        payable(msg.sender).transfer(amount);
        emit Withdrawal(msg.sender, amount);
    }
}

Sự khác nhau giữa fallbackreceive, chúng ta có thể xem lại ở đây.

41. Payment Splitting

Payment Splitting

Payment splitting là một khái niệm liên quan đến việc phân chia thanh toán hoặc chi trả giữa nhiều bên hoặc các đối tượng khác nhau trong một giao dịch tài chính. Nôm na nghĩa là chia tiền cho nhiều bên theo tỷ lệ khác nhau.

image.png

Payment Split Contract

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

contract PaymentSplit {
    // event
    event PayeeAdded(address account, uint256 shares);
    event PaymentReleased(address to, uint256 amount); 
    event PaymentReceived(address from, uint256 amount);

    uint256 public totalShares; // Tổng số người nhận
    uint256 public totalReleased; // Tổng tiền chi trả

    mapping(address => uint256) public shares; // số tiền thụ hưởng đối với mỗi địa chỉ trong danh sách
    mapping(address => uint256) public released; // số tiền mỗi địa chỉ đã được tri chả
    address[] public payees; // Danh sách người nhận

   // Khởi tạo danh sách người nhận và số tiền tương ứng với mỗi địa chỉ
    constructor(address[] memory _payees, uint256[] memory _shares) payable {
        require(
            _payees.length == _shares.length,
            "PaymentSplitter: payees and shares length mismatch"
        );
        
        require(_payees.length > 0, "PaymentSplitter: no payees");
        
        for (uint256 i = 0; i < _payees.length; i++) {
            _addPayee(_payees[i], _shares[i]);
        }
    }

    /**
     * @dev Callback function, hàm nhận ETH
     */
    receive() external payable virtual {
        emit PaymentReceived(msg.sender, msg.value);
    }

    /**
     * @dev Trả tiền cho người nhận
     */
    function release(address payable _account) public virtual {
        // Địa chỉ phải có trong danh sách
        require(shares[_account] > 0, "PaymentSplitter: account has no shares");
        
        // Tính toán lượng tiền sẽ trả
        uint256 payment = releasable(_account);
        
        // số tiền trả phải lớn hơn 0
        require(payment != 0, "PaymentSplitter: account is not due payment");
        
        // Cập nhật các biến trạng thái
        totalReleased += payment;
        released[_account] += payment;
        
        // Chuyển tiền cho người nhận
        _account.transfer(payment);
        emit PaymentReleased(_account, payment);
    }

    /**
     * @dev Tính toán lượng tiền địa chỉ sẽ được nhận
     * gọi pendingPayment() 
     */
    function releasable(address _account) public view returns (uint256) {
        // Calculate the total income of the profit-sharing contract
        uint256 totalReceived = address(this).balance + totalReleased;
        // Call _pendingPayment to calculate the amount of ETH that account is entitled to
        return pendingPayment(_account, totalReceived, released[_account]);
    }

    /**
     * @dev Tính toán số tiền còn lại mà người nhận chưa được hưởng
     */
    function pendingPayment(
        address _account,
        uint256 _totalReceived,
        uint256 _alreadyReleased
    ) public view returns (uint256) {
        // Số lượng ETH = Total ETH due - ETH received
        return
            (_totalReceived * shares[_account]) /
            totalShares -
            _alreadyReleased;
    }

    /**
     * @dev thêm người nhận vào danh sách, hàm này chỉ được gọi từ constructor khi contract được triển khai
     */
    function _addPayee(address _account, uint256 _accountShares) private {
        
        require(
            _account != address(0),
            "PaymentSplitter: account is the zero address"
        );
        
        require(_accountShares > 0, "PaymentSplitter: shares are 0");
        
        require(
            shares[_account] == 0,
            "PaymentSplitter: account already has shares"
        );
        
        // Cập nhật các biến 
        payees.push(_account);
        shares[_account] = _accountShares;
        totalShares += _accountShares;
       
       // emit add payee event
        emit PayeeAdded(_account, _accountShares);
    }
}

Triển khai

1. Deploy contract, khởi tạo với 2 địa chỉ nhận

image.png

2. Kiểm tra các biến trạng thái

image.png

image.png

3. Gọi hàm release để nhận tiền

image.png

4. Kiểm tra lại trạng thái mới

image.png

42. Token Vesting

Vesting

Token Vesting là hình thức trả thưởng theo từng đợt, mỗi đợt cách nhau một khoảng thời gian. Việc phân phối thành nhiều đợt giúp giảm áp lực bán ra cho token và duy trì sự cam kết lâu dài của đội ngũ hay các nhà đầu tư đối với dự án.

Smart contract


// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";


contract TokenVesting {
    // Event
    event ERC20Released(address indexed token, uint256 amount); // Withdraw event

   // số lượng đã trả ứng với mỗi token
    mapping(address => uint256) public erc20Released;
    
    address public immutable beneficiary; // địa chỉ nhận token
    uint256 public immutable start; // thời gian bắt đầu (tính theo timestamp)
    uint256 public immutable duration; // Khoảng thời gian khóa token

    /**
     * @dev Khởi tạo các biến trạng thái khi deploy
     */
    constructor(address beneficiaryAddress, uint256 durationSeconds) {
        require(
            beneficiaryAddress != address(0),
            "VestingWallet: beneficiary is zero address"
        );
        beneficiary = beneficiaryAddress;
        start = block.timestamp;
        duration = durationSeconds;
    }

    /**
     * @dev Rút tiền
     * Emit {ERC20Released} event.
     */
    function release(address token) public {
        // Gọi vestedAmount để tính toán số tiền có thể nhận
        uint256 releasable = vestedAmount(token, uint256(block.timestamp)) -
            erc20Released[token];
            
        erc20Released[token] += releasable;
        
        // Chuyển token
        emit ERC20Released(token, releasable);
        IERC20(token).transfer(beneficiary, releasable);
    }

    /**
     * @param token: địa chỉ Token rút
     * @param timestamp: thời điểm rút
     */
    function vestedAmount(
        address token,
        uint256 timestamp
    ) public view returns (uint256) {
        uint256 totalAllocation = IERC20(token).balanceOf(address(this)) +
            erc20Released[token];
        
        if (timestamp < start) {
            return 0;
        } else if (timestamp > start + duration) {
            return totalAllocation;
        } else {
            // Dựa trên thời gian rút, tính toán xem đã qua được bao nhiêu chu kỳ (duration) để tính toán lượng token
            return (totalAllocation * (timestamp - start)) / duration;
        }
    }
}

43. Token Locker

Khác với Token Vesting mở khóa theo từng khoảng thời gian. Token Locker sẽ khóa toàn bộ token trong khoảng thời gian được chỉ định và chỉ có thể được rút sau khoảng thời gian đó.

Contract Token Locker có thể ứng dụng trong việc khóa các LP Token, tránh tình trạng rug-pull, rút cạn thanh khoản của cặp giao dịch trên sàn phi tập trung (DEX).

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract TokenLocker {
    // Event
    event TokenLockStart(
        address indexed beneficiary,
        address indexed token,
        uint256 startTime,
        uint256 lockTime
    );
    
    event Release(
        address indexed beneficiary,
        address indexed token,
        uint256 releaseTime,
        uint256 amount
    );

    // địa chỉ contract ERC20 sẽ được khóa
    IERC20 public immutable token;
    
    // địa chỉ người dùng address
    address public immutable beneficiary;
    
    // Thời gian khóa (giây)
    uint256 public immutable lockTime;
    
    // Thời gian bắt đầu khóa (giây)
    uint256 public immutable startTime;

     // Khởi tạo
    constructor(IERC20 token_, address beneficiary_, uint256 lockTime_) {
        require(lockTime_ > 0, "TokenLock: lock time should greater than 0");
        token = token_;
        beneficiary = beneficiary_;
        lockTime = lockTime_;
        startTime = block.timestamp;

        emit TokenLockStart(
            beneficiary_,
            address(token_),
            block.timestamp,
            lockTime_
        );
    }

    /**
     * @dev Sau khi thời gian khóa đã hết, người dùng có thể gọi để rút token
     */
    function release() public {
        require(
            block.timestamp >= startTime + lockTime,
            "TokenLock: current time is before release time"
        );

        uint256 amount = token.balanceOf(address(this));
        require(amount > 0, "TokenLock: no tokens to release");

        token.transfer(beneficiary, amount);

        emit Release(msg.sender, address(token), block.timestamp, amount);
    }
}

44. TimeLock

TimeLock (Khóa thời gian) là một cơ chế thường thấy ở các ngân hàng hay những nơi cần sự mật cao. Là một bộ đếm thời gian được thiết kế để ngăn chặn việc mở két sắt trong một thời gian nhất định, ngay cả khi người mở khóa biết mật khẩu chính xác.

Trong blockchain, TimeLock được sử dụng rộng rãi trong DeFi và DAO. Việc trì hoãn giao dịch trong một khoảng thời gian giúp phòng tránh và giảm thiểu rủi ro trong các ứng dụng tài chính. Ví dụ: nếu kẻ xấu hack được đa chữ ký của Uniswap và có ý định rút tiền từ vault, nhưng phải chờ 2 ngày vì nó áp dụng timelock, hacker cần đợi 2 ngày kể từ khi tạo giao dịch rút tiền để thực sự rút được tiền. Trong giai đoạn này, bên dự án có thể tìm ra biện pháp đối phó và nhà đầu tư có thể bán token trước để giảm lỗ.

image.png

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

contract Timelock {
    // Event
    // transaction cancel event
    event CancelTransaction(
        bytes32 indexed txHash,
        address indexed target,
        uint value,
        string signature,
        bytes data,
        uint executeTime
    );
    
    // transaction execution event
    event ExecuteTransaction(
        bytes32 indexed txHash,
        address indexed target,
        uint value,
        string signature,
        bytes data,
        uint executeTime
    );
    
    // transaction created and queued event
    event QueueTransaction(
        bytes32 indexed txHash,
        address indexed target,
        uint value,
        string signature,
        bytes data,
        uint executeTime
    );
    
    // Event to change administrator address
    event NewAdmin(address indexed newAdmin);

    address public admin; // Admin address
    uint public constant GRACE_PERIOD = 7 days; // Thời gian giao dịch còn hiệu lực, sau thời gian này nếu thực thi giao dịch sẽ lỗi
    uint public delay; // thời gian khóa giao dịch (giây)
    mapping (bytes32 => bool) public queuedTransactions; // Lưu trạng thái của tất cả giao dịch timelock

    modifier onlyOwner() {
        require(msg.sender == admin, "Timelock: Caller not admin");
        _;
    }

    modifier onlyTimelock() {
        require(msg.sender == address(this), "Timelock: Caller not Timelock");
        _;
    }

    constructor(uint delay_) {
        delay = delay_;
        admin = msg.sender;
    }
    
    // Chỉ có thể được gọi từ chính nó, đây là giao dịch gọi hàm được áp dụng timelock
    function changeAdmin(address newAdmin) public onlyTimelock {
        admin = newAdmin;

        emit NewAdmin(newAdmin);
    }

    /**
     * @dev Tạo giao dịch và thêm vào hàng đợi timelock
     * @param target: địa chỉ contract đích của giao dịch
     * @param value: lượng ETH 
     * @param signature: function signature
     * @param data
     * @param executeTime: Thời gian thực thi giao dịch
     *
     * Yêu cầu: executeTime phải lớn hơn block.timestamp + delay
     */
    function queueTransaction(
        address target,
        uint256 value,
        string memory signature,
        bytes memory data,
        uint256 executeTime
    ) public onlyOwner returns (bytes32) {
        require(
            executeTime >= getBlockTimestamp() + delay,
            "Timelock::queueTransaction: Estimated execution block must satisfy delay."
        );
        
        // định danh giao dịch bằng mã băm
        bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
        
        // thêm vào hàng đợi
        queuedTransactions[txHash] = true;

        emit QueueTransaction(
            txHash,
            target,
            value,
            signature,
            data,
            executeTime
        );
        
        return txHash;
    }

    /**
     * @dev Hủy giao dịch
     * yêu cầu: giao dịch phải đang trạng thái chờ trong hàng đợi timelock 
     */
    function cancelTransaction(
        address target,
        uint256 value,
        string memory signature,
        bytes memory data,
        uint256 executeTime
    ) public onlyOwner {
        
        bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
        
        require(
            queuedTransactions[txHash],
            "Timelock::cancelTransaction: Transaction hasn't been queued."
        );
        
        // dequed
        queuedTransactions[txHash] = false;

        emit CancelTransaction(
            txHash,
            target,
            value,
            signature,
            data,
            executeTime
        );
    }

    /**
     * @dev Thực thi 
     *
     * 1. Giao dịch ở trong hàng đợi timelock
     * 2. Hết thời gian khóa
     * 3. Thời gian hiệu lực vẫn còn (chưa quá 7 ngày)
     */
    function executeTransaction(
        address target,
        uint256 value,
        string memory signature,
        bytes memory data,
        uint256 executeTime
    ) public payable onlyOwner returns (bytes memory) {
        bytes32 txHash = getTxHash(target, value, signature, data, executeTime);
        
        require(
            queuedTransactions[txHash],
            "Timelock::executeTransaction: Transaction hasn't been queued."
        );
        
        // Kiểm tra executeTime so với thời gian hiện tại 
        require(
            getBlockTimestamp() >= executeTime,
            "Timelock::executeTransaction: Transaction hasn't surpassed time lock."
        );
        
        // Kiểm tra xem hết thời gian hiệu lực chưa
        require(
            getBlockTimestamp() <= executeTime + GRACE_PERIOD,
            "Timelock::executeTransaction: Transaction is stale."
        );
        
        // xóa khỏi hàng đợi
        queuedTransactions[txHash] = false;

        // get callData
        bytes memory callData;
        if (bytes(signature).length == 0) {
            callData = data;
        } else {
            callData = abi.encodePacked(
                bytes4(keccak256(bytes(signature))),
                data
            );
        }
        
        // Thực thi giao dịch
        (bool success, bytes memory returnData) = target.call{value: value}(
            callData
        );
        
        // kiểm tra trạng thái
        require(
            success,
            "Timelock::executeTransaction: Transaction execution reverted."
        );

        emit ExecuteTransaction(
            txHash,
            target,
            value,
            signature,
            data,
            executeTime
        );

        return returnData;
    }

    /**
     * @dev Get the current blockchain timestamp
     */
    function getBlockTimestamp() public view returns (uint) {
        return block.timestamp;
    }

    /**
     * @dev transaction identifier
     */
    function getTxHash(
        address target,
        uint value,
        string memory signature,
        bytes memory data,
        uint executeTime
    ) public pure returns (bytes32) {
        return
            keccak256(abi.encode(target, value, signature, data, executeTime));
    }
}

Chạy thử

1. Deploy với delay = 120

image.png

2. Gọi hàm changeAdmin => lỗi vì không thể gọi từ bên ngoài

image.png

3.

  • target: địa chỉ contract Timelock
  • value: không gửi ETH nên truyền vào 0
  • signature: gọi hàm changeAdmin nên chữ ký sẽ là "changeAdmin(address)"
  • data: encode tham số sẽ truyền vào khi gọi hàm
Address before encoding:0xd9145CCE52D386f254917e481eB44e9943F39138
encoded address:0x000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2
  • executeTime: lấy block.timestamp hiện tại cộng thêm 150 (>120)

image.png

4. Gọi hàm queueTransaction với các thông số ở trên

image.png

5. Chưa hết thời gian khóa và gọi hàm excuteTransaction => lỗi

image.png

6. Chờ hết thời gian khóa và gọi hàm excuteTransaction

image.png

7. Kiểm tra địa chỉ admin => địa chỉ mới (giao dịch thành công)

image.png

45. ProxyContract

Proxy pattern

Smart contract sau khi đã được triển khai sẽ không thể thay đổi. Đây là một ưu điểm nhưng đồng thời cũng là một hạn chế.

  • Ưu điểm: An toàn khi không ai có thể sửa đổi logic hợp đồng để chuộc lợi.
  • Hạn chế: Khi phát hiện lỗi hoặc muốn nâng cấp phiên bản thì phải triển khai một hợp đồng hoàn toàn mới. Các dữ liệu trên hợp đồng cũ nếu mới chuyển sang hợp đồng mới cũng tốn rất nhiều chi phí và thời gian.

Proxy pattern được đề xuất giúp có thể "sửa đổi" và nâng cấp hợp đồng thông minh. Nó sẽ bao gồm 2 hợp đồng

  1. Proxy contract: Lưu trữ dữ liệu, các biến trạng thái
  2. Logic contract: Chứa logic, các hàm

Khi người dùng gọi tới Proxy contract, nó sẽ gọi bằng lệnh delegate đến Logic contract để thực thi. Chúng ta có thể đọc lại về delegateCall ở đây

image.png

Lợi ích của proxy pattern:

  • Upgradeable (Khả năng nâng cấp): Khi cần thay đổi logic của hợp đồng, chỉ cần trỏ Proxy contract sang Logic contract mới.
  • Gas saving (tiết kiệm gas)

Proxy contract

Proxy contract không dài, nhưng chứa nhiều lệnh inline assembly khá phức tạp và khó hiểu.

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

contract Proxy {
    // Address of the logic contract
    address public implementation; 

    constructor(address implementation_) {
        implementation = implementation_;
    }

    /**
     * @dev Khi proxy contract được gọi, chuyển hướng gọi đến logic contract bằng delegateCall 
     */
    fallback() external payable {
        address _implementation = implementation;
        assembly {
            // copy msg.data vào memory
            calldatacopy(0, 0, calldatasize())

            // dùng delegatecall để gọi implementation contract (logic contract)
            // các tham số của opcode delegatecall lần lượt là: gas, target contract address, start position of input memory, length of input memory, start position of output memory, length of output memory
            // đặt vị trí bắt đầu của bộ nhớ đầu ra và độ dài của bộ nhớ đầu ra thành 0
            // delegatecall trả về 1 nếu thành công, 0 nếu lỗi
            let result := delegatecall(
                gas(),
                _implementation,
                0,
                calldatasize(),
                0,
                0
            )

            // copy returndata vô memory
            // đối số của opcode returndata: start position of memory, start position of returndata, length of retundata
            returndatacopy(0, 0, returndatasize())

            switch result
            // nếu delegateCall lỗi thì revert
            case 0 {
                revert(0, returndatasize())
            }
			      // Nếu thành công thì trả về kết quả
            default {
                return(0, returndatasize())
            }
        }
    }
}

Logic contract

// Tạo 1 logic cơ 
contract Logic {
    address public implementation;
    uint public x = 99;
    event CallSuccess();

    function increment() external returns(uint) {
        emit CallSuccess();
        return x + 1;
    }
}

Caller contract

contract Caller{
    address public proxy; // proxy contract address

    constructor(address proxy_){
        proxy = proxy_;
    }

    // gọi hàm increment() thông qua proxy contract
    function increment() external returns(uint) {
        ( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()"));
        return abi.decode(data,(uint));
    }
}

Chạy thử

1. Deploy Logic contract

2. Gọi thẳng hàm increment() từ logic contract => trả về 100

3. Deploy proxy contract

4. Gọi increment thông qua proxy

5. Deploy Caller

6. Gọi increment từ Caller, trả về 1

46. Upgrade

Bây giờ chúng ta thử thay đổi logic contract với proxy và xem điều gì sẽ xảy ra.

image

Proxy contract

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

contract SimpleUpgrade {
    // logic contract's address
    address public implementation; 

    // admin address
    address public admin;

    // string variable, could be changed by logic contract's function
    string public words; 
    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }

    // fallback function
    fallback() external payable {
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }

    // upgrade function,thay đổi địa chỉ logic contract
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}

Logic contract cũ

// Logic contract 1
contract Logic1 {
    address public implementation; 
    address public admin;
    string public words; 

    // Change state variables in Proxy contract, selector: 0xc2985578
    function foo() public {
        words = "old";
    }
}

Logic contract mới

contract Logic2 {
    address public implementation; 
    address public admin;
    string public words; 

    // Change state variables in Proxy contract, selector: 0xc2985578
    function foo() public{
        words = "new";
    }
}

Chạy thử

1. Deploy contract Logic1 và Logic2

47-2.png 47-3.png

2. Deploy proxy contract với implementation là địa chỉ logic contract cũ

47-4.png

3. Gọi hàm foo với selector 0xc2985578 => biến words bây giờ có giá trị là "old"

47-5.png

4. Upgrade contract (truyền địa chỉ Logic contract mới vào)

47-6.png

5. Gọi hàm foo, giá trị words được thay đổi thành 'new'

47-7.png

47. Transparent Proxy

Selector Clash (trùng selector)

function signature trong Solidity gồm 4 bytes. Ví dụ như mint(address account) sẽ là 0x6a627842. Xem lại về selector ở đây

Do không gian chỉ có 4 bytes nên việc 2 hàm trùng selector sẽ không khó gặp 😦


// Đều là 0x42966c68
// Việc hai hàm có cùng giá trị selector gọi là Selector Clash
contract Foo {
    function burn(uint256) external {}
    function collate_propagate_storage(bytes16) external {}
}

Xuất hiện vấn đề là Proxy và Logic contract có thể xuất hiện 2 hàm có giá trị selector trùng nhau. Trong trường hợp 1 hàm a nào đó trên Logic contract trùng với hàm upgrade trên proxy contract. Như vậy, do trùng selector nên hàm a sẽ không gọi được, thay vào đó là hàm upgrade, đây là 1 rủi ro bảo mật lớn.

Transparent

Transparent là 1 giải pháp giúp giải quyết vấn đề selector clash nêu trên với ý tưởng rất đơn giản. Chỉ admin mới có thể gọi hàm upgrade, người dùng thông thường sẽ không thể gọi hàm upgrade.

Proxy Contract

// DO NOT USE IN PRODUCTION
contract TransparentProxy {
    // logic contract's address
    address implementation; 
    // admin address
    address admin; 
   
    string public words;

    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }

    fallback() external payable {
        require(msg.sender != admin);
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }

    // chỉ có thể gọi bởi admin
    function upgrade(address newImplementation) external {
        if (msg.sender != admin) revert();
        implementation = newImplementation;
    }
}

Logic contract

// logic contract cũ
contract Logic1 {
    address public implementation; 
    address public admin; 
    string public words; 

    // selector 0xc2985578
    function foo() public{
        words = "old";
    }
}

// logic contract mới
contract Logic2 {
    address public implementation; 
    address public admin; 
    string public words;

    // selector 0xc2985578
    function foo() public{
        words = "new";
    }
}

Chạy thử

1. Deploy Logic1 và Logic2

48-2.png 48-3.png

2. Deploy Proxy

48-4.png

3. Sử dụng selector 0xc2985578 để gọi foo() trong Logic1 bằng tài khoản admin

Giao dịch bị revert do admin không thể gọi hàm của Logic contract.

48-5.png

4. Đổi ví, gọi lại foo()

48-6.png

5. Dùng ví admin để gọi hàm upgrade()

48-7.png

6. Sử dụng ví người dùng, gọi foo để kiểm tra trạng thái mới

48-8.png

48. UUPS

UUPS là một giải pháp khác giúp giải quyết vấn đề selector clash cùng với Transparent Proxy.

Ý tưởng của UUPS là chuyển hàm upgrade sang Logic contract thay vì nằm tại Proxy contract. Do đó, nếu 2 hàm bất kỳ trong Logic contract bị trùng selector, quá trình biên dịch sẽ báo lỗi 😄

UUPS proxy contract

contract UUPSProxy {
    // Address of the logic contract
    address public implementation; 
    // Address of admin
    address public admin;
  
    string public words; 

    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }

    // Fallback function
    fallback() external payable {
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }
}

UUPS Logic Contract

contract UUPS1 {
    address public implementation; 
    address public admin; 
    string public words;

    // selector: 0xc2985578
    function foo() public{
        words = "old";
    }

    // upgrade function, chỉ admin mới thực thi được. selector: 0x0900f010
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}

// UUPS logic contract mới
contract UUPS2{
    address public implementation; 
    address public admin; 
    string public words; 

    // selector: 0xc2985578
    function foo() public{
        words = "new";
    }

    // upgrade function
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}

Chạy thử

1. Deploy UUPS1 và UUPS2

demo

2. Deploy UUPSProxy

demo

3. Gọi hàm foo với selector, biến words được đặt thành old

demo

4. Upgrade sang UUPS2

Tính toán data để gọi hàm upgrade (vẫn phải gọi qua fallback function từ contract proxy)

encoding demo

5.Gọi hàm foo() với logic contract mới

49. Multisig Wallet

Khác với các ví người dùng thông thường khác, ví multisig yêu cầu từ 2 chữ ký trở lên để có thể thực hiện giao dịch. Ví Multisig có thể ngăn chặn rủi ro như private key hay ý đồ xấu từ số ít cá nhân và được sử dụng phổ biến trong các DAO.

Vitalik từng đang 1 tweet đại ý rằng ví Multisig an toàn hơn ví cứng

Multisig wallet contract

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

contract MultisigWallet {
    event ExecutionSuccess(bytes32 txHash); 
    event ExecutionFailure(bytes32 txHash);
		
		// danh sách các chủ sở hữu ví multisig 
    address[] public owners;

		// mapping kiểm tra xem địa chỉ truyền vào có phải là 1 trong các chủ sở hữu không ?
    mapping(address => bool) public isOwner;
	  // số lượng chủ của ví
    uint256 public ownerCount;
    // số lượng chữ ký cần tối thiểu để thực thi giao dịch (threshold <= ownerCount)
    uint256 public threshold;
    uint256 public nonce; // nonce,prevent signature replay attack

    receive() external payable {}

    constructor(address[] memory _owners, uint256 _threshold) {
        _setupOwners(_owners, _threshold);
    }

	  // Khởi tạo các giá trị của biến trạng thái
    function _setupOwners(
        address[] memory _owners,
        uint256 _threshold
    ) internal {
        require(threshold == 0, "WTF5000");
        require(_threshold <= _owners.length, "WTF5001");
        // số lượng chữ ký tối thiểu phải lớn hơn 1
        require(_threshold >= 1, "WTF5002");

        for (uint256 i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            require(
                owner != address(0) &&
                    owner != address(this) &&
                    !isOwner[owner],
                "WTF5003"
            );
            owners.push(owner);
            isOwner[owner] = true;
        }
        ownerCount = _owners.length;
        threshold = _threshold;
    }


   // dựa trên dataHash và chữ ký, xác thực các chữ ký
    function checkSignatures(
        bytes32 dataHash,
        bytes memory signatures
    ) public view {
        uint256 _threshold = threshold;
        require(_threshold > 0, "WTF5005");

        // kiểm tra xem có đủ số chữ ký tối thiểu không (mỗi chữ ký dài 65 bytes)
        require(signatures.length >= _threshold * 65, "WTF5006");

        address lastOwner = address(0);
        address currentOwner;
        uint8 v;
        bytes32 r;
        bytes32 s;
        uint256 i;
        for (i = 0; i < _threshold; i++) {
            (v, r, s) = signatureSplit(signatures, i);
            // sử dụng ECDSA để xác thực chữ ký
            // địa chỉ ký phải có trong danh sách owners
            currentOwner = ecrecover(
                keccak256(
                    abi.encodePacked(
                        "\x19Ethereum Signed Message:\n32",
                        dataHash
                    )
                ),
                v,
                r,
                s
            );
            require(
                currentOwner > lastOwner && isOwner[currentOwner],
                "WTF5007"
            );
            lastOwner = currentOwner;
        }
    }

    // tách chữ ký dạng bytes thành dạng (v, r, s)
    function signatureSplit(
        bytes memory signatures,
        uint256 pos
    ) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
        // signature format: {bytes32 r}{bytes32 s}{uint8 v}
        assembly {
            let signaturePos := mul(0x41, pos)
            r := mload(add(signatures, add(signaturePos, 0x20)))
            s := mload(add(signatures, add(signaturePos, 0x40)))
            v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)
        }
    }

    function encodeTransactionData(
        address to,
        uint256 value,
        bytes memory data,
        uint256 _nonce,
        uint256 chainid
    ) public pure returns (bytes32) {
        bytes32 safeTxHash = keccak256(
            abi.encode(to, value, keccak256(data), _nonce, chainid)
        );
        return safeTxHash;
    }
	
		/* thực thi giao dịch khi số lượng chữ ký đã đủ
		- signatures: biểu diễn bằng bytes tất cả các chữ ký của người sở hữu
		Transaction hash: 0xc1b055cf8e78338db21407b425114a2e258b0318879327945b661bfdea570e66

		Multisig person A signature: 		0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c

Multisig person B signature: 0x2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b

Packaged signatures:
0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b
		*/
		
		function execTransaction(
        address to,
        uint256 value,
        bytes memory data,
        bytes memory signatures
    ) public payable virtual returns (bool success) {
        bytes32 txHash = encodeTransactionData(
            to,
            value,
            data,
            nonce,
            block.chainid
        );
        nonce++;
        
        // Check signatures
        checkSignatures(txHash, signatures);
        
        // Thực thi giao dịch và kiểm tra kết quả
        (success, ) = to.call{value: value}(data);
        require(success, "WTF5004");
        if (success) emit ExecutionSuccess(txHash);
        else emit ExecutionFailure(txHash);
    }
}

Chạy thử trên Remix

1. Deploy contract multisig

Có 2 người tham gia và cần cả hai chữ ký để thực hiện giao dịch.

Người dùng 1: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
Người dùng 2: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2

demo

2. Chuyển 1 ETH vào multisig contract

50-3.png

3. Gọi hàm encodeTransaction với tham số

Tham số:
to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
value: 1000000000000000000
data: 0x
_nonce: 0
chainid: 1

Kết quả:
Transaction hash: 0xb43ad6901230f2c59c3f7ef027c9a372f199661c61beeec49ef5a774231fc39b

=> Tạo giao dịch gửi 1 ETH từ multisig tới ví của người dùng 1.

50-4.png

4. Ký

Chữ ký của người dùng 1: 0x014db45aa753fefeca3f99c2cb38435977ebb954f779c2b6af6f6365ba4188df542031ace9bdc53c655ad2d4794667ec2495196da94204c56b1293d0fbfacbb11c

Chữ ký của người dùng 2: 0xbe2e0e6de5574b7f65cad1b7062be95e7d73fe37dd8e888cef5eb12e964ddc597395fa48df1219e7f74f48d86957f545d0fbce4eee1adfbaff6c267046ade0d81c

Ghép 2 chữ ký lại:  0x014db45aa753fefeca3f99c2cb38435977ebb954f779c2b6af6f6365ba4188df542031ace9bdc53c655ad2d4794667ec2495196da94204c56b1293d0fbfacbb11cbe2e0e6de5574b7f65cad1b7062be95e7d73fe37dd8e888cef5eb12e964ddc597395fa48df1219e7f74f48d86957f545d0fbce4eee1adfbaff6c267046ade0d81c

50-5-1.png

50-5-2.png

50-5-3.png

5. Gọi hàm execTransaction()

Do 2 chữ ký đều hợp lệ nên giao dịch sẽ được thực thi.

50-6.png

Nguồn, tài liệu tham khảo

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

https://www.wtf.academy/en/solidity-start/


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí