回顾 3 个底层调用 call()
, delegatecall()
, callcode()
和 3 个转币函数 call.value()()
, send()
, transfer()
:
- call()
call()
用于 Solidity 进行外部调用,例如调用外部合约函数 <address>.call(bytes4(keccak("somefunc(params)"), params))
,外部调用 call()
返回一个 bool 值来表明外部调用成功与否:
- delegatecall()
除了 delegatecall()
会将外部代码作直接作用于合约上下文以外,其他与 call()
一致,同样也是只能获取一个 bool 值来表示调用成功或者失败(发生异常)。
- callcode()
callcode()
其实是 delegatecall()
之前的一个版本,两者都是将外部代码加载到当前上下文中进行执行,但是在 msg.sender
和 msg.value
的指向上却有差异。
例如 Alice 通过 callcode()
调用了 Bob 合约里同时 delegatecall()
了 Wendy 合约中的函数,这么说可能有点抽象,看下面的代码:
如果还是不明白 callcode()
与 delegatecall()
的区别,可以将上述代码在 remix-ide 里测试一下,观察两种调用方式在 msg.sender
和 msg.value
上的差异。
- call.value()()
在合约中直接发起 TX 的函数之一(相当危险),
- send()
通过 send()
函数发送 Ether 失败时直接返回 false;这里需要注意的一点就是,send()
的目标如果是合约账户,则会尝试调用它的 fallbcak() 函数,fallback() 函数中执行失败,send()
同样也只会返回 false。但由于只会提供 2300 Gas 给 fallback() 函数,所以可以防重入漏洞(恶意递归调用)。
- transfer()
transfer()
也可以发起 Ether 交易,但与 send()
不同的时,transfer()
是一个较为安全的转币操作,当发送失败时会自动回滚状态,该函数调用没有返回值。同样的,如果 transfer()
的目标是合约账户,也会调用合约的 fallback() 函数,并且只会传递 2300 Gas 用于 fallback() 函数执行,可以防止重入漏洞(恶意递归调用)。
这里以一个简单的示例来说明严格验证底层调用返回值的重要性:
function withdraw(uint256 _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount;
etherLeft -= _amount;
msg.sender.send(_amount); // 未验证 send() 返回值,若 msg.sender 为合约账户 fallback() 调用失败,则 send() 返回 false
}
上面给出的提币流程中使用 send()
函数进行转账,因为这里没有验证 send()
返回值,如果 msg.sender 为合约账户 fallback() 调用失败,则 send() 返回 false,最终导致账户余额减少了,钱却没有拿到。
这里先提供一些链接便于参考学习:
https://news.bitcoinworld.com/a/2599?locale=zh_HANT
https://github.com/ConsenSys/smart-contract-best-practices/blob/master/README-zh.md
后续会有更多有关细节方面的介绍......