以太坊構建DApps系列教程(五):智慧合約通訊和代幣銷售
在本系列關於使用以太坊構建DApps教程的第4部分中,我們開始構建和測試我們的DAO智慧合約。 現在讓我們更進一步,根據我們的介紹,處理向故事Story新增內容和代幣。
新增代幣
對於能夠與另一個合約進行互動的合約,它需要知道其他合約的介面——可用的函式。由於我們的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購買代幣並在故事Story中新增提交的能力。DAO合約的另一部分功能仍然是:投票和民主化。這就是我們將在下一篇文章中處理的內容。
======================================================================
分享一些以太坊、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系列教程(五):智慧合約通訊和代幣銷售