# Solidity学习——神奇的构造函数 **Published by:** [rbtree](https://paragraph.com/@rbtree/) **Published on:** 2022-10-11 **URL:** https://paragraph.com/@rbtree/solidity-2 ## Content 之前分析过合约创建的过程 https://mirror.xyz/rbtree.eth/15XiwTWWFYdLdhf-QSqTWsPuZliIgpP1o-_bz0TdGbs 构造函数是一个特殊的函数,它只在合约部署的时候执行一次,部署之后的字节码是不包含构造函数逻辑的。在构造函数执行时,合约还没有被创建完成,那么此时如果访问这个合约的变量/函数,会发生什么事情呢?1 address(this)在构造函数里,能否获取合约地址?// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract TestConstructor { address public addr; constructor() { addr = address(this); } } 在remix里部署后我们可以发现,addr的值的确是合约的地址,这说明在构造函数里,可以获取合约地址。 合约的部署实际上有两种方式,一种是传统的CREATE,一种是后来新增的CREATE2. https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1014.md 对于CREATE,地址计算方式为:keccak256(rlp([sender, nonce])) 而对于CREATE2,地址计算方式为:keccak256( 0xff ++ sender ++ salt ++ keccak256(init_code)) 这些并不需要等待合约部署完毕才能计算,因此构造函数里可以正确取得合约地址。2 address(this).code和address(this).codehash在构造函数里,能否获取合约地址的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为空的地址,可能目前还没有合约但将来会被部署合约(合约地址可以预计算)、可能有合约正在构造、可能曾经被部署过合约但已经被销毁。 所以,千万不可以依赖这个方法预防合约地址的攻击。3 接收转账是否会触发receive/fallback在构造的时候,向合约转账,会触发合约的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了。4 调用合约中的函数在构造函数中,是否可以调用合约中的其他函数?// 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(); } } 5 调用合约中的依赖immutable变量的函数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. 这很合理,很安全! 最后,总结一下构造函数中使用如下合约变量/函数的结果: ## Publication Information - [rbtree](https://paragraph.com/@rbtree/): Publication homepage - [All Posts](https://paragraph.com/@rbtree/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@rbtree): Subscribe to updates