编码规范问题
编译器版本
旧版编译器可能存在安全问题, 例如:
-
solidity 0.4.12 之前, 编译器会跳过参数中的空字符串, 比如
send(from,to,"",amount)
会被编译成send(from,to,amount)
-
V0.4.22 中, 如果合约使用了两种构造函数, 会忽略其中一个函数
contract a { function a() public{ ... } constructor() public{ ... } }
-
V0.4.25 修复了未初始化存储指针问题: https://etherscan.io/solcbuginfo
构造函数书写问题
参考上述第 2 点
返回标准
遵循 ERC20 规范, 要求 transfer、approve 函数应返回 bool 值, 需要添加返回值代码
function transfer(address _to, uint256 _value) public returns (bool success)
transferFrom 返回结果应该和 transfer 返回结果一致
事件标准
遵循 ERC20 规范, 要求 transfer、approve 函数触发相应的事件
function approve(address _spender, uint256 _value) public returns (bool success){
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value)
return true
假充值问题
转账函数中,对余额以及转账金额的判断,需要使用 require 函数抛出错误,否则会错误 的判断为交易成功
function transfer(address _to, uint256 _amount) public returns (bool success) {
// 使用require判断输入
require(_to != address(0));
require(_amount <= balances[msg.sender]);
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = balances[_to].add(_amount);
emit Transfer(msg.sender, _to, _amount);
return true;
}
设计缺陷问题
approve 函数条件竞争
使用 approve 授权函数修改 allowance 前, 需要先将 value 置零, 否则可以通过更高的 gas price 抢先使用代币
//判断_value是否为0
require((_value == 0) || (allowance[msg.sender][_spender] == 0));
循环 Dos 问题
循环消耗问题
循环次数越大, 交易消耗的 gas 越多, 当超过 gas 限制时会导致交易失败
案例:
- Simoleon(SIM): https://paper.seebug.org/646/
- Pandemica: https://bcsec.org/index/detail/id/260/tag/2
循环安全问题
避免让用户控制循环次数, 否则容易受到 Dos 攻击, 比如以下代码的_addresses.length 由用户所控制:
function Distribute(address[] _addresses, uint256[] _values) payable returns(bool){
for (uint i = 0; i < _addresses.length; i++) {
transfer(_addresses[i], _values[i]);
}
return true;
}
编码安全问题
溢出问题
-
算术溢出, 应该使用 SafeMath 库进行运算
-
铸币烧币溢出, 应该设置 totalSupply 的上限
重入漏洞
在智能合约中提供了 call、send、transfer 三种方式来交易以太坊,其中 call 最大的区
别就是 没有限制 gas ,而其他两种在 gas 不够的情况下都会报 out of gas
重入漏洞有以下特征:
- 使用了 call 函数作为转账函数
- 没有限制 call 函数的 gas
- 扣余额在转账之后
- call 时加入了
()
来执行 fallback 函数
demo:
function withdraw(uint _amount) {
require(balances[msg.sender] >= _amount);
// 转账给msg.sender
// 并会执行msg.sender的fallback函数(如果是合约账户)
// 此时balances[msg.sender]还没减少
// 如果fallback函数继续调用当前withdraw函数, 将成功重入
msg.sender.call.value(_amount)();
balances[msg.sender] -= _amount;
}
通过互斥锁避免递归:
contract EtherStore {
// initialise the mutex
bool reEntrancyMutex = false;
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(!reEntrancyMutex);
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
// set the reEntrancy mutex before the external call
reEntrancyMutex = true;
msg.sender.transfer(_weiToWithdraw);
// release the mutex after the external call
reEntrancyMutex = false;
}
}
call 注入
call 函数调用时,应该做严格的权限控制,或直接写死 call 调用的函数
案例:
权限控制
检查合约中各函数是否正确使用了 public、private 等关键词进行可见性修饰,检查合约是 否正确定义并使用了 modifier 对关键函数进行访问限制,避免越权导致的问题
以下函数不该为 public:
function initContract() public {
owner = msg.sender;
}
案例:
重放攻击
合约中如果涉及委托管理的需求,应注意验证的不可复用性,避免重放攻击
这里举例子为 transferProxy 函数,该函数用于当 user1 转 token 给 user3,但没有 eth 来支付 gasprice,所以委托 user2 代理支付,(user2)通过调用 transferProxy 来完成
function transferProxy(address _from, address _to, uint256 _value, uint256 _fee,
uint8 _v, bytes32 _r, bytes32 _s) public returns (bool){
if(balances[_from] < _fee + _value
|| _fee > _fee + _value) revert();
uint256 nonce = nonces[_from];
bytes32 h = keccak256(_from,_to,_value,_fee,nonce,address(this));
if(_from != ecrecover(h,_v,_r,_s)) revert();
if(balances[_to] + _value < balances[_to]
|| balances[msg.sender] + _fee < balances[msg.sender]) revert();
balances[_to] += _value;
emit Transfer(_from, _to, _value);
balances[msg.sender] += _fee;
emit Transfer(_from, msg.sender, _fee);
balances[_from] -= _value + _fee;
nonces[_from] = nonce + 1;
return true;
}
这个函数的问题在于 nonce 值是可以预判的,其他变量不变的情况下,可以进行重放攻击, 多次转账
编码设计问题
地址初始化
涉及到地址的函数中,建议加入 require(_to!=address(0))
验证,有效避免用户误操作或
未知错误导致的不必要的损失
判断函数
涉及到条件判断的地方,使用 require 函数而不是 assert 函数,因为 assert 会导致剩余的 gas 全部消耗掉,而他们在其他方面的表现都是一致的
余额判断
不要假设合约创建时余额为 0,可以强制转账
攻击者可以用 1wei 来创建合约,然后调用 selfdestruct(victimAddress)
来销毁,这样余
额就会强制转移给目标,而且目标合约没有代码执行,无法阻止
转账函数
在完成交易时,默认情况下推荐使用 transfer 而不是 send 完成交易
当 transfer 或者 send 函数的目标是合约时,会调用合约的 fallback 函数。但 fallback 函数执 行失败时, transfer 会抛出错误并自动回滚,而 send 会返回 false ,所以在使用 send 时需 要判断返回类型,否则可能会导致转账失败但余额减少的情况
代码外部调用设计
对于外部合约优先使用 pull 而不是 push
错误样例:
contract auction {
address highestBidder;
uint highestBid;
function bid() payable {
if (msg.value < highestBid) throw;
if (highestBidder != 0) {
if (!highestBidder.send(highestBid)) { // 可能会发生错误
throw;
}
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
当需要向某一方转账时,将转账改为定义 withdraw 函数,让用户自己来执行合约将余额取出, 这样可以最大程度的避免未知的损失
范例代码:
contract auction {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
function bid() payable external {
if (msg.value < highestBid) throw;
if (highestBidder != 0) {
refunds[highestBidder] += highestBid; // 记录在refunds中
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawRefund() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
if (!msg.sender.send(refund)) {
refunds[msg.sender] = refund; // 如果转账错误还可以挽回
}
}
}
错误处理
合约中涉及到 call 等在 address 底层操作的方法时,做好合理的错误处理
操作如果遇到错误并不会抛出异常,而是会返回 false 并继续执行:
address.call()
address.callcode()
address.delegatecall()
address.send()
弱随机数
合约中要保证生成的随机数无法被猜测
案例:
- 8万笔交易「封死」以太坊网络,只为抢夺Fomo3D大奖?
- https://paper.seebug.org/672/
- https://www.reddit.com/r/ethereum/comments/916xni/how_to_pwn_fomo3d_a_beginners_guide/
变量覆盖
在合约中避免 array 变量 key 可以被控制
编码问题隐患
语法特性
在智能合约中,所有的整数除法都会向下取整到最接近的整数,当我们需要更高的精度时, 我们需要使用乘数来加大这个数字
错误样例:
uint x = 5 / 2; // 2
正确代码:
uint multiplier = 10;
uint x = (5 * multiplier) / 2;
数据隐私
在合约中,所有的数据包括私有变量都是公开的,不可以将任何有私密性的数据储存在链上
数据可靠性
合约中不应该让时间戳参与到代码中,容易受到矿工的干扰,应使用 block.height 等不变的 数据
gas 消耗优化
对于某些不涉及状态变化的函数和变量可以加 constant 来避免 gas 的消耗
合约用户
合约中,应尽量考虑交易目标为合约时的情况,避免因此产生的各种恶意利用
日志记录
关键事件应有 Event 记录,为了便于运维监控,除了转账,授权等函数以外,其他操作也需 要加入详细的事件记录,如转移管理员权限、其他特殊的主功能