一、EVM地址类型
在EVM的账户模型中,地址分为两种类型:
一种是普通用户地址(EOA, Externally Owned Account),就是我们可以持有私钥,可以发交易的账户地址。普通用户地址是根据私钥计算出来的。
一种是合约地址,合约地址没有私钥,是在创建智能合约的时候生成的。合约地址是根据部署者的地址以及一些其他信息计算出来的。
二、Nonce的用途和变化
不管是用户地址,还是合约地址,他们都有一个nonce的属性,但是对于不同类型的地址,nonce的意义和变化方式有所不同。
普通用户地址的nonce值:
- 用途:普通用户地址的nonce主要是用来避免重放攻击和避免重复交易。
- 变化:普通用户地址每发起一次交易,只要交易被打包,nonce值就会加一,无论是普通转账交易,还是合约调用交易。即使交易失败,只要交易被打包,nonce值也会加一。
合约地址的nonce值:
- 用途:合约地址的nonce主要是用来计算子合约的地址。
- 变化:合约地址的nonce只有在合约内执行create和create2这两个操作的时候才会加一
三、合约地址计算方式
合约部署可以分为两种方式,被普通用户部署和被合约部署。
普通账户部署合约:
普通用户部署,合约地址是根据(普通用户地址 + 用户发起这笔交易时的nonce值)进行哈希计算出来的。
合约部署合约:
合约部署合约还有2种方式,一种是通过create 一种是通过create2 。
现在我们假设有合约A, 分别通过create的方式部署了子合约B,通过create2的方式部署了子合约C
- 合约B的地址计算方式和普通用户创建新合约相同,是用(合约A的地址 + 合约A创建B时的nonce值),哈希计算出来的
- 合约C的地址计算方式则不同,由于使用create2创建合约时,传入了额外的两个参数:
所以计算地址的时候,也需要使用到这两个参数,合约C的地址计算方式为(合约A的地址 + salt + 合约C的bytecodeHash)进行哈希计算出来的。
具体计算代码示例(solidity)
- 普通合约和使用create方式部署的合约,合约地址的计算方式为:
function addressFromCreate(address _origin, uint _nonce) external pure returns (address _address) {
bytes memory data;
if(_nonce == 0x00) data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, bytes1(0x80));
else if(_nonce <= 0x7f) data = abi.encodePacked(bytes1(0xd6), bytes1(0x94), _origin, uint8(_nonce));
else if(_nonce <= 0xff) data = abi.encodePacked(bytes1(0xd7), bytes1(0x94), _origin, bytes1(0x81), uint8(_nonce));
else if(_nonce <= 0xffff) data = abi.encodePacked(bytes1(0xd8), bytes1(0x94), _origin, bytes1(0x82), uint16(_nonce));
else if(_nonce <= 0xffffff) data = abi.encodePacked(bytes1(0xd9), bytes1(0x94), _origin, bytes1(0x83), uint24(_nonce));
else data = abi.encodePacked(bytes1(0xda), bytes1(0x94), _origin, bytes1(0x84), uint32(_nonce));
bytes32 hash = keccak256(data);
assembly {
mstore(0, hash)
_address := mload(0)
}
}
- 使用create2方式部署的合约,合约地址的计算方式为
function addressFromCreate2(address _origin, bytes32 _salt, bytes32 _bytecodeHash) external pure returns (address _address) {
_address = address(uint160(uint(
keccak256(
abi.encodePacked(
hex"ff",
_origin,
_salt,
_bytecodeHash
)
)
)));
}