# Solidity学习——神奇的构造函数

By [rbtree](https://paragraph.com/@rbtree) · 2022-10-11

---

之前分析过合约创建的过程

[https://mirror.xyz/rbtree.eth/15XiwTWWFYdLdhf-QSqTWsPuZliIgpP1o-\_bz0TdGbs](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);
        }
    }
    

![](https://storage.googleapis.com/papyrus_images/d86eb2c0842e23cb0764d23da89afc7558dbfa2e06facac4171f3769323652b9.png)

在remix里部署后我们可以发现，addr的值的确是合约的地址，这说明在构造函数里，可以获取合约地址。

合约的部署实际上有两种方式，一种是传统的CREATE，一种是后来新增的CREATE2.

[https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1014.md](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;
        }
    }
    

![](https://storage.googleapis.com/papyrus_images/2a730a77e81e1b844f1edbcdf51bf804aee3ea839b13aa0e3115d7bb7a025a6f.png)

![](https://storage.googleapis.com/papyrus_images/3cacfad45f397d72bd2236bbc2bc8727975b31d522b29696fab670b2ea436ac0.png)

我们可以看到，在构造函数中，获得的code为空，相应的codehash显然也是错误的，实际上构造函数中获取的codehash是空串的hash。

如果你经常看代码的话，可能看见下面这个用来判断一个地址是否为合约地址的函数：

    function isContract(address account) internal view returns (bool) {
            return account.code.length > 0;
    }
    

实际上，这个代码是不可靠的，只有在一个合约已经完成构造之后、被selfdestruct之前，判断才会有效。某个code为空的地址，可能目前还没有合约但将来会被部署合约(合约地址可以预计算)、可能有合约正在构造、可能曾经被部署过合约但已经被销毁。

所以，千万不可以依赖这个方法预防合约地址的攻击。

![](https://storage.googleapis.com/papyrus_images/d5cdce90af9a3bf3e1b8a455dd87959ccdb9768c007c9c30b8472ca26b453ee5.png)

### 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。

![](https://storage.googleapis.com/papyrus_images/ef6c82bac556dfbbb5d484dba16a500e7e51bde2f20890b0652308a32064a56f.png)

接下来部署TestConstructor，观察会发生什么。

![](https://storage.googleapis.com/papyrus_images/8ba43f12011cbbb99ffc7da1c41f6034ae3cfc27f7f17b4e6cccc88e5f875183.png)

![](https://storage.googleapis.com/papyrus_images/70b433289b78f465b0bfeb3ca7245c49c748f8d84fb28af0b0a7280b6c4ba000.png)

我们可以看出，转账成功了，TestConstructor的确收到了ETH，但是日志空空如也，说明receive和fallback里的日志都没有被触发。

其实也很好理解，我们上面说过，在构造时地址的code为空，此时没有办法判断该地址是合约，因此该地址此时会被当成EOA地址，当然不会触发receive或fallback。

在部署完成之后，我们运行run。

![](https://storage.googleapis.com/papyrus_images/f4753dd474108a0ac480a67373aee67cc432f318eaf007846fbb793452f06b8e.png)

![](https://storage.googleapis.com/papyrus_images/c32dc74b6f33ebd9494649d152e2a9cc5ef0fa5faf85c96f6c5bb0f979a80f86.png)

可见此时转账会正常触发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来验证。

![](https://storage.googleapis.com/papyrus_images/cf390b50b44ba29bd1e883990b573a9b5035beda6dd167f6f102a0b8b0ece7f2.png)

我们看一下日志可以发现，只有内部调用的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;
        }
    }
    

![](https://storage.googleapis.com/papyrus_images/24261b13977aac9f4f95725c6cee455013a679836900a9db5c79ab75488d6bb4.png)

一切正常，构造函数中可以使用immutable变量。

不过上面的例子immutable变量的初始化在使用之前，如果颠倒位置，把val=1234拿到最后呢？

尝试一下就会发现，编译器报错：

TypeError: Immutable variables cannot be read before they are initialized.

这很合理，很安全！

最后，总结一下构造函数中使用如下合约变量/函数的结果：

![](https://storage.googleapis.com/papyrus_images/4494ee28041ca99b080ec34392569afd66538c9ed6a87130e335ee2fac5a66d2.png)

---

*Originally published on [rbtree](https://paragraph.com/@rbtree/solidity-2)*
