3月25日,BSN第二次开发者大赛正式启动,本次大赛以“编写基于多种底层框架的智能合约”为主题,开发者可基于CITA等主流底层框架,结合业务场景设计、开发并部署智能合约。
为了让大家更好上手智能合约开发,区块链服务网络发展联盟与CITAHub开源社区共同推出本教程,助力开发者学习并熟悉合约开发,轻松应对此次大赛并拔得头筹。
一、CITA入门
溪塔科技的研发工程师李耀荣:毕业于杭州电子科技大学计算机学院。拥有 15 年的计算机程序设计经验,曾就职于华为,工作经历涉及芯片设计、硬件电路板设计、编译器软件设计、分布式基础软件开发。
很高兴能够在这里跟大家见面。今天晚上由我来跟大家分享CITA入门,今晚我想分享的内容以下4个部分:
第一部分:CITA 开源社区介绍;
第二部分:深入讲解 CITA 底层架构设计;
第三部分:CITA 底层配套的组件;
第四部分:如何快速构建一个CITA测试环境
CITAHub 开源社区介绍
我们先进入第一部分,先简单介绍一下我们的开源社区:CITAHub。CITAHub 是溪塔科技发起,基于开源企业级区块链内核 CITA 所构建的开源社区。CITAHub 的使命是更好的连接产业应用方与技术开发方。区块链是用来创造价值网络的工具,基于该理解,溪塔科技希望通过 CITAHub 开源技术社区为开发价值网络提供场景,开发工具及最佳实践。
我们之前为什么想要做这个事情?
CITAHub 服务
CITAHub 为会员企业输出强有力的技术支持
当前,对于产业应用方来说,由于信息不对称性问题,难以找到区块链技术与自身业务结合点。CITAHub 通过社区内丰富的区块链企业资源可以为产业方提供多样的技术咨询服务;当产业方打磨好自身的区块链产品后,区块链节点网络部署的差异性会带来运维压力,CITAHub 有丰富的插件应用及运维团队,可以为产业方提供节点运维支持,降低成本。
对于技术团队来说,会面临找工具难,重复造轮子的问题,甚至还需要技术开发团队大量的二次开发。CITAHub 整合生态中资源,为开发团队提供插件平台,方便技术提供方更加简单的开发区块链项目;此外,CITAHub 还提供长期区块链底层技术支持,为技术团队解决后顾之忧。
CITAHub 为会员企业对接丰富的合作项目
对于产业方来说,即使有合适的项目想要寻求区块链团队支持时,由于缺乏行业信息无法选择合适团队。CITAHub 能够为产业方提供庞大的区块链解决方案供应商生态,覆盖众多技术领域与渠道资源;在验收与定价环节,CITAHub 可以为企业提供标准化合作模式。
对于技术提供方来说,技术团队市场能力有限,项目资源较少,又缺乏大型项目资质以及合作渠道。CITAHub 汇总产业内的项目对接给合适的开发团队,又开创了大型项目合作模式,根据大型项目不同需求进行拆分,与合作伙伴共同承接。
CITAHub 为会员企业提供知产授权服务
目前,CITAHub 已经拥有超过 60 项专利,CITA 技术相关专利超过 40 项。CITAHub 相关技术均许可会员使用。会员就 CITA 相关技术申请的发明专利,在会员有效期内均许可给甲方使用。
另外,CITAHub 建立了专利池。在专利池中,会员以自愿原则将专利选择性放入专利池内供其它 CITAHub 会员使用的,同期获得已存在于专利池内其他专利使用的权利。
CITAHub 为会员企业提供全方位宣传服务
CITAHub 与行业媒体社区紧密合作,定期组织线上线下分享活动,邀请会员企业作为分享嘉宾,介绍自身企业在区块链上的技术进展以及应用案例落地。通过分享,企业能够为媒体社区提供一手资料以供采编成文,提高媒体社区的文章内容质量。媒体社区也能够从企业实践中获得行业发展的最新动态,通过自身平台进行有效传播,促进行业的健康发展。
那么,我们希望通过构建 CITAHub 社区来帮助大家能够在社区上寻找到各自所需要的关键资源。现在CITAHub采用的是会员机制,为会员提供丰富资源,在这里我们也欢迎大家加入。
CITAHub 规模
CITAHub 自 2019 年初成立以来,经过一年多的发展,目前已有近 100 家企业加入。其中包括招商银行、秘猿科技、你好现在、荷月科技、加密矩阵、矢链科技等在内的多家核心开发企业,共同推进 CITA 研究与开发,促进社区生态的繁荣发展;中钞区块链研究院、轻信科技、欧冶金服、志顶科技、派盾、仟金顶、秒钛坊、法捕快、数秦科技等超过 50 家合作企业加入 CITAHub,提供不同行业的区块链解决方案。
此外,包括火鸟财经、碳链价值、锌链接、零壹财经等 20 多家媒体也加入到了 CITAHub 的开源生态建设中,为 CITAHub 以及社区内企业的宣传作出了巨大的贡献。作为国内领先区块链开源社区,CITAHub 得到了国内各大开源技术社区的支持,包括思否segmentfault、开源中国、CSDN、开源社等为区块链技术的发展提供交流平台。
CITA底层架构设计
进入第二部分,我们来讲一下 CITA 架构设计。
微服务架构
CITA 本身是一个微服务架构。微服务是指将一个逻辑节点拆成了6个不同的微服务,另外还有一个是1个监控服务,在这里并没有列出,但实际上真正运行时会包括监控服务。从上图可以看到, 6 个微服务分别:
• RPC 服务网关:作为整个 CITA 底层软件,与应用层相连接的一个接口,为应用提供JSON-RPC的接入;
• Auth 交易验证:提供交易相关合法性验证等服务;
• Consensus 交易共识:确定一个区块何时出块,由谁出块等信息;
• Chain 链式存储:提供区块链存储服务;
• Executor 合约引擎:用来执行交易的模块;
• Network 网络同步:各个节点之间的数据通讯,都是由这个模块来来承接的。
大家可以发现CITA对区块链的各层逻辑的定义非常清晰。那么,这样设计的好处是什么?
• 好处一:可利用云计算基础设施来按需提升性能。
• 好处二:各个组件可独立替换升级。
• 好处三:采用消息总线进行通信,各个微服务可以利用异步消息简化处理,又可以确保消息的安全、可靠送达。当与外部系统集成时,外部系统可以直接访问消息总线,而无需 CITA 做适配。
这样一来,对 CITA 的后续扩展与开发有非常大的好处的。这是一个基本的逻辑架构图。那么,接下来会以一个简单的交易为例,一个交易是如何在逻辑架构图里进行工作的。
交易处理
交易处理第一步:应用(包括 DApp、CLI 交互工具等)构造交易,并对交易进行数字签名,然后发送至 CITA 区块链网络中。也就是说交易进入CITA网络的时候就已经是经过签名的一个交易了。
第二步:CITA 的 RPC 接受交易,通过消息总线传递给 Auth 进行交易验证。
第三步:Auth 验证通过,将交易放入交易池,并生成交易 Hash, 返回给 RPC。
第四步:当放到交易池后,就可以得到一个交易 Hash。此时,链就可以通过消息总线把交易 Hash 再返还给用户。所以说大家平时用CITA时,发现拿到的交易,实际上这个时候交易可能其实并没有上链,你还需要通过希去查回执,才能得到交易是否已经上链的这个消息。
第五步:那么, Auth 模块返回交易Hash后,会通过消息总线把交易交给 Network
第六步:Network 把交易广播给其他节点,其他节点它也会做同样的事情。也就是说当一个交易发到链上后,在正常的网络情况下,每个节点都会拥有这个节点的全量交易,就是说在这网络当中每个节点都拥有网络的所有的交易。那么,交易有可能是通过node1进来的,也可能通过node4进来,这在网络设计上都是被允许的。
出块处理
讲完交易处理后,那么,接下来讲如何进行出块处理?在这一页的材料中,将会给大家介绍CITA 中是如何出块?
第一步:Consensus 根据出块策略,当出块时机到达时,从 Auth 的交易池中获取一组交易。
这里有两个点需要强调:
1. 出块策略是什么?指的是 PBFT 的共识算法。每个节点都会运行一个确定性的出块选择的一个算法。
2. 出块时机是什么?指的是在CITA中,我们定义成 3 秒出块。在共识中,实际上我们将3秒出块划分成很多阶段,其实是一个复杂过程。
第二步:Consensus 将所获取的交易打包成区块传递给 Network。
第三步:Network 将区块发送给其它节点。所有共识节点根据共识算法,对区块进行共识。
值得注意的是,在前面的分享中有提到正常网络情况下,理论上讲每个节点拥有全量的交易数据。所以,在第二步进行广播交易时,实际上CITA做了优化。在广播区块时,并没有把交易实体给模式广播出去的,只把交易Hash广播给节点,这样一来会使交易package会大大降低,提高带宽的利用率。
第四步:区块共识完成后,Consensus 将区块发送给 Executor 执行。
第五步:Executor 执行完成后,将执行结果传递给 Chain;由 Chain 将执行结果及区块存储到数据库中,出块完成。为了达到更高的性能,我们在这一块做了很多的工作。比方在共识阶段,在提出Proposal时,就已经把提案这个块就交给Execute去进行预执行。这样可以让交易执行与共识同时进行。这样可以大大提升我交易处理性能。一般情况下,大概率提交的 Proposa 就是所出的块。因此,到第4步时,其实这个交易已经执行完成。这样优化会带来性能提升。
如何进行节点部署?
节点类型
在CITA中有两类节点,即:
共识节点:共识节点具有出块和投票权限,交易由共识节点排序并打包成块,共识完成后即被确认为合法区块。简单地说,共识节点就是参与出块的节点。
普通节点:该节点可以同步区块链上的交易,并且对交易进行验证执行,同时,还可以接受 DApp 的 JSON-RPC 请求;但它并不参与共识投票,也不会主动发起区块让其它节点投票。普通节点除了没有出块和投票的权限外,其它能与与共识节点相同。
节点个数
之前经常有用户在CITAHub论坛里向我们提问:CITA到底要布几个节点,是不是一定要部署4个节点才可以执行?
对于 CITA 节点个数:
• CITA 可以部署任意多个节点(包括 1 个)。
• CITA 采用的是类 PBFT 共识,具有一定的容错能力,其容错的节点个数(这里讲的是共识节点个数): n = (N - 1) / 3, 其中 n 与 N 都是自然数
所谓节点容错能力指的是在区块链网络中,某个共识节点出错无法参与共识时,整个网络还是能够正常工作。容错节点个数,指的是能够容忍多少个节点出错,然后整个网络不受影。
· 在选择容错部署时,建议跨物理机器或跨云平台部署 。
CITA 组件介绍
第三部分,会简单介绍在CITA底层链之上,有一些关键的组件来帮助大家去使用CITA和开发应用。
• CITA : 区块链软件,提供区块链核心功能。
• CITA CLI : CITA 命令行交互工具。方便调测。
• SDK : CITA 区块链应用开发套件,目前官方维护的 SDK 有:
– sdk-java
– sdk-js
• ReBirth : 区块链数据缓存器。
• Cyton : 区块链钱包,管理用户的私钥及交易签名。当前有两个版本:
– Cyton-android
– Cyton-iOS
• Microscope : 区块链浏览器。
• CITA IDE : 智能合约开发与调试。
• CITA Truffle Box : 智能合约管理与测试。
• CITA Web Debugger : 网页插件版私钥管理及交易签名工具。
30 秒构建测试环境
接下来给大家演示如何快速搭建 CITA 的测试环境。为了方便新用户快速使用CITA做成了一个docker镜像。具体操作请查看视频教学:
http://kb.bsnbase.com/webdoc/view/Pub4028813e711a7c3901719cc6ff637f3d.html。
1. 新建 CITA 配置:
docker run -v "`pwd`":/opt/cita-run cita/cita-ce:20.2.0-secp256k1-sha3 cita create --super_admin "0x37d1c7449bfe76fe9c445e626da06265e9377601" --nodes "127.0.0.1:4000"
2. 启动 CITA :
docker run -d -p 1337:1337 -v "`pwd`":/opt/cita-run cita/cita-ce:20.2.0-secp256k1-sha3 /bin/bash -c 'cita setup test-chain/0 && cita start test-chain/0 && sleep infinity’
3. 启动 CITA-CLI, 进行基本操作 :
启动 CITA-CLI:
docker run -it cita/cita-ce-cli:20.2.2
查询块高:
cita> rpc blockNumber
{
"id": 1,
"jsonrpc": "2.0",
"result": "0x143"
}
二、CITA智能合约开发
智能合约历史
1994年,计算机科学家和密码学家 Nick Szabo 首次提出“智能合约”概念。它早于区块链概念的诞生。Szabo 描述了什么是“以数字形式指定的一系列承诺,包括各方履行这些承诺的协议”。虽然有它的好处,但智能合约的想法一直未取得进展——主要是缺乏可以让它发挥出作用的区块链。
直到 2008 年,第一个加密货币比特币才出现,同时引入了现代区块链技术。区块链最初是以比特币的底层技术出现的,各种区块链分叉导致发生很大的变化。智能合约在 2008 年依然无法融入比特币区块链网络,但在五年后,以太坊让它浮出水面。从此,涌现出了各种不同形式的智能合约,其中以太坊智能合约使用最广。
自以太坊开始,区块链是一个运行着智能合约的分布式平台:应用程序可以按照程序运行,不存在故障、审查、欺诈或第三方干预的可能性。智能合约给予了我们使用区块链技术来验证我们运行的代码的执行情况的能力。
智能合约定义
智能合约(英语:Smart contract )是一种旨在以信息化方式传播、验证或执行的计算机协议。智能合约允许在没有第三方的情况下进行可信交易,这些交易可追踪且不可逆转。
智能合约简单定义就是智能合约是可以处理 token 的脚本,围绕它可以发行,转移和销毁资产。这里说的资产是一个泛化的定义,不一定是币,可以是任何一种虚拟物品(比如应收,支付信息甚至加密猫)和现实世界的物品在区块链上的映射(比如舱单,抵押)。
CITA 智能合约
CITA 区块链框架使用的虚拟机 CITA-VM 和 EVM 采取同样的指令集,所以合约所使用的语言也是 solidity。由于 Ethereum 是目前全球最广泛的区块链网络,所以 solidity 也是使用最广泛的智能合约语言,围绕它的生态是非常丰富的,包括了合约调试,部署工具和保护合约安全的一些库。
这里再谈一下合约是由谁来执行的问题,在公链上,比如比特币或者以太坊,这些合约由我们称为“矿工”的参与方强制执行和证明。矿工其实是多台电脑(也可以称为矿机),它们把一项交易(执行智能合约,代币转账等) 以区块的形式添加到一个公开分账本上。使用者给这些矿工支付 “Gas”也就是手续费,它是运行一份合约的成本。
由于 CITA 是针对于企业的开放许可链框架,在 CITA 中矿工是出块节点,使用智能合约所需要的手续费是支付给出块节点的, gas 在这里叫做 quota。当然这里支付比例是可以自定义调整的,具体可以见文档。同时 CITA 可以调节为无币模式,在无币模式下,不存在手续费。
智能合约开发
现在,我们开始智能合约的开发部分,Solidity 与 Javascript 很接近,但它们并不相同。而且不能在一段代码上强加 JQuery,智能合约是无法调用区块链体系之外的代码的。同时还有一个特点是,你在开发的时候需要特别注意安全性,因为在区块链上的交易是不可逆的。
智能合约定义
通过一个例子说明基本语法,这里参考了 ethfans 上的一个例子,如果难以理解的话可以换一个,使用当时 PeckShield 讲的一个分饼干的例子。
现在,关于我们的第一个例子,我正在考虑一个由电影《时间规划局》启发的脚本。电影中,人们生活在一个反乌托邦式的未来,改用时间作为货币流通。他们可以通过掰手腕的方式赢取对手的时间(他们的“手臂”上存储着时间,输方的时间将会传送给赢家),我们也可以这么做!用智能合约以角力( Wrestling )的方式赚钱。
首先,solidity 脚本的基础是下面这段代码,pragma 指明正在使用的 Solidity 版本。Wrestling 是合约的名称,是一种与 Javascrip 上的类(class)相似的结构。
pragma solidity ^0.4.18;
contract Wrestling {
// our code will go here}
我们需要两个参与者,所以我们要添加两个保存他们账户地址的变量(他们的公钥),分别是 wrestler1 和 wrestler2 ,变量声明方式如下。
address public wrestler1;
address public wrestler2;
在我们的小游戏中,每一轮的比赛,参与者都可以投入一笔钱,如果一个人投入的钱是另一个人的两倍(总计),那他就赢了。定义两个玩家是否已经投入的flag wrestler1Played 和 wrestler2Played 以及两位玩家投入的金额 wrestler1Deposit 和 wrestler1Deposit。
bool public wrestler1Played;
bool public wrestler2Played;
uint private wrestler1Deposit;
uint private wrestler2Deposit;
还有判断游戏结束与否,赢家和收益的变量。
bool public gameFinished;
address public theWinner;
uint gains;
下面介绍一些关于公钥/私钥的规则,在区块链上每一个账户都是一对公私钥,私钥可以对一个信息进行签名,从而使这条信息可以被他人验证,被验证的时候它的公钥需要被使用到。在整个签名和验证的过程中,没有信息是加密的,实际上任何信息都是公开课查验的。
对于合约里面的变量,本质上来讲,也是可以被公开访问的。在这里要注意是的,即使一个变量是私有的,并不是说其他人不能读取它的内容,而是意味着它只能在合约中被访问。但实际上,由于整个区块链存储在许多计算机上,所以存储在变量中的信息总是可以被其他人看到,这是在区块链中一个很重要额原则。
另一方面,和很多编程语言很像,编译器会自动为公共变量创建 getter 函数。为了使其他的合约和用户能够更改公共变量的值,通知也需要针对不同的变量创建一个 setter 函数。
现在我们将为游戏的每一步添加三个事件。
1. 开始,参与者注册;
2. 游戏期间,登记每一轮赛果;
3. 最后,其中一位参与者获胜。
事件是简单的日志,可以在分布式应用程序(也称为 dapps)的用户界面中调用 JavaScript 回调函数。在开发过程中,事件甚至可以用于调试的目的,因为不同于 JavaScript 有console.log() 函数,solidity 中是没有办法在 console 中打印出信息的。代码如下:
event WrestlingStartsEvent(address wrestler1, address wrestler2);
event EndOfRoundEvent(uint wrestler1Deposit, uint wrestler2Deposit);
event EndOfWrestlingEvent(address winner, uint gains);
现在我们将添加构造函数,在 Solidity 中,它与我们的合约具有相同的名称,并且在创建合约时只调用一次。在这里,第一位参与者将是创造合约的人。msg.sender 是调用该函数的人的地址。
function Wrestling() public { wrestler1 = msg.sender;}
接下来,我们让另一个参与者使用以下函数进行注册:
function registerAsAnOpponent() public {
require(wrestler2 == address(0));
wrestler2 = msg.sender;
WrestlingStartsEvent(wrestler1, wrestler2);
}
Require 函数是 Solidity 中一个特殊的错误处理函数,如果条件不满足,它会回滚更改。在我们的示例中,如果变量参与者2等于0x0地址(地址等于0),我们可以继续;如果参与者2的地址与0x0地址不同,这就意味着某个玩家已经注册为对手,所以我们会拒绝新的注册。可以把它认为是 solidity 中的 if() {} else{} 条件判断。
再次强调, msg.sender 是调用该函数的帐户地址,并且当我们触发一个事件,就标志着角力的开始。
现在,每一个参与者都会调用一个函数, wrestle() ,并投入资金。如果双方已经玩过这场游戏,我们就能知道其中一方是否获胜(我们的规则是其中一方投入的资金必须是另一方的双倍)。关键字 payable 意味着函数可以接收资金,如果它不是集合,函数则不会接受币。msg.value 是发送到合约中的币的数量。
function wrestle() public payable {
require(!gameFinished && (msg.sender == wrestler1 || msg.sender == wrestler2)); if(msg.sender == wrestler1) {
require(wrestler1Played == false);
wrestler1Played = true;
wrestler1Deposit = wrestler1Deposit + msg.value;
} else {
require(wrestler2Played == false);
wrestler2Played = true;
wrestler2Deposit = wrestler2Deposit + msg.value;
}
if(wrestler1Played && wrestler2Played) {
if(wrestler1Deposit >= wrestler2Deposit * 2) {
endOfGame(wrestler1);
} else if (wrestler2Deposit >= wrestler1Deposit * 2) {
endOfGame(wrestler2);
} else {
endOfRound();
}
}
}
请注意,我们不是直接把钱交给赢家,在此情况下这并不重要,因为赢家会把该合约所有的钱提取出来;而在其他情况下,当多个用户要把合约中的以太币提取出来,使用 withdraw 模式会更安全,可以避免重入,在合约安全部分我们会详细讨论这些情况。
简单地说,如果多个用户都可以从合约中提取资金,那么任谁都能一次性多次调用 withdraw 函数并多次得到报酬。所以我们需要以这样一种方式来编写我们的取款功能:在他继续得到报酬之前,他应得的数额会作废。
它看起来像这样:
function withdraw() public {
require(gameFinished && theWinner == msg.sender);
uint amount = gains;
gains = 0;
msg.sender.transfer(amount);
}
代码段链接:
https://github.com/devzl/ethereum-walkthrough-1/blob/master/Wrestling.sol
智能合约的 IDE
在区块链技术中,不仅转账是一笔交易,对合约中函数的调用和合约的部署都是以发送交易的方式完成。整个过程比较繁琐,正如同其他的变成语言一样,针对于 solidity 智能合约,我们也提供了 IDE (CITA IDE) 来编译和部署合约。
CITA 的 IDE
CITA IDE 是基于 Ethereum 的 Solidity 编辑器进行修改并适配了 CITA ,是面向 CITA 的智能合约编辑器,能够编写、编译、debug、部署智能合约。可直接运行官方 CITA IDE 1(
https://cita-ide.citahub.com) 进行体验。
使用说明
• browser 内置常用的模板合约,首先从内置合约模板中选择合适的模板开始开发
• Compile 本地编译,选择当前 solidity 版本,与合约 pragma 一致
• 进入右侧的 Run 标签, 在 Deploy to CITA 中填入相关信息
– 勾选 Auto ValidUntilBlock 则发送交易前会自动更新 validUntilBlock 字段
– 勾选 store ABI on chain 则会在合约部署成功后将合约 ABI 存储到 CITA 上
– 此处特别注意 Quota 的设置, 一般合约需要较多 Quota, 若 quota 不足, 在交易信息打印的时候可以查看 Error Message 获知
• 点击 Load Contracts 加载当前编译完成的合约, 并选择要部署的合约
• 点击 Deploy to CITA 发起部署合约的交易
• 观察控制台的输出, 交易详细信息会显示在控制台上, 当流程结束时, 会输出交易 hash 和合约地址, 并且以链接形式支持到 Microscope 查看
DApp 及智能合约开发实例
First Forever 是一个DApp demo,展示了在 CITA 上开发一个最小可用的 DApp 的完整流程
FIrst Forever 地址:
https://github.com/citahub/first-forever-demo/blob/develop/README-CN.md
以下是区块链DApp的开发步骤示意图:
在该项目中使用了一个简单的可以存储用户提交内容的智能合约,源码:SimpleStore
地址:
https://github.com/citahub/first-forever-demo/blob/develop/src/contracts/SimpleStore.sol
更详细的介绍看:如何动手做一个DApp 地址:
https://github.com/citahub/first-forever-demo/blob/develop/README-CN.md
智能合约安全性
因为智能合约是不可逆的,所以他的交易一旦形成,是无法回退的。在这种情形下,智能合约的安全性尤为重要。以下先介绍几种合约常见的合约安全性隐患,然后会给出改善他们的方法。
参考视频 :
https://www.bilibili.com/video/av58299098
智能合约溢出型漏洞
16bit 整数:0x0000,0x0001,0x0002,…,0xfffd,0xffff
0x8000 + 0x8000 = 0x10000 = 0x0000 = 0
0xffff + 0x0003 = 0x10002 = 0x0002 = 2
0x0000 - 0x0001 = 0xffff = -1 = 65535
function transferMulti(address[] _to, uint256[] _value) public returns (uint256 amount) {
require(_to.length == _value.length);
for(uint8 j; j<len; j++) {
amount += _value[j];
}
require(balanceOf[msg.sender] >= amount);
for(uint8 i ; i < len; i++) {
address _toI = _to[i];
uint256 _valueI = _value[i];
balanceOf[_toI] += _valueI;
balanceOf[msg.sender] -= _valueI;
Transfer(msg.sender, _tiI, _valueI);
}
}
这个函数想要做到的是把 msg.sender 在合约中的 token 转给多个人, amount += _value[j]; 这个操作会存在溢出的风险,如果在加的时候出现状况 amount = 0x8000 + 0x8000 = 0,那么在后面一步的判断 require(balanceOf[msg.sender] >= amount);中会出现的实际判断的是 balanceOf[msg.sender] >= 0 那么可以从空的账户中把钱转出。
代码注入漏洞
function approveAndCallcode(address _spender, uint256 _value, bytes _extraData) returns (bool success) {
allowed[msg.sender][_spender] = _value;
Approval(msg.sender, _spender, _value);
if(!_spender.call(_extraData)) {
revert();
}
return true;
}
可以把这个合约本身拥有的代币偷走转给别的用户,因为对于extraData 来说,自由度非常高, _spender.call(_extraData) 可以是任何一个地址调用任何一个函数。
itchyDAO in MakerDAO 投票系统
这个主要是以一个比较复杂的例子来给学员讲合约中函数调用需要知道的地方,暗示智能合约还是比较难以把控的,需要多学习
以下是一个在 MakerDAO 中的投票系统,在这个投票系统中,一个sender 需要根据自己的权重对一个提案进行投票。
function etch(address[] memory yays) public note returns (bytes32 slate){
require(yays.length <= MAX_YAYS); requireByOrderSet(yays);
bytes32 hash = keccak256(abi.encodePacked(yays));
emit Etch(hash);
return hash;
}function vote(address[] memory yays) public returns (bytes32){
bytes32 slate = etch(yays); vote(slate);
return slate;
}function vote(bytes32 slate) public note {
uint weight = deposit[msg.sender];
subWeight(weight, vote[msg.sender]);
votes[msg.sender] = slate;
addWeight(weight, vote[msg.sender]);
}
以下是投票函数,在投票以后把票数进行 addWeight 和 subWeight 操作。
function addWeight(uint weight, bytes32 slate) internal { address[] storage yays = slates[slate]; for(uint i = 0; i < yays.lenght; i++) { approvals[yays[i]] = add(approvals[yays[i]], weight); }}function subWeight(uint weight, bytes32 slate) internal { address[] storage yays = slates[slate]; for(uint i = 0; i < yays.length; i++) { approvals[yays[i]] = sub(approvals[yays[i]], weight); }}
最后一步是在 lock 一种币,在 lock 以后可以进行投票操作,在投票完成以后,可以 free 从而退回自己的币。
function lock(uint wad) public note{
GOV.pull(msg.sender,wad);
IOU.mint(msg.sender, wad);
deposits[msg.sender] = add(deposits[msg.sender], wad);
addWeight(wad, votes[msg.sender]);
}function free(uint wad) public note{
deposits[msg.sender] = sub(deposits[msg.sender], wad);
subWeight(wad, votes[msg.sender]);
IOU.burn(msg.sender, wad);
GOV.push(msg.sender, wad);
}
智能合约场景
长远看,遵循标准有很多不应忽视的益处。首先,如果遵照某个标准生成代币,那么每个人都会知道该代币的基础功能,并知道如何与之交互,因此就会有更多信任。去中心化程序(DApps)可以直接辨别出其代币特征,并通过特定的 UI 来与其打交道。另外,一种代币智能合约的标准实现已经被社区开发出来,它采用类似 OpenZeppelin 的架构。这种实现已经被很多大神验证过,可以用来作为代币开发的起点。
本文中会从头开始提供一个不完整的,但是遵循 ERC20 标准的,基础版的代币实现,然后将它转换成遵循 ERC721 标准的实现。这样就能让读者看出两个标准之间的不同。
出发点是希望大家了解代币是如何工作的,其过程并不是一个黑箱;另外,对于 ERC20 这个标准,尽管它至少已经被广泛接受两年以上,如果只是从标准框架简单地生成自己的代币,也还会存在某些不易发现的故障点。
ERC20 标准
ERC20(
https://theethereum.wiki/w/index.php/ERC20_Token_Standard)是为同质(Fungible)代币标准设立的标准,可以被其它应用(从钱包到去中心化交易所)重复使用。同质意味着可以用同类的代币互换,换句话说,所有的代币都是等价的(就像钱币,某一美金和其它美金之间没有区别)。而一个非同质代币(Non-fungible Token)代表一种特定价值(例如房屋,财产,艺术品等)。同质代币有其内在价值,而非同质代币只是一种价值智能合约的代表。
要提供符合ERC20标准的代币,需要实现如下功能和事件:
contract ERC20Interface {
function totalSupply() public constant returns (uint);
function balanceOf(address tokenOwner) public constant returns (uint balance);
function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
function transfer(address to, uint tokens) public returns (bool success);
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);}
标准不提供功能的实现,这是因为大家可以用自己喜欢的方式写出任何代码,如果不需要提供某些功能只需要按照标准(
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md)返回 null/false 的值就可以了。
注意:这里并不很强调代码,大家只需了解内部机理,全部代码将会在文末附上链接。
实现
首先,需要给代币起一个名字,因此会采用一个公有变量(Public Variable):
string public name = “Our Tutorial Coin”;
然后给代币起一个代号:
string public symbol = “OTC”;
当然还要有具体小数位数:
uint8 public decimals = 2;
因为 Solidity 并不完全支持浮点数,因此必须把所有数表示成整数。例如,对于一个数字 “123456”,如果使用 2 位小数,则代表 “1234.56”;如果采用4位小数,则代表 “12.3456”。0 位小数代表代币不可分。而以太坊的加密币以太币则使用18位小数。一般地,代币不需要使用18位小数,因为遵循了以太坊的惯例,也没有什么特别的目的。
你需要统计一共发行了多少代币,并跟踪每人拥有多少:
uint256 public totalSupply;mapping(address => uint256) balances;
当然,你需要从0个代币开始,除非在代币智能合约创建时候就生成了一些,如下例:
// The constructor function of our Token smart contract
function TutoCoin() public {
// We create 100 tokens (With 2 decimals, in reality it’s 1.00 token)
totalSupply = 100;
// We give all the token to the msg.sender (in this case, it’s the creator of the contract)
balances[msg.sender] = 100;
// With coins, don’t forget to keep track of who has how much in the smart contract, or they’ll be “lost”. }
totalSupply() 函数只是从 totalSupply 变量中获取数值:
function totalSupply() public constant returns (uint256 _totalSupply) {
return totalSupply;
}
balanceOf()## 也类似:// Gets the balance of the specified address.
function balanceOf(address tokenOwner) public view returns (uint256 balance) {
return balances[tokenOwner];
}
接下来就是ERC20的神奇之处了, transfer() 函数是将代币从一个地址发送到另外一个地址的函数:
function transfer(address _to, uint256 _value) public returns (bool) {
// avoid sending tokens to the 0x0 address
require(_to != address(0));
// make sure the sender has enough tokens
require(_value <= balances[msg.sender]); // we substract the tokens from the sender’s balance
balances[msg.sender] = balances[msg.sender] - _value;
// then add them to the receiver
balances[_to] = balances[_to] + _value; // We trigger an event, note that Transfer have a capital “T”, it’s not the function itself with a lowercase “t”
Transfer(msg.sender, _to, _value); // the transfer was successfull, we return a true
return true;
}
以上基本就是 ERC20 代币标准的核心内容。
鉴于 ERC20 还存在其他一些问题,更安全容错的 transferFrom() 实现和其它方案被发布出来(如之前所说,该标准只是一些功能原型和行为定义,具体细节则靠开发者自己实现),并正在讨论中,其中就包括
ERC223(https://github.com/ethereum/EIPs/issues/223) ERC777(https://github.com/ethereum/EIPs/issues/777)
ERC223 方案的动机是避免将代币发送到错误地址或者不支持这种代币的合约上,成千上万的金钱因为上述原因丢失,这一需求作为以太坊后续开发功能的第 223 条记录第 223 条记录在案。ERC777 标准在支持其它功能的同时,对接收地址进行“即将收到代币”的提醒功能,ERC777 方案看起来很有可能替代 ERC20.
ERC721 标准
ERC721目前看,ERC721 跟 ERC20 及其近亲系列有本质上的不同。ERC721 中,代币都是唯一的。ERC721 提出来后的众多使用案例中,CryptoKitties,这款使用ERC721标准实现的收集虚拟猫游戏使得它备受瞩目。以太猫游戏实际就是智能合约中的非同质代币 (non-fungible token),并在游戏中用猫的形象来表现出来。
如果想将一个 ERC20 合约转变成 ERC721 合约,我们需要知道 ERC721 是如何跟踪代币的。在 ERC20 中,每个地址都有一个账目表,而在 ERC721 合约中,每个地址都有一个代币列表:
mapping(address => uint[]) internal listOfOwnerTokens;
由于 Solidity 自身限制,不支持对队列进行 indexOF() 的操作,我们不得不手动进行队列代币跟踪:
mapping(uint => uint) internal tokenIndexInOwnerArray;
当然可以用自己实现的代码库来发现元素的索引,考虑到索引时间有可能很长,最佳实践还是采用映射方式。
为了更容易跟踪代币,还可以为代币的拥有者设置一个映射表:
mapping(uint => address) internal tokenIdToOwner;
以上就是两个标准之间最大的不同,ERC721 中的 transfer() 函数会为代币设置新的拥有者:
function transfer(address _to, uint _tokenId) public (_tokenId){ // we make sure the token exists
require(tokenIdToOwner[_tokenId] != address(0)); // the sender owns the token
require(tokenIdToOwner[_tokenId] == msg.sender); // avoid sending it to a 0x0 require(_to != address(0));
// we remove the token from last owner list
uint length = listOfOwnerTokens[msg.sender].length; // length of owner tokens
uint index = tokenIndexInOwnerArray[_tokenId]; // index of token in owner array
uint swapToken = listOfOwnerTokens[msg.sender][length - 1]; // last token in array
listOfOwnerTokens[msg.sender][index] = swapToken; // last token pushed to the place of the one that was transferred
tokenIndexInOwnerArray[swapToken] = index; // update the index of the token we moved
delete listOfOwnerTokens[msg.sender][length - 1]; // remove the case we emptied
listOfOwnerTokens[msg.sender].length—; // shorten the array’s length
// We set the new owner of the token
tokenIdToOwner[_tokenId] = _to; // we add the token to the list of the new owner
listOfOwnerTokens[_to].push(_tokenId);
tokenIndexInOwnerArray[_tokenId] = listOfOwnerTokens[_to].length - 1;
Transfer(msg.sender, _to, _tokenId);
}
尽管代码比较长,但却是转移代币流程中必不可少的步骤。
还必须注意,ERC721 也支持 approve() 和 transferFrom() 函数,因此我们必须在 transfer 函数内部加上其它限制指令,这样一来,当某个代币有了新的拥有者,之前的被授权地址就无法其代币进行转移操作,代码如下:
function transfer(address _to, uint _tokenId) public (_tokenId){
// …
approvedAddressToTransferTokenId[_tokenId] = address(0);
}
挖矿基于以上两种标准,可能面对同一种需求,要么产生同质代币,要么产生非同质代币,一般都会用一个叫做 Mint() 的函数完成。
实现以上功能函数的代码如下:
function mint(address _owner, uint256 _tokenId) public (_tokenId){
// We make sure that the token doesn’t already exist
require(tokenIdToOwner[_tokenId] == address(0)); // We assign the token to someone
tokenIdToOwner[_tokenId] = _owner;
listOfOwnerTokens[_owner].push(_tokenId);
tokenIndexInOwnerArray[_tokenId] = listOfOwnerTokens[_owner].length - 1; // We update the total supply of managed tokens by this contract
totalSupply = totalSupply + 1; // We emit an event
Minted(_owner, _tokenId);
}
用任意一个数字产生一个新代币,根据不同应用场景,一般在合约内部只会授权部分地址可以对它进行铸币(mint)操作。
这里需要注意 mint() 函数并没有出现在协议标准定义中,而是我们添加上去的,也就是说我们可以对标准进行扩充,添加其它对代币的必要操作。例如,可以添加用以太币来买卖代币的系统,或者删除不再需要代币的功能。
CITA 源码地址:
https://github.com/citahub
CITA 配套工具链:
https://www.citahub.com/#componentArea
CITA 技术支持:
https://talk.citahub.com
文章来源:溪塔科技