Solidity学习——神奇的构造函数
October 11th, 2022

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

构造函数是一个特殊的函数,它只在合约部署的时候执行一次,部署之后的字节码是不包含构造函数逻辑的。在构造函数执行时,合约还没有被创建完成,那么此时如果访问这个合约的变量/函数,会发生什么事情呢?

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.

对于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.

这很合理,很安全!

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

Subscribe to rbtree
Receive the latest updates directly to your inbox.
Nft graphic
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from rbtree

Skeleton

Skeleton

Skeleton