满城尽带比特币:程序员如何发布自己的 ICO?
滿城盡帶比特幣:一夜身價(jià)暴漲千倍,程序員如何發(fā)布自己的 ICO?
沒有任何門檻的ICO為何割韭菜無數(shù)?萬(wàn)字長(zhǎng)文帶你一睹ICO技術(shù)的全貌,讓你也能發(fā)token賣token,然而學(xué)會(huì)以后如何選擇還是要從心吶~
人性本惡,技術(shù)無罪!
你可曾想梭哈全部存款,參與 ICO,一夜身價(jià)暴漲千倍,獲得財(cái)富自由,從此走上人生巔峰?
ICO 是借用 IPO 生造出來的一種概念,同樣具有非常相似的募資機(jī)制,但 IPO 有著嚴(yán)格的上市流程、政策監(jiān)管,如下圖所示。
即便如此,參與 IPO 仍然有著相當(dāng)大的風(fēng)險(xiǎn),且為股市帶來了相當(dāng)大的不穩(wěn)定因素。而與 ICO 比起來簡(jiǎn)直就是小巫見大巫了。
一家公司想要進(jìn)行 IPO 起碼要達(dá)到能夠上市的標(biāo)準(zhǔn),而想發(fā)布 ICO 你只要有一個(gè)好聽的 idea 就足夠了。并且嚴(yán)重缺乏監(jiān)管,雖然各國(guó)政府都在不斷發(fā)出聲明,但截至本分享寫作前,也沒有正式出臺(tái)比較明朗的有關(guān)規(guī)定。
這也導(dǎo)致無數(shù)的空氣項(xiàng)目披著虛擬貨幣和區(qū)塊鏈的高科技?xì)?#xff0c;到處招搖撞騙割韭菜,有過之無不及的還搞什么 AI+ 區(qū)塊鏈,IOT+ 區(qū)塊鏈,技術(shù)名詞堆積越多的項(xiàng)目,死得往往越快。
甚至一些有頭有臉的大公司,也忍不住打打擦邊球,收割一波,炒作炒作,股價(jià)就能翻幾個(gè)漲停。
可就像馬老爺子說的:
如果有 10% 的利潤(rùn),它就保證到處被使用;有 20% 的利潤(rùn),它就活躍起來;有 50% 的利潤(rùn),它就鋌而走險(xiǎn);為了 100% 的利潤(rùn),它就敢踐踏一切人間法律;有 300% 的利潤(rùn),它就敢犯任何罪行,甚至絞首的危險(xiǎn)。
即便如此,仍然有很多人躍躍欲試不信邪。這一場(chǎng) Chat 就手把手教你為 ICO 做好所有技術(shù)面上的準(zhǔn)備。在和大家一起點(diǎn)亮新技能的同時(shí),也揭一揭所謂 ICO 的老底。
內(nèi)容概要
目前市場(chǎng)上 99% 的項(xiàng)目 ICO 都是基于以太坊(Ethereum)智能合約(Smart Contracts)技術(shù)發(fā)布的 token(ERC20 Token)。
本次分享也是基于這一套技術(shù)棧,介紹內(nèi)容包括以下幾個(gè)方面。
本地開發(fā)環(huán)境構(gòu)建
以太坊智能合約開發(fā)
本地開發(fā)環(huán)境發(fā)布
線上測(cè)試網(wǎng)絡(luò)發(fā)布
主網(wǎng)絡(luò)發(fā)布
ERC20 Token 合約開發(fā)
ICO Crowdsale 合約開發(fā)
補(bǔ)充說明與權(quán)限控制
合約的發(fā)布及調(diào)試
Dapp 開發(fā)
web3.js 的使用
Metamask 簡(jiǎn)介
truffle-contract 的使用
ICO 前端應(yīng)用開發(fā)
Dapp 部署
IPNS
Nginx 反向代理
IPFS 簡(jiǎn)介
發(fā)布應(yīng)用
域名解析
使用到的技術(shù)棧包括:
Truffle:http://truffleframework.com
[Ganache:http://truffleframework.com/ganache
Metamask:https://metamask.io
Solidity:http://solidity.readthedocs.io/en/develop
openzeppelin:https://openzeppelin.org
Infura:https://infura.io
web3.js:https://web3js.readthedocs.io/en/1.0/index.html
truffle-contract:https://github.com/trufflesuite/truffle-contract
ipfs:https://ipfs.io
對(duì)讀者的基本要求有:
了解編程
會(huì) JavaScript
本地開發(fā)環(huán)境構(gòu)建
以太坊官方提供的 Mist?(https://github.com/ethereum/mist/releases)?和 Ethereum-Wallet?(https://github.com/ethereum/mist/releases)。
其中 Mist 是一個(gè)可以用來訪問 Dapp 的瀏覽器,Ethereum-Wallet 是 Mist 的一個(gè)獨(dú)立發(fā)布版本,也算是瀏覽器,但只能用來訪問以太坊錢包這個(gè)應(yīng)用。
在網(wǎng)絡(luò)同步過程中或多或少都會(huì)遇到問題,而且目前網(wǎng)絡(luò)擁堵,完整節(jié)點(diǎn)過大,同步完成相當(dāng)困難。但事實(shí)上我們進(jìn)行以太坊開發(fā)時(shí)并不需要同步完整的節(jié)點(diǎn),也可以選擇使用相應(yīng)的模擬開發(fā)環(huán)境。
Truffle?(http://truffleframework.com)?框架為你提供本地進(jìn)行智能合約開發(fā)的所有依賴支持,使你可以在本地進(jìn)行智能合約及 Dapp 的開發(fā)、編譯、發(fā)布。安裝非常簡(jiǎn)單,只需要:
npm install -g truffle
Ganache?(http://truffleframework.com/ganache)?也是 Truffle 框架中提供的一個(gè)應(yīng)用,可以在你的本地開啟模擬一個(gè)以太坊節(jié)點(diǎn),讓你能夠?qū)㈤_發(fā)好的智能合約發(fā)布至本地測(cè)試節(jié)點(diǎn)中運(yùn)行調(diào)試。
安裝也非常簡(jiǎn)單,官網(wǎng)下載即可,雙擊打開運(yùn)行。
不過這里有一個(gè)隱藏的坑,如果你使用的是 Windows 系統(tǒng)的話,Ganache 提供的是后綴名為?.appx?的 Windows 應(yīng)用商店版安裝包。你需要打開 Windows 設(shè)置 -> 系統(tǒng) -> 針對(duì)開發(fā)人員 -> 選擇 “旁加載應(yīng)用” 這個(gè)選項(xiàng)。
確認(rèn)之后就可以雙擊?Ganache.appx?進(jìn)行安裝了,假如系統(tǒng)仍然無法識(shí)別這一后綴名,你可以手動(dòng)打開?powershell?輸入如下命令進(jìn)行安裝。
Add-AppxPackage .\Ganache.appx
至此本地開發(fā)智能合約及 Dapp 的環(huán)境就算安裝完成了,Truffle 官方提供了許多示例教程以及應(yīng)用腳手架(truffle box),其中就包括教你開發(fā)以太坊寵物商?(http://truffleframework.com/tutorials/pet-shop)?的教程等內(nèi)容。
在此不再贅述,感興趣的同學(xué)自己動(dòng)手可以試試。
以太坊智能合約開發(fā)
首先使用 Truffle 初始化我們的項(xiàng)目,命令如下。
mkdir my-icocd my-iconpm init -ytruffle init
腳本運(yùn)行完成之后 Truffle 會(huì)自動(dòng)為我們的項(xiàng)目創(chuàng)建一系列文件夾和文件,如下圖所示。
這里有一個(gè)隱藏的坑,如果你使用 Windows 命令行的話,需要?jiǎng)h掉?truffle.js?文件,否則在項(xiàng)目目錄執(zhí)行 truffle 相關(guān)命令時(shí),CMD 會(huì)混淆?truffle?與?truffle.js?文件。
因此,你應(yīng)該將配置寫在?truffle-config.js?文件當(dāng)中。
ERC20 Token 合約開發(fā)
現(xiàn)在我們的項(xiàng)目目錄大概是這個(gè)樣子:
contracts/
Migrations.sol
migrations/
1_initial_migration.js
test/
package.json
truffle-config.js 或 truffle.js
我們?cè)诰帉懼悄芎霞s時(shí),需要在?contracts?目錄下新建相應(yīng)的智能合約文件。
在以太坊開發(fā)智能合約的編程語(yǔ)言叫做 Solidity?(https://goo.gl/hCHh3w)。它是一種在語(yǔ)法上非常類似 JavaScript 的語(yǔ)言,其后綴名為?.sol?。
例如在這里我們可以創(chuàng)建一個(gè)名為?GitCoin.sol?的文件,命令如下。
// *nixtouch GitCoin.sol// wincopy NUL > GitCoin.sol
ERC20(Ethereum Request for Comments NO.20)(https://goo.gl/aX4x5F)?是官方發(fā)行的 token 標(biāo)準(zhǔn)。
如果你希望你發(fā)布的 token 能夠在以太坊網(wǎng)絡(luò)上流通、上市交易所、支持以太坊錢包,在開發(fā) token 的合約時(shí)就必須遵從這一規(guī)范。
ERC20 規(guī)定了合約中的一系列變量、方法、事件,你可以參考官網(wǎng)教程 Create your own CRYPTO-CURRENCY with Ethereum?(https://www.ethereum.org/token)?當(dāng)中的示例代碼:
pragma solidity ^0.4.16;interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; }contract TokenERC20 { ? ?// Public variables of the token ? ?string public name; ? ?string public symbol; ? ?uint8 public decimals = 18; ? ?// 18 decimals is the strongly suggested default, avoid changing it ? ?uint256 public totalSupply; ? ?// This creates an array with all balances ? ?mapping (address => uint256) public balanceOf; ? ?mapping (address => mapping (address => uint256)) public allowance; ? ?// This generates a public event on the blockchain that will notify clients ? ?event Transfer(address indexed from, address indexed to, uint256 value); ? ?// This notifies clients about the amount burnt ? ?event Burn(address indexed from, uint256 value); ? ?/** ? ? * Constrctor function ? ? * ? ? * Initializes contract with initial supply tokens to the creator of the contract ? ? */ ? ?function TokenERC20( ? ? ? ?uint256 initialSupply, ? ? ? ?string tokenName, ? ? ? ?string tokenSymbol ? ?) public { ? ? ? ?totalSupply = initialSupply * 10 ** uint256(decimals); ?// Update total supply with the decimal amount ? ? ? ?balanceOf[msg.sender] = totalSupply; ? ? ? ? ? ? ? ?// Give the creator all initial tokens ? ? ? ?name = tokenName; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // Set the name for display purposes ? ? ? ?symbol = tokenSymbol; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // Set the symbol for display purposes ? ?} ? ?/** ? ? * Internal transfer, only can be called by this contract ? ? */ ? ?function _transfer(address _from, address _to, uint _value) internal { ? ? ? ?// Prevent transfer to 0x0 address. Use burn() instead ? ? ? ?require(_to != 0x0); ? ? ? ?// Check if the sender has enough ? ? ? ?require(balanceOf[_from] >= _value); ? ? ? ?// Check for overflows ? ? ? ?require(balanceOf[_to] + _value > balanceOf[_to]); ? ? ? ?// Save this for an assertion in the future ? ? ? ?uint previousBalances = balanceOf[_from] + balanceOf[_to]; ? ? ? ?// Subtract from the sender ? ? ? ?balanceOf[_from] -= _value; ? ? ? ?// Add the same to the recipient ? ? ? ?balanceOf[_to] += _value; ? ? ? ?Transfer(_from, _to, _value); ? ? ? ?// Asserts are used to use static analysis to find bugs in your code. They should never fail ? ? ? ?assert(balanceOf[_from] + balanceOf[_to] == previousBalances); ? ?} ? ?/** ? ? * Transfer tokens ? ? * ? ? * Send `_value` tokens to `_to` from your account ? ? * ? ? * @param _to The address of the recipient ? ? * @param _value the amount to send ? ? */ ? ?function transfer(address _to, uint256 _value) public { ? ? ? ?_transfer(msg.sender, _to, _value); ? ?} ? ?/** ? ? * Transfer tokens from other address ? ? * ? ? * Send `_value` tokens to `_to` on behalf of `_from` ? ? * ? ? * @param _from The address of the sender ? ? * @param _to The address of the recipient ? ? * @param _value the amount to send ? ? */ ? ?function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { ? ? ? ?require(_value <= allowance[_from][msg.sender]); ? ? // Check allowance ? ? ? ?allowance[_from][msg.sender] -= _value; ? ? ? ?_transfer(_from, _to, _value); ? ? ? ?return true; ? ?} ? ?/** ? ? * Set allowance for other address ? ? * ? ? * Allows `_spender` to spend no more than `_value` tokens on your behalf ? ? * ? ? * @param _spender The address authorized to spend ? ? * @param _value the max amount they can spend ? ? */ ? ?function approve(address _spender, uint256 _value) public ? ? ? ?returns (bool success) { ? ? ? ?allowance[msg.sender][_spender] = _value; ? ? ? ?return true; ? ?} ? ?/** ? ? * Set allowance for other address and notify ? ? * ? ? * Allows `_spender` to spend no more than `_value` tokens on your behalf, and then ping the contract about it ? ? * ? ? * @param _spender The address authorized to spend ? ? * @param _value the max amount they can spend ? ? * @param _extraData some extra information to send to the approved contract ? ? */ ? ?function approveAndCall(address _spender, uint256 _value, bytes _extraData) ? ? ? ?public ? ? ? ?returns (bool success) { ? ? ? ?tokenRecipient spender = tokenRecipient(_spender); ? ? ? ?if (approve(_spender, _value)) { ? ? ? ? ? ?spender.receiveApproval(msg.sender, _value, this, _extraData); ? ? ? ? ? ?return true; ? ? ? ?} ? ?} ? ?/** ? ? * Destroy tokens ? ? * ? ? * Remove `_value` tokens from the system irreversibly ? ? * ? ? * @param _value the amount of money to burn ? ? */ ? ?function burn(uint256 _value) public returns (bool success) { ? ? ? ?require(balanceOf[msg.sender] >= _value); ? // Check if the sender has enough ? ? ? ?balanceOf[msg.sender] -= _value; ? ? ? ? ? ?// Subtract from the sender ? ? ? ?totalSupply -= _value; ? ? ? ? ? ? ? ? ? ? ?// Updates totalSupply ? ? ? ?Burn(msg.sender, _value); ? ? ? ?return true; ? ?} ? ?/** ? ? * Destroy tokens from other account ? ? * ? ? * Remove `_value` tokens from the system irreversibly on behalf of `_from`. ? ? * ? ? * @param _from the address of the sender ? ? * @param _value the amount of money to burn ? ? */ ? ?function burnFrom(address _from, uint256 _value) public returns (bool success) { ? ? ? ?require(balanceOf[_from] >= _value); ? ? ? ? ? ? ? ?// Check if the targeted balance is enough ? ? ? ?require(_value <= allowance[_from][msg.sender]); ? ?// Check allowance ? ? ? ?balanceOf[_from] -= _value; ? ? ? ? ? ? ? ? ? ? ? ? // Subtract from the targeted balance ? ? ? ?allowance[_from][msg.sender] -= _value; ? ? ? ? ? ? // Subtract from the sender's allowance ? ? ? ?totalSupply -= _value; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// Update totalSupply ? ? ? ?Burn(_from, _value); ? ? ? ?return true; ? ?}}
我只是想割韭菜而已,用得著寫幾百行代碼嗎?
當(dāng)然不必,這時(shí)我們就需要使用到智能合約開發(fā)框架 OpenZeppelin?(https://openzeppelin.org),安裝命令如下。
npm install zeppelin-solidity --save
GitCoin.sol
引入 OpenZeppelin,代碼如下。
// 聲明 solidity 編譯版本pragma solidity ^0.4.18;// 引入框架為我們提供的編寫好的 ERC20 Token 的代碼import "zeppelin-solidity/contracts/token/StandardToken.sol";// 通過 is 關(guān)鍵字繼承 StandardTokencontract GitToken is StandardToken { ?string public name = "GitToken"; // Token 名稱 ?string public symbol = "EGT"; // Token 標(biāo)識(shí) 例如:ETH/EOS ?uint public decimals = 18; // 計(jì)量單位,和 ETH 保持一樣就設(shè)置為 18 ?uint public INITIAL_SUPPLY = 10000 * (10 ** decimals); // 初始供應(yīng)量 ?// 與 contract 同名的函數(shù)為本 contract 的構(gòu)造方法,類似于 JavaScript 當(dāng)中的 constructor ?function GitToken() { ? ?totalSupply = INITIAL_SUPPLY; // 設(shè)置初始供應(yīng)量 ? ?balances[msg.sender] = INITIAL_SUPPLY; // 將所有初始 token 都存入 contract 創(chuàng)建者的余額 ?}}
好了,至此一個(gè)可以用來交易的符合 ERC20 標(biāo)準(zhǔn)的 token 就編寫完畢了。
就這么簡(jiǎn)單?就這么簡(jiǎn)單!當(dāng)然智能合約的功能不止如此,token 中可以玩轉(zhuǎn)設(shè)計(jì)的地方也不止這些。
不過我們要稍微放在后面一些來討論,接下來還是趕快著手 ICO 合約開發(fā),為我們的項(xiàng)目募集資金吧。
ICO Crowdsale 合約開發(fā)
同樣,以太坊官網(wǎng)文檔在教程 CROWDSALE Raising funds from friends without a third party?(https://www.ethereum.org/crowdsale)?中也為我們提供了用來 crowdsale 做 ICO 募資的示例代碼:
pragma solidity ^0.4.18;/*** interface 的概念和其他編程語(yǔ)言當(dāng)中類似,在這里相當(dāng)于我們可以通過傳參引用之前發(fā)布的 token 合約* 我們只需要使用其中的轉(zhuǎn)賬 transfer 方法,所以就只聲明 transfer**/interface token { ? ?function transfer(address receiver, uint amount);}contract Crowdsale { ? ?// 這里是發(fā)布合約時(shí)需要傳入的參數(shù) ? ?address public beneficiary; // ICO 募資成功后的收款方 ? ?uint public fundingGoal; // 騙多少錢 ? ?uint public amountRaised; // 割到多少韭菜 ? ?uint public deadline; // 割到啥時(shí)候 ? ?/** ? ?* 賣多貴,即你的 token 與以太坊的匯率,你可以自己設(shè)定 ? ?* 注意到,ICO 當(dāng)中 token 的價(jià)格是由合約發(fā)布方自行設(shè)定而不是市場(chǎng)決定的 ? ?* 也就是說你項(xiàng)目值多少錢你可以自己編 ? ?**/ ? ?uint public price; ? ?token public tokenReward; // 你要賣的 token ? ?mapping(address => uint256) public balanceOf; ? ?bool fundingGoalReached = false; // 是否達(dá)標(biāo) ? ?bool crowdsaleClosed = false; // 售賣是否結(jié)束 ? ?/** ? ?* 事件可以用來記錄信息,每次調(diào)用事件方法時(shí)都能將相關(guān)信息存入?yún)^(qū)塊鏈中 ? ?* 可以用作憑證,也可以在你的 Dapp 中查詢使用這些數(shù)據(jù) ? ?**/ ? ?event GoalReached(address recipient, uint totalAmountRaised); ? ?event FundTransfer(address backer, uint amount, bool isContribution); ? ?/** ? ? * Constrctor function ? ? * ? ? * Setup the owner ? ? */ ? ?function Crowdsale( ? ? ? ?address ifSuccessfulSendTo, ? ? ? ?uint fundingGoalInEthers, ? ? ? ?uint durationInMinutes, ? ? ? ?uint etherCostOfEachToken, ? ? ? ?address addressOfTokenUsedAsReward ? ?) { ? ? ? ?beneficiary = ifSuccessfulSendTo; ? ? ? ?fundingGoal = fundingGoalInEthers * 1 ether; ? ? ? ?deadline = now + durationInMinutes * 1 minutes; ? ? ? ?price = etherCostOfEachToken * 1 ether; ? ? ? ?tokenReward = token(addressOfTokenUsedAsReward); // 傳入已發(fā)布的 token 合約的地址來創(chuàng)建實(shí)例 ? ?} ? ?/** ? ? * Fallback function ? ? * ? ? * payable 用來指明向合約付款時(shí)調(diào)用的方法 ? ? */ ? ?function () payable { ? ? ? ?require(!crowdsaleClosed); ? ? ? ?uint amount = msg.value; ? ? ? ?balanceOf[msg.sender] += amount; ? ? ? ?amountRaised += amount; ? ? ? ?tokenReward.transfer(msg.sender, amount / price); ? ? ? ?FundTransfer(msg.sender, amount, true); ? ?} ? ?/** ? ?* modifier 可以理解為其他語(yǔ)言中的裝飾器或中間件 ? ?* 當(dāng)通過其中定義的一些邏輯判斷通過之后才會(huì)繼續(xù)執(zhí)行該方法 ? ?* _ 表示繼續(xù)執(zhí)行之后的代碼 ? ?**/ ? ?modifier afterDeadline() { if (now >= deadline) _; } ? ?/** ? ? * Check if goal was reached ? ? * ? ? * Checks if the goal or time limit has been reached and ends the campaign ? ? */ ? ?function checkGoalReached() afterDeadline { ? ? ? ?if (amountRaised >= fundingGoal){ ? ? ? ? ? ?fundingGoalReached = true; ? ? ? ? ? ?GoalReached(beneficiary, amountRaised); ? ? ? ?} ? ? ? ?crowdsaleClosed = true; ? ?} ? ?/** ? ? * Withdraw the funds ? ? * ? ? * Checks to see if goal or time limit has been reached, and if so, and the funding goal was reached, ? ? * sends the entire amount to the beneficiary. If goal was not reached, each contributor can withdraw ? ? * the amount they contributed. ? ? */ ? ?function safeWithdrawal() afterDeadline { ? ? ? ?if (!fundingGoalReached) { ? ? ? ? ? ?uint amount = balanceOf[msg.sender]; ? ? ? ? ? ?balanceOf[msg.sender] = 0; ? ? ? ? ? ?if (amount > 0) { ? ? ? ? ? ? ? ?if (msg.sender.send(amount)) { ? ? ? ? ? ? ? ? ? ?FundTransfer(msg.sender, amount, false); ? ? ? ? ? ? ? ?} else { ? ? ? ? ? ? ? ? ? ?balanceOf[msg.sender] = amount; ? ? ? ? ? ? ? ?} ? ? ? ? ? ?} ? ? ? ?} ? ? ? ?if (fundingGoalReached && beneficiary == msg.sender) { ? ? ? ? ? ?if (beneficiary.send(amountRaised)) { ? ? ? ? ? ? ? ?FundTransfer(beneficiary, amountRaised, false); ? ? ? ? ? ?} else { ? ? ? ? ? ? ? ?//If we fail to send the funds to beneficiary, unlock funders balance ? ? ? ? ? ? ? ?fundingGoalReached = false; ? ? ? ? ? ?} ? ? ? ?} ? ?}}
至此我們的 ICO 合約也開發(fā)完畢了,基本上一行代碼都沒有寫,只是改了幾個(gè)參數(shù),一個(gè)鍵盤上只有三個(gè)按鍵的程序員都能夠完成這類智能合約的開發(fā),沒有比這更友好的編程體驗(yàn)了。
雖然 solidity 是一種非圖靈完備的編程語(yǔ)言,但我們?nèi)匀荒軌蛴盟帉懺S多邏輯。
上述的 ICO 示例代碼寫得算比較客氣的一種,在最后的提款方法中,如果籌資達(dá)標(biāo),ICO 發(fā)布方則可以取走所有籌款,而如果未達(dá)標(biāo),參與者則能夠取回自己的投資,由合約來持有所有款項(xiàng)。
但事實(shí)上,我們?nèi)匀豢梢噪S意修改其中的邏輯,看下面代碼。
function () payable { ?require(!crowdsaleClosed); ?uint amount = msg.value; ?balanceOf[msg.sender] += amount; ?amountRaised += amount; ?tokenReward.transfer(msg.sender, amount / price); ?// 每次有人付款直接取走籌資 ?beneficiary.send(amountRaised); ?amountRaised = 0; ?FundTransfer(msg.sender, amount, true);}// 刪除剩余代碼
補(bǔ)充說明與權(quán)限控制
既然咱是鐵了心來割韭菜的,如此簡(jiǎn)單的代碼怎么能夠滿足咱的貪欲呢?一定要學(xué)比特幣固定供給量嗎?
我是來賣 token 的呀,萬(wàn)一有一天賣完了怎么辦,萬(wàn)一有人手里籌碼比我自己都多了控盤怎么辦,萬(wàn)一發(fā)的數(shù)量太多賣的不好怎么辦?
事實(shí)上解決這些問題的邏輯全部都可以寫在智能合約里。
Ownable token
在我們的潛在觀念里,區(qū)塊鏈自有不可變屬性。
這種不可變屬性在一些狂熱信徒的演繹當(dāng)中變成了平權(quán)屬性,甚至帶有了共產(chǎn)主義色彩,仿佛擁抱區(qū)塊鏈技術(shù)就能夠?yàn)槲磥淼娜祟愇拿鲙硐M?#xff0c;把人民從集權(quán)的手中解救出來。
然而事實(shí)上這種不可變性同樣是兩面的,它能夠帶來的也包括所有權(quán)的不可變性。
ERC20 標(biāo)準(zhǔn)只規(guī)定了我們的合約中應(yīng)該包含哪些方法,而沒有限制合約中不能出現(xiàn)哪些方法,因此在之前的基礎(chǔ)上,我們還可以繼續(xù)編寫一些特殊的方法,賦予合約發(fā)布者一些管理員特權(quán)。
請(qǐng)看下面代碼:
contract Ownable { ? ?address public owner; ? ?function Ownable() public { ? ? ? ?owner = msg.sender; ? ?} ? ?// 通過 onlyOwner 我們可以限定一些方法只有所有者才能夠調(diào)用 ? ?modifier onlyOwner { ? ? ? ?require(msg.sender == owner); ? ? ? ?_; ? ?} ? ?function transferOwnership(address newOwner) onlyOwner public { ? ? ? ?owner = newOwner; ? ?}}// 合約可以同時(shí)有多個(gè)繼承contract GitToken is StandardToken, Ownable { ?...
MintableToken
接下來我們來解決 token 不夠賣的問題,萬(wàn)一我的 initial offer 賣斷貨了怎么辦,萬(wàn)一我賣完一次還想賣怎么辦?
這時(shí)我們就需要把 token 編寫成為 MintableToken,在我們想增發(fā)的時(shí)候就能增發(fā),代碼如下:
// 用 onlyOwner 限定只有 token 的所有者才能夠進(jìn)行增發(fā)操作function mint(address _to, uint256 _amount) onlyOwner public returns (bool) { ?totalSupply_ = totalSupply_.add(_amount); ?balances[_to] = balances[_to].add(_amount); ?Mint(_to, _amount); ?Transfer(address(0), _to, _amount); ?return true;}
BurnableToken
萬(wàn)一我們的 token 不小心發(fā)了太多,賣的時(shí)間久了貶值怎么辦?
當(dāng)然是銷毀了,可參照下面代碼:
/*** Destroy tokens** Remove `_value` tokens from the system irreversibly** @param _value the amount of money to burn*/function burn(uint256 _value) public returns (bool success) { ?require(balanceOf[msg.sender] >= _value); ? // Check if the sender has enough ?balanceOf[msg.sender] -= _value; ? ? ? ? ? ?// Subtract from the sender ?totalSupply -= _value; ? ? ? ? ? ? ? ? ? ? ?// Updates totalSupply ?Burn(msg.sender, _value); ?return true;}
萬(wàn)一有人手里的籌碼太多,或者 token 被競(jìng)爭(zhēng)對(duì)手買走了怎么辦?沒關(guān)系,我們還可以指定銷毀某一賬戶中的 token,請(qǐng)看下面代碼:
/** ?* Destroy tokens from other account ?* ?* Remove `_value` tokens from the system irreversibly on behalf of `_from`. ?* ?* @param _from the address of the sender ?* @param _value the amount of money to burn ?*/function burnFrom(address _from, uint256 _value) public returns (bool success) { ?require(balanceOf[_from] >= _value); ? ? ? ? ? ? ? ?// Check if the targeted balance is enough ?require(_value <= allowance[_from][msg.sender]); ? ?// Check allowance ?balanceOf[_from] -= _value; ? ? ? ? ? ? ? ? ? ? ? ? // Subtract from the targeted balance ?allowance[_from][msg.sender] -= _value; ? ? ? ? ? ? // Subtract from the sender's allowance ?totalSupply -= _value; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// Update totalSupply ?Burn(_from, _value); ?return true;}
只要上述的方法全部都出現(xiàn)在合約里,我們發(fā)布的 token 就能夠具備上述所有屬性。
這樣一來,不夠的時(shí)候我們可以發(fā)錢,發(fā)多了可以銷毀,我們成功創(chuàng)建了屬于自己的一所中央銀行,甚至看某人不爽還能夠指定銷毀其賬戶存款,這哪里是平權(quán),簡(jiǎn)直是超級(jí)集權(quán)。
而事實(shí)上,在已發(fā)布的 ERC20 token 當(dāng)中,例如排名第一的 EOS 的合約?(https://goo.gl/L2AmQP)?里也是存在類似方法的,如下所示。
function mint(uint128 wad) auth stoppable note { ?_balances[msg.sender] = add(_balances[msg.sender], wad); ?_supply = add(_supply, wad);}function burn(uint128 wad) auth stoppable note { ?_balances[msg.sender] = sub(_balances[msg.sender], wad); ?_supply = sub(_supply, wad);}
當(dāng)然在其官方網(wǎng)站和白皮書中是標(biāo)明了會(huì)發(fā)布多少 token,創(chuàng)始團(tuán)隊(duì)持有多少,投資人分配多少,公開發(fā)布多少,如何銷毀等內(nèi)容的。
但白皮書又不具備法律效力,token 的所有權(quán)也不在你手里,萬(wàn)一人家哪天想要跑路或者中途變卦豈是咱能攔得住的。
換個(gè)角度講,假如你現(xiàn)在手里有一家可以印錢的公司,印多少就有多少,你印還是不印?
通過這一部分內(nèi)容的介紹,我只是想要證明,智能合約本身并不具備可無條件信任的特性,充其量就是一段沒法改一直跑的程序而已。
你也可以在邏輯中加入管理員權(quán)限,token 的發(fā)布方并不比央行可信多少,只要所有者愿意可以隨時(shí)進(jìn)行修改。以太坊官方宣傳的所謂 “trustless” 這一概念根本不成立。
沒有第三方擔(dān)保,沒有法律法規(guī)的維護(hù),僅憑智能合約本身你的投資得不到任何保證。智能合約的不可變性反而給割韭菜的一方提供了巨大的便利。
從前你看不慣某家公司還能夠黑掉它的系統(tǒng),獲取管理員權(quán)限,如今所有程序都跑在區(qū)塊鏈上,黑無可黑,集權(quán)永遠(yuǎn)都在合約發(fā)布者的手里。
講到這里,希望你能理解這次分享的良苦用心,不要輕信任何 ICO 項(xiàng)目。
合約的發(fā)布及調(diào)試
本地開發(fā)環(huán)境發(fā)布
合約開發(fā)完成之后,我們需要編譯并發(fā)布合約至區(qū)塊鏈網(wǎng)絡(luò)中,只需要進(jìn)行以下兩步操作。
首先在?migrations?文件夾下新建?2-deploy-contract.js?文件,配置部署腳本如下。
// 引入我們編寫的合約const GitCoin = artifacts.require("./GitCoin.sol")const GitCoinCrowdsale = artifacts.require("./GitCoinCrowdsale.sol")module.exports = function(deployer, network, accounts) { ?// 設(shè)定參數(shù),此處的參數(shù)即使傳入合約構(gòu)造方法的參數(shù),與你自己編寫的合約保持一致 ?const ifSuccessfulSendTo = accounts[0] // 當(dāng)前以太坊網(wǎng)絡(luò)中的默認(rèn)賬戶 ?const fundingGoalInEthers = 1000 ?const durationInMinutes = 36000000 ?const etherCostOfEachToken = 0.01 ?// 這里的 Promise 可以保證我們?cè)诎l(fā)布完 token 合約之后再發(fā)布 ICO 合約,并將已發(fā)布 token 的地址作為參數(shù)傳入 ?deployer.deploy(GitCoin).then(function() { ? ?return deployer.deploy(GitCoinCrowdsale, ifSuccessfulSendTo, fundingGoalInEthers, durationInMinutes, etherCostOfEachToken, GitCoin.address); ?});};
接著在?truffle-config.js?或?truffle.js?中設(shè)置發(fā)布網(wǎng)絡(luò),腳本如下。
module.exports = { ?networks: { ? ?development: { ? ? ?host: "127.0.0.1", ? ? ?port: 7545, // 與你本地的 ganache 設(shè)置保持一致 ? ? ?network_id: "*" // Match any network id ? ?} ?}};
現(xiàn)在只需要開啟 Ganache:
然后在命令行中輸入:
truffle compiletruffle migrate
你的合約就會(huì)順利發(fā)布至測(cè)試網(wǎng)絡(luò)中了。然后你可以輸入:
truffle console
這樣就能夠進(jìn)入本地的命令行調(diào)試了:
# 所有的合約方法都是 Promise 對(duì)象truffle(development)> GitCoinCrowdsale.deployed().then(inst=>{crowd=inst})truffle(development)> GitCoin.deployed().then(inst=>{git=inst})truffle(development)> crowd.sendTransaction({from:web3.eth.accounts[0],value:web3.toWei(1, "ether")})truffle(development)> git.mint(web3.eth.accounts[0],web3.toWei(100, "ether"))
線上測(cè)試網(wǎng)絡(luò)發(fā)布
以太坊網(wǎng)絡(luò)分為測(cè)試網(wǎng)和主網(wǎng),在正式發(fā)布主網(wǎng)之前,我們可以先發(fā)送到測(cè)試網(wǎng)絡(luò)進(jìn)行調(diào)試。
發(fā)布至以太坊網(wǎng)絡(luò)也無需同步完整節(jié)點(diǎn),我們可以使用 Infura 為我們提供的公共接口。
填寫表單提交后,Infura 會(huì)為你提供專用的接口地址,然后我們只需要將網(wǎng)絡(luò)地址填入到配置文件中,如下所示。
var HDWalletProvider = require("truffle-hdwallet-provider"); // 在這里我們需要通過 js 調(diào)用以太坊錢包,通過 npm install truffle-hdwallet-provider 安裝這個(gè)庫(kù)var infura_apikey = "ubQWERwasd"; // infura 為你提供的 apikey 請(qǐng)與你申請(qǐng)到的 key 保持一致,此處僅為示例var mnemonic = "apple banana carray dog egg fault great"; // 你以太坊錢包的 mnemonic ,可以從 Metamask 當(dāng)中導(dǎo)出,mnemonic 可以獲取你錢包的所有訪問權(quán)限,請(qǐng)妥善保存,在開發(fā)中切勿提交到 gitmodule.exports = { ?networks: { ? ?development: { ? ? ?host: "127.0.0.1", ? ? ?port: 7545, ? ? ?network_id: "*" ? ?}, ? ?ropsten: { ? ? ?provider: function() { ? ? ? ?return new HDWalletProvider(mnemonic, "https://ropsten.infura.io/"+infura_apikey) ? ? ?}, ? ? ?network_id: 3, ? ? ?gas: 3012388, ? ? ?gasPrice: 30000000000 ? ?}, ? ?main: { ? ? ?provider: function() { ? ? ? ?return new HDWalletProvider(mnemonic, "https://mainnet.infura.io/"+infura_apikey) ? ? ?}, ? ? ?network_id: 3, ? ? ?gas: 3012388, ? ? ?gasPrice: 1000000000 ? ?} ?}};
在以太坊網(wǎng)絡(luò)中發(fā)布合約需要使用 ETH 支付礦工的 gas 費(fèi)用,你可以在 Ethereum Ropsten Faucet?(http://faucet.ropsten.be:3001)?免費(fèi)獲取到用于 Ropsten 測(cè)試網(wǎng)絡(luò)的 ETH。
由于網(wǎng)絡(luò)環(huán)境的變化,不同的擁堵狀況可能造成燃料費(fèi)用和消耗的不同。
如果發(fā)布不成功,可以調(diào)整?gas/gasPrice?的數(shù)值,你可以通過?web3.getBlock('latest').gasLimit?這一數(shù)值判斷當(dāng)前網(wǎng)絡(luò)的消耗。
在命令行輸入如下命令:
truffle migrate --network ropsten
通過?--network?設(shè)置發(fā)布的目標(biāo)網(wǎng)絡(luò)。
主網(wǎng)絡(luò)發(fā)布
同理,在發(fā)布至主網(wǎng)絡(luò)時(shí),只需要執(zhí)行如下命令。
truffle migrate --network main
但由于當(dāng)前的以太坊網(wǎng)絡(luò)的現(xiàn)實(shí)狀況,如果設(shè)置燃料費(fèi)太低,可能要等待數(shù)天后合約才會(huì)被網(wǎng)絡(luò)確認(rèn),注意到我們編寫的發(fā)布腳本是需要合約地址回調(diào)的。
介于這種狀況,我們可以將 token 合約和 crowdsale 合約分開發(fā)布,只需要再新建?3-deploy-crowdsale.js?文件,腳本如下。
const LeekCoinCrowdsale = artifacts.require("./GitCoinCrowdsale.sol")module.exports = function(deployer, network, accounts) { ?const ifSuccessfulSendTo = accounts[0] ?const fundingGoalInEthers = 1000 ?const durationInMinutes = 36000 ?const etherCostOfEachToken = 0.01 ?const tokenAddress = '0x123456789ABCDFGHSDWDVC' // 先單獨(dú)發(fā)布 token 合約,上線成功后將其合約地址填在此處 ?deployer.deploy(GitCoinCrowdsale, ifSuccessfulSendTo, fundingGoalInEthers, durationInMinutes, etherCostOfEachToken, tokenAddress);};
在發(fā)布至主網(wǎng)絡(luò)時(shí),可以分開兩次進(jìn)行,確保你設(shè)置的賬戶里有真實(shí)的 ETH 余額,注意設(shè)置好合理的 gas 數(shù)值,根據(jù)確認(rèn)時(shí)間的長(zhǎng)短,可能需要 0.08~1 ETH 不等。
上線合約驗(yàn)證
無論是發(fā)布至以太坊的測(cè)試網(wǎng)絡(luò)還是主網(wǎng)絡(luò),在發(fā)布完成之后都需要在 Etherscan?(https://etherscan.io)?進(jìn)行線上驗(yàn)證。
在 Etherscan 上打開你剛剛發(fā)布的合約地址,你可以看到如下內(nèi)容:
點(diǎn)擊?Verify And Publish?鏈接就可以進(jìn)入驗(yàn)證頁(yè)面:
在填寫表單時(shí)有以下注意事項(xiàng)。
Compiler 選擇最新版本;
Optimization 選擇 No。
雖然 solidity 支持 import 語(yǔ)法,但 Etherscan 對(duì)使用 import 進(jìn)行開發(fā)的合約支持很雞肋,目前它要求你需要把庫(kù)文件也當(dāng)作合約發(fā)布至網(wǎng)絡(luò)才能夠在表單中填寫進(jìn)行驗(yàn)證。
當(dāng)然我們也可以選擇手動(dòng)把 import 庫(kù)文件的內(nèi)容手動(dòng)復(fù)制粘貼到代碼框里,注意要保留全部?jī)?nèi)容,包括 pragma 聲明一行。
當(dāng)然你也可以選擇使用官方的 Remix?(https://remix.ethereum.org/)?預(yù)先 concrete 你的合約文件,也可以安裝 solidity compiler?(https://goo.gl/aKsXxH)?在本地編譯好再發(fā)布。
ICO 和 token 的合約如此簡(jiǎn)單,根本不需要這些玩意兒,所以此處不再贅述,感興趣的同學(xué)可以自行研究。
Dapp 開發(fā)
智能合約相當(dāng)于我們的后端邏輯,以太坊的 EVM 就是我們的云服務(wù)器,Infura 為我們提供 API 接口,接下來我們就只需要給韭菜開發(fā)一個(gè)可以花錢消費(fèi)的前端界面了。
ICO 項(xiàng)目的網(wǎng)站把握以下幾個(gè)原則就好。
文字不要太多,頁(yè)面要大片留白,簡(jiǎn)潔明了有現(xiàn)代感;
配色一定要深,加上動(dòng)態(tài)幾何圖形,設(shè)計(jì)要有未來感;
開發(fā)團(tuán)隊(duì)全配齊,不是常春藤,沒有硅谷背景的不要,一定要國(guó)際化;
各種站臺(tái)大佬,海量媒體報(bào)道,一線互聯(lián)網(wǎng)公司合作全放上去。
言歸正傳,我們還是專注于技術(shù)。
web3.js 的使用
web3.js?(https://github.com/ethereum/web3.js)?為我們提供了一系列訪問以太坊網(wǎng)絡(luò)的 JavaScript 編程接口,完整的說明文檔可以在 web3.js Doc?(https://goo.gl/zp2yEQ)?中參閱。
我們一般通過如下腳本來初始化 web3 對(duì)象。
// 判斷當(dāng)前瀏覽器中有未注入 web3 對(duì)象if (typeof web3 !== 'undefined') { ?App.web3Provider = web3.currentProvider; ?web3 = new Web3(web3.currentProvider);} else { ?// 注意設(shè)置到你自己的 infura 地址 ?App.web3Provider = new Web3.providers.HttpProvider('https://ropsten.infura.io/ubQWERawsd'); ?web3 = new Web3(App.web3Provider);}
Metamask 簡(jiǎn)介
Metamask?(https://metamask.io)?是一個(gè)瀏覽器插件,通過 Metamask 我們可以在瀏覽器中使用以太坊錢包,在訪問 Dapp 應(yīng)用時(shí),也可以為其注入 web3 對(duì)象。
具體配合應(yīng)用開發(fā)的文檔可以在 MetaMask Compatibility Guide?(https://goo.gl/7wKPtp)?查閱,一般我們通過如下腳本來監(jiān)測(cè) Metamask 狀態(tài)獲取以太坊賬戶。
var account = web3.eth.accounts[0];var accountInterval = setInterval(function() { ?if (web3.eth.accounts[0] !== account) { ? ?account = web3.eth.accounts[0]; ? ?updateInterface(); ?}}, 100);
truffle-contract 的使用
web3.js 默認(rèn)為我們提供的接口還是太底層,許多調(diào)用需要 hard code 設(shè)置參數(shù),以太坊網(wǎng)絡(luò)使用的 BigNumber 也需要我們手動(dòng)轉(zhuǎn)換。
我們可以選擇使用 truffle-contract?(https://github.com/trufflesuite/truffle-contract)?來調(diào)用更高一層的封裝對(duì)象,并且在之前使用 truffle 開發(fā)構(gòu)建的智能合約文件也能派上用場(chǎng)。
我們可以在?build/contracts/?下找到編譯好的?GitCoin.json?和?GitCoinCrowdsale.json?文件,之后可以在我們的應(yīng)用中通過如下腳本獲取合約對(duì)象。
<script type="text/javascript" src="./dist/truffle-contract.min.js"></script><script>var GitCoin;$.getJSON('contracts/GitCoin.json', function(data) { ?// 獲取編譯好的合約文件 ?var GitCoinArtifact = data; ?// 通過 truffle-contract 獲取合約對(duì)象 ?GitCoin = TruffleContract(GitCoinArtifact); ?// 將合約綁定至當(dāng)前 web3 對(duì)象 ?GitCoin.setProvider(App.web3Provider);});</script>
之后我們就可以像在?truffle console?當(dāng)中一樣,對(duì)合約對(duì)象進(jìn)行各種操作啦。
ICO 前端應(yīng)用開發(fā)
我們的 ICO 應(yīng)用只需要解決一個(gè)核心需求,那就是買幣;只需要兩個(gè)核心功能,一個(gè)是選擇買多少,另一個(gè)就是付款,所以我們的界面自然是相當(dāng)簡(jiǎn)單,如下圖所示。
然后再稍微美化一下,如下面兩張圖所示。
一場(chǎng)成功的 ICO,自然需要精雕細(xì)琢,完整的代碼示例可以在 Leek Ecological Chain?(http://lec.yubolun.com/)?找到,同時(shí)此網(wǎng)站也是上述教程的一個(gè)完整示例,你可以切換到 Ropsten 網(wǎng)絡(luò)在本網(wǎng)站上購(gòu)買 LEC?(https://goo.gl/4uNskB)?韭菜幣。
Dapp 部署
既然我們開發(fā)的是 Dapp 去中心化應(yīng)用,怎么能夠部署在中心化的服務(wù)器上呢?這不是自掉身價(jià)嗎?Dapp 自然有其部署的解決方案。
IPFS 簡(jiǎn)介
IPFS 提供去中心化的點(diǎn)對(duì)點(diǎn)的 Web 服務(wù)。
說簡(jiǎn)單點(diǎn),你可以把它理解成為一個(gè) p2p 的網(wǎng)盤,你網(wǎng)站的靜態(tài)文件可以發(fā)布到 IPFS 上面托管,而且只要 IPFS 的節(jié)點(diǎn)不掛,你的網(wǎng)站就永遠(yuǎn)都不會(huì)掛,而不像部署到單獨(dú)服務(wù)器上。
同時(shí) IPFS 上的一個(gè)文件也就對(duì)應(yīng)著一個(gè) hash 地址,普通用戶可以通過公共的 http gateway 訪問到你的頁(yè)面,不像云服務(wù)器還要備案,正好也方便你割完韭菜跑路。
使用也非常簡(jiǎn)單,只需要在 Install Go IPFS?(https://ipfs.io/docs/install)?下載安裝。
發(fā)布應(yīng)用
只需要一行命令,把你 Dapp 的所有靜態(tài)文件上傳至 IPFS,命令如下。
ipfs add -r your-ico/# 返回 hash 地址,此處僅為示例added QWERabcd1234qwerABCD your-ico/
然后你就能夠通過 https://goo.gl/5SyBwN 訪問你的網(wǎng)站。當(dāng)然這樣的域名十分不友好,為 IPFS 站點(diǎn)設(shè)置解析需要一些不常用的操作。
域名解析
IPNS
你的站點(diǎn)必然包含多個(gè)文件,每個(gè)文件對(duì)應(yīng)著獨(dú)立的 hash 地址,而且你也不能保證你的網(wǎng)站只需要發(fā)布一次。
因此在網(wǎng)站發(fā)布后,我們需要使用 ipns 來獲取到對(duì)應(yīng)的唯一地址,之后的 DNS 解析也會(huì)對(duì)應(yīng)到這一地址,同樣只需要一行命令,如下所示。
# 站點(diǎn)發(fā)布后的 hash 地址,此處僅為示例ipfs name publish QWERabcd1234qwerABCD# 返回 ipns 地址Published to ABCDqwer1234abcdQWER
之后你就能夠通過 https://goo.gl/8YMLBi 訪問你的站點(diǎn)了。在設(shè)置域名解析時(shí),我們需要添加一條?TXT?類型的解析記錄,解析值為:
dnslink=/ipns/ABCDqwer1234abcdQWER
這樣我們就能夠通過 https://goo.gl/VjSm1K 訪問你的 Dapp,這樣是不是友好多了?
Nginx 反向代理
當(dāng)然你也可能希望使用自己的獨(dú)立域名,這時(shí)我們只需要使用 Nginx 設(shè)置反向代理即可。
server { ?listen 80; ?server_name yourico.com; ?location / { ? ?proxy_pass https://ipfs.io/ipns/yourico.com/; ?}}
寫在后面
以太坊官網(wǎng),第一篇教程教你發(fā) token,第二篇就教你賣 token,居心何在我也不好評(píng)判。
除了 ICO 還有 IMO/IFO ,IMO 你只用賣個(gè)路由器,IFO 只需要 fork 一份 Bitcoin 的代碼,稍微調(diào)調(diào)參數(shù),就不需要什么教程了。
程序員總是妄圖通過技術(shù)手段解決社會(huì)問題,然而人性是不變的。以太坊希望建立一個(gè) trustless 的網(wǎng)絡(luò),可惜被無數(shù)人濫用,巧立空氣項(xiàng)目,搞空殼公司,逃避監(jiān)管搞非法集資。
區(qū)塊鏈和虛擬貨幣期望用點(diǎn)對(duì)點(diǎn)分布式的網(wǎng)絡(luò),脫離第三方,讓世界上任何角落的兩個(gè)人都能夠低成本地進(jìn)行交易,結(jié)果大量投機(jī)者涌入,導(dǎo)致網(wǎng)絡(luò)堵塞,如今我們連一筆交易的礦工費(fèi)都支付不起。
當(dāng)然我信奉技術(shù)本身是無罪,就好像這篇教你割韭菜的文章一樣,你是選擇擦亮雙眼,看清 ICO 的本質(zhì),從此勢(shì)不兩立;還是選擇投機(jī)倒把,濫用以太坊技術(shù),墜身同流合污?
您可以在訪問 https://github.com/discountry/gitcoin 查看完整的智能合約示例。
您可以訪問 https://github.com/discountry/lec 查看完整的 Dapp 示例。
Read at your own risk.
總結(jié)
以上是生活随笔為你收集整理的满城尽带比特币:程序员如何发布自己的 ICO?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 区块链游戏为何只剩下“炒币”的价值?
- 下一篇: 十分钟教你开发EOS智能合约