1 智能合约的概念与演变
1.1 智能合约的概念
智能合约,所谓“合约”就是条文、合同一类的东西,里面记录了发生的条件与对应执行的条款,以支持确权等操作;所谓"智能",就意味着自动化、可编程。
智能合约就是可编程的合同,也可以理解为一段自动执行的条文合同,在计算机中,就是一段自动执行的程序片段。
1.2 智能合约与区块链
智能合约上世纪就被提出,为什么智能合约与区块链却产生了如此紧密的关联?因为区块链可以保证智能合约的不可篡改。智能合约的本体是一份代码,非常容易被篡改,如何为其提供强力的存储介质就成了问题。这正好是区块链擅长解决的。同时智能合约也在反哺着区块链,它极大地扩展了区块链的业务场景。
1.3 以太坊的诞生
比特币:用户通过脚本代码来定制一些内容,例如如何解锁一笔资金。这些脚本代码会随着交易一起保存,从而享有不可篡改的特质。所以从某种角度来说,这些脚本也可看作智能合约。可是脚本语言并不是图灵完备,难以实现复杂的业务逻辑。
以太坊:以太坊中智能合约的语言是Solidity,它是图灵完备且较为上层的语言,极大地扩展了智能合约的能力范畴,降低了智能合约编写难度。
1.4 智能合约的现状
智能合约就是一段代码。相比常规代码,智能合约具有许多差别与限制,例如:
- 单线程执行
- 代码执行会消耗资源,不能超出资源限制
- 目前难以获取链外数据
- 其他限制,如TPS
1.5 联盟链的智能合约
Hyperledger Fabric:基于容器运行链码,可以支持多语言的链码运行。
FISCO-BCOS:基于EVM虚拟机运行链码,采用Solidity作为智能合约语言。
2 Solidity的基础特性
通过一个简单的合约示例,来了解智能合约的代码结构。
pragma solidity ^0.4.25;
contract Sample{
address private _admin;
uint private _state;
modifier onlyAdmin(){
require(msg.sender == _admin, "you are not admin");
_;
}
event setState(uint value);
constructor() public{
_admin = msg.sender;
}
function setstate(uint value) public onlyAdmin{
_state = value;
emit setState(value);
}
function getstate() public view returns (uint){
return _state;
}
}
- 状态变量 - _admin, _state,这些变量会被永久保存,也可以被函数修改
- 构造函数 - 用于部署并初始化合约
- 事件 - SetState, 功能类似日志,记录了一个事件的发生
- 修饰符 - onlyAdmin, 用于给函数加一层"外衣"
- 函数 - setState, getState,用于读写状态变量
状态变量
状态变量是合约的骨髓,它记录了合约的业务信息。用户可以通过函数来修改这些状态变量,这些修改也会被包含到交易中;交易经过区块链网络确认后,修改即为生效。
构造函数
构造函数用于初始化合约,它允许用户传入一些基本的数据,写入到状态变量中。构造函数不支持重载
函数
函数被用来读写状态变量。对变量的修改将会被包含在交易中,经区块链网络确认后才生效。生效后,修改会被永久的保存在区块链账本中。
view修饰符,表示了该函数不会修改任何状态变量。
pure修饰符,表示了该函数是纯函数,连状态变量都不用读,函数的运行仅仅依赖于参数。
事件
事件类似于日志,会被记录到区块链中,客户端可以通过web3订阅这些事件。
修饰符
修饰符是合约中非常重要的一环。它挂在函数声明上,为函数提供一些额外的功能,例如检查、清理等工作。修饰符onlyAdmin要求函数调用前,需要先检测函数的调用者是否为函数部署时设定的那个管理员
3 Solidity的高级特性
函数的可见类型
external:推荐只向外部暴露的函数使用。
public:权限最大,可供外部、子合约、合约内部访问。
internal:只能是内部访问(即从当前合约内部或从它派生的合约访问)
private :仅在当前定义它们的合约中使用,并且不能被派生合约使用。
状态常量
状态常量是指被声明为constant的状态变量,那么该变量值只能为编译时确定的值,无法被修改,不会给变量预留储存空间。
面向对象之重载
重载是指合约具有多个不同参数的同名函数。对于调用者来说,可使用相同函数名来调用功能相同,但参数不同的多个函数。
面向对象之继承
Solidity使用"is"作为继承关键字。
contract A {
}
contract B is A {
}
继承的合约B可以访问被继承合约A的所有非private函数和状态变量。
继承的底层实现原理为:当一个合约从多个合约继承时,在区块链上只有一个合约被创建,所有基类合约的代码被复制到创建的合约中。
面向对象之抽象类和接口
接口使用关键字interface
interface Vehicle {
//抽象方法
function brand() public returns (bytes32);
}
4 Solidity的设计模式
4.1 安全性(Security)
智能合约编写,首要考虑的就是安全性问题。比如,外部调用可通过恶意回调,使代码被反复执行,从而破坏合约状态,这种攻击手法就是著名的Reentrance Attack(重放攻击)。下面将介绍两个可有效解除此类攻击的设计模式。
Checks-Effects-Interaction - 保证状态完整,再做外部调用
这个模式要求合约按照Checks-Effects-Interaction的顺序来组织代码。它的好处在于进行外部调用之前,Checks-Effects已完成合约自身状态所有相关工作,使得状态完整、逻辑自洽,这样外部调用就无法利用不完整的状态进行攻击了。
Mutex - 禁止递归
Mutex模式也是解决重放攻击的有效方式。它通过提供一个简单的修饰符来防止函数被递归调用.
contract Mutex {
bool locked;
modifier noReentrancy() {
//防止递归
require(!locked, "Reentrancy detected");
locked = true;
_;
locked = false;
}
//调用该函数将会抛出Reentrancy detected错误
function some() public noReentrancy{
some();
}
}
4.2 可维护性(Maintaince)
区块链中合约一旦部署,就无法更改。当需要更改合约逻辑时将面临很多问题,例如合约上的数据怎么处理,依赖的其他合约怎么办?需要将变化的事物和不变的事物相分离,以阻隔变化在系统中的传播。所以,设计良好的代码通常都组织得高度模块化、高内聚低耦合。利用这个经典的思想可解决上面的问题。
Data segregation - 数据与逻辑相分离
该模式要求一个业务合约和一个数据合约:数据合约只管数据存取,这部分是稳定的;而业务合约则通过数据合约来完成逻辑操作。
Satellite - 分解合约功能
Satellite模式运用单一职责原则解决上述问题,提倡将合约子功能放到子合约里,每个子合约(也称为卫星合约)只对应一个功能。当某个子功能需要修改,只要创建新的子合约,并将其地址更新到主合约里即可,其余功能不受影响。
Contract Registry - 跟踪最新合约
该设计模式下,会有一个专门的合约Registry跟踪子合约的每次升级情况,主合约可通过查询此Registyr合约取得最新的子合约地址。卫星合约重新部署后,新地址通过Registry.update函数来更新。
Contract Relay - 代理调用最新合约
该设计模式所解决问题与Contract Registry一样,即主合约无需暴露维护性接口就可调用最新子合约。该模式下,存在一个代理合约,和子合约享有相同接口,负责将主合约的调用请求传递给真正的子合约。卫星合约重新部署后,新地址通过update函数来更新。
4.3 生命周期(Lifecycle)
默认情况下,一个合约的生命周期近乎无限——除非赖以生存的区块链被消灭。但很多时候,用户希望缩短合约的生命周期。
Mortal - 允许合约自毁
字节码中有一个selfdestruct指令,用于销毁合约。所以只需要暴露出自毁接口即可
contract Mortal{
//自毁
function destroy() public{
selfdestruct(msg.sender);
}
}
Automatic Deprecation - 允许合约自动停止服务
如果你希望一个合约在指定期限后停止服务,而不需要人工介入,可以使用Automatic Deprecation模式。
contract AutoDeprecated{
uint private _deadline;
function setDeadline(uint time) public {
_deadline = time;
}
modifier notExpired(){
require(now <= _deadline);
_;
}
function service() public notExpired{
//some code
}
}
4.4 权限(Authorization)
许多管理性接口,如果任何人都可调用会造成严重后果,所以,一套保证只有特定账户能够访问的权限控制设计模式显得尤为重要。
Ownership
对于权限的管控,可以采用Ownership模式。该模式保证了只有合约的拥有者才能调用某些函数。首先需要有一个Owned合约:
contract Owned{
address public _owner;
constructor() {
_owner = msg.sender;
}
modifier onlyOwner(){
require(_owner == msg.sender);
_;
}
}
4.5 行为控制(Action And Control)
Oracle - 读取链外数据
链上的智能合约生态相对封闭,无法获取链外数据,影响了智能合约的应用范围。
获取外部数据会通过名为Oracle的链外数据层来执行。当业务方的合约尝试获取外部数据时,会先将查询请求存入到某个Oracle专用合约内;Oracle会监听该合约,读取到这个查询请求后,执行查询,并调用业务合约响应接口使合约获取结果。
下面定义了一个Oracle合约:
contract Oracle {
address oracleSource = 0x123; // known source
struct Request {
bytes data;
function(bytes memory) external callback;
}
Request[] requests;
event NewRequest(uint);
modifier onlyByOracle() {
require(msg.sender == oracleSource); _;
}
function query(bytes data, function(bytes memory) external callback) public {
requests.push(Request(data, callback));
emit NewRequest(requests.length - 1);
}
//回调函数,由Oracle调用
function reply(uint requestID, bytes response) public onlyByOracle() {
requests[requestID].callback(response);
}
}
业务方合约与Oracle合约进行交互:
contract BizContract {
Oracle _oracle;
constructor(address oracle){
_oracle = Oracle(oracle);
}
modifier onlyByOracle() {
require(msg.sender == address(_oracle));
_;
}
function updateExchangeRate() {
_oracle.query("USD", this.oracleResponse);
}
//回调函数,用于读取响应
function oracleResponse(bytes response) onlyByOracle {
// use the data
}
}
参考摘抄
https://blog.csdn.net/FISCO_BCOS/article/details/105619266