以太坊构建DApps系列教程(五):智能合约通讯和代币销售

在本系列关于使用以太坊构建DApps教程的第4部分中,咱们开始构建和测试咱们的DAO智能合约。 如今让咱们更进一步,根据咱们的介绍,处理向故事Story添加内容和代币。php

添加代币

对于可以与另外一个合约进行交互的合约,它须要知道其余合约的接口——可用的函数。因为咱们的TNS代币具备至关简单的接口,所以咱们能够将其包含在DAO的智能合约中, contract StoryDao声明之上以及咱们的import语句中加入:html

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);
}

请注意,咱们不须要粘贴函数的“内容”,而只须要粘贴它们的签名(骨架)。这就是合约之间交互所需的所有内容。java

如今咱们能够在DAO合约中使用这些函数。计划以下:node

  • 启动代币(咱们已经这样作了)。
  • 从同一地址启动DAO。
  • 将全部代币从代币启动器发送到DAO,而后经过合约将全部权转移到DAO自己。
  • 此时,DAO拥有全部代币并可使用发送功能将其出售给人员,或者可使用批准功能(在投票期间有用)等将其保留用于支出。

但DAO如何知道部署代币的地址?咱们告诉它。python

首先,咱们在DAO合约的顶部添加一个新变量:android

LockableToken public token;

而后,咱们添加一些函数:程序员

constructor(address _token) public {
    require(_token != address(0), "Token address cannot be null-address");
    token = LockableToken(_token);
}

构造函数是在部署合约时自动调用的函数。它对于初始化连接合约,默认值等值颇有用。在咱们的例子中,咱们将使用它来使用和保存TNS代币的地址。require检查是为了确保代币的地址有效。web

在咱们处理它时,让咱们添加一个函数,让用户能够检查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()mongodb

代币地址更改功能容许全部者(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)到目标_buyertokens金额。

如今咱们知道人们能够得到代币,让咱们看看咱们是否能够实施提交。

结构和提交

根据咱们的介绍帖子,提交一个条目将花费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;
}

这就像咱们智能合约中的“对象类型”。该对象具备不一样类型的属性。contentbytes类型值。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.decimals10**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系列教程(五):智能合约通讯和代币销售

相关文章
相关标签/搜索