目录

  • 一、智能合约的永固性
  • 二、OpenZeppelin库的Ownable合约
  • 三、函数修饰符modifier
  • 四、Gas - 驱动以太坊DApps的能源
  • 五、省 gas :结构封装 (Struct packing)
  • 六、时间单位
  • 七、将结构体作为参数传入
  • 八、公共函数和安全性
  • 九、带参数的函数修饰符
  • 十、利用 'View' 函数节省 Gas
  • 十一、 在内存中声明数组
  • 十二、for循环节省gas

一、智能合约的永固性

在你把智能协议传上以太坊之后,它就变得不可更改, 这种永固性意味着你的代码永远不能被调整或更新。

你编译的程序会一直,永久的,不可更改的,存在以太坊上。这就是 Solidity 代码的安全性如此重要的一个原因。如果你的智能协议有任何漏洞,即使你发现了也无法补救。你只能让你的用户们放弃这个智能协议,然后转移到一个新的修复后的合约上。

但这恰好也是智能合约的一大优势。代码说明一切。如果你去读智能合约的代码,并验证它,你会发现,一旦函数被定义下来,每一次的运行,程序都会严格遵照函数中原有的代码逻辑一丝不苟地执行,完全不用担心函数被人篡改而得到意外的结果。

二、OpenZeppelin库的Ownable合约

下面是一个 Ownable 合约的例子: 来自 _ OpenZeppelin _ Solidity 库的 Ownable 合约。 OpenZeppelin 是主打安保和社区审查的智能合约库,您可以在自己的 DApps中引用。

/*** @title Ownable* @dev The Ownable contract has an owner address, and provides basic authorization control* functions, this simplifies the implementation of "user permissions".*/
contract Ownable {address public owner;event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);/*** @dev The Ownable constructor sets the original `owner` of the contract to the sender* account.*/function Ownable() public {owner = msg.sender;}/*** @dev Throws if called by any account other than the owner.*/modifier onlyOwner() {require(msg.sender == owner);_;}/*** @dev Allows the current owner to transfer control of the contract to a newOwner.* @param newOwner The address to transfer ownership to.*/function transferOwnership(address newOwner) public onlyOwner {require(newOwner != address(0));OwnershipTransferred(owner, newOwner);owner = newOwner;}
}

Ownable 合约基本都会这么干:

合约创建,构造函数先行,将其 owner 设置为msg.sender(其部署者)

为它加上一个修饰符 onlyOwner,它会限制陌生人的访问,将访问某些函数的权限锁定在 owner 上。

允许将合约所有权转让给他人。

onlyOwner 简直人见人爱,大多数人开发自己的 Solidity DApps,都是从复制/粘贴 Ownable 开始的,从它再继承出的子类,并在之上进行功能开发。

三、函数修饰符modifier

函数修饰符看起来跟函数没什么不同,不过关键字modifier 告诉编译器,这是个modifier(修饰符),而不是个function(函数)。它不能像函数那样被直接调用,只能被添加到函数定义的末尾,用以改变函数的行为。

咱们仔细读读 onlyOwner:

/*** @dev 调用者不是‘主人’,就会抛出异常*/
modifier onlyOwner() {require(msg.sender == owner);_;
}

onlyOwner 函数修饰符是这么用的:

contract MyContract is Ownable {event LaughManiacally(string laughter);//注意! `onlyOwner`上场 :function likeABoss() external onlyOwner {LaughManiacally("Muahahahaha");}
}

注意 likeABoss 函数上的 onlyOwner 修饰符。 当你调用 likeABoss 时,首先执行 onlyOwner 中的代码, 执行到 onlyOwner 中的 _; 语句时,程序再返回并执行 likeABoss 中的代码。

可见,尽管函数修饰符也可以应用到各种场合,但最常见的还是放在函数执行之前添加快速的 require检查。

因为给函数添加了修饰符 onlyOwner,使得唯有合约的主人(也就是部署者)才能调用它。

注意:主人对合约享有的特权当然是正当的,不过也可能被恶意使用。比如,万一,主人添加了个后门,允许他偷走别人的代币呢?
所以非常重要的是,部署在以太坊上的 DApp,并不能保证它真正做到去中心,你需要阅读并理解它的源代码,才能防止其中没有被部署者恶意植入后门;作为开发人员,如何做到既要给自己留下修复 bug 的余地,又要尽量地放权给使用者,以便让他们放心你,从而愿意把数据放在你的 DApp 中,这确实需要个微妙的平衡。

四、Gas - 驱动以太坊DApps的能源

在 Solidity 中,你的用户想要每次执行你的 DApp 都需要支付一定的 gas,gas 可以用以太币购买,因此,用户每次跑 DApp 都得花费以太币。

一个 DApp 收取多少 gas 取决于功能逻辑的复杂程度。每个操作背后,都在计算完成这个操作所需要的计算资源,(比如,存储数据就比做个加法运算贵得多), 一次操作所需要花费的 gas 等于这个操作背后的所有运算花销的总和。

由于运行你的程序需要花费用户的真金白银,在以太坊中代码的编程语言,比其他任何编程语言都更强调优化。同样的功能,使用笨拙的代码开发的程序,比起经过精巧优化的代码来,运行花费更高,这显然会给成千上万的用户带来大量不必要的开销。

五、省 gas :结构封装 (Struct packing)

在第1课中,我们提到除了基本版的 uint 外,还有其他变种 uint:uint8,uint16,uint32等。

通常情况下我们不会考虑使用 uint 变种,因为无论如何定义 uint的大小,Solidity 为它保留256位的存储空间。例如,使用 uint8 而不是uint(uint256)不会为你节省任何 gas。

除非,把 uint 绑定到 struct 里面。

如果一个 struct 中有多个 uint,则尽可能使用较小的 uint, Solidity 会将这些 uint 打包在一起,从而占用较少的存储空间。例如:

struct NormalStruct {uint a;uint b;uint c;
}struct MiniMe {uint32 a;uint32 b;uint c;
}// 因为使用了结构打包,`mini` 比 `normal` 占用的空间更少
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);

所以,当 uint 定义在一个 struct 中的时候,尽量使用最小的整数子类型以节约空间。 并且把同样类型的变量放一起(即在 struct 中将把变量按照类型依次放置),这样 Solidity 可以将存储空间最小化。例如,有两个 struct:

uint c; uint32 a; uint32 b; 和 uint32 a; uint c; uint32 b;

前者比后者需要的gas更少,因为前者把uint32放一起了。

六、时间单位

Solidity 使用自己的本地时间单位。

变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。

注意:Unix时间传统用一个32位的整数进行存储。这会导致“2038年”问题,当这个32位的unix时间戳不够用,产生溢出,使用这个时间的遗留系统就麻烦了。所以,如果我们想让我们的 DApp 跑够20年,我们可以使用64位整数表示时间,但为此我们的用户又得支付更多的 gas。真是个两难的设计!

Solidity 还包含秒(seconds),分钟(minutes),小时(hours),天(days),周(weeks) 和 年(years) 等时间单位。它们都会转换成对应的秒数放入 uint 中。所以 1分钟 就是 60,1小时是 3600(60秒×60分钟),1天是86400(24小时×60分钟×60秒),以此类推。
下面是一些使用时间单位的实用案例:

uint lastUpdated;// 将‘上次更新时间’ 设置为 ‘现在’
function updateTimestamp() public {lastUpdated = now;
}// 如果到上次`updateTimestamp` 超过5分钟,返回 'true'
// 不到5分钟返回 'false'
function fiveMinutesHavePassed() public view returns (bool) {return (now >= (lastUpdated + 5 minutes));
}

七、将结构体作为参数传入

由于结构体的存储指针可以以参数的方式传递给一个 private 或 internal 的函数,因此结构体可以在多个函数之间相互传递。

遵循这样的语法:

function _doStuff(Zombie storage _zombie) internal {// do stuff with _zombie
}

这样我们可以将某僵尸的引用直接传递给一个函数,而不用是通过参数传入僵尸ID后,函数再依据ID去查找。

八、公共函数和安全性

你必须仔细地检查所有声明为 public 和 external的函数,一个个排除用户滥用它们的可能,谨防安全漏洞。请记住,如果这些函数没有类似 onlyOwner 这样的函数修饰符,用户能利用各种可能的参数去调用它们。
检查完这个函数,用户就可以直接调用这个它,并传入他们所需要的参数。仔细观察,如果这个函数只需被 同个合约中的函数 调用,因此,想要防止漏洞,最简单的方法就是设其可见性为 internal。

九、带参数的函数修饰符

之前我们已经读过一个简单的函数修饰符了:onlyOwner。函数修饰符也可以带参数。例如:

// 存储用户年龄的映射
mapping (uint => uint) public age;// 限定用户年龄的修饰符
modifier olderThan(uint _age, uint _userId) {require(age[_userId] >= _age);_;
}// 必须年满16周岁才允许开车 (至少在美国是这样的).
// 我们可以用如下参数调用`olderThan` 修饰符:
function driveCar(uint _userId) public olderThan(16, _userId) {// 其余的程序逻辑
}

看到了吧, olderThan 修饰符可以像函数一样接收参数,是“宿主”函数 driveCar 把参数传递给它的修饰符的。

十、利用 ‘View’ 函数节省 Gas

当玩家从外部调用一个view函数,是不需要支付一分 gas 的。

这是因为 view 函数不会真正改变区块链上的任何数据 - 它们只是读取。因此用 view 标记一个函数,意味着告诉 web3.js,运行这个函数只需要查询你的本地以太坊节点,而不需要在区块链上创建一个事务(事务需要运行在每个节点上,因此花费 gas)。

稍后我们将介绍如何在自己的节点上设置 web3.js。但现在,你关键是要记住,在所能只读的函数上标记上表示“只读”的“external view 声明,就能为你的玩家减少在 DApp 中 gas 用量。

注意:如果一个 view 函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的。

十一、 在内存中声明数组

Solidity 使用storage(存储)是相当昂贵的,”写入“操作尤其贵。

这是因为,无论是写入还是更改一段数据, 这都将永久性地写入区块链。”永久性“啊!需要在全球数千个节点的硬盘上存入这些数据,随着区块链的增长,拷贝份数更多,存储量也就越大。这是需要成本的!

为了降低成本,不到万不得已,避免将数据写入存储。有时这涉及到看似低效的编程逻辑 - 比如每次调用一个函数,都需要在 memory(内存) 中重建一个数组,而不是简单地将上次计算的数组给存储下来以便快速查找。

在大多数编程语言中,遍历大数据集合都是昂贵的。但是在 Solidity 中,使用一个标记了external view的函数,遍历比 storage 要便宜太多,因为 view 函数不会产生任何花销。 (gas可是真金白银啊!)。
在数组后面加上 memory关键字, 表明这个数组是仅仅在内存中创建,不需要写入外部存储,并且在函数调用结束时它就解散了。与在程序结束时把数据保存进 storage 的做法相比,内存运算可以大大节省gas开销 – 把这数组放在view里用,完全不用花钱。

以下是申明一个内存数组的例子:

function getArray() external pure returns(uint[] memory) {// Instantiate a new array in memory with a length of 3uint[] memory values = new uint[](3);// Put some values to itvalues[0] = 1;values[1] = 2;values[2] = 3;return values;
}

注意:内存数组必须使用长度参数创建(在本例中为3)。它们目前无法像存储阵列那样调整大小array.push(),尽管这可能会在 Solidity 的未来版本中改变。

十二、for循环节省gas

一个僵尸游戏,如果我们添加一个将僵尸从一个所有者转移到另一个所有者的函数会发生什么。

该传递函数需要:

1.将僵尸推到新主人的ownerToZombies阵列中,
2.从旧主人的ownerToZombies阵列中移除僵尸,
3.将老主人阵列中的每个僵尸向上移动一个位置以填充空白,然后
4.将数组长度减 1。
第 3 步在 gas 方面将是极其昂贵的,因为我们必须为每个我们改变了位置的僵尸写一个。如果所有者有 20 个僵尸并交易掉第一个,我们将不得不执行 19 次写入来维护数组的顺序。

由于写入存储是 Solidity 中最昂贵的操作之一,因此每次调用此传输函数在 gas 方面都是极其昂贵的。更糟糕的是,每次调用它会花费不同数量的gas,具体还取决于用户在原主军团中的僵尸头数,以及移走的僵尸所在的位置。所以用户不知道要发送多少gas。

注意:当然,我们也可以把数组中最后一个僵尸往前挪来填补空槽,并将数组长度减少一。但这样每做一笔交易,都会改变僵尸军团的秩序。

solidity修饰符、节省gas技巧相关推荐

  1. Vue常见的事件修饰符

    前言 vue一共给我们准备了6个事件修饰符,前三个比较常用,后三个少见,这里着重讲下前三个 1.prevent:阻止默认事件(常用) 2. stop:阻止事件冒泡(常用) 3. once:事件只触发一 ...

  2. 死磕solidity之如何有效的节省gas.md

    为什么要强调优化gas的重要性 DAPP中收取的费用取决于功能逻辑的复杂程度,越复杂消耗的计算资源越多.并且需要用户承担一部分gas,所以solidity 的优化显得非常的重要.同时注重优化gas的合 ...

  3. .NET架构小技巧(2)——访问修饰符正确姿势

    在C#中,访问修饰符是使用频率很高的一组关键字,一共四个单词六个组合:public,internal,protected internal,protected,private protected,pr ...

  4. Solidity语言学习笔记————20、函数修饰符

    函数修饰符(Function Modifiers) 修饰符可以用来轻松改变函数的行为,比如在执行的函数之前自动检查条件.他们可继承合约的属性,也可被派生的合约重写. pragma solidity ^ ...

  5. solidity:4.函数可见性与修饰符

    一. 函数可见性 public - 支持内部或外部调用 private - 仅在当前合约调用,且不可被继承 internal- 只支持内部调用 external - 不支持内部调用 // SPDX-L ...

  6. 以太坊知识教程------智能合约(3)函数修饰符

    1. solidity的五个关键字修饰符 操作 定义 public 用来修饰公开的函数 /变量,表明该函数/变量既可以在合约外部访问,也可以在合约内部访问 . private 私有函数和变量,只有当前 ...

  7. python修饰符作用_python函数修饰符@的使用

    python函数修饰符@的作用是为现有函数增加额外的功能,常用于插入日志.性能测试.事务处理等等. 创建函数修饰符的规则: (1)修饰符是一个函数 (2)修饰符取被修饰函数为参数 (3)修饰符返回一个 ...

  8. 关于Java中各种修饰符与访问修饰符的说明

    关于Java中各种修饰符与访问修饰符的说明 类: 访问修饰符  修饰符  class 类名称 extends 父类名称 implement 接口名称 (访问修饰符与修饰符的位置可以互换) 访问修饰符 ...

  9. JAVA修饰符类型(public,protected,private,friendly)

    JAVA修饰符类型(public,protected,private,friendly) public的类.类属变量及方法,包内及包外的不论什么类均能够訪问: protected的类.类属变量及方法, ...

最新文章

  1. 多个不同的app应用间应该如何进行消息推送呢?
  2. Qt实现应用程序单实例运行--LocalServer方式
  3. Python 实例方法,类方法和静态方法的区别
  4. python爬取酷狗音乐top500_python获取酷狗音乐top500的下载地址 MP3格式
  5. ThreadLocal中的3个大坑,内存泄露都是小儿科!
  6. android10手机众筹,最小Android 10手机?屏幕仅3英寸的Jelly 2开始众筹
  7. CoinList将销售价值4000万美元的社交代币RLY
  8. jieba结巴分词--关键词抽取_初学者 | 知否?知否?一文学会Jieba使用方法
  9. 圆柱与平面接触宽度_圆柱滚子轴承保持架锁爪变形引起的轴承故障
  10. 统计学中常见的分布汇总及相关概念
  11. python里我最容易搞不清楚问题之一的encode和decode
  12. VS2010_x86_编译错误
  13. python 通过模板生成文章_自动生成文章_python自动生成文章 - 云+社区 - 腾讯云
  14. 解决octavia failed to run
  15. [1] UI原型设计工具Pencil Project 学习系列----- 为什么选择
  16. Mysql和Oracle实现序列自增
  17. Intellij IDEA 2019 英文界面乱码问题解决
  18. 163邮箱登录入口大全,你知道163邮箱登录入口怎么登录吗?
  19. 移动端查看预览图片放大缩小
  20. PLSQL 与 PLPGSQL

热门文章

  1. Linux中关闭进程
  2. 给应届毕业生的一些话
  3. Linux sudo命令和sudoers文件
  4. 分享一套微信门户应用管理系统源码 微信公众号平台开发框架源码
  5. java里引用css没反应,解决HTML外部引用CSS文件不生效问题
  6. Linux系统chmod,chown,chgrp和setfacl的区别
  7. 【泛微E9开发】第三方移动端集成Emobile7(2)
  8. Excel利用VBA实现去掉单元格两边空格
  9. js 封装ajax方法吗,原生JS封装ajax方法
  10. 计算机中磁盘与硬盘的区别,电脑内存和硬盘的区别