之前分析过合约创建的过程
构造函数是一个特殊的函数,它只在合约部署的时候执行一次,部署之后的字节码是不包含构造函数逻辑的。在构造函数执行时,合约还没有被创建完成,那么此时如果访问这个合约的变量/函数,会发生什么事情呢?
在构造函数里,能否获取合约地址?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TestConstructor {
address public addr;
constructor() {
addr = address(this);
}
}
在remix里部署后我们可以发现,addr的值的确是合约的地址,这说明在构造函数里,可以获取合约地址。
合约的部署实际上有两种方式,一种是传统的CREATE,一种是后来新增的CREATE2.
对于CREATE,地址计算方式为:keccak256(rlp([sender, nonce]))
而对于CREATE2,地址计算方式为:keccak256( 0xff ++ sender ++ salt ++ keccak256(init_code))
这些并不需要等待合约部署完毕才能计算,因此构造函数里可以正确取得合约地址。
在构造函数里,能否获取合约地址的code和codehash?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TestConstructor {
bytes public code0;
bytes public code1;
bytes32 public codehash0;
bytes32 public codehash1;
constructor() {
code0 = address(this).code;
codehash0 = address(this).codehash;
}
function run() external {
code1 = address(this).code;
codehash1 = address(this).codehash;
}
}
我们可以看到,在构造函数中,获得的code为空,相应的codehash显然也是错误的,实际上构造函数中获取的codehash是空串的hash。
如果你经常看代码的话,可能看见下面这个用来判断一个地址是否为合约地址的函数:
function isContract(address account) internal view returns (bool) {
return account.code.length > 0;
}
实际上,这个代码是不可靠的,只有在一个合约已经完成构造之后、被selfdestruct之前,判断才会有效。某个code为空的地址,可能目前还没有合约但将来会被部署合约(合约地址可以预计算)、可能有合约正在构造、可能曾经被部署过合约但已经被销毁。
所以,千万不可以依赖这个方法预防合约地址的攻击。
在构造的时候,向合约转账,会触发合约的receive/fallback吗?
我们有如下两个合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VisitWhenConstructing {
constructor() payable {}
function sendEther() external {
(bool result,) = (msg.sender).call{value: address(this).balance / 2}("");
require(result, "sendEther fail!");
}
}
contract TestConstructor {
// 下面这个地址是VisitWhenConstructing的部署地址
address constant visit = 0x7EF2e0048f5bAeDe046f6BF797943daF4ED8CB47;
event Receive(uint);
event Fallback(uint);
constructor() {
VisitWhenConstructing(visit).sendEther();
}
function run() external {
VisitWhenConstructing(visit).sendEther();
}
receive() payable external {
emit Receive(msg.value);
}
fallback() payable external {
emit Fallback(msg.value);
}
}
我们首先部署VisitWhenConstructing,给它赋予1ETH。
接下来部署TestConstructor,观察会发生什么。
我们可以看出,转账成功了,TestConstructor的确收到了ETH,但是日志空空如也,说明receive和fallback里的日志都没有被触发。
其实也很好理解,我们上面说过,在构造时地址的code为空,此时没有办法判断该地址是合约,因此该地址此时会被当成EOA地址,当然不会触发receive或fallback。
在部署完成之后,我们运行run。
可见此时转账会正常触发receive函数,因为定义了receive,所以不会触发fallback了。
在构造函数中,是否可以调用合约中的其他函数?
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VisitWhenConstructing {
function callSender() external {
(bool result,) = (msg.sender).call(abi.encodeWithSignature("fun2()"));
require(result, "call fail!");
// TestConstructor(msg.sender).fun2(); // revert
}
}
contract TestConstructor {
// 下面这个地址是VisitWhenConstructing的部署地址
address constant visit = 0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B;
event Fun1();
event Fun2();
constructor() {
fun1();
VisitWhenConstructing(visit).callSender();
}
function fun1() internal {
emit Fun1();
}
function fun2() external {
emit Fun2();
}
}
调用函数有两种方式,一种是内部调用,一种是外部调用,我们分别用fun1和fun2来验证。
我们看一下日志可以发现,只有内部调用的fun1被执行了,外部调用的fun2并没有被执行。
这个原理应该和刚才的转账逻辑一样,外部执行的时候,因为该地址code为空,所以并不知道这个地址是合约地址,因此依然把该地址作为EOA。
虽然我们加了require(result, "call fail!");但交易并没有revert。这是因为对EOA而言,calldata是无效的,evm也不会对calldata做任何检查,因此也没有触发错误。
不过,如果我把VisitWhenConstructing里对call2的调用换成直接调用,而不用low-level call,会revert。这个现象我目前还没有想通,如果此时依然把TestConstructor地址当成EOA地址,也应该不报错才对。
此外,我还想到了一种验证方式,那就是自己对自己进行外部函数调用。
执行TestConstructor(this).fun2();的时候会直接revert。这个和上面的从其他合约做外部函数调用是一样的结果。
contract TestConstructor {
event Fun1();
event Fun2();
constructor() {
fun1();
TestConstructor(this).fun2();
}
function fun1() internal {
emit Fun1();
}
function fun2() external {
emit Fun2();
}
}
immutable变量是可以在构造的时候才进行初始化的,理论上依赖immutable变量的函数需要等初始化之后才可以被调用。
这一小节我们不必看外部调用的情形了,外部调用一定是不可以的(和第4小节的情况一样)。我们只看内部调用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TestConstructor {
uint immutable val;
event PrintVal(uint);
constructor() {
val = 1234;
uint v = getVal();
emit PrintVal(v);
//val = 1234;
}
function getVal() internal view returns (uint) {
return val;
}
}
一切正常,构造函数中可以使用immutable变量。
不过上面的例子immutable变量的初始化在使用之前,如果颠倒位置,把val=1234拿到最后呢?
尝试一下就会发现,编译器报错:
TypeError: Immutable variables cannot be read before they are initialized.
这很合理,很安全!
最后,总结一下构造函数中使用如下合约变量/函数的结果: