时间锁钱包——对以太坊智能合约的介绍

区块链及其应用是当今最受欢迎的。尤其是以太坊,它提供智能合约功能,为以分布式、不可变和不可信任的方式实现的新想法打开了大门。
在以太坊智能合约领域起步可能有点势不可挡,因为学习曲线相当陡峭。我们希望这篇文章(以及以太坊系列的后续文章)能够缓解这种痛苦,让您快速启动和运行。


Truffle, Solidity, and ĐApps

在本文中,我们假设您对区块链应用程序和以太坊有一些基本的了解。如果你觉得你需要温习一下你的知识,我们推荐这篇来自Truffle框架的以太坊概述
本文内容:

  1. 定时钱包的应用
  2. 开发环境设置
  3. 利用Truffle框架开发智能合约
    1)Solidity合约的说明
    2)如何编译、迁移和测试智能合约
  4. 使用ĐApps从浏览器与智能合约交互
    1)使用MetaMask进行浏览器初始化
    2)主要用例的遍历

时间锁定钱包:用例:

以太坊智能合约有许多不同的应用。目前最受欢迎的是加密货币(作为ERC20代币实现)和众筹代币销售(又称初始硬币发行或ICO)。实用ERC20代币的一个很好的例子就是Motoro硬币。在这篇博文中,我们将探讨一些不同的东西:在加密钱包合约中锁定资金的想法。这个想法本身有各种各样的用例。

  • 投资ICO
    目前最常见时间锁钱包就是“投资(vesting)”。假设您刚刚成功募集了一个ICO,而您的公司仍然持有团队成员的大部分代币。
    确保员工持有的代币不能立即交易,这对相关各方都是有益的。如果没有适当的控制措施,任何一个特定的员工都可能采取行动,卖掉他们所有的代币,兑现现金,然后离开公司。这将对市场价格产生负面影响,并使项目的所有剩余贡献者不满意。
  • 基于密码的“遗嘱”
    另一个想法是使用智能合约来生成加密遗嘱。设想一下,我们希望将加密货币存储在一份合约中,该合约可供家庭成员使用,我们定期去合约中签到,如果我们没有按时签到,我们可能出了什么事,他们可以取款。他们各自获得的资金比例可以在合约中明确规定,也可以由家庭成员协商一致决定。
  • 养老金或信托基金
    锁定基金的另一个应用可以是创建一个小型养老基金或基于时间的储蓄账户,即防止所有者在未来某个时间之前提取任何资金。(对于上瘾的密码交易员来说,这可能特别有用,有助于保持他们的代币完好无损。)
    我们将在本博客文章的其余部分中探讨的这些用例:将一些加密钱存起来,以便以后送给其他人,比如将来的生日礼物。
    让我们想象一下,我们想在某人18岁生日时送给他们一个礼物。我们可以在一张纸上写下账户的私钥和存放资金的钱包的地址,然后用信封交给他们。他们唯一要做的就是在他们18岁的时候从他们的账户上调用一个功能,所有的资金都会转移到他们身上,或者,我们可以使用一个简单的应用程序。听起来不错?我们开始吧!

以太坊开发环境搭建:

在安装以太坊开发环境之前,你需要在你的机器上安装Node.js和Git。在这个博客中,我们将使用Truffle框架。尽管你可以不用它,但是Truffle框架可以降低开发、测试和部署以太坊智能合约的门槛。我们完全同意他们的说法:“Truffle是以太坊最受欢迎的开发框架,它的使命是让你的生活更轻松。”
要安装它,请运行以下命令:

npm install -g truffle

现在,获取此项目的代码:

git clone https://github.com/radek1st/time-locked-wallets
cd time-locked-wallets

需要注意的是,该项目遵循标准Truffle项目结构,相关目录如下:

  • contracts: 保存所有的Solidity合约
  • migrations: 包含了迁移脚本
  • src: 包含了ÐApp代码
  • test: 包含所有测试代码

智能合约的描述

这个项目包括几个合约,包括:

  • TimeLockedWallet.sol:是本项目的主合约,具体描述将在下文给出。
  • Timelocketwalletfactory.sol:工厂合约,让任何人都可以轻松部署自己的TimeLockedWallet。
  • ERC20.sol:用于以太坊令牌的ERC20标准的接口。
  • ToptalToken.sol:自定义的ERC20令牌。
  • SafeMath.sol:是ToptalToken合约用于执行安全算术操作的小型数学库。
  • Migrations.sol:是一个内部的Truffle合约,便于迁移。
    有关编写以太坊合约的任何问题,请参阅Solidity官方智能合约文档
  1. TimeLockedWallet.sol
    我们的TimeLockedWallet.sol智能合约如下:
pragma solidity ^0.4.18;

上面一行表示本合约所需的Solidity编译器的最低版本。

import "./ERC20.sol";

在这里,我们导入其他合约定义,稍后在代码中使用。

contract TimeLockedWallet {
    ...
}

上面是合约对象,这个合约中包含了合约逻辑,下面描述的代码来自合约的大括号内。

address public creator;
address public owner;
uint public unlockDate;
uint public createdAt;

这里我们定义了几个公共变量,默认情况下这些变量生成相应的getter方法。其中一对是uint(无符号整数)类型,一对是address(16个字符长的以太坊地址)

modifier onlyOwner {
  require(msg.sender == owner);
  _;
}

modifier是在开始执行它所附加的函数之前必须满足的一个先决条件。

function TimeLockedWallet(
    address _creator, address _owner, uint _unlockDate
) public {
    creator = _creator;
    owner = _owner;
    unlockDate = _unlockDate;
    createdAt = now;
}

这是我们的第一个函数。由于名称与我们的合约名称完全相同,因此它是构造函数,在创建合约时只调用一次。
请注意,如果您要更改合约的名称,这个构造函数将成为一个任何人都可以调用的正常函数,并在您的合约中形成后门。另外,注意大小写也很重要,所以如果这个构造函数名是小写的,它也会再次成为一个常规函数,而不是您在这里想要的。

function() payable public { 
  Received(msg.sender, msg.value);
}

上述函数是一种特殊类型,称为fallback回退函数。如果有人寄任何以太币给这个合约,我们会很高兴地收到它。合约的ETH余额将增加,并将触发接收事件。要使任何其他函数能够接受传入的ETH,你可以使用payable关键字标记它们。

function info() public view returns(address, address, uint, uint, uint) {
    return (creator, owner, unlockDate, createdAt, this.balance);
}

这是我们的第一个正规的函数。它没有函数参数,并定义要返回的输出元组。请注意这个。this.balance返回此合约的当前以太币余额。

function withdraw() onlyOwner public {
   require(now >= unlockDate);
   msg.sender.transfer(this.balance);
   Withdrew(msg.sender, this.balance);
}

只有满足前面定义的onlyOwner修饰符时,才能执行上述函数。如果require语句不为true,则合约将退出并出现错误。那是我们检查unlockDate是否已经过去的地方。msg.sender是此函数的调用者,它将转移合约的整个以太余额。在最后一行中,我们还触发了一个Withdrew函数。稍后对其进行描述。
有趣的是,now的值等于block.timestamp,可能不像人们想象的那样准确,他由矿工来选择,因此可能与正确的时间偏差15分钟(900秒),如下式所述:

parent.timestamp >= block.timestamp <= now + 900 seconds

因此,now不应该用于测量小时间单位。

function withdrawTokens(address _tokenContract) onlyOwner public {
   require(now >= unlockDate);
   ERC20 token = ERC20(_tokenContract);
   uint tokenBalance = token.balanceOf(this);
   token.transfer(owner, tokenBalance);
   WithdrewTokens(_tokenContract, msg.sender, tokenBalance);
}

这是我们提取ERC20代币的函数。由于合约本身并不知道分配给这个地址多少以太币,所以我们必须传入要提取的已部署ERC20令牌的地址。我们用ERC20(_tokenContract)实例化它,然后找到整个令牌余额并将其传输给接收方。我们还调用了WithdrewTokens函数。

event Received(address _from, uint _amount);
event Withdrew(address _to, uint _amount);
event WithdrewTokens(address _tokenContract, address _to, uint _amount);

在这个片段中,我们定义了几个事件。被触发的events基本上是附加到区块链上transaction交易凭证的日志条目。每个交易可以附加零个或多个日志条目。events的主要用途是调试和监视。
以上就是锁定以太币和ERC20 tokens的实现方法,还不错吧?
现在让我们看看我们的另一份合约,TimeLockedWalletFactory.sol.

  1. TimeLockedWalletFactory.sol

创建一个高水平的Factory背后有两个主要原因。第一就是安全问题,通过将资金拆分到不同的钱包中,我们不会只得到一份含有大量以太币的合约。这将给钱包所有者100%的控制权,并可以阻止黑客试图利用它。第二个原因是,工厂合约允许轻松轻松地创建TimeLockedWallet合约,而无需任何开发设置,你只需要从另一个钱包或应用程序调用一个功能。

pragma solidity ^0.4.18;
import "./TimeLockedWallet.sol";
contract TimeLockedWalletFactory {
    ...
}

上述内容简单明了,与之前的合约非常相似。

mapping(address => address[]) wallets;

在这里,我们定义了一个mapping类型,它类似于字典或map,但是所有可能的键都预置并指向默认值。对于地址address类型,默认值为零地址0x00。我们还有一个数组类型address[],它保存很多地址。
在Solidity语言中,数组总是包含一个类型,并且可以有固定长度或可变长度。在我们的例子中,数组是无界的。
为了更好地表示我们的业务逻辑,我们定义了一个名为wallets的mapping,它由用户地址,包括合约创建者和所有者组成,每个地址指向一组相关的钱包合约地址。

function getWallets(address _user) 
    public
    view
    returns(address[])
{
    return wallets[_user];
}

在这里,我们使用上述函数返回用户创建或有权使用的所有合约钱包。请注意,view(在较旧的complier版本中称为constant)表示这是一个不改变区块链状态的函数,因此可以免费调用,而不需要花费任何gas。

function newTimeLockedWallet(address _owner, uint _unlockDate)
    payable
    public
    returns(address wallet)
{
    wallet = new TimeLockedWallet(msg.sender, _owner, _unlockDate);
    wallets[msg.sender].push(wallet);
    if(msg.sender != _owner){
        wallets[_owner].push(wallet);
    }
    wallet.transfer(msg.value);
    Created(wallet, msg.sender, _owner, now, _unlockDate, msg.value);
}

这是合约最重要的部分:工厂法。通过调用其构造函数new TimeLockedWallet(msg.sender, _owner, _unlockDate),我们可以动态创建一个新的时间锁定钱包,然后我们为创建者和接收者存储它的地址。稍后,我们将此函数执行过程中传递的所有可选以太币传输到新创建的钱包地址。最后,我们发出Create事件的信号,定义为:
event Created(address wallet, address from, address to, uint createdAt, uint unlockDate, uint amount);

  1. ToptalToken.sol

如果我们没有创建自己的以太坊令牌,那么本教程就不会那么有趣了,所以为了完整起见,我们将ToptalToken带到程序中。ToptalToken是一个标准的ERC20令牌,实现了以下接口:

contract ERC20 {
  uint256 public totalSupply;

  function balanceOf(address who) public view returns (uint256);
  function transfer(address to, uint256 value) public returns (bool);
  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);
  event Transfer(address indexed from, address indexed to, uint256 value);
}

它与其他令牌的区别定义如下:

string public constant name = "Toptal Token";
string public constant symbol = "TTT";
uint256 public constant decimals = 6;

totalSupply = 1000000 * (10 ** decimals);

我们给它取了一个名字,一个符号,总供应量为100万,并使它最多可分到6位小数。

Truffle 控制台: 编译, 迁移, 和测试智能合约

要快速开始,请运行Truffle命令:

truffle develop

你应该看到这样的画面:

Truffle Develop started at http://localhost:9545/

Accounts:
(0) 0x627306090abab3a6e1400e9345bc60c78a8bef57
(1) 0xf17f52151ebef6c7334fad080c5704d77216b732
(2) 0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef
(3) 0x821aea9a577a9b44299b9c15c88cf3087f3b5544
(4) 0x0d1d4e623d10f9fba5db95830f7d3839406c6af2
(5) 0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e
(6) 0x2191ef87e392377ec08e7c08eb105ef5448eced5
(7) 0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5
(8) 0x6330a553fc93768f612722bb8c2ec78ac90b3bbc
(9) 0x5aeda56215b167893e80b4fe645ba6d5bab767de

Mnemonic: candy maple cake sugar pudding cream honey rich smooth crumble sweet treat

助记符种子允许您重新创建私钥和公钥。例如,将其导入MetaMask,如下所示:
在这里插入图片描述

要编译合约,请运行:

> compile

你应该看到:

Compiling ./contracts/ERC20.sol...
Compiling ./contracts/Migrations.sol...
Compiling ./contracts/SafeMath.sol...
Compiling ./contracts/TimeLockedWallet.sol...
Compiling ./contracts/TimeLockedWalletFactory.sol...
Compiling ./contracts/ToptalToken.sol...
Writing artifacts to ./build/contracts

现在,我们需要定义我们想要部署哪些合约,这是在migrations/2_deploy_contracts.js中做的:

var TimeLockedWalletFactory = artifacts.require("TimeLockedWalletFactory");
var ToptalToken = artifacts.require("ToptalToken");

module.exports = function(deployer) {
  deployer.deploy(TimeLockedWalletFactory);
  deployer.deploy(ToptalToken);
};

我们首先导入两个合约TimeLockedWalletFactory和ToptalToken。然后我们简单地部署它们。我们故意不使用TimeLockedWallet,因为这个合约是动态部署的,有关迁移的更多信息,请参阅Truffle迁移文档

要迁移合约,请运行:

> migrate

这将导致类似以下情况:

Running migration: 1_initial_migration.js
     Deploying Migrations...
     ... 0x1c55ae0eb870ac1baae86eeb15f3aba3f521df46d9816e04400e9b5951ecc099
     Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
   Saving successful migration to network...
     ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
   Saving artifacts...
   Running migration: 2_deploy_contracts.js
     Deploying TimeLockedWalletFactory...
     ... 0xe9d9c37508bb58a1591d0f052d6870810118a0a19f728bf0cea4f4e5c17acd7a
     TimeLockedWalletFactory: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
     Deploying ToptalToken...
     ... 0x0469ce110735f27bbb1a85c85a77ba4b0ba0d5aa52c3d67164045b849d8b2ed6
     ToptalToken: 0xf25186b5081ff5ce73482ad761db0eb0d25abfbf
   Saving successful migration to network...
     ... 0x059cf1bbc372b9348ce487de910358801bbbd1c89182853439bec0afaee6c7db
   Saving artifacts...

可以看出TimeLockedWalletFactory和ToptalToken部署成功。
最后,为了确保一切正常,让我们运行一些测试。测试代码位于test目录中,并与主合约相对应TimeLockedWalletTest.js和TimeLockedWalletFactoryTest.js. 为了简洁起见,我们不讨论编写测试的细节,而把它留给读者作为练习。要执行测试,只需运行:

> test

…希望你能看到所有的测试都是这样通过的:

Contract: TimeLockedWalletFactory
  ✓ Factory created contract is working well (365ms)

Contract: TimeLockedWallet
  ✓ Owner can withdraw the funds after the unlock date (668ms)
  ✓ Nobody can withdraw the funds before the unlock date (765ms)
  ✓ Nobody other than the owner can withdraw funds after the unlock date (756ms)
  ✓ Owner can withdraw the ToptalToken after the unlock date (671ms)
  ✓ Allow getting info about the wallet (362ms)

6 passing (4s)

时间锁钱包 ÐApp

是时候来开发一个应用了。与区块链交互的最简单方法是使用带有web UI的分布式应用程序,即所谓的“ÐApp”(有时称为“dapps”)

分布式应用程序设置

为了运行这个ÐApp,你需要一个支持以太坊的浏览器。实现这一点最简单的方法是安装MetaMask Chrome插件。这里有一个关于安装和配置带有Truffle的MetaMask的可视化指南

智能合约场景

回到我们的场景,我们为什么不先介绍演员呢?假设Alice将成为时间锁钱包的创建者,Bob是资金的接收者/最终所有者。
在这里插入图片描述
场景概要:

  • Alice为Bob创建了一个锁定时间的钱包,并发送了一些以太币
  • Alice还发送了一些ERC20 Toptal total代币
  • Bob可以看到他可以使用的钱包和他创建的钱包
  • Bob不能在钱包的时间锁过期之前提取任何资金
  • 当ETH解锁时,Bob将取款
  • Bob收回所有ERC20 Toptal代币

首先,Alice为Bob创建了一个时间锁定的钱包,并发送了一个初始的一个以太币。我们可以看到,一个新的合约钱包已经被创建并归Bob所有:
在这里插入图片描述
在创建合约后的任何时候,钱包都可以被加满。充值可以来自任何人,并且是以太币或ERC20代币的形式。让Alice向Bob的新钱包发送100张Toptal代币,如下所示:
在这里插入图片描述
从Alice的角度来看,装满后的钱包会是这样的:
在这里插入图片描述
现在我们交换角色,以Bob的身份登录。Bob应该能够看到他创建的或是接收者的所有钱包。由于Alice创建的合约仍然是时间锁定的,他不能提取任何资金:
在这里插入图片描述
耐心地等待直到锁过期…
在这里插入图片描述
…Bob现在准备收回ether和Toptal代币:
在这里插入图片描述
在这里插入图片描述

清空锁着时间的钱包后,他的余额增加了,这让他非常高兴和感激Alice:
在这里插入图片描述

以太坊网络

如果您想与以上描述的合约进行交互,您不必在本地运行它们:我们已经将它们部署到以太坊Rinkeby 测试网络中,ToptalToken部署在这里,TimeLockedWalletFactory部署在这里

您可以使用我们部署的ÐApp,链接到GitHub页面提供的上述合约。请注意,您需要安装MetaMask并连接到Rinkeby。

故障排除

我们在开发这个项目时遇到了几个问题。第一个问题是Chrome中MetaMask的片状(比如抱怨无效的nonce)。我们找到的最简单的解决方法就是重新安装插件。

此外,Truffle在编辑智能合约时有时会变得不同步,并抱怨solidity参数错误的数量无效。我们发现一个简单的rm-r构建并再次进行编译/迁移就可以清除它。

以太坊开发:值得陡峭的学习曲线

我们希望这篇文章激起了您的兴趣,并希望您将开始您的开发旅程,进入以太坊的土地。通往目的地的道路将是陡峭而费时的,但是有很多资源可以帮助你(就像这一个帮助了我们一点的资源)。请随时通过下面的评论与您联系。
这个项目的源代码可以在GitHub上找到。

如果你想知道如何使用一个uPort的移动应用程序而不是MetaMask,可以看看这个项目的另一个替代版本hackathon的演示源代码

我也欢迎您阅读我的后续教程,它侧重于应用程序的创建。

信用

非常感谢Maciek Zielinski对这个项目的贡献。

了解基础知识:

  • 什么是智能合约
    智能合约是在以太坊虚拟机上执行的计算机代码。智能合约可以发送和接受以太网和数据。合约本质上是不变的,除非另有规定。

  • 什么是以太坊虚拟机
    以太坊虚拟机(EVM)是智能合约的沙盒运行时环境,实现为执行字节码的堆栈机器。它专注于为全世界的计算机提供安全性和执行不可信的代码。

  • 什么是Mist和以太坊钱包
    Mist是分布式应用程序(ĐApps,有时是DAPP)的官方浏览器,这些应用程序是以太坊网络的用户友好前端/UI。以太坊钱包就是其中之一。两者都是由建造以太坊的同一个人开发的。

  • 以太坊是开源的么?
    引用以太坊的发明者Vitalik Buterin的话:“以太坊的所有东西,包括网站、工具、白皮书,当然还有所有的软件和编译器,都是100%的开放源码,并遵循GPL。”

  • 以太坊是去中心化的么?
    是的,它本质上是完全去中心化的。读写操作完全分散,目前有一个工作证明机制。以太坊的设计方式使得没有一个人或一个团队控制区块链。

  • 以太坊的编程语言有什么?
    以太坊目前选择的语言是Solidity。坚固性是一种面向合约的编程语言,主要由JavaScript、C++和Python所激励,用于编写智能合约。还有其他语言也在地平线上,比如Vyper。

  • 什么是ERC20 token
    代币是实现ERC20标准的智能合约。它们包括获取总供给和余额等操作,以及转移代币的方法。代币从未真正离开合约,只是在内部映射中被重新分配到不同持有者的钱包地址。