iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0
自我挑戰組

Solidity 初學之路系列 第 28

DAY 28 - WETH、分帳

  • 分享至 

  • xImage
  •  

WETH

WETH(Wrapped ETH)是 ETH 的包裝版本。我們常見的 WETH、WBTC、WBNB,都是帶包裝的原生代幣。那我們為什麼要包裝它們?
2015 年,ERC20 標準出現,此代幣標準旨在為以太坊上的代幣制定一套標準化的規則,從而簡化了新代幣的發布,並使區塊鏈上的所有代幣相互可比。不幸的是,以太幣本身並不符合 ERC20 標準。 WETH 的開發是為了提高區塊鏈之間的互通性,並使 ETH 可用於去中心化應用程式(dApps)。它就像是給原生代幣穿了一件智能合約做的衣服:穿上衣服的時候,就變成了 WETH,符合 ERC20 同質化代幣標準,可以跨鏈,可以用於 dApp;脫下衣服,它可 1:1 兌換ETH。

WETH 合約

目前在用的主網 WETH 合約寫於 2015 年,非常老,那時 solidity 是 0.4 版本。我們用 0.8 版本重寫一個 WETH。
WETH 符合 ERC20 標準,它比普通的 ERC20 多了兩個功能:

  1. 存款:包裝,使用者將 ETH 存入 WETH 合約,並獲得等量的 WETH。
  2. 提款:拆包裝,使用者銷毀 WETH,並獲得等量的 ETH。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract WETH is ERC20{
    // 事件:存款和提款
    event  Deposit(address indexed dst, uint wad);
    event  Withdrawal(address indexed src, uint wad);

    // 建構子,初始化ERC20的名字和代號
    constructor() ERC20("WETH", "WETH"){
    }

    // 回傳函數,當使用者往WETH合約轉ETH時,會觸發deposit()函數
    fallback() external payable {
        deposit();
    }
    // 回傳函數,當使用者往WETH合約轉ETH時,會觸發deposit()函數
    receive() external payable {
        deposit();
    }

    // 存款函數,當使用者存入ETH時,給他鑄造等量的WETH
    function deposit() public payable {
        _mint(msg.sender, msg.value);
        emit Deposit(msg.sender, msg.value);
    }

    // 提款函數,用戶銷毀WETH,取回等量的ETH
    function withdraw(uint amount) public {
        require(balanceOf(msg.sender) >= amount);
        _burn(msg.sender, amount);
        payable(msg.sender).transfer(amount);
        emit Withdrawal(msg.sender, amount);
    }
}

繼承

WETH 符合 ERC20 代幣標準,因此 WETH 合約繼承了 ERC20 合約。

事件

WETH 合約共有 2 個事件:

  • Deposit:存款事件,在存款的時候釋放。
  • Withdraw:取款事件,在取款的時候釋放。

函數

除了 ERC20 標準的函數外,WETH 合約有 5 個函數:

  • 建構子:初始化 WETH 的名字和代號。
  • 回傳函數:fallback() 和 receive(),當使用者往 WETH 合約轉 ETH 的時候,會自動觸發 deposit() 存款函數,獲得等量的 WETH。
  • deposit():存款函數,當使用者存入 ETH 時,給他鑄造等量的 WETH。
  • withdraw():提款函數,讓使用者銷毀 WETH,並歸還等量的 ETH。

分帳

分帳合約允許將 ETH 按權重轉給一組帳戶後,進行分帳。程式碼部分由 OpenZeppelin 函式庫的 PaymentSplitter 合約簡化而來。
分帳就是依照一定比例分錢財。在現實中,經常會有「分贓不均」的事情發生;而在區塊鏈的世界裡 Code is Law,我們可以事先把每個人應分的比例寫在智能合約中,獲得收入後,再由智能合約來進行分帳。

分帳合約

分帳合約(PaymentSplit)具有以下幾個特點:

  1. 在創建合約時定好分帳受益人 payees 和每人的份額 shares。
  2. 份額可以是相等,也可以是其他任意比例。
  3. 在該合約收到的所有 ETH 中,每個受益人將能夠提取與其分配的份額成比例的金額。
  4. 分帳合約遵循 Pull Payment 模式,付款不會自動轉入帳戶,而是保存在此合約中。受益人透過呼叫 release() 函數觸發實際轉帳。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

/**
 * 分帳合約
 * @dev 這個合約會把收到的ETH按事先定好的份額分給幾個帳戶。收到ETH會存在分帳合約中,需要每個受益人呼叫release()函數來領取。
 */
contract PaymentSplit{
    // 事件
    event PayeeAdded(address account, uint256 shares); // 增加受益人事件
    event PaymentReleased(address to, uint256 amount); // 受益人提款事件
    event PaymentReceived(address from, uint256 amount); // 合約收款事件
    
    // 狀態變數
    uint256 public totalShares; // 總份額,為shares的和
    uint256 public totalReleased; // 總支付,從分帳合約向受益人支付出去的ETH,為 released 的和

    mapping(address => uint256) public shares; // address到uint256的映射,記錄每個受益人的份額
    mapping(address => uint256) public released; // address到uint256的映射,記錄分帳合約支付給每個受益人的金額
    address[] public payees; // address陣列,記錄受益人地址
    
    // 函數
    /**
     * @dev 建構子:始化受益人陣列_payees和分帳份額陣列_shares
     * 陣列長度不能為0,兩個陣列長度要相等。 _shares中元素要大於0,_payees中位址不能為0位址且不能有重複位址。
     */
    constructor(address[] memory _payees, uint256[] memory _shares) payable {
        // 檢查_payees和_shares儲存容量相同,且不為0
        require(_payees.length == _shares.length, "PaymentSplitter: payees and shares length mismatch");
        require(_payees.length > 0, "PaymentSplitter: no payees");
        // 呼叫_addPayee,更新受益人地址payees、受益人份額shares和總份額totalShares
        for (uint256 i = 0; i < _payees.length; i++) {
            _addPayee(_payees[i], _shares[i]);
        }
    }

    /**
     * @dev 回傳函數,在分帳合約收到ETH時釋放PaymentReceived事件
     */
    receive() external payable virtual {
        emit PaymentReceived(msg.sender, msg.value);
    }

    /**
     * @dev 分帳函數,為有效受益人地址_account分配對應的ETH。任何人都可以觸發這個函數,但ETH會轉給受益人地址account。
     * 呼叫了releasable()函數。
     */
    function release(address payable _account) public virtual {
        // account必須是有效受益人
        require(shares[_account] > 0, "PaymentSplitter: account has no shares");
        // 計算account應得的eth
        uint256 payment = releasable(_account);
        // 應得的eth不能為0
        require(payment != 0, "PaymentSplitter: account is not due payment");
        // 更新總支付totalReleased和支付給每個受益人的金額released
        totalReleased += payment;
        released[_account] += payment;
        // 轉帳
        _account.transfer(payment);
        emit PaymentReleased(_account, payment);
    }

    /**
     * @dev 計算一個受益人地址應領取的ETH。
     * 呼叫了pendingPayment()函數。
     */
    function releasable(address _account) public view returns (uint256) {
        // 計算分帳合約總收入totalReceived
        uint256 totalReceived = address(this).balance + totalReleased;
        // 呼叫_pendingPayment計算帳戶應得的ETH
        return pendingPayment(_account, totalReceived, released[_account]);
    }

    /**
     * @dev 根據受益人地址_account, 分帳合約總收入_totalReceived和該地址已領取的錢_alreadyReleased,計算該受益人現在應分的ETH。
     */
    function pendingPayment(
        address _account,
        uint256 _totalReceived,
        uint256 _alreadyReleased
    ) public view returns (uint256) {
        // account應得的ETH = 總應得ETH - 已領到的ETH
        return (_totalReceived * shares[_account]) / totalShares - _alreadyReleased;
    }

    /**
     * @dev 新增受益人函數及其份額函數。在合約初始化的時候被呼叫,之後不能修改。
     */
    function _addPayee(address _account, uint256 _accountShares) private {
        // 檢查_account不為0地址
        require(_account != address(0), "PaymentSplitter: account is the zero address");
        // 檢查_accountShares不為0
        require(_accountShares > 0, "PaymentSplitter: shares are 0");
        // 檢查帳戶不重複
        require(shares[_account] == 0, "PaymentSplitter: account already has shares");
        // 更新payees,shares和totalShares
        payees.push(_account);
        shares[_account] = _accountShares;
        totalShares += _accountShares;
        // 釋放增加受益人事件
        emit PayeeAdded(_account, _accountShares);
    }

上一篇
DAY 27 - 鏈上隨機數、EIP1155
下一篇
DAY 29 - 線性釋放、代幣鎖
系列文
Solidity 初學之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言