以太坊構建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系列教程(六):使用定製代幣進行投票

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章