dex币|以太坊技术 | Solidity 函数修改器以及异常处理
摘要:链闻 ChainNews:Solidity 是以太坊上最主流的函数修改器。本篇文章从技术角度展示了如何针对异常情况在代码层面进行详细分析,最后总结出异常情况最易
链闻 ChainNews:
Solidity 是以太坊上最主流的函数修改器。本篇文章从技术角度展示了如何针对异常情况在代码层面进行详细分析,最后总结出异常情况最易出现的一些场景,并给出相对应的建议。
来源 | 矩阵数字经济智库
作者 | 闫莺等
函数修改器
Solidity 提供了一个函数修改器(Function Modifiers)的特性。
函数修改器与 Python 中的装饰器类似,可以在一定程度上改变函数的行为,比如可以自动在函数执行前检查参数是否合法。函数修改器是可以被继承的,同时可以被派生类覆盖重写。
下面代码展示了如何声明并使用函数修改器。
contract owned { function owned() { owner = msg.sender; } address owner; // 这个合约定义了一个在派生合约中使用的函数修改器 // ";" 指代被修改函数的函数体。 // 当这个函数执行前,先检查 msg.sender 是否是合约创建者 // 如果不是就会抛出异常 modifier onlyOwner { require(msg.sender == owner);; } } contract Contract is owned { // 从 owned 合约继承了 onlyOwner 函数修改器并且将其作用于 close 函数 // 确保了这个函数只有在调用者为合约创建者时才会生效 function close() onlyOwner { selfdestruct(owner); } }
下面代码进一步展示了函数修改器是如何接收参数的,函数修改器的参数可以是上下文中存在的任意变量组成的表达式。
contract priced { // 函数修改器可以接收参数 modifier costs(uint price) { if (msg.value >= price) {_; } } } contract Register is priced, owned { mapping (address => bool) registeredAddresses; uint price; function Register(uint initialPrice) { price = initialPrice; } // 这里需要 payable 修饰词,否则无法通过该方法转账以太币 // 函数修饰器 costs 接收参数 price // 使用 costs 修饰器确保 registe 函数在 msg.val 大于 price 时才会生效 function registe() payable costs(price) { registeredAddresses[msg.sender] = true; } function changePrice(uint_price) onlyOwner { price =price; } }
下面的例子展示了如何使用函数修改器实现一个重入锁机制。
contract Mutex { bool locked; modifier noReentrancy() { require(!locked); locked = true;; locked = false; } // 这个函数使用了 noReentrancy 修改器,这保证了在 f 函数内部无法再次调用 f 函数 // 在执行 return 7 时也执行了函数修改器中的 locked = false 语句 function f() noReentrancy returns (uint) { require(msg.sender.call()); return 7; } } 异常处理 以太坊使用状态回退机制处理异常。如果发生了异常,当前消息调用和子消息调用产生的所有状态变化都将被撤销并且返回调用者一个报错信号。Solidity 语言提供了两个方便的函数 assert 和 require 来检查条件,并且当条件不满足的时候抛出一个异常。assert 函数通常用于检查变量和内部错误,require 函数用于确保程序执行的必要条件是成立的。一个正常运行的程序不应该遇到 assert 和 require 失败,否则程序代码中一定存在需要修复的问题。 revert 函数和 throw 关键词会标识发生了错误并且回退当前的消息调用产生的状态改变。 当前调用收到子消息调用产生的异常时会自动抛出,所以异常会一层层上浮直到最上层的根调用,代码会立刻终止执行并回退状态改变。但是, .send、call、delegatecall 是例外,这些函数在执行过程中抛出异常会返回 false,而不是自动抛出异常。 下面的例子展示了如何使用 assert 和 require 确保程序正确运行。 contract AssertExample { function sendHalf(address addr) payable returns (uint balance) { require(msg.value % 2 == 0); // 只允许偶数 uint balanceBeforeTransfer = this.balance; addr.transfer(msg.value / 2); // 使用 assert 确保 transfer 转账成功,否则抛出异常 assert(this.balance == balanceBeforeTransfer - msg.value / 2); return this.balance; } }
一个 assert 类型的异常会在下述场景抛出:
- 访问数组越界,下标为负数或者超出长度。
- 访问固定长度的 bytesN 越界,下标为负数或者超出长度。
- 对 0 做除法或者对 0 取模,比如 5/0,5%0。
- 移位操作使用了一个负数做操作数。
- 转换一个特别大的数或者负数到枚举类型变量。
- 使用移位操作时给一个负数值。
- 使用枚举时将过大值或赋值转为枚举类型变量。
- 调用 assert 函数并且参数值为 false。
一个 require 类型的异常会在下述场景抛出:
- 调用 throw。
- 调用 require 并且参数值为 false。
- 发起一个消息调用,但是这个调用没有正常完成,比如 Gas 耗尽、被调用函数不存在或者函数本身抛出一个异常。(
- .send、call、delegatecall 例外)
- 使用 new 创建一个合约,但是和 3 中提到的原因一样构造函数没有正常完成。
- 调用外部函数时指向一个不包含代码的地址。
- 合约通过一个没有 payable 修辞词的函数接收以太币,包括构造函数和 fallback 函数。
- 合约通过一个公开的访问函数接收以太币。
- .transfer() 失败。
在 require 类型的异常发生时会执行回退操作(指令号 0xfd),对于一个 assert 类型的异常执行一个无效操作(指令号 0xfe)。在这两种情况下,以太坊虚拟机都会撤销所有的状态改变。这样做是因为发生了意料之外的情况,交易无法安全执行下去,为了保证交易的原子性,最安全的操作就是撤销该交易对状态造成的影响。
在编写合约代码时,我们需要合理使用 assert 和 require 来保证代码能够按我们预期的设计进行。
更多精彩内容,关注链闻 ChainNews 公众号(id:chainnewscom),或者来微博 **@ 链闻 ChainNews**与我们互动!转载请注明版权和原文链接!
- 免责声明
- 世链财经作为开放的信息发布平台,所有资讯仅代表作者个人观点,与世链财经无关。如文章、图片、音频或视频出现侵权、违规及其他不当言论,请提供相关材料,发送到:2785592653@qq.com。
- 风险提示:本站所提供的资讯不代表任何投资暗示。投资有风险,入市须谨慎。
- 世链粉丝群:提供最新热点新闻,空投糖果、红包等福利,微信:juu3644。

链闻研究院



