区块链智能合约( solidity) 安全编程

引言:本文由天玄链开源开发者提供,欢迎报名公益天玄链训练营

https://blockchain.163.com/trainingCamp

一、重入和竞态

重入和竞态在solidity 编程安全中会多次提及,历史上也造成了重大的损失。

1.1 问题分析

竞态的描述不严格,竞态是在并发编程中的概念。而以太坊智能合约是单线程运行的。不存在并发。它的问题来自于重入。

我们在实现智能合约无法做到可重入的,所以就要添加重入的保护。一般情况下,重入问题来自于fallback 或者 recevie 方法,因为以太坊转账是最常用的功能。

当然如果调用了其他不安全的合约一样有这个问题。

1.2 可重入保护

一般的解决方案,"检查-生效-交互",这个方法是现在常用的,也是推荐使用的。

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| function untrustedWithdraw(address recipient) ``public { ``uint amountToWithdraw = userBalances[recipient]; ``rewardsForA[recipient] = ``0``; ``if (!(recipient.call.value(amountToWithdraw)())) { ``throw``; } } function untrustedGetFirstWithdrawalBonus(address recipient) ``public { ``if (claimedBonus[recipient]) { ``throw``; } ``// 检查 ``claimedBonus[recipient] = ``true``;``//生效 ``rewardsForA[recipient] += ``100``; ``untrustedWithdraw(recipient); ``// 交互 } |

另外,既然它表现的像多线程并发。还有一种解决方案,就是加锁。openzeppelin 中 ReentrancyGuard合约中的nonReentrant modifier也是阻止重入的一种方法。

但是需要注意的是,这个modifier 只能对单个方法起作用,需要在所有状态相关方法上添加才能防止交叉重入。

综上,"检查-生效-交互" 推荐使用来防止重入。

1.3 显示标示untrust

在调用不安全外部合约的方法时,需要将该方法标示为untrust。同时,调用该untrust 的方法也应该是untrust。

对待untrust 方法规范是,首先处理完内部状态,最后调用untrust 方法。向外部地址转账,显然也是调用了外部合约方法,属于untrust 调用

二、安全转账

在uniswap中有很多转账的范例,比如下面的代码,将常用的ERC20 转账,注意,在safeTransferETH 中,它使用了to.call 而不是 to.transfer,因为而.send 或者 .transfer 是发送固定gas,

solidity 升级后,一些操作码的gas 可能有变化。这就导致之前的fallback 方法可能会失败。对建立在uniswap上的生态造成影响。

但是直接使用.call 有很多风险,比如要校验返回值,再比如重入攻击问题。所以尽量使用成熟的范式,是增强代码安全的有效手段。这里可能引入的重入风险,我们后面再单独分析。

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| function safeTransfer( ``address token, ``address to, ``uint256 value ) internal { ``// bytes4(keccak256(bytes('transfer(address,uint256)'))); ``(bool success, bytes memory data) = token.call(abi.encodeWithSelector(``0xa9059cbb``, to, value)); ``require( ``success && (data.length == ``0 || abi.decode(data, (bool))), ``'TransferHelper::safeTransfer: transfer failed' ``); } function safeTransferFrom( ``address token, ``address from, ``address to, ``uint256 value ) internal { ``// bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); ``(bool success, bytes memory data) = token.call(abi.encodeWithSelector(``0x23b872dd``, from, to, value)); ``require( ``success && (data.length == ``0 || abi.decode(data, (bool))), ``'TransferHelper::transferFrom: transferFrom failed' ``); } function safeTransferETH(address to, uint256 value) internal { ``(bool success, ) = to.call{value: value}(``new bytes(``0``)); ``require(success, ``'TransferHelper::safeTransferETH: ETH transfer failed'``); } |

三、以太发送方法

3.1 常用方法

常用的两种方法,send, transfer

send与transfer对比简析

相同之处

(1)均是向address发送ETH(以Wei做单位)

(2)发送的同时传输2300gas(gas数量很少,只允许接收方合约执行最简单的操作)

不同之处

(1)send执行失败返回false,transfer执行失败则会throw。这也就意味着使用send时一定要判断是否执行成功。

推荐

(1)默认情况下最好使用transfer(因为内置了执行失败的处理)

安全推荐是transfer,相信大家都知道。那么为什么transfer 更好,在于它是更高级的方法,封装了校验返回值,并抛出异常。

3.2 引申一下

我想引申一点是,在我们写方法的时候,当校验时,应该使用require 而不是 if,使用if 使得方法没有完成必要逻辑,而交易成功。这其实有隐患的。

除非你非常清楚这个差别,并认为只有if 才能满足函数需求。

比如有一个编程模式做频率检查,模式设计推荐的是

|----------------------------------------------------------------------------------------------------------------|
| modifier enabledEvery(uint t) { ``if (now >= enabledAt) { ``enabledAt = now + t; ``_; ``} } |

其实这个设计就有我上面说的问题,虽然达到了限制频率的目的,但是整个交易时成功的。正常使用就会有很多困惑,一旦被集成到其他合约,就会造成攻击的隐患。

建议是使用require,一旦失败,交易会revert,而且原因清晰。

|-----------------------------------------------------------------------------------------------------------------------------------|
| modifier enabledEvery(uint t) { ``require(now >= enabledAt,``"too freq, wait a minute"``); ``enabledAt = now + t; ``_; |

3.3 转账成功的条件

如果对方是合约地址,那么它需要具备receive 方法,或者payable 的fallback 方法。如果两者都没有则转账失败。但是从另一面说,没有这两个方法不代表不能收取以太。

这还是要从概念上区分一下。

这就引申出两个问题,

3.3.1 合约为用户转账

要考虑到失败的可能。尽量使用pull 而不是push 方法。也就是让用用户发起withdraw收取以太,而不是合约dispatch。

3.3.2 合约需要收帐的。

即便没有设置payable 的fallback,和 receive 方法,你依然不能阻止绕过收款方法,直接转账给合约,使得合约balance 跟合约内账本对不上。

所以,业务逻辑要使用自己的账本。

3.3.3 非标准ERC20 的影响

业务逻辑建立在自己的账本上就够了吗?对于有些token 虽然符合ERC 标准,但是具体实现却又做了一些改动,比如有个项目做了转账收手续费的操作,

在转账过程中收取。也就是实际到账比交易执行的要少,如果只关注交易是否成功,然后记账是造成漏洞,所以这种情况,还需要去查balance 的变化。

以上是在转账过程需要注意的,不仅要合约记录账本,还需要核对balance,以保证业务逻辑没有漏洞。

当然如果业务确信没有此类问题。可以简化,比如只涉及USDT 的入账,那么可以不考虑3.3.3。

四、慎用tx.origin

永远不要 tx.origin 用于授权,另一个合约可以有一个方法来调用你的合约(例如,用户有一些资金)并且你的合约将授权该交易,因为你的地址位于tx.origin.

contract MyContract {

    address owner;

    function MyContract() public {
        owner = msg.sender;
    }

    function sendTo(address receiver, uint amount) public {
        require(tx.origin == owner);
        (bool success, ) = receiver.call.value(amount)("");
        require(success);
    }

}

contract AttackingContract {

    MyContract myContract;
    address attacker;

    function AttackingContract(address myContractAddress) public {
        myContract = MyContract(myContractAddress);
        attacker = msg.sender;
    }

    function() public {
        myContract.sendTo(attacker, msg.sender.balance);
    }

}

这是最常见的错误。

如果我们用tx.origin 能用来干嘛?我们先引申一下。EOA 的概念。

4.1 如何判断一个账户是EOA 账户而不是contract。

一种做法是如下,很多业务是这么写的。但其实是有问题。某些情况会失效,比如constructor中。

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| /** @dev Modifier requiring sender to be EOA. This check could be bypassed by a malicious ``* contract via initcode, but it takes care of the user error we want to avoid. ``*/ modifier onlyEOA() { ``// Used to stop deposits from contracts (avoid accidentally lost tokens) ``require(!Address.isContract(msg.sender), ``"Account not EOA"``); ``_; } |

那么如何判断EOA 呢,这里就用到了tx.origin.

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ``modifier onlyEOA() { ``// Used to stop deposits from contracts (avoid accidentally lost tokens) ``require(tx.origin == msg.sender, ``"Account not EOA"``); ``_; ``} |

相关推荐
Coding~1 小时前
逆向攻防世界CTF系列56-easy_Maze
c语言·汇编·算法·安全·网络安全
鑫宝Code2 小时前
【区块链】区块链密码学基础
区块链·密码学
浩浩测试一下2 小时前
渗透测试之SQLMAP工具详解 kali自带SQLmap解释 重点sqlmap --tamper 使用方式详解 搞完你就很nice了
数据库·安全·web安全·网络安全·系统安全·安全架构
老猿讲编程3 小时前
【零成本抽象】汽车嵌入式软件开发中零成本抽象的功能安全考量与应对策略
c++·安全·汽车·零成本抽象
Hacker_Fuchen5 小时前
一些常见网络安全术语
安全·web安全·php
网络安全Ash5 小时前
网络安全-态势感知
网络·安全·web安全
运维有小邓@5 小时前
U2F和FIDO2 两种安全认证技术优劣势对比
服务器·网络·安全
小翼不想投降5 小时前
nmap详解
网络·安全·web安全
渗透测试老鸟-九青5 小时前
面试经验分享 | 杭州某安全大厂渗透测试岗
经验分享·安全·网络安全·面试·渗透·漏洞·代码审计
黑客K-ing11 小时前
从 HTTP 到 HTTPS 再到 HSTS
网络·网络协议·安全·web安全