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

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

添加代币

对于能够与另一个合约进行交互的合约,它需要知道其他合约的接口——可用的函数。由于我们的TNS代币具有相当简单的接口,因此我们可以将其包含在DAO的智能合约中, contract StoryDao声明之上以及我们的import语句中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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合约的顶部添加一个新变量:

1
LockableToken public token;

然后,我们添加一些函数:

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

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

在我们处理它时,让我们添加一个函数,让用户可以检查DAO中待售的代币数量,以及更改为另一个代币的函数,如果出现问题并且需要进行此类更改。这种变化也需要一个事件,所以我们也要添加它。

1
2
3
4
5
6
7
8
9
10
11
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函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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倍于故事中的条目数量。我们只需要计算未删除的提交(因为提交可以删除),所以让我们添加这个所需的属性和一个方法来帮助我们。

1
2
3
4
5
6
uint256 public submissionZeroFee = 0.0001 ether;
uint256 public nonDeletedSubmissions = 0;

function calculateSubmissionFee() view internal returns (uint256) {
return submissionZeroFee * nonDeletedSubmissions;
}

注意:Solidity具有内置时间和以太单位。在这里阅读更多相关信息。

此费用只能由业主更改,但只能降低。为了增加,需要投票。让我们写下减函数:

1
2
3
4
5
function lowerSubmissionFee(uint256 _fee) onlyOwner external {
require(_fee < submissionZeroFee, "New fee must be lower than old fee.");
submissionZeroFee = _fee;
emit SubmissionFeeChanged(_fee);
}

我们发出一个事件来通知所有观察客户费用已经改变,所以让我们声明这个事件:

1
event SubmissionFeeChanged(uint256 newFee);

提交可以是最多256个字符的文本,并且相同的限制适用于图像。只有他们的类型改变。这是自定义结构的一个很好的用例。让我们定义一个新的数据类型。

1
2
3
4
5
6
7
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隐式添加。此外,它使以后更容易“删除”条目。

注意:从技术上讲,我们还可以检查以确保提交者的地址不是零地址。

当我们在这里时,让我们定义两个事件:一个用于删除条目,一个用于创建条目。

1
2
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传递给算法的变量,因为它需要我们的一个参数。这就是这个函数的作用。

我们还需要在某处存储提交内容,所以让我们再定义两个合约变量。

1
2
mapping (bytes32 => Submission) public submissions;
bytes32[] public submissionIndex;

好的,我们现在尝试构建createSubmission函数。

1
2
3
4
5
6
7
8
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;
}

让我们逐行说明:

1
function createSubmission(bytes _content, bool _image) external payable {

该函数接受字节内容(字节是一个动态大小的字节数组,对存储任意数量的数据很有用)和一个布尔标志,表示该输入是否是图像。该函数只能从外部世界调用,并且应支付,这意味着它在交易调用时接受以太。

1
2
uint256 fee = calculateSubmissionFee();
require(msg.value >= fee, "Fee for submitting an entry must be sufficient.");

接下来,我们计算提交新条目的成本,然后检查与交易一起发送的价值是否等于或大于费用。

1
2
bytes32 hash = keccak256(abi.encodePacked(_content, block.number));
require(!submissions[hash].exists, "Submission must not already exist in same block!");

然后我们计算这个条目的哈希值(bytes32是一个32字节的固定大小数组,所以32个字符也是keccak256输出)。我们使用此哈希来查明是否已存在具有该哈希的提交,如果确实存在,则取消所有内容。

1
submissions[hash] = Submission( _content, _image, submissionIndex.push(hash), msg.sender, true );

此部分在submissions映射中的哈希位置创建新提交。它只是通过合约中上面定义的新结构传递值。请注意,虽然你可能习惯使用其他语言的new关键字,但这里没有必要(或允许)。然后我们发出事件(不言自明),最后,还有nonDeletedSubmissions += 1;:这是增加下次提交费用的原因(参见calculateSubmissionFee)。

但是这里缺少很多逻辑。我们仍然需要:

  • 图像的帐户
  • 检查提交帐户的白名单/黑名单存在和1个TNS代币所有权。

我们先做图像吧。我们的原始计划表示,每50个文本只能提交一张图像。我们还需要两个合约属性:

1
2
uint256 public imageGapMin = 50;
uint256 public imageGap = 0;

当然你已经可以假设我们将如何处理这个问题?让我们在创建新submissions[hash] = ...的之前立即将以下内容添加到我们的createSubmission方法中。

1
2
3
4
5
6
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次(或更多次)提交现有内容可以成为一个图像。

最后,让我们进行访问检查。我们可以在费用计算之前和紧接在函数入口点之后放置此代码,因为访问检查应该首先发生。

1
2
3
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()自动生成的,但它没有明确列在我们在合约顶部列出的代币的接口中。为了解决这个问题,我们可以通过添加以下行来更改接口:

1
function decimals() public view returns (uint256);

还有一件事:因为使用目前设定为1%的合约的所有者费用,让我们放弃所有者可以提取的金额并将其余部分保留在DAO中。最简单的方法是跟踪所有者可以提取多少,并在每次提交创建后增加该数量。让我们在合约中添加一个新属性:

1
uint256 public withdrawableByOwner = 0;

然后将其添加到我们的createSubmission函数的末尾:

1
withdrawableByOwner += fee.div(daofee);

我们可以通过这样的功能让所有者退出:

1
2
3
4
function withdrawToOwner() public {
owner.transfer(withdrawableByOwner);
withdrawableByOwner = 0;
}

这会将允许的金额发送给所有者,并将计数器重置为0.如果所有者不想取出全部金额,我们可以为该情况添加另一个函数:

1
2
3
4
5
6
7
8
function withdrawAmountToOwner(uint256 _amount) public {
uint256 withdraw = _amount;
if (withdraw > withdrawableByOwner) {
withdraw = withdrawableByOwner;
}
owner.transfer(withdraw);
withdrawableByOwner = withdrawableByOwner.sub(withdraw);
}

由于我们经常会通过哈希引用提交,让我们编写一个函数来检查提交是否存在,以便我们可以替换我们的submissions[hash].exists检查:

1
function submissionExists(bytes32 hash) public view returns (bool) { return submissions[hash].exists; }

还需要一些其他帮助函数来读取提交内容:

1
2
3
4
5
6
7
8
9
10
11
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函数现在看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(在跟踪用户以后删除的条目数时非常有用;这可能导致黑名单!),我们发出删除事件。

最后,我们通过减少系统中未删除的提交数量来减少新的提交创建费用。我们不要忘记在我们的合约中添加一个新属性:一个用于跟踪这些删除。

1
mapping (address => uint256) public deletions;

部署变得更加复杂

现在我们在另一个合约中使用代币,我们需要更新部署脚本(3_deploy_storydao)以将代币的地址传递给StoryDao的构造函数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
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系列教程(五):智能合约通信和代币销售