您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何構建和測試DAO智能合約”,在日常操作中,相信很多人在如何構建和測試DAO智能合約問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何構建和測試DAO智能合約”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
對于能夠與另一個合約進行交互的合約,它需要知道其他合約的接口——可用的函數。由于我們的TNS代幣具有相當簡單的接口,因此我們可以將其包含在DAO的智能合約中, contract StoryDao
聲明之上以及我們的import
語句中加入:
contract LockableToken is Ownable { function totalSupply() public view returns (uint256); function balanceOf(address who) public view returns (uint256); function transfer(address to, uint256 value) public returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); function allowance(address owner, address spender) public view returns (uint256); function transferFrom(address from, address to, uint256 value) public returns (bool); function approve(address spender, uint256 value) public returns (bool); event Approval(address indexed owner, address indexed spender, uint256 value); function approveAndCall(address _spender, uint256 _value, bytes _data) public payable returns (bool); function transferAndCall(address _to, uint256 _value, bytes _data) public payable returns (bool); function transferFromAndCall(address _from, address _to, uint256 _value, bytes _data) public payable returns (bool); function increaseLockedAmount(address _owner, uint256 _amount) public returns (uint256); function decreaseLockedAmount(address _owner, uint256 _amount) public returns (uint256); function getLockedAmount(address _owner) view public returns (uint256); function getUnlockedAmount(address _owner) view public returns (uint256); }
請注意,我們不需要粘貼函數的“內容”,而只需要粘貼它們的簽名(骨架)。這就是合約之間交互所需的全部內容。
現在我們可以在DAO合約中使用這些函數。計劃如下:
啟動代幣(我們已經這樣做了)。
從同一地址啟動DAO。
將所有代幣從代幣啟動器發送到DAO,然后通過合約將所有權轉移到DAO本身。
此時,DAO擁有所有代幣并可以使用發送功能將其出售給人員,或者可以使用批準功能(在投票期間有用)等將其保留用于支出。
但DAO如何知道部署代幣的地址?我們告訴它。
首先,我們在DAO合約的頂部添加一個新變量:
LockableToken public token;
然后,我們添加一些函數:
constructor(address _token) public { require(_token != address(0), "Token address cannot be null-address"); token = LockableToken(_token); }
構造函數是在部署合約時自動調用的函數。它對于初始化鏈接合約,默認值等值很有用。在我們的例子中,我們將使用它來使用和保存TNS代幣的地址。require
檢查是為了確保代幣的地址有效。
在我們處理它時,讓我們添加一個函數,讓用戶可以檢查DAO中待售的代幣數量,以及更改為另一個代幣的函數,如果出現問題并且需要進行此類更改。這種變化也需要一個事件,所以我們也要添加它。
event TokenAddressChange(address token); function daoTokenBalance() public view returns (uint256) { return token.balanceOf(address(this)); } function changeTokenAddress(address _token) onlyOwner public { require(_token != address(0), "Token address cannot be null-address"); token = LockableToken(_token); emit TokenAddressChange(_token); }
第一個函數設置為view
因為它不會改變區塊鏈的狀態;它不會改變任何值。這意味著它是對區塊鏈的免費,只讀函數調用:它不需要付費交易。它還將標記的余額作為數字返回,因此需要在函數的簽名上使用returns (uint256)
進行聲明。代幣有一個balanceOf
函數(參見我們上面粘貼的接口),它接受一個參數——要檢查其余額的地址。我們正在檢查DAO的余額,我們將“this”變成一個address()
。
代幣地址更改功能允許所有者(admin)更改代幣地址。它與構造函數的邏輯相同。
讓我們看看我們如何讓人們現在購買代幣。
根據該系列的前一部分,用戶可以通過以下方式購買代幣:
如果已經列入白名單,請使用后備功能。換句話說,只需將以太送到DAO合約即可。
使用whitelistAddress
功能發送超過白名單所需的費用。
直接調用buyTokens
函數。
但是,有一個警告。當有人從外部調用buyTokens
函數時,如果DAO中沒有足夠的代幣可供出售,我們希望它失敗提示。但是當有人通過白名單功能通過在第一次白名單嘗試中發送太多以太來購買代幣時,我們不希望它失敗,因為白名單處理過程將被取消。以太坊中的交易要么一切都必須成功,要么就是一無所獲。所以我們將制作兩個buyTokens
函數。
// This goes at the top of the contract with other properties uint256 public tokenToWeiRatio = 10000; function buyTokensThrow(address _buyer, uint256 _wei) external { require(whitelist[_buyer], "Candidate must be whitelisted."); require(!blacklist[_buyer], "Candidate must not be blacklisted."); uint256 tokens = _wei * tokenToWeiRatio; require(daoTokenBalance() >= tokens, "DAO must have enough tokens for sale"); token.transfer(_buyer, tokens); } function buyTokensInternal(address _buyer, uint256 _wei) internal { require(!blacklist[_buyer], "Candidate must not be blacklisted."); uint256 tokens = _wei * tokenToWeiRatio; if (daoTokenBalance() < tokens) { msg.sender.transfer(_wei); } else { token.transfer(_buyer, tokens); } }
因此,存在1億個TNS代幣。如果我們為每個以太設置10000個代幣的價格,則每個代幣的價格降至4-5美分,這是可以接受的。
這些函數在對違禁用戶和其他因素進行完整性檢查后進行一些計算,并立即將代幣發送給買方,買方可以按照自己的意愿開始使用它們——無論是投票還是在交易所銷售。如果DAO中的代幣數量少于買方試圖購買的代幣,則退還買方。
部分token.transfer(_buyer, tokens)
是我們使用TNS代幣合約來啟動從當前位置(DAO)到目標_buyer
的tokens
金額。
現在我們知道人們可以獲得代幣,讓我們看看我們是否可以實施提交。
根據我們的介紹帖子,提交一個條目將花費0.0001 eth倍于故事中的條目數量。我們只需要計算未刪除的提交(因為提交可以刪除),所以讓我們添加這個所需的屬性和一個方法來幫助我們。
uint256 public submissionZeroFee = 0.0001 ether; uint256 public nonDeletedSubmissions = 0; function calculateSubmissionFee() view internal returns (uint256) { return submissionZeroFee * nonDeletedSubmissions; }
注意:Solidity具有內置時間和以太單位。在這里閱讀更多相關信息。
此費用只能由業主更改,但只能降低。為了增加,需要投票。讓我們寫下減函數:
function lowerSubmissionFee(uint256 _fee) onlyOwner external { require(_fee < submissionZeroFee, "New fee must be lower than old fee."); submissionZeroFee = _fee; emit SubmissionFeeChanged(_fee); }
我們發出一個事件來通知所有觀察客戶費用已經改變,所以讓我們聲明這個事件:
event SubmissionFeeChanged(uint256 newFee);
提交可以是最多256個字符的文本,并且相同的限制適用于圖像。只有他們的類型改變。這是自定義結構的一個很好的用例。讓我們定義一個新的數據類型。
struct Submission { bytes content; bool image; uint256 index; address submitter; bool exists; }
這就像我們智能合約中的“對象類型”。該對象具有不同類型的屬性。content
是bytes
類型值。image
屬性是一個布爾值,表示它是否是圖像(true/false)。index
是一個數字等于提交時的順序數字; 它在所有提交列表中的索引(0,1,2,3 ......)。submitter
是提交條目的帳戶的地址,并且exists
標志,因為在映射中,即使密鑰尚不存在,所有密鑰的所有值都被初始化為默認值(false)。
換句話說,當你有一個address => bool
映射時,該映射已經將世界上的所有地址都設置為“false”。這就是以太坊的運作方式。因此,通過檢查提交是否存在于某個哈希,我們會得到“是”,而提交可能根本就不存在。存在標志有助于此。它讓我們檢查提交是否存在且存在——即提交,而不是僅由EVM隱式添加。此外,它使以后更容易“刪除”條目。
注意:從技術上講,我們還可以檢查以確保提交者的地址不是零地址。
當我們在這里時,讓我們定義兩個事件:一個用于刪除條目,一個用于創建條目。
event SubmissionCreated(uint256 index, bytes content, bool image, address submitter); event SubmissionDeleted(uint256 index, bytes content, bool image, address submitter);
但是有一個問題。以太坊中的映射是不可迭代的:我們無法在沒有嚴重黑客攻擊的情況下遍歷它們。
為了遍歷它們,我們將為這些提交創建一個標識符數組,其中數組的鍵將是提交的索引,而值將是我們將為每個提交生成的唯一哈希值。keccak256
為我們提供了keccak256
哈希算法,用于從任意值生成哈希值,我們可以將其與當前塊號一起使用,以確保條目不會在同一塊中重復,并為每個條目獲得一定程度的唯一性。我們這樣使用它: keccak256(abi.encodePacked(_content, block.number));
。我們需要encodePacked
傳遞給算法的變量,因為它需要我們的一個參數。這就是這個函數的作用。
我們還需要在某處存儲提交內容,所以讓我們再定義兩個合約變量。
mapping (bytes32 => Submission) public submissions; bytes32[] public submissionIndex;
好的,我們現在嘗試構建createSubmission
函數。
function createSubmission(bytes _content, bool _image) external payable { uint256 fee = calculateSubmissionFee(); require(msg.value >= fee, "Fee for submitting an entry must be sufficient."); bytes32 hash = keccak256(abi.encodePacked(_content, block.number)); require(!submissions[hash].exists, "Submission must not already exist in same block!"); submissions[hash] = Submission( _content, _image, submissionIndex.push(hash), msg.sender, true ); emit SubmissionCreated( submissions[hash].index, submissions[hash].content, submissions[hash].image, submissions[hash].submitter ); nonDeletedSubmissions += 1; }
讓我們逐行說明:
function createSubmission(bytes _content, bool _image) external payable {
該函數接受字節內容(字節是一個動態大小的字節數組,對存儲任意數量的數據很有用)和一個布爾標志,表示該輸入是否是圖像。該函數只能從外部世界調用,并且應支付,這意味著它在交易調用時接受以太。
uint256 fee = calculateSubmissionFee(); require(msg.value >= fee, "Fee for submitting an entry must be sufficient.");
接下來,我們計算提交新條目的成本,然后檢查與交易一起發送的價值是否等于或大于費用。
bytes32 hash = keccak256(abi.encodePacked(_content, block.number)); require(!submissions[hash].exists, "Submission must not already exist in same block!");
然后我們計算這個條目的哈希值(bytes32
是一個32字節的固定大小數組,所以32個字符也是keccak256
輸出)。我們使用此哈希來查明是否已存在具有該哈希的提交,如果確實存在,則取消所有內容。
submissions[hash] = Submission( _content, _image, submissionIndex.push(hash), msg.sender, true );
此部分在submissions
映射中的哈希位置創建新提交。它只是通過合約中上面定義的新結構傳遞值。請注意,雖然你可能習慣使用其他語言的new
關鍵字,但這里沒有必要(或允許)。然后我們發出事件(不言自明),最后,還有nonDeletedSubmissions += 1;
:這是增加下次提交費用的原因(參見calculateSubmissionFee
)。
但是這里缺少很多邏輯。我們仍然需要:
圖像的帳戶
檢查提交帳戶的白名單/黑名單存在和1個TNS代幣所有權。
我們先做圖像吧。我們的原始計劃表示,每50個文本只能提交一張圖像。我們還需要兩個合約屬性:
uint256 public imageGapMin = 50; uint256 public imageGap = 0;
當然你已經可以假設我們將如何處理這個問題?讓我們在創建新submissions[hash] = ...
的之前立即將以下內容添加到我們的createSubmission
方法中。
if (_image) { require(imageGap >= imageGapMin, "Image can only be submitted if more than {imageGapMin} texts precede it."); imageGap = 0; } else { imageGap += 1; }
非常簡單:如果條目應該是圖像,那么首先檢查圖像之間的間隙是否超過49,如果是,則將其重置為0。否則,將間隙增加一。就像那樣,每50次(或更多次)提交現有內容可以成為一個圖像。
最后,讓我們進行訪問檢查。我們可以在費用計算之前和緊接在函數入口點之后放置此代碼,因為訪問檢查應該首先發生。
require(token.balanceOf(msg.sender) >= 10**token.decimals()); require(whitelist[msg.sender], "Must be whitelisted"); require(!blacklist[msg.sender], "Must not be blacklisted");
第一行檢查消息發送者是否具有比代幣合約中小數位數更多的代幣(因為我們可以更改代幣地址,因此可能另一個代幣將在稍后使用我們的代幣,并且可能沒有18位小數。)。換句話說,在我們的例子中,10**token.decimals
是10**18
,即1000 000 000 000 000 000
,1后跟18個零。如果我們的代幣有18位小數,那就是1.000000000000000000
,或者是一(1)個TNS代幣。請注意,在分析此代碼時,你的編譯器或linter
可能會給你一些警告。這是因為代幣的decimals
屬性是公共的,因此它的getter
函數是decimals()
自動生成的,但它沒有明確列在我們在合約頂部列出的代幣的接口中。為了解決這個問題,我們可以通過添加以下行來更改接口:
function decimals() public view returns (uint256);
還有一件事:因為使用目前設定為1%的合約的所有者費用,讓我們放棄所有者可以提取的金額并將其余部分保留在DAO中。最簡單的方法是跟蹤所有者可以提取多少,并在每次提交創建后增加該數量。讓我們在合約中添加一個新屬性:
uint256 public withdrawableByOwner = 0;
然后將其添加到我們的createSubmission
函數的末尾:
withdrawableByOwner += fee.div(daofee);
我們可以通過這樣的功能讓所有者退出:
function withdrawToOwner() public { owner.transfer(withdrawableByOwner); withdrawableByOwner = 0; }
這會將允許的金額發送給所有者,并將計數器重置為0.如果所有者不想取出全部金額,我們可以為該情況添加另一個函數:
function withdrawAmountToOwner(uint256 _amount) public { uint256 withdraw = _amount; if (withdraw > withdrawableByOwner) { withdraw = withdrawableByOwner; } owner.transfer(withdraw); withdrawableByOwner = withdrawableByOwner.sub(withdraw); }
由于我們經常會通過哈希引用提交,讓我們編寫一個函數來檢查提交是否存在,以便我們可以替換我們的submissions[hash].exists
檢查:
function submissionExists(bytes32 hash) public view returns (bool) { return submissions[hash].exists; }
還需要一些其他幫助函數來讀取提交內容:
function getSubmission(bytes32 hash) public view returns (bytes content, bool image, address submitter) { return (submissions[hash].content, submissions[hash].image, submissions[hash].submitter); } function getAllSubmissionHashes() public view returns (bytes32[]) { return submissionIndex; } function getSubmissionCount() public view returns (uint256) { return submissionIndex.length; }
getSubmission
獲取提交數據,getAllSubmissionHashes
獲取系統中的所有唯一哈希,getSubmissionCount
列出總共提交的數量(包括已刪除的提交)。我們在客戶端(在UI中)使用這些功能的組合來獲取內容。
完整的createSubmission
函數現在看起來像這樣:
function createSubmission(bytes _content, bool _image) storyActive external payable { require(token.balanceOf(msg.sender) >= 10**token.decimals()); require(whitelist[msg.sender], "Must be whitelisted"); require(!blacklist[msg.sender], "Must not be blacklisted"); uint256 fee = calculateSubmissionFee(); require(msg.value >= fee, "Fee for submitting an entry must be sufficient."); bytes32 hash = keccak256(abi.encodePacked(_content, block.number)); require(!submissionExists(hash), "Submission must not already exist in same block!"); if (_image) { require(imageGap >= imageGapMin, "Image can only be submitted if more than {imageGapMin} texts precede it."); imageGap = 0; } else { imageGap += 1; } submissions[hash] = Submission( _content, _image, submissionIndex.push(hash), msg.sender, true ); emit SubmissionCreated( submissions[hash].index, submissions[hash].content, submissions[hash].image, submissions[hash].submitter ); nonDeletedSubmissions += 1; withdrawableByOwner += fee.div(daofee); }
那么刪除提交呢?這很容易:我們只是將exists
標志切換為false
!
function deleteSubmission(bytes32 hash) internal { require(submissionExists(hash), "Submission must exist to be deletable."); Submission storage sub = submissions[hash]; sub.exists = false; deletions[submissions[hash].submitter] += 1; emit SubmissionDeleted( sub.index, sub.content, sub.image, sub.submitter ); nonDeletedSubmissions -= 1; }
首先,我們確保提交存在且尚未刪除;然后我們從存儲中檢索它。接下來,我們將其exists
標志設置為false
,將該地址的DAO中的刪除次數增加1(在跟蹤用戶以后刪除的條目數時非常有用;這可能導致黑名單!),我們發出刪除事件。
最后,我們通過減少系統中未刪除的提交數量來減少新的提交創建費用。我們不要忘記在我們的合約中添加一個新屬性:一個用于跟蹤這些刪除。
mapping (address => uint256) public deletions;
現在我們在另一個合約中使用代幣,我們需要更新部署腳本(3_deploy_storydao
)以將代幣的地址傳遞給StoryDao的構造函數,如下所示:
var Migrations = artifacts.require("./Migrations.sol"); var StoryDao = artifacts.require("./StoryDao.sol"); var TNSToken = artifacts.require("./TNSToken.sol"); module.exports = function(deployer, network, accounts) { if (network == "development") { deployer.deploy(StoryDao, TNSToken.address, {from: accounts[0]}); } else { deployer.deploy(StoryDao, TNSToken.address); } };
到此,關于“如何構建和測試DAO智能合約”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。