CryptoZombies是個(gè)在編游戲的過(guò)程中學(xué)習(xí)Solidity智能合約語(yǔ)言的互動(dòng)教程诵次。本教程是為了Solidity初學(xué)者而設(shè)計(jì)的察藐,會(huì)從最基礎(chǔ)開(kāi)始教起歪脏,即便你從來(lái)沒(méi)有接觸過(guò)Solidity也可以學(xué),CryptoZombies會(huì)手把手地教你技扼。
在這一課伍玖,將綜合利用在前面課程中學(xué)到的許多知識(shí),創(chuàng)建一個(gè)僵尸作戰(zhàn)系統(tǒng)剿吻。 我們也將學(xué)習(xí)payable函數(shù)窍箍,學(xué)習(xí)如何開(kāi)發(fā)可以接收其他玩家付款的DApp。
1.可支付
我們先復(fù)習(xí)一下函數(shù)修飾符丽旅,我們有如下決定函數(shù)何時(shí)和被誰(shuí)調(diào)用的可見(jiàn)性修飾符:
-
private
意味著它只能被合約內(nèi)部調(diào)用椰棘; -
internal
就像private但是也能被繼承的合約調(diào)用券膀; -
external
只能從合約外部調(diào)用最易; -
public
可以在任何地方調(diào)用守伸,不管是內(nèi)部還是外部漓摩。
我們也有狀態(tài)修飾符,告訴我們函數(shù)如何和區(qū)塊鏈交互:
-
view
告訴我們運(yùn)行這個(gè)函數(shù)不會(huì)更改和保存任何數(shù)據(jù)沐批; -
pure
告訴我們這個(gè)函數(shù)不但不會(huì)往區(qū)塊鏈寫(xiě)數(shù)據(jù)痢毒,它甚至不從區(qū)塊鏈讀取數(shù)據(jù)冠王。
這兩種在被從合約外部調(diào)用的時(shí)候都不花費(fèi)任何gas(但是它們?cè)诒粌?nèi)部其他函數(shù)調(diào)用的時(shí)候?qū)?huì)耗費(fèi)gas)米丘。
然后我們有了自定義的modifiers剑令,例如在第三課學(xué)習(xí)的:onlyOwner和aboveLevel。 對(duì)于這些修飾符我們可以自定義其對(duì)函數(shù)的約束邏輯拄查。這些修飾符可以同時(shí)作用于一個(gè)函數(shù)定義上:
function test() external view onlyOwner anotherModifier { /* ... */ }
在這一章吁津,我們來(lái)學(xué)習(xí)一個(gè)新的修飾符payable。
payable修飾符
payable方法是讓Solidity和以太坊變得如此酷的一部分——它們是一種可以接收以太的特殊函數(shù)堕扶。當(dāng)你在調(diào)用一個(gè)普通網(wǎng)站服務(wù)器上的API函數(shù)的時(shí)候碍脏,你無(wú)法用你的函數(shù)傳送美元——你也不能傳送比特幣。但是在以太坊中稍算, 因?yàn)殄X(qián) (以太)潮酒,數(shù)據(jù) (事務(wù)負(fù)載),以及合約代碼本身都存在于以太坊邪蛔。你可以在同時(shí)調(diào)用函數(shù)并付錢(qián)給另外一個(gè)合約。
這就允許出現(xiàn)很多有趣的邏輯扎狱, 比如向一個(gè)合約要求支付一定的錢(qián)來(lái)運(yùn)行一個(gè)函數(shù)侧到。來(lái)看個(gè)例子:
contract OnlineStore {
function buySomething() external payable {
// 檢查以確定0.001以太發(fā)送出去來(lái)運(yùn)行函數(shù):
require(msg.value == 0.001 ether);
// 如果為真,一些用來(lái)向函數(shù)調(diào)用者發(fā)送數(shù)字內(nèi)容的邏輯
transferThing(msg.sender);
}
}
在這里淤击,msg.value
是一種可以查看向合約發(fā)送了多少以太的方法匠抗,另外 ether
是一個(gè)內(nèi)建單元。這里發(fā)生的事是污抬,一些人會(huì)從web3.js調(diào)用這個(gè)函數(shù) (從DApp的前端)汞贸, 像這樣 :
// 假設(shè) `OnlineStore` 在以太坊上指向你的合約:
OnlineStore.buySomething().send(from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001))
注意這個(gè)value字段绳军, JavaScript 調(diào)用來(lái)指定發(fā)送多少(0.001)以太。如果把事務(wù)想象成一個(gè)信封矢腻,你發(fā)送到函數(shù)的參數(shù)就是信的內(nèi)容门驾。 添加一個(gè) value很像在信封里面放錢(qián) —— 信件內(nèi)容和錢(qián)同時(shí)發(fā)送給了接收者。
注意: 如果一個(gè)函數(shù)沒(méi)標(biāo)記為payable多柑, 而你嘗試?yán)蒙厦娴姆椒òl(fā)送以太奶是,函數(shù)將拒絕你的事務(wù)。
2.提現(xiàn)
在你發(fā)送以太之后竣灌,它將被存儲(chǔ)進(jìn)以合約的以太坊賬戶(hù)中聂沙,并凍結(jié)在那里—— 除非你添加一個(gè)函數(shù)來(lái)從合約中把以太提現(xiàn)。你可以寫(xiě)一個(gè)函數(shù)來(lái)從合約中提現(xiàn)以太初嘹,類(lèi)似這樣:
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
注意我們使用Ownable合約中的owner和onlyOwner及汉,假定它已經(jīng)被引入了。你可以通過(guò) transfer 函數(shù)向一個(gè)地址發(fā)送以太屯烦, 然后this.balance 將返回當(dāng)前合約存儲(chǔ)了多少以太坷随。 所以如果100個(gè)用戶(hù)每人向我們支付1以太, this.balance將是100以太漫贞。你可以通過(guò) transfer 向任何以太坊地址付錢(qián)甸箱。 比如,你可以有一個(gè)函數(shù)在 msg.sender超額付款的時(shí)候給他們退錢(qián):
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
或者在一個(gè)有買(mǎi)家和賣(mài)家的合約中迅脐, 你可以把賣(mài)家的地址存儲(chǔ)起來(lái)芍殖, 當(dāng)有人買(mǎi)了它的東西的時(shí)候,把買(mǎi)家支付的錢(qián)發(fā)送給它seller.transfer(msg.value)
谴蔑。
3.隨機(jī)數(shù)
用keccak256
來(lái)制造隨機(jī)數(shù)豌骏。
Solidity 中最好的隨機(jī)數(shù)生成器是 keccak256
哈希函數(shù),我們可以這樣來(lái)生成一些隨機(jī)數(shù):
// 生成一個(gè)0到100的隨機(jī)數(shù):
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
這個(gè)方法首先拿到now
的時(shí)間戳隐锭、 msg.sender
窃躲、 以及一個(gè)自增數(shù)nonce
(一個(gè)僅會(huì)被使用一次的數(shù),這樣我們就不會(huì)對(duì)相同的輸入值調(diào)用一次以上哈希函數(shù)了)钦睡。然后利用 keccak
把輸入的值轉(zhuǎn)變?yōu)橐粋€(gè)哈希值, 再將哈希值轉(zhuǎn)換為uint
蒂窒,然后利用% 100
來(lái)取最后兩位, 就生成了一個(gè)0到100之間隨機(jī)數(shù)了。
這個(gè)方法很容易被不誠(chéng)實(shí)的節(jié)點(diǎn)攻擊
在以太坊上, 當(dāng)你在和一個(gè)合約上調(diào)用函數(shù)的時(shí)候, 你會(huì)把它廣播給一個(gè)節(jié)點(diǎn)或者在網(wǎng)絡(luò)上的 transaction 節(jié)點(diǎn)們荞怒。 網(wǎng)絡(luò)上的節(jié)點(diǎn)將收集很多事務(wù)洒琢,試著成為第一個(gè)解決計(jì)算密集型數(shù)學(xué)問(wèn)題的人,作為“工作證明”褐桌,然后將“工作證明”(Proof of Work, PoW)和事務(wù)一起作為一個(gè) block 發(fā)布在網(wǎng)絡(luò)上衰抑。一旦一個(gè)節(jié)點(diǎn)解決了一個(gè)PoW, 其他節(jié)點(diǎn)就會(huì)停止嘗試解決這個(gè) PoW,并驗(yàn)證其他節(jié)點(diǎn)的事務(wù)列表是有效的荧嵌,然后接受這個(gè)節(jié)點(diǎn)轉(zhuǎn)而嘗試解決下一個(gè)節(jié)點(diǎn)呛踊。
這就讓我們的隨機(jī)數(shù)函數(shù)變得可利用了砾淌。
假設(shè)有一個(gè)硬幣翻轉(zhuǎn)合約——正面你贏雙倍錢(qián),反面你輸?shù)羲械腻X(qián)谭网。假如它使用上面的方法來(lái)決定是正面還是反面 (random >= 50
算正面, random < 50
算反面)汪厨。
如果我正運(yùn)行一個(gè)節(jié)點(diǎn),我可以只對(duì)我自己的節(jié)點(diǎn)發(fā)布一個(gè)事務(wù)蜻底,且不分享它骄崩。 我可以運(yùn)行硬幣翻轉(zhuǎn)方法來(lái)偷窺我的輸贏——如果我輸了,我就不把這個(gè)事務(wù)包含進(jìn)我要解決的下一個(gè)區(qū)塊中去薄辅。我可以一直運(yùn)行這個(gè)方法要拂,直到我贏得了硬幣翻轉(zhuǎn)并解決了下一個(gè)區(qū)塊,然后獲利站楚。
所以我們?cè)撊绾卧谝蕴簧习踩厣呻S機(jī)數(shù)呢脱惰?
因?yàn)閰^(qū)塊鏈的全部?jī)?nèi)容對(duì)所有參與者來(lái)說(shuō)是透明的, 這就讓這個(gè)問(wèn)題變得很難窿春,它的解決方法不在本課程討論范圍拉一,你可以閱讀StackOverflow上的討論 來(lái)獲得一些靈感。 一個(gè)方法是利用 oracle 來(lái)訪問(wèn)以太坊區(qū)塊鏈之外的隨機(jī)數(shù)函數(shù)旧乞。
當(dāng)然蔚润, 因?yàn)榫W(wǎng)絡(luò)上成千上萬(wàn)的以太坊節(jié)點(diǎn)都在競(jìng)爭(zhēng)解決下一個(gè)區(qū)塊,我能成功解決下一個(gè)區(qū)塊的幾率非常之低尺栖。 這將花費(fèi)我們巨大的計(jì)算資源來(lái)開(kāi)發(fā)這個(gè)獲利方法——但是如果獎(jiǎng)勵(lì)異常地高(比如我可以在硬幣翻轉(zhuǎn)函數(shù)中贏得 1個(gè)億)嫡纠, 那就很值得去攻擊了。
所以盡管這個(gè)方法在以太坊上不安全延赌,在實(shí)際中除盏,除非我們的隨機(jī)函數(shù)有一大筆錢(qián)在上面,你游戲的用戶(hù)一般是沒(méi)有足夠的資源去攻擊的挫以。
4.總結(jié)
在本課者蠕,我們重新復(fù)習(xí)了可見(jiàn)性修飾符、狀態(tài)修飾符和自定義修飾符掐松,學(xué)習(xí)了payable
修飾符踱侣,它是可以接收以太的特殊函數(shù),從而通過(guò)這個(gè)修飾符使區(qū)塊鏈上付費(fèi)變得簡(jiǎn)單大磺。有付費(fèi)就有提現(xiàn)(可能轉(zhuǎn)移更準(zhǔn)確)泻仙,提現(xiàn)是通過(guò)普通的函數(shù)完成,它除了自身的含義外量没,還包括找零和代付。隨機(jī)數(shù)在智能合約中的使用頻率很高突想,Solidity中最好的隨機(jī)數(shù)生成器是keccak256
哈希函數(shù)殴蹄,雖然這個(gè)函數(shù)很容易被攻擊究抓,但要攻擊的代價(jià)也挺高。
結(jié)合前面幾節(jié)課所學(xué)袭灯,使用本課的學(xué)到的付費(fèi)刺下、體現(xiàn)和隨機(jī)數(shù),一個(gè)僵尸作戰(zhàn)系統(tǒng)的邏輯就出來(lái)了(代碼就不貼了稽荧,有興趣的可以去學(xué)習(xí))橘茉。
【CryptoZombies|編寫(xiě)區(qū)塊鏈游戲?qū)W智能合約】Lesson1: 搭建僵尸工廠
【CryptoZombies|編寫(xiě)區(qū)塊鏈游戲?qū)W智能合約】Lesson2: 僵尸攻擊人類(lèi)
【CryptoZombies|編寫(xiě)區(qū)塊鏈游戲?qū)W智能合約】Lesson3: 搭建僵尸工廠
【CryptoZombies|編寫(xiě)區(qū)塊鏈游戲?qū)W智能合約】Lesson4: 僵尸作戰(zhàn)系統(tǒng)
【CryptoZombies|編寫(xiě)區(qū)塊鏈游戲?qū)W智能合約】Lesson5: ERC721標(biāo)準(zhǔn)和加密收藏品