1. 程式人生 > >以太坊構建DApps系列教程(六):使用定製代幣進行投票

以太坊構建DApps系列教程(六):使用定製代幣進行投票

在本系列關於使用以太坊構建DApps教程的第5部分中,我們討論瞭如何為Story新增內容,檢視如何新增參與者從DAO購買代幣的功能以及在Story中新增提交內容。現在是編寫DAO最終形式的時候了:投票,黑名單,股息分配和退出。我們將提供一些額外的輔助函式以便進行監測。

如果你對這一切感覺迷失了,那麼repo中會提供完整的原始碼。

投票和提案

我們將釋出Votes並投票。這需要兩個新的結構:

struct Proposal {
    string description;
    bool executed;
    int256 currentResult;
    uint8 typeFlag; // 1 = delete
    bytes32 target; // ID of the proposal target. I.e. flag 1, target XXXXXX (hash) means proposal to delete submissions[hash]
    uint256 creationDate;
    uint256 deadline;
    mapping (address => bool) voters;
    Vote[] votes;
    address submitter;
}

Proposal[] public proposals;
uint256 proposalCount = 0;
event ProposalAdded(uint256 id, uint8 typeFlag, bytes32 hash, string description, address submitter);
event ProposalExecuted(uint256 id);
event Voted(address voter, bool vote, uint256 power, string justification);

struct Vote {
    bool inSupport;
    address voter;
    string justification;
    uint256 power;
}

提案將對選民進行對映,以防止人們對提案進行兩次投票,以及其他一些應該不言自明的元資料。投票將是一個是或否投票,並將記住選民以及他們以某種方式投票的理由,以及投票權——他們希望投入該投票的代幣數量。我們還添加了一系列Proposals,以便我們可以將它們儲存在某個地方,並提供一個計數器來計算有多少提案。

讓我們現在構建他們的附屬函式,從投票函式開始:

modifier tokenHoldersOnly() {
    require(token.balanceOf(msg.sender) >= 10**token.decimals());
    _;
}

function vote(uint256 _proposalId, bool _vote, string _description, uint256 _votePower) tokenHoldersOnly public returns (int256) {

    require(_votePower > 0, "At least some power must be given to the vote.");
    require(uint256(_votePower) <= token.balanceOf(msg.sender), "Voter must have enough tokens to cover the power cost.");

    Proposal storage p = proposals[_proposalId];

    require(p.executed == false, "Proposal must not have been executed already.");
    require(p.deadline > now, "Proposal must not have expired.");
    require(p.voters[msg.sender] == false, "User must not have already voted.");

    uint256 voteid = p.votes.length++;
    Vote storage pvote = p.votes[voteid];
    pvote.inSupport = _vote;
    pvote.justification = _description;
    pvote.voter = msg.sender;
    pvote.power = _votePower;

    p.voters[msg.sender] = true;

    p.currentResult = (_vote) ? p.currentResult + int256(_votePower) : p.currentResult - int256(_votePower);
    token.increaseLockedAmount(msg.sender, _votePower);

    emit Voted(msg.sender, _vote, _votePower, _description);
    return p.currentResult;
}

注意函式修飾符:通過將該修飾符新增到我們的合約中,我們可以將它附加到任何將來的函式,並確保只有令牌持有者才能執行該函式。這是一個可重複使用的安全檢查!

投票功能做了一些健壯性檢查,例如投票權是積極的,選民有足夠的代幣實際投票等。然後我們從儲存中獲取提案並確保它既沒有過期也沒有已經執行。對已經完成的提案進行投票是沒有意義的。我們還需要確保這個人還沒有投票。我們可以允許改變投票權,但這會讓DAO面臨一些漏洞,例如人們在最後一刻撤回投票等等。也許是未來版本的候選人?

然後我們在提案中註冊一個新的投票,更改當前結果以便於查詢分數,最後發出Voted事件。但是什麼是token.increaseLockedAmount

這一點邏輯增加了使用者的鎖定代幣數量。該功能只能由代幣合約的所有者執行(此時希望是DAO)並且將阻止使用者傳送超過其帳戶註冊的鎖定金額的令牌數量。提案落實或執行後,此鎖定被解除。

讓我們編寫現在提議刪除條目的函式。

投票刪除和黑名單

如本系列第1部分所述 ,我們計劃了三個條目刪除功能:

  • 1.刪除條目:通過投票確認後,目標條目將被刪除。投票時間:48小時。
  • 2.緊急刪除條目[僅限所有者]:只能由所有者觸發。通過投票確認後,目標條目將被刪除。投票時間:24小時。
  • 3.緊急刪除影象[僅限所有者]:僅適用於影象條目。只能由所有者觸發。通過投票確認後,目標條目將被刪除。投票時間:4小時。

單個地址條目的五個刪除導致黑名單。

讓我們看看我們現在該怎麼做。首先,刪除功能:

modifier memberOnly() {
    require(whitelist[msg.sender]);
    require(!blacklist[msg.sender]);
    _;
}

function proposeDeletion(bytes32 _hash, string _description) memberOnly public {

    require(submissionExists(_hash), "Submission must exist to be deletable");

    uint256 proposalId = proposals.length++;
    Proposal storage p = proposals[proposalId];
    p.description = _description;
    p.executed = false;
    p.creationDate = now;
    p.submitter = msg.sender;
    p.typeFlag = 1;
    p.target = _hash;

    p.deadline = now + 2 days;

    emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender);
    proposalCount = proposalId + 1;
}

function proposeDeletionUrgent(bytes32 _hash, string _description) onlyOwner public {

    require(submissionExists(_hash), "Submission must exist to be deletable");

    uint256 proposalId = proposals.length++;
    Proposal storage p = proposals[proposalId];
    p.description = _description;
    p.executed = false;
    p.creationDate = now;
    p.submitter = msg.sender;
    p.typeFlag = 1;
    p.target = _hash;

    p.deadline = now + 12 hours;

    emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender);
    proposalCount = proposalId + 1;
}    

function proposeDeletionUrgentImage(bytes32 _hash, string _description) onlyOwner public {

    require(submissions[_hash].image == true, "Submission must be existing image");

    uint256 proposalId = proposals.length++;
    Proposal storage p = proposals[proposalId];
    p.description = _description;
    p.executed = false;
    p.creationDate = now;
    p.submitter = msg.sender;
    p.typeFlag = 1;
    p.target = _hash;

    p.deadline = now + 4 hours;

    emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender);
    proposalCount = proposalId + 1;
}

一旦提出,建議書就會被新增到提案列表中,並記錄條目雜湊所針對的條目。儲存說明並新增一些預設值,並根據提案型別計算截止日期。該提案添加了事件,並且提案總數增加了。

接下來讓我們看看如何執行提案。為了可執行,提案必須有足夠的票數,並且必須超過其截止日期。執行功能將接受要執行的提議的ID。沒有簡單的方法可以讓EVM立即執行所有待處理的提案。可能有太多人要等待執行,並且他們會對DAO中的資料進行大的更改,這可能會超過以太坊塊的氣體限制,從而導致交易失敗。構建一個可以由具有明確規則的任何人呼叫的手動執行功能要容易得多,因此社群可以關注需要執行的提議。

function executeProposal(uint256 _id) public {
    Proposal storage p = proposals[_id];
    require(now >= p.deadline && !p.executed);

    if (p.typeFlag == 1 && p.currentResult > 0) {
        assert(deleteSubmission(p.target));
    }

    uint256 len = p.votes.length;
    for (uint i = 0; i < len; i++) {
        token.decreaseLockedAmount(p.votes[i].voter, p.votes[i].power);
    }

    p.executed = true;
    emit ProposalExecuted(_id);
}

我們通過其ID獲取提案,檢查它是否符合未執行的要求和截止日期過期,然後如果提案的型別是刪除提案且投票結果是肯定的,我們使用已經寫入的刪除功能,最後發出了我們新增的新事件(將其新增到合約的頂部)。assert呼叫與require語句具有相同的用途:斷言通常在“斷言”結果為真時使用。要求用於先決條件。在功能上它們是相同的,assert語句的差異在它們失敗時無法接受訊息引數。該功能通過為該一個提案中的所有投票解鎖代幣而結束。

我們可以使用相同的方法新增其他型別的提案,但首先,讓我們更新deleteSubmission函式以禁止在其帳戶上有五個或更多刪除的使用者:這意味著他們一直在提交社群投票反對的內容。讓我們更新deleteSubmission函式:

function deleteSubmission(bytes32 hash) internal returns (bool) {
    require(submissionExists(hash), "Submission must exist to be deletable.");
    Submission storage sub = submissions[hash];

    sub.exists = false;
    deletions[submissions[hash].submitter] += 1;
    if (deletions[submissions[hash].submitter] >= 5) {
        blacklistAddress(submissions[hash].submitter);
    }

    emit SubmissionDeleted(
        sub.index,
        sub.content,
        sub.image,
        sub.submitter
    );

    nonDeletedSubmissions -= 1;
    return true;
}

那更好。自動將五個刪除列入黑名單。但是,如果不給黑名單地址提供贖回的機會,那是不公平的。我們還需要定義黑名單功能本身。讓我們做這兩件事並將不合理的費用設定為例如0.05以太。

function blacklistAddress(address _offender) internal {
    require(blacklist[_offender] == false, "Can't blacklist a blacklisted user :/");
    blacklist[_offender] == true;
    token.increaseLockedAmount(_offender, token.getUnlockedAmount(_offender));
    emit Blacklisted(_offender, true);
}

function unblacklistMe() payable public {
    unblacklistAddress(msg.sender);
}

function unblacklistAddress(address _offender) payable public {
    require(msg.value >= 0.05 ether, "Unblacklisting fee");
    require(blacklist[_offender] == true, "Can't unblacklist a non-blacklisted user :/");
    require(notVoting(_offender), "Offender must not be involved in a vote.");
    withdrawableByOwner = withdrawableByOwner.add(msg.value);
    blacklist[_offender] = false;
    token.decreaseLockedAmount(_offender, token.balanceOf(_offender));
    emit Blacklisted(_offender, false);
}

function notVoting(address _voter) internal view returns (bool) {
    for (uint256 i = 0; i < proposalCount; i++) {
        if (proposals[i].executed == false && proposals[i].voters[_voter] == true) {
            return false;
        }
    }
    return true;
}

請注意,列入黑名單的帳戶的令牌會被鎖定,直到他們傳送不合格的費用為止。

其他型別的投票

使用我們上面寫的函式的靈感,嘗試編寫其他提議。對於劇透,請檢視專案的GitHub倉庫並從那裡複製最終程式碼。為簡潔起見,讓我們繼續討論DAO中剩下的其他功能。

章節的結束

一旦達到故事的時間或章節限制,就應該結束故事了。任何人都可以在允許提取股息的日期之後呼叫結束函式。首先,我們需要一個新的StoryDAO屬性和一個事件:

bool public active = true;
event StoryEnded(); 

然後,讓我們構建函式:

function endStory() storyActive external {
    withdrawToOwner();
    active = false;
    emit StoryEnded();
}

簡單:它將收集的費用傳送給所有者併發出事件後停用故事。但實際上,這並沒有真正改變整個DAO中的任何內容:其他功能對它的結束沒有反應。那麼讓我們構建另一個修飾符:

modifier storyActive() {
    require(active == true);
    _;
}

然後,我們將此修飾符新增到除withdrawToOwner之外的所有函式中,如下所示:

function whitelistAddress(address _add) storyActive public payable { 

如果DAO中遺留了任何代幣,讓我們將它們取回並接管這些代幣的所有權,以便以後能夠在另一個故事中使用它們:

function withdrawLeftoverTokens() external onlyOwner {
    require(active == false);
    token.transfer(msg.sender, token.balanceOf(address(this)));
    token.transferOwnership(msg.sender);
}

function unlockMyTokens() external {
    require(active == false);
    require(token.getLockedAmount(msg.sender) > 0);

    token.decreaseLockedAmount(msg.sender, token.getLockedAmount(msg.sender));
}

unlockMyTokens函式用於解鎖所有鎖定的代幣,以防某些鎖定代幣為特定使用者鎖定。它不應該發生,並且應該通過大量測試來移除此功能。

股息分配和提款

現在故事已經結束,收集的費用需要分配給所有代幣持有者。我們可以重新使用我們的白名單來標記所有取消費用的人:

function withdrawDividend() memberOnly external {
    require(active == false);
    uint256 owed = address(this).balance.div(whitelistedNumber);
    msg.sender.transfer(owed);
    whitelist[msg.sender] = false;
    whitelistedNumber--;
}

如果這些股息未在一定時限內撤回,業主可以抓住其餘股息:

function withdrawEverythingPostDeadline() external onlyOwner {
    require(active == false);
    require(now > deadline + 14 days);
    owner.transfer(address(this).balance);
}

留個家庭作業,考慮重新使用相同部署的智慧合約,清除其資料,並將代幣保留在底池中並重新啟動另一章而無需重新部署是多麼容易或困難。嘗試自己這樣做,並密切關注回購,以便將來更新本系列教程!還要考慮額外的激勵機制:也許賬戶中的代幣數量會影響他們從故事結束中獲得的紅利?你的想象力是極限!

部署問題

鑑於我們的合約現在非常大,部署和/或測試它可能會超過以太坊區塊的gas限制。這是限制大型應用程式部署在以太坊網路上的原因。無論如何要部署它,在編譯期間嘗試使用程式碼優化器,方法是更改truffle.js檔案以包含用於優化的solc設定,如下所示:

// ...

module.exports = {
  solc: {
    optimizer: {
      enabled: true,
      runs: 200
    }
  },
  networks: {
    development: {
// ...

這將在程式碼中執行優化器200次以查詢在部署之前可以縮小,移除或抽象的區域,這將顯著降低部署成本。

結論

這就是我們詳盡的DAO開發——但課程還沒有結束!我們仍然需要為這個故事構建和部署UI。幸運的是,後端完全託管在區塊鏈上,構建前端的複雜程度要低得多。讓我們看看這個系列的倒數第二部分。

======================================================================

分享一些以太坊、EOS、比特幣等區塊鏈相關的互動式線上程式設計實戰教程:

  • java以太坊開發教程,主要是針對java和android程式設計師進行區塊鏈以太坊開發的web3j詳解。
  • python以太坊,主要是針對python工程師使用web3.py進行區塊鏈以太坊開發的詳解。
  • php以太坊,主要是介紹使用php進行智慧合約開發互動,進行賬號建立、交易、轉賬、代幣開發以及過濾器和交易等內容。
  • 以太坊入門教程,主要介紹智慧合約與dapp應用開發,適合入門。
  • 以太坊開發進階教程,主要是介紹使用node.js、mongodb、區塊鏈、ipfs實現去中心化電商DApp實戰,適合進階。
  • C#以太坊,主要講解如何使用C#開發基於.Net的以太坊應用,包括賬戶管理、狀態與交易、智慧合約開發與互動、過濾器和交易等。
  • EOS教程,本課程幫助你快速入門EOS區塊鏈去中心化應用的開發,內容涵蓋EOS工具鏈、賬戶與錢包、發行代幣、智慧合約開發與部署、使用程式碼與智慧合約互動等核心知識點,最後綜合運用各知識點完成一個便籤DApp的開發。
  • java比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈儲存、去中心化共識機制、金鑰與指令碼、交易與UTXO等,同時也詳細講解如何在Java程式碼中整合比特幣支援功能,例如建立地址、管理錢包、構造裸交易等,是Java工程師不可多得的比特幣開發學習課程。
  • php比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈儲存、去中心化共識機制、金鑰與指令碼、交易與UTXO等,同時也詳細講解如何在Php程式碼中整合比特幣支援功能,例如建立地址、管理錢包、構造裸交易等,是Php工程師不可多得的比特幣開發學習課程。
  • tendermint區塊鏈開發詳解,本課程適合希望使用tendermint進行區塊鏈開發的工程師,課程內容即包括tendermint應用開發模型中的核心概念,例如ABCI介面、默克爾樹、多版本狀態庫等,也包括代幣發行等豐富的實操程式碼,是go語言工程師快速入門區塊鏈開發的最佳選擇。

匯智網原創翻譯,轉載請標明出處。這裡是原文以太坊構建DApps系列教程(六):使用定製代幣進行投票